From b65f476ec8a88a2ce6188bc2f8ea3edbd4164964 Mon Sep 17 00:00:00 2001 From: "Aalyria Technologies, Inc" Date: Fri, 1 Nov 2024 22:08:07 +0000 Subject: [PATCH] Import changes. - ce6b88f4aa7da1e8835312c05abab6929fb63689 - dbaa747b4321b7f70bd507a06bdb495a1cf8ede4 - 1650ab4bc15593150cc0c1559ded54fead3b22e0 - af7fec7dc2c322e1a7099f60c285b2e906e24885 - 80460d3318f47b2f9e5e6cddb6dd8b488935e9c9 - 435840fcf3b514db85af8c5cfd80f2d8bf74eca3 - 41cb9c50323de3095cbb59667899376435a65126 - 1c601f0afc71b24b2595d04bb5983e5e28a9a17e - c23c3e4486819c65c571b37c807be2089ac71457 - 4e14eea68432cd3566baa930f6e45d05249e18c9 - 508945e57836dd9413411b8ed56c68430319773d - c8c823f3391f6ca4f886f3ba6b1c14273970d2aa - c6c19617bca395352f22b0b366fbec6d722980ec - a8b27a413fb8f664cf5098bacd031fb6daa29804 - 80f5047e8e94020dbf5c30d5f11f4a9a1af86908 - ab413042808479fb4c9e7d55c1e9bfc46fc38e6b - 9eae88b1e62cf07f5b2ca6c38d0014e66f50c64e - e383aea3ac4870fd9ca80b7f8b600e0e68a3260a - cb5a168753ac040dbb59d71b0c7c931bde4b706d - 3a13bf3dc4046fc77025c3265c2edd0047b52ec2 - dd19b59ece863619cfcbfbdc8750d1a4c9a4c0cc - 5ebcb0a802d30b8d25aec8b0899bc65e4cbf0b06 - 5e54d172ee755aa0a35bc42e59d38ad65370a724 - f6c5f90283fd4ceb55449255be78767b8ed6390e - e34750e6fd578107509399ba8fc199c29f0aebbc - df603eb9170e40326a4641d8118f6bf9577a20da - 6360252443e6d779c7e63e5b10ede62e5dada3c0 - 0c127654a7f416e361c310aab985037cd0e3d5e1 - b17c9be6e0f3ba94de1e340b7042e7d2d15941d4 - 917eb0b00ef345c47d7d92255dfccc1c6d0c30ad - 564c3982ef58d89c4af511d2bc0a135e7eeba3e5 - 0bee5ab0684986244d9ba235f45e6102dca0cdda - ea8026e42bc8480866d4eaa16de44ec92d4cdb2d - 21f1c2aaa2f56fa2bf947381853a100d8106d21f - fc31d91a0e02adeb73815c55aebfd1a423838bbd - c5980b9d8fa973f934aa004182779c0ffb3516cc - 1f15e89868dea374f397e79a38847154be41956f - 8a5ee30223e099074a9407a6a022a86e770c906d - 00f79c7898638ba3eade2d3c5a2c80601f54fbe8 - 4b0169bfa5e2a053803cd256012a1454d41b9ba7 - a22b6b8d23cd9b13f21b38367073793ff5af6dba - b8a1c5fecb8c7514f42ff74b1cd3855b5f6496db - 23085b913d1b9d3c87e31ff3a7110d0a201c25ff - d23848601c13ca326ee0eee83c0b6db03e0d4bd2 - e936f7d4d8a931cd771c89e1c3b305dd6e4a7683 - 9ff5ced5c636d9244f8d952749516ce9cefcecb1 - f114b62bca81500926884815008da5794dbaf60c - eebb9878f11357ce40a07985ad074b9bae4e59d0 - e5a841b26e76837feb94556ed3e4b03114fb2b8e - 340d55223bb6402ba74a9bd097c1bdee846c08d3 - 4d079d2b4b4cb8e134a157387b6bd23463e5c134 - f6f2b4877254c0301bf9b969c3e8e824646d49ad - b36594d9aec0ef1d0605ebe21aab2713c2e10d76 - b016ec27537e3de4646c59794c0ea6dbb6854d5e - 6137b4b4f5248aa23ad02e78f66752207d39b7da - d1f2ea72f434ca8cd168f92717a11ba75c4739f0 - 27db69fd74e6efb99605e7f8d8045a1cf5ae9648 - 65bd042cb00cb05973fcdf5e3a359bac10ee1535 - 17b5f4fd52d2eda4ecceaddb6d92041daef5d9ef - 6c04f102210a89c2d4e8cd9fcedcfb63d0f5e6ad - 457f23b45936ef716f5461e27cfa371e950f7290 - c7c98788ce160206f8097513b23a6e2edc3ce179 - bf28142334ad24f9bafbdcd71a8bc1fd4bfd1115 - 5678bb5fffd71ac25c844699798fa10c2273e819 - 5a959083b65c52e64a6e5ec29dbf81c92d9789ed - dfb1d8118975e94269f235fcd6ae05755b670809 - 504b35a645ec19417e4df5220326ce409a3f61e9 - d24618a5ed4ce221b9b97f500e191e94c3276e2c - 9a1dd404ddf3747994687ab7c11d8a174b794fff - 0d8d568e74064bc2fb5918e4fbfd0a9e0e09b15b - e00c176ef7dbb71ded617a528e9e20871c2e4d61 - d7b2f5d90aeb013583cad805f550a604eefa34ea - 54783af9d1b03bb7a8f0fbbcab16e16bbbb1e3d7 - bf420d58a5d38a84b097872410522792e0b27f8b - 8717a4928a05bca14bbec9e9dbefe06d15ce0347 - 13a780dc899aae7ad04af5123543e190264a8117 - 991666291b35f88fa9de0cd734c26c1227e477cc - 1ce3cb82b1ca3e54122915bd6704db60778988e3 - f9c819d8b921acf42834236358722bab2814f9d4 - 76b9a381916249c1c92bb79e271eaba06e6b66dd - fca248c3f3525ff8f4ddf813b52d08c31b2e9cdb - c21bff135997d09788e19565a825820f4e166512 - 59e224b6d56c5cd66c437e26ff87c41021650d3e - 1419faf3bd11169b25fbd25d661d9164e21f8e09 - c32204765c775ac1046399c9b3668435fc637959 - 03c7091120988792e768b158827b4b9f5cbe4efd - b38d9628c8ab0ad86d47df6680bd38438b08a502 - 6700df51f5eb350ca4d0ce0cb62b7ef72fb6b945 - 7a0d5bafd03d51c056c60884b397cd9e94c5937e - 2cca022b5fdf9f7dfab55a5abde2cded6c2102e8 - 354e874ab2503db4c3dc9cf096147b0c420ae5c2 - 1548d6aa6c0762e940cc3769117f734a15468be0 - 7b4233ec3268b7c3af67a8163e4277586916a8cf - af8a32749e717a1ba82cd9df5b3e9ad6689e5be6 - 97d5ce0837958b44c211365863c11b75606fca67 - 5a5e73c1b92f33b7adccc01885333be3ffa7bd51 - 155343892f7a55f514d4e52a73185101b46c347e - b0a1438cad90ee8ca82f112363e877d197355f12 - 2a58027abf2799c4fee4f8d12e60889d6cb3854c - cef36963428975c3853b1adc326197a0f5d4a3eb (And 260 more changes) GitOrigin-RevId: ce6b88f4aa7da1e8835312c05abab6929fb63689 --- .bazelrc | 19 + .bazelversion | 1 + .github/workflows/main.yml | 102 + .gitignore | 15 + BUILD | 13 + GOVERNANCE.md | 16 + LICENSE | 201 + MODULE.bazel | 174 + NOTICE | 13 + README.md | 26 + WORKSPACE | 24 + agent/BUILD | 84 + agent/README.md | 239 + agent/agent.go | 212 + agent/agent_test.go | 50 + agent/cmd/agent/BUILD | 31 + agent/cmd/agent/agent.go | 36 + agent/cmd/prom2spacetime/BUILD | 38 + agent/cmd/prom2spacetime/prom2spacetime.go | 109 + agent/common_test.go | 66 + agent/enactment/BUILD | 24 + agent/enactment/enactment.go | 38 + agent/enactment/extproc/BUILD | 40 + agent/enactment/extproc/extproc.go | 79 + agent/enactment/extproc/extproc_test.go | 36 + agent/enactment/netlink/BUILD | 50 + agent/enactment/netlink/container_test/BUILD | 70 + .../enactment/netlink/container_test/debug.sh | 23 + .../container_test/netlink_exercise.go | 344 + .../netlink_exercise_tests.yaml | 50 + agent/enactment/netlink/errors.go | 144 + agent/enactment/netlink/netlink.go | 498 + agent/enactment/netlink/netlink_test.go | 435 + agent/enactment_service.go | 447 + agent/enactment_test.go | 448 + agent/examples/enact_flow_forward_updates.py | 132 + agent/examples/flow_update_request.json | 26 + agent/internal/agentcli/BUILD | 69 + agent/internal/agentcli/agentcli.go | 579 + agent/internal/agentcli/netlink_linux.go | 54 + agent/internal/agentcli/netlink_other.go | 36 + agent/internal/channels/BUILD | 27 + agent/internal/channels/channels.go | 103 + agent/internal/configpb/BUILD | 38 + agent/internal/configpb/config.proto | 243 + agent/internal/extprocs/BUILD | 37 + agent/internal/extprocs/extprocs.go | 56 + agent/internal/extprocs/extprocs_test.go | 75 + agent/internal/loggable/BUILD | 41 + agent/internal/loggable/loggable.go | 36 + agent/internal/loggable/loggable_test.go | 117 + agent/internal/protofmt/BUILD | 34 + agent/internal/protofmt/protofmt.go | 84 + agent/internal/protofmt/protofmt_test.go | 52 + agent/internal/task/BUILD | 42 + agent/internal/task/task.go | 277 + agent/internal/task/task_test.go | 96 + agent/internal/worker/BUILD | 35 + agent/internal/worker/worker.go | 140 + agent/internal/worker/worker_test.go | 103 + agent/node_controller.go | 144 + agent/telemetry/BUILD | 45 + agent/telemetry/extproc/BUILD | 32 + agent/telemetry/extproc/extproc.go | 83 + agent/telemetry/netlink/BUILD | 51 + agent/telemetry/netlink/container_test/BUILD | 67 + .../container_test/netlink_telemetry.go | 185 + .../netlink_telemetry_tests.yaml | 21 + agent/telemetry/netlink/netlink.go | 116 + agent/telemetry/netlink/netlink_test.go | 373 + agent/telemetry/periodic_driver.go | 86 + agent/telemetry/periodic_driver_test.go | 112 + agent/telemetry/prometheus/BUILD | 45 + .../node_exporter_metrics_testdata.txt | 343 + agent/telemetry/prometheus/scraper.go | 186 + agent/telemetry/prometheus/scraper_test.go | 186 + agent/telemetry/telemetry.go | 36 + agent/telemetry_service.go | 47 + agent/telemetry_test.go | 143 + agent/timing.go | 95 + agent/timing_test.go | 92 + api/BUILD | 41 + api/cdpi/v1alpha/BUILD | 79 + api/cdpi/v1alpha/cdpi.proto | 252 + api/common/BUILD | 95 + api/common/bent_pipe.proto | 115 + api/common/channel.proto | 72 + api/common/control.proto | 145 + api/common/control_beam.proto | 169 + api/common/control_flow.proto | 144 + api/common/control_radio.proto | 152 + api/common/control_tunnel.proto | 87 + api/common/coordinates.proto | 582 + api/common/network.proto | 193 + api/common/platform.proto | 99 + api/common/platform_antenna.proto | 212 + api/common/telemetry.proto | 267 + api/common/time.proto | 49 + api/common/tunnel.proto | 96 + api/common/wireless.proto | 64 + api/common/wireless_modcod.proto | 115 + api/common/wireless_receiver.proto | 148 + api/common/wireless_transceiver.proto | 141 + api/common/wireless_transmitter.proto | 68 + api/federation/BUILD | 57 + api/federation/federation.proto | 585 + api/model/v1alpha/BUILD | 60 + api/model/v1alpha/model.proto | 115 + api/nbi/v1alpha/BUILD | 85 + api/nbi/v1alpha/nbi.proto | 273 + api/nbi/v1alpha/resources/BUILD | 96 + .../v1alpha/resources/antenna_pattern.proto | 384 + api/nbi/v1alpha/resources/coverage.proto | 179 + .../v1alpha/resources/devices_in_region.proto | 47 + api/nbi/v1alpha/resources/intent.proto | 324 + .../v1alpha/resources/motion_evaluation.proto | 47 + .../v1alpha/resources/network_element.proto | 249 + api/nbi/v1alpha/resources/network_link.proto | 334 + .../v1alpha/resources/service_request.proto | 137 + .../resources/wireless_evaluation.proto | 138 + .../resources/wireless_interference.proto | 123 + api/nbi/v1alpha/signal_propagation.proto | 103 + api/nbi/v1alpha/txtpb_entities.proto | 31 + api/resources/BUILD | 15 + api/resources/html.tmpl | 440 + api/scheduling/v1alpha/BUILD | 68 + api/scheduling/v1alpha/scheduling.proto | 411 + api/solver/v1alpha/BUILD | 92 + api/solver/v1alpha/beam_hopping.proto | 153 + api/solver/v1alpha/solver.proto | 139 + api/telemetry/BUILD | 60 + api/telemetry/telemetry.proto | 298 + api/types/BUILD | 50 + api/types/ethernet.proto | 45 + api/types/ietf.proto | 99 + auth/BUILD | 41 + auth/auth.go | 298 + auth/auth_test.go | 200 + auth/authtest/BUILD | 23 + auth/authtest/authtest.go | 71 + auth/doc.go | 23 + bazel/bazel_downloader.cfg | 17 + bazel/java_format_rules/BUILD | 18 + bazel/java_format_rules/def.bzl | 74 + bazel/java_format_rules/runner.bash.template | 79 + bazel/java_rules/BUILD | 15 + bazel/java_rules/def.bzl | 105 + bazel/java_rules/jardoctor/BUILD | 27 + bazel/java_rules/jardoctor/main.go | 351 + common.bazelrc | 78 + contrib/README.md | 6 + contrib/loon/antenna_pattern.textproto | 36191 ++++++++++++++++ .../build_a_scenario_tutorial/BUILD | 20 + .../build_a_scenario_tutorial/README.md | 22 + .../antenna_pattern.textproto | 27 + .../band_profile.textproto | 37 + .../gateway_network_node.textproto | 38 + .../gateway_platform_definition.textproto | 89 + .../point_of_presence_network_node.textproto | 30 + .../satellite_network_node.textproto | 43 + .../satellite_platform_definition.textproto | 149 + .../service_request.textproto | 27 + .../terrestrial_link_backward.textproto | 34 + .../terrestrial_link_forward.textproto | 34 + .../user_terminal_network_node.textproto | 36 + ...ser_terminal_platform_definition.textproto | 90 + gen_tagname | 39 + go.mod | 53 + go.sum | 424 + .../aalyria/spacetime/authentication/BUILD | 37 + .../spacetime/authentication/JwtManager.java | 259 + .../SpacetimeCallCredentials.java | 278 + .../spacetime/codesamples/nbi/client/BUILD | 33 + .../codesamples/nbi/client/ListEntities.java | 81 + .../codesamples/nbi/client/README.md | 21 + .../aalyria/spacetime/authentication/BUILD | 42 + .../SpacetimeCallCredentialsTest.java | 271 + .../resources/test_private_key.pem | 51 + patches/BUILD | 18 + patches/protobuf.patch | 191 + patches/rules_proto_grpc_python.patch | 49 + py/authentication/BUILD | 29 + py/authentication/jwt_manager.py | 101 + .../spacetime_call_credentials.py | 128 + py/codesamples/BUILD | 24 + py/codesamples/README.md | 21 + py/codesamples/list_entities.py | 66 + requirements.txt | 94 + third_party/java/google_java_format/BUILD | 28 + tools/gopackagesdriver.sh | 17 + tools/nbictl/BUILD | 111 + tools/nbictl/README.md | 217 + tools/nbictl/cmd/nbictl/BUILD | 31 + tools/nbictl/cmd/nbictl/nbictl.go | 29 + tools/nbictl/config.go | 257 + tools/nbictl/config_test.go | 276 + tools/nbictl/connection.go | 158 + tools/nbictl/connection_test.go | 204 + tools/nbictl/fake_modelapi_server_test.go | 117 + tools/nbictl/fake_nbi_server_test.go | 147 + tools/nbictl/generate_auth_token.go | 107 + tools/nbictl/generate_auth_token_test.go | 145 + tools/nbictl/generate_rsa_key.go | 173 + tools/nbictl/generate_rsa_key_test.go | 199 + tools/nbictl/grpcurl.go | 257 + tools/nbictl/model.go | 278 + tools/nbictl/model_test.go | 447 + tools/nbictl/nbictl.go | 945 + tools/nbictl/nbictl_test.go | 579 + tools/nbictl/proto/BUILD | 34 + tools/nbictl/proto/nbi_ctl_config.proto | 52 + tools/nbictl/readme_test.sh | 36 + 212 files changed, 63284 insertions(+) create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 BUILD create mode 100644 GOVERNANCE.md create mode 100644 LICENSE create mode 100644 MODULE.bazel create mode 100644 NOTICE create mode 100644 README.md create mode 100644 WORKSPACE create mode 100644 agent/BUILD create mode 100644 agent/README.md create mode 100644 agent/agent.go create mode 100644 agent/agent_test.go create mode 100644 agent/cmd/agent/BUILD create mode 100644 agent/cmd/agent/agent.go create mode 100644 agent/cmd/prom2spacetime/BUILD create mode 100644 agent/cmd/prom2spacetime/prom2spacetime.go create mode 100644 agent/common_test.go create mode 100644 agent/enactment/BUILD create mode 100644 agent/enactment/enactment.go create mode 100644 agent/enactment/extproc/BUILD create mode 100644 agent/enactment/extproc/extproc.go create mode 100644 agent/enactment/extproc/extproc_test.go create mode 100644 agent/enactment/netlink/BUILD create mode 100644 agent/enactment/netlink/container_test/BUILD create mode 100644 agent/enactment/netlink/container_test/debug.sh create mode 100644 agent/enactment/netlink/container_test/netlink_exercise.go create mode 100644 agent/enactment/netlink/container_test/netlink_exercise_tests.yaml create mode 100644 agent/enactment/netlink/errors.go create mode 100644 agent/enactment/netlink/netlink.go create mode 100644 agent/enactment/netlink/netlink_test.go create mode 100644 agent/enactment_service.go create mode 100644 agent/enactment_test.go create mode 100644 agent/examples/enact_flow_forward_updates.py create mode 100644 agent/examples/flow_update_request.json create mode 100644 agent/internal/agentcli/BUILD create mode 100644 agent/internal/agentcli/agentcli.go create mode 100644 agent/internal/agentcli/netlink_linux.go create mode 100644 agent/internal/agentcli/netlink_other.go create mode 100644 agent/internal/channels/BUILD create mode 100644 agent/internal/channels/channels.go create mode 100644 agent/internal/configpb/BUILD create mode 100644 agent/internal/configpb/config.proto create mode 100644 agent/internal/extprocs/BUILD create mode 100644 agent/internal/extprocs/extprocs.go create mode 100644 agent/internal/extprocs/extprocs_test.go create mode 100644 agent/internal/loggable/BUILD create mode 100644 agent/internal/loggable/loggable.go create mode 100644 agent/internal/loggable/loggable_test.go create mode 100644 agent/internal/protofmt/BUILD create mode 100644 agent/internal/protofmt/protofmt.go create mode 100644 agent/internal/protofmt/protofmt_test.go create mode 100644 agent/internal/task/BUILD create mode 100644 agent/internal/task/task.go create mode 100644 agent/internal/task/task_test.go create mode 100644 agent/internal/worker/BUILD create mode 100644 agent/internal/worker/worker.go create mode 100644 agent/internal/worker/worker_test.go create mode 100644 agent/node_controller.go create mode 100644 agent/telemetry/BUILD create mode 100644 agent/telemetry/extproc/BUILD create mode 100644 agent/telemetry/extproc/extproc.go create mode 100644 agent/telemetry/netlink/BUILD create mode 100644 agent/telemetry/netlink/container_test/BUILD create mode 100644 agent/telemetry/netlink/container_test/netlink_telemetry.go create mode 100644 agent/telemetry/netlink/container_test/netlink_telemetry_tests.yaml create mode 100644 agent/telemetry/netlink/netlink.go create mode 100644 agent/telemetry/netlink/netlink_test.go create mode 100644 agent/telemetry/periodic_driver.go create mode 100644 agent/telemetry/periodic_driver_test.go create mode 100644 agent/telemetry/prometheus/BUILD create mode 100644 agent/telemetry/prometheus/node_exporter_metrics_testdata.txt create mode 100644 agent/telemetry/prometheus/scraper.go create mode 100644 agent/telemetry/prometheus/scraper_test.go create mode 100644 agent/telemetry/telemetry.go create mode 100644 agent/telemetry_service.go create mode 100644 agent/telemetry_test.go create mode 100644 agent/timing.go create mode 100644 agent/timing_test.go create mode 100644 api/BUILD create mode 100644 api/cdpi/v1alpha/BUILD create mode 100644 api/cdpi/v1alpha/cdpi.proto create mode 100644 api/common/BUILD create mode 100644 api/common/bent_pipe.proto create mode 100644 api/common/channel.proto create mode 100644 api/common/control.proto create mode 100644 api/common/control_beam.proto create mode 100644 api/common/control_flow.proto create mode 100644 api/common/control_radio.proto create mode 100644 api/common/control_tunnel.proto create mode 100644 api/common/coordinates.proto create mode 100644 api/common/network.proto create mode 100644 api/common/platform.proto create mode 100644 api/common/platform_antenna.proto create mode 100644 api/common/telemetry.proto create mode 100644 api/common/time.proto create mode 100644 api/common/tunnel.proto create mode 100644 api/common/wireless.proto create mode 100644 api/common/wireless_modcod.proto create mode 100644 api/common/wireless_receiver.proto create mode 100644 api/common/wireless_transceiver.proto create mode 100644 api/common/wireless_transmitter.proto create mode 100644 api/federation/BUILD create mode 100644 api/federation/federation.proto create mode 100644 api/model/v1alpha/BUILD create mode 100644 api/model/v1alpha/model.proto create mode 100644 api/nbi/v1alpha/BUILD create mode 100644 api/nbi/v1alpha/nbi.proto create mode 100644 api/nbi/v1alpha/resources/BUILD create mode 100644 api/nbi/v1alpha/resources/antenna_pattern.proto create mode 100644 api/nbi/v1alpha/resources/coverage.proto create mode 100644 api/nbi/v1alpha/resources/devices_in_region.proto create mode 100644 api/nbi/v1alpha/resources/intent.proto create mode 100644 api/nbi/v1alpha/resources/motion_evaluation.proto create mode 100644 api/nbi/v1alpha/resources/network_element.proto create mode 100644 api/nbi/v1alpha/resources/network_link.proto create mode 100644 api/nbi/v1alpha/resources/service_request.proto create mode 100644 api/nbi/v1alpha/resources/wireless_evaluation.proto create mode 100644 api/nbi/v1alpha/resources/wireless_interference.proto create mode 100644 api/nbi/v1alpha/signal_propagation.proto create mode 100644 api/nbi/v1alpha/txtpb_entities.proto create mode 100644 api/resources/BUILD create mode 100644 api/resources/html.tmpl create mode 100644 api/scheduling/v1alpha/BUILD create mode 100644 api/scheduling/v1alpha/scheduling.proto create mode 100644 api/solver/v1alpha/BUILD create mode 100644 api/solver/v1alpha/beam_hopping.proto create mode 100644 api/solver/v1alpha/solver.proto create mode 100644 api/telemetry/BUILD create mode 100644 api/telemetry/telemetry.proto create mode 100644 api/types/BUILD create mode 100644 api/types/ethernet.proto create mode 100644 api/types/ietf.proto create mode 100644 auth/BUILD create mode 100644 auth/auth.go create mode 100644 auth/auth_test.go create mode 100644 auth/authtest/BUILD create mode 100644 auth/authtest/authtest.go create mode 100644 auth/doc.go create mode 100644 bazel/bazel_downloader.cfg create mode 100644 bazel/java_format_rules/BUILD create mode 100644 bazel/java_format_rules/def.bzl create mode 100644 bazel/java_format_rules/runner.bash.template create mode 100644 bazel/java_rules/BUILD create mode 100644 bazel/java_rules/def.bzl create mode 100644 bazel/java_rules/jardoctor/BUILD create mode 100644 bazel/java_rules/jardoctor/main.go create mode 100644 common.bazelrc create mode 100644 contrib/README.md create mode 100644 contrib/loon/antenna_pattern.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/BUILD create mode 100644 entity_samples/build_a_scenario_tutorial/README.md create mode 100644 entity_samples/build_a_scenario_tutorial/antenna_pattern.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/band_profile.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/gateway_network_node.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/gateway_platform_definition.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/point_of_presence_network_node.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/satellite_network_node.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/satellite_platform_definition.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/service_request.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/terrestrial_link_backward.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/terrestrial_link_forward.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/user_terminal_network_node.textproto create mode 100644 entity_samples/build_a_scenario_tutorial/user_terminal_platform_definition.textproto create mode 100755 gen_tagname create mode 100644 go.mod create mode 100644 go.sum create mode 100644 java/com/aalyria/spacetime/authentication/BUILD create mode 100644 java/com/aalyria/spacetime/authentication/JwtManager.java create mode 100644 java/com/aalyria/spacetime/authentication/SpacetimeCallCredentials.java create mode 100644 java/com/aalyria/spacetime/codesamples/nbi/client/BUILD create mode 100644 java/com/aalyria/spacetime/codesamples/nbi/client/ListEntities.java create mode 100644 java/com/aalyria/spacetime/codesamples/nbi/client/README.md create mode 100644 javatests/com/aalyria/spacetime/authentication/BUILD create mode 100644 javatests/com/aalyria/spacetime/authentication/SpacetimeCallCredentialsTest.java create mode 100644 javatests/com/aalyria/spacetime/authentication/resources/test_private_key.pem create mode 100644 patches/BUILD create mode 100644 patches/protobuf.patch create mode 100644 patches/rules_proto_grpc_python.patch create mode 100644 py/authentication/BUILD create mode 100644 py/authentication/jwt_manager.py create mode 100644 py/authentication/spacetime_call_credentials.py create mode 100644 py/codesamples/BUILD create mode 100644 py/codesamples/README.md create mode 100644 py/codesamples/list_entities.py create mode 100644 requirements.txt create mode 100644 third_party/java/google_java_format/BUILD create mode 100755 tools/gopackagesdriver.sh create mode 100644 tools/nbictl/BUILD create mode 100644 tools/nbictl/README.md create mode 100644 tools/nbictl/cmd/nbictl/BUILD create mode 100644 tools/nbictl/cmd/nbictl/nbictl.go create mode 100644 tools/nbictl/config.go create mode 100644 tools/nbictl/config_test.go create mode 100644 tools/nbictl/connection.go create mode 100644 tools/nbictl/connection_test.go create mode 100644 tools/nbictl/fake_modelapi_server_test.go create mode 100644 tools/nbictl/fake_nbi_server_test.go create mode 100644 tools/nbictl/generate_auth_token.go create mode 100644 tools/nbictl/generate_auth_token_test.go create mode 100644 tools/nbictl/generate_rsa_key.go create mode 100644 tools/nbictl/generate_rsa_key_test.go create mode 100644 tools/nbictl/grpcurl.go create mode 100644 tools/nbictl/model.go create mode 100644 tools/nbictl/model_test.go create mode 100644 tools/nbictl/nbictl.go create mode 100644 tools/nbictl/nbictl_test.go create mode 100644 tools/nbictl/proto/BUILD create mode 100644 tools/nbictl/proto/nbi_ctl_config.proto create mode 100755 tools/nbictl/readme_test.sh diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..2f42846 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,19 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import %workspace%/common.bazelrc + +# Enable bzlmod, which is the new system of managing external dependencies +# using a MODULE.bazel file instead of the legacy WORKSPACE system. +common --enable_bzlmod diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..643916c --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +7.3.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5789b62 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,102 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Bazel + +on: + pull_request: {} + push: {} + release: + types: [published] + +jobs: + build-and-test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cache/bazelisk + ~/.cache/bazel + key: bazel-${{ hashFiles('.bazelrc', '.bazelversion', 'WORKSPACE', 'MODULE.bazel', 'requirements.txt') }} + restore-keys: bazel- + - run: bazelisk test //... + + build-and-upload-tools: + needs: [build-and-test] + strategy: + matrix: + # Only the go_binary targets support cross-compilation at the moment: + target: + - //agent/cmd/agent + - //tools/nbictl/cmd/nbictl + os: [linux, windows, darwin] + arch: [amd64, arm64] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cache/bazelisk + ~/.cache/bazel + key: bazel-${{ hashFiles('.bazelrc', '.bazelversion', 'WORKSPACE', 'MODULE.bazel', 'requirements.txt') }} + restore-keys: bazel- + - run: bazelisk build "--platforms=@rules_go//go/toolchain:${{ matrix.os }}_${{ matrix.arch }}" "${{ matrix.target }}" + - name: Upload nbictl binary + uses: actions/upload-artifact@v4 + if: ${{ matrix.target == '//tools/nbictl/cmd/nbictl' && matrix.os != 'windows' }} + with: + name: nbictl-${{ matrix.os }}-${{ matrix.arch }} + path: bazel-bin/tools/nbictl/cmd/nbictl/nbictl_/nbictl + - name: Upload nbictl binary - Windows + uses: actions/upload-artifact@v4 + if: ${{ matrix.target == '//tools/nbictl/cmd/nbictl' && matrix.os == 'windows' }} + with: + name: nbictl-${{ matrix.os }}-${{ matrix.arch }} + path: bazel-bin/tools/nbictl/cmd/nbictl/nbictl_/nbictl.exe + - name: Upload agent binary + uses: actions/upload-artifact@v4 + if: ${{ matrix.target == '//agent/cmd/agent' && matrix.os != 'windows' }} + with: + name: agent-${{ matrix.os }}-${{ matrix.arch }} + path: bazel-bin/agent/cmd/agent/agent_/agent + - name: Upload agent binary - Windows + uses: actions/upload-artifact@v4 + if: ${{ matrix.target == '//agent/cmd/agent' && matrix.os == 'windows' }} + with: + name: agent-${{ matrix.os }}-${{ matrix.arch }} + path: bazel-bin/agent/cmd/agent/agent_/agent.exe + + build-and-upload-docs: + needs: [build-and-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cache/bazelisk + ~/.cache/bazel + key: bazel-${{ hashFiles('.bazelrc', '.bazelversion', 'WORKSPACE', 'MODULE.bazel', 'requirements.txt') }} + restore-keys: bazel- + - run: bazelisk build "//api:api.html" + - name: Upload API docs + uses: actions/upload-artifact@v4 + with: + name: api.html + path: | + bazel-bin/api/api.html/api.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5bb7fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +bazel-* diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..eb5fc71 --- /dev/null +++ b/BUILD @@ -0,0 +1,13 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..29a2bb6 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,16 @@ +# Governance + +Spacetime’s APIs are under active development by the internal developer team. Contributions from the wider developer community will be considered according to the guidelines in this document. + +## Contributing +To propose a change to the APIs, please first email the Maintainers at spacetime-maintainers@aalyria.com to discuss the change. + +Major changes to the API, such as fundamental re-architectures, will require additional discussion and will proceed once the Spacetime Maintainers have aligned. + +Minor changes to the API, such as non-breaking changes that do not affect core functionality, should be discussed over email as well, but will proceed without a major design discussion. + +## License +All contributions to Spacetime’s APIs will be licensed under its Apache 2.0 license. + +## Contributor License Agreement (CLA) +When you open your first PR, you will be prompted to submit a CLA. Follow the prompts to sign and complete it. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..250ba4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..85b3b91 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,174 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module(name = "aalyria_api") + +bazel_dep(name = "protobuf") + +# Patch protobuf to backport +# https://github.com/protocolbuffers/protobuf/pull/17402 to protobuf 28.2. +archive_override( + module_name = "protobuf", + integrity = "sha256-C37zUG/ECthArdE4031EPEqfWwswfCXVFtu+9NwLxco=", + patch_strip = 1, + patches = [ + "//patches:protobuf.patch", + ], + strip_prefix = "protobuf-28.2", + urls = "https://github.com/protocolbuffers/protobuf/archive/refs/tags/v28.2.zip", +) + +bazel_dep(name = "gazelle", version = "0.39.1") +bazel_dep(name = "googleapis", version = "0.0.0-20240326-1c8d509c5") + +# This override is necessary to prevent bzlmod from resolving grpc-java to a +# version impacted by https://github.com/grpc/grpc-java/issues/11275. +single_version_override( + module_name = "grpc-java", + version = "1.64.0", +) + +bazel_dep(name = "rules_go", version = "0.49.0") +bazel_dep(name = "rules_java", version = "7.9.0") +bazel_dep(name = "rules_jvm_external", version = "6.2") +bazel_dep(name = "rules_oci", version = "1.7.5") +bazel_dep(name = "rules_pkg", version = "0.10.1") +bazel_dep(name = "rules_proto_grpc_cpp", version = "5.0.0") +bazel_dep(name = "rules_proto_grpc_doc", version = "5.0.0") +bazel_dep(name = "rules_proto_grpc_java", version = "5.0.0") +bazel_dep(name = "rules_proto_grpc_python", version = "5.0.0") + +# Patch rules_proto_gprc_python to use python protobuf v28.2 +archive_override( + module_name = "rules_proto_grpc_python", + integrity = "sha256-b9r/nAMUoakRmZn2IlVYhaNYgNUXeWHGV4wgd2cCmV8=", + patch_strip = 1, + patches = [ + "//patches:rules_proto_grpc_python.patch", + ], + strip_prefix = "rules_proto_grpc_python-5.0.0", + urls = "https://github.com/rules-proto-grpc/rules_proto_grpc/releases/download/5.0.0/rules_proto_grpc_python-5.0.0.tar.gz", +) + +bazel_dep(name = "rules_python", version = "0.35.0") +bazel_dep(name = "toolchains_protoc") + +# Override toolchains_protoc to obtain a version with protoc v28.2 available. +git_override( + module_name = "toolchains_protoc", + commit = "0ffd305814e08a4d835efbd106d609001f0d354c", + remote = "https://github.com/aspect-build/toolchains_protoc.git", +) + +# Configure the protoc toolchain used by rules_proto_grpc to use protoc v28.2. +# This ensures that the version agrees with the protobuf dependency version, +# which is necessary because Protobuf C++ requires an exact match between its +# generated code version and its runtime version. +# (https://protobuf.dev/support/cross-version-runtime-guarantee/#cpp) +protoc = use_extension("@toolchains_protoc//protoc:extensions.bzl", "protoc") +protoc.toolchain( + google_protobuf = "com_google_protobuf", + version = "v28.2", +) + +bazel_dep(name = "container_structure_test", version = "1.16.0", dev_dependency = True) + +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") +maven.install( + artifacts = [ + "com.google.code.gson:gson:2.10.1", + "com.google.googlejavaformat:google-java-format:1.17.0", + "com.google.guava:guava:33.3.0-jre", + "io.grpc:grpc-testing:1.66.0", + "io.helidon.grpc:helidon-grpc-core:3.2.5", + "junit:junit:4.13.2", + "org.bouncycastle:bcprov-jdk15on:1.70", + "org.mockito:mockito-core:4.5.1", + ], + repositories = [ + "https://repo.maven.apache.org/maven2/", + ], +) +use_repo(maven, "maven") + +go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk") +go_sdk.download( + version = "1.23.0", +) + +go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") +use_repo( + go_deps, + "com_github_fullstorydev_grpcurl", + "com_github_golang_jwt_jwt_v5", + "com_github_google_go_cmp", + "com_github_google_uuid", + "com_github_jhump_protoreflect", + "com_github_jonboulle_clockwork", + "com_github_prometheus_client_model", + "com_github_prometheus_prom2json", + "com_github_rs_zerolog", + "com_github_urfave_cli_v2", + "com_github_vishvananda_netlink", + "io_opentelemetry_go_contrib_instrumentation_google_golang_org_grpc_otelgrpc", + "io_opentelemetry_go_otel", + "io_opentelemetry_go_otel_exporters_otlp_otlptrace_otlptracegrpc", + "io_opentelemetry_go_otel_sdk", + "io_opentelemetry_go_otel_trace", + "org_golang_google_genproto", + "org_golang_google_genproto_googleapis_rpc", + "org_golang_google_grpc", + "org_golang_google_protobuf", + "org_golang_x_sync", + "org_golang_x_sys", +) + +switched_rules = use_extension("@googleapis//:extensions.bzl", "switched_rules") +switched_rules.use_languages( + cc = True, + java = True, + python = True, +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = "3.11", +) + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "python_deps", + python_version = "3.11", + requirements_lock = "//:requirements.txt", +) +use_repo(pip, "python_deps") + +oci = use_extension("@rules_oci//oci:extensions.bzl", "oci") +oci.pull( + name = "alpine_base", + # docker.io/library/alpine:3.19.1 + digest = "sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", # 3.19.1 2024-01-26 + image = "docker.io/library/alpine", + platforms = ["linux/amd64"], +) +use_repo(oci, "alpine_base") + +bazel_dep(name = "org_outernetcouncil_nmts") +archive_override( + module_name = "org_outernetcouncil_nmts", + strip_prefix = "nmts-0.0.8", + urls = "https://github.com/outernetcouncil/nmts/archive/refs/tags/v0.0.8.tar.gz", +) diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..f4e66dc --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Copyright 2023 Aalyria Technologies, Inc., and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf20d21 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Spacetime's APIs + +Spacetime has 2 APIs: +- The **Northbound Interface (NBI)** allows humans or applications to define and orchestrate a network. This includes functions such as specifying the time-dynamic position and orientation of platforms and antennas, defining networking parameters on each node in the network, and creating requests for service that will be scheduled and routed through the network. +- The **Control to Data-plane Interface (CDPI)**, or Southbound Interface, allows Spacetime to control network devices and receive updates in return. This includes functions such as steering antenna beams to establish new links and configuring RF parameters like the transmit power and channel. + +## Developer Guides +You can find developer guides and tutorials on [this website](https://docs.spacetime.aalyria.com). + +This site contains: +- [NBI Developer Guide](https://docs.spacetime.aalyria.com/nbi-developer-guide) +- [CDPI Developer Guide](https://docs.spacetime.aalyria.com/southbound-interface-developer-guide) +- [Building a Scenario Tutorial](https://docs.spacetime.aalyria.com/scenario-building) +- [Authentication](https://docs.spacetime.aalyria.com/authentication) + +## Repo Contents +In this repo, you will find the following directories: +- [api](/api): The [gRPC](https://grpc.io/) and [Protocol Buffers](https://protobuf.dev/) definitions of the API. +- [agent](/agent): A Go implementation of an SBI agent. +- [contrib](/contrib): An open-source directory of real hardware that has been modeled in Spacetime and used in real networks. Contributions are welcome! + +## Contributing +Spacetime welcomes contributions to its APIs. Read the [governance](GOVERNANCE.md) document to learn more about policies and processes for suggesting changes to the APIs. + +### License +Spacetime's APIs are licensed under Apache 2.0 (see the [LICENSE](./LICENSE) file). diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..7182852 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,24 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +SCIP_JAVA_VERSION = "0.0.6" + +http_archive( + name = "scip_java", + sha256 = "7013fa54a6c764999cb5dce0b8a04a983d6335d3fa73e474f4f229dcbf2ce30b", + strip_prefix = "scip-java-{}".format(SCIP_JAVA_VERSION), + url = "https://github.com/ciarand/scip-java/archive/refs/tags/v{}.zip".format(SCIP_JAVA_VERSION), +) diff --git a/agent/BUILD b/agent/BUILD new file mode 100644 index 0000000..dacab65 --- /dev/null +++ b/agent/BUILD @@ -0,0 +1,84 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build rules for the Go CDPI agent. These shouldn't depend on any other +# internal packages apart from those found in //api. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "agent", + srcs = [ + "agent.go", + "enactment_service.go", + "node_controller.go", + "telemetry_service.go", + "timing.go", + ], + importpath = "aalyria.com/spacetime/agent", + deps = [ + "//agent/enactment", + "//agent/internal/channels", + "//agent/internal/loggable", + "//agent/internal/task", + "//agent/telemetry", + "//api/common:common_go_proto", + "//api/scheduling/v1alpha:scheduling_go_grpc", + "//api/telemetry:telemetry_go_grpc", + "@com_github_google_uuid//:uuid", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + "@io_opentelemetry_go_otel//attribute", + "@org_golang_google_grpc//:grpc", + "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//status", + "@org_golang_google_protobuf//proto", + "@org_golang_x_sync//errgroup", + ], +) + +go_test( + name = "agent_test", + size = "small", + srcs = [ + "agent_test.go", + "common_test.go", + "enactment_test.go", + "telemetry_test.go", + "timing_test.go", + ], + embed = [":agent"], + deps = [ + "//agent/internal/channels", + "//agent/internal/task", + "//api/cdpi/v1alpha:cdpi_go_grpc", + "//api/common:common_go_proto", + "//api/scheduling/v1alpha:scheduling_go_grpc", + "//api/telemetry:telemetry_go_grpc", + "@com_github_google_go_cmp//cmp", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + "@org_golang_google_grpc//:grpc", + "@org_golang_google_grpc//credentials/insecure", + "@org_golang_google_grpc//status", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//testing/protocmp", + "@org_golang_google_protobuf//types/known/emptypb", + "@org_golang_google_protobuf//types/known/timestamppb", + "@org_golang_x_sync//errgroup", + ], +) diff --git a/agent/README.md b/agent/README.md new file mode 100644 index 0000000..6ba0e8f --- /dev/null +++ b/agent/README.md @@ -0,0 +1,239 @@ +# agent + +A Go implementation of a CDPI agent. + +## Overview + +The Control-to-Data-Plane Interface (CDPI) works by exchanging +[Protocol Buffers](https://protobuf.dev/), a language-agnostic format and toolchain for serializing +messages, transmitted using [gRPC](https://grpc.io/), a performant RPC framework with many +sophisticated features. + +This directory provides two important pieces for interacting with the Spacetime CDPI: + +- The `agent` (`//agent/cmd/agent`), a Go binary that handle the CDPI protocol and + authentication details while delegating the actual implementation of enactments to a + user-configured external process. While the details of the CDPI protocol are still subject to + change, the command line interface for these binaries is intended to be significantly more stable + and provide an easy to develop against abstraction that insulates platform integrators from the + majority of those changes. + +- The `agent` library (`//agent`), a Go package that provides a growing set of + abstractions for writing a new CDPI agent. This library is subject to change alongside the CDPI + protocol, so platform integrators are encouraged to use the `agent` binary until the underlying + APIs reach a stable milestone. + +## Building + +This repo uses the [bazel](https://bazel.build/) build system. Once you have a copy of `bazel` in +your `$PATH`, running `bazel build //agent/cmd/agent` will build the Go binary. Similarly, +running `bazel build //agent` will build the Go library. + +For a full list of available build targets, you can use `bazel query`: + +```bash +bazel query //agent/...:all +``` + +## Getting started with the `agent` binary + +### Configuration + +The `agent` binary accepts configuration in the form of a protobuf message, documented in +[config.proto](internal/configpb/config.proto). The message can be encoded in +[prototext format](https://protobuf.dev/reference/protobuf/textformat-spec/) (human readable and +writable), [json](https://protobuf.dev/programming-guides/proto3/#json), or the +[binary proto format](https://protobuf.dev/programming-guides/encoding/). Most users will find the +prototext format the easiest to use, and as such it's the default. + +More details for each aspect of the configuration are provided below, but a simple configuration +file might look like this: + +```textproto +# each network_node is configured with a stanza like so: +network_nodes: { + id: "node-a" + enactment_driver: { + connection_params: { + # TODO: change to point to the domain of your spacetime instance + endpoint_uri: "scheduling.my_instance.spacetime.aalyria.com" + + transport_security: { + system_cert_pool: {} + } + + auth_strategy: { + jwt: { + # TODO: change to the domain of your spacetime instance + audience: "scheduling.my_instance.spacetime.aalyria.com" + # TODO: use the email your Aalyria representative will share with you + email: "my-cdpi-agent@example.com" + # TODO: use the private key ID your Aalyria representative will share with you + private_key_id: "BADDB0BACAFE" + signing_strategy: { + # TODO: change to the path of your PEM-encoded RSA private key + private_key_file: "/path/to/agent/private/key.pem" + } + } + } + } + + external_command: { + # while each command invocation will receive a node ID as part of the + # enactment request, you can also pass additional arguments here to help + # integrate with your own systems + args: "/usr/local/bin/do_enactments" + args: "--node=a" + args: "--format=json" + # Encode enactment requests as JSON to the process's standard input and + # expect any new state messages to be written as JSON to the process's + # standard output (this is the default) + proto_format: JSON + } + } +} + +network_nodes: { + id: "node-b" + enactment_driver: { + connection_params: { + # TODO: change to point to the domain of your spacetime instance + endpoint_uri: "scheduling.my_instance.spacetime.aalyria.com" + + transport_security: { + system_cert_pool: {} + } + + auth_strategy: { + jwt: { + # TODO: change to the domain of your spacetime instance + audience: "scheduling.my_instance.spacetime.aalyria.com" + # TODO: use the email your Aalyria representative will share with you + email: "my-cdpi-agent@example.com" + # TODO: use the private key ID your Aalyria representative will share with you + private_key_id: "BADDB0BACAFE" + signing_strategy: { + # TODO: change to the path of your PEM-encoded RSA private key + private_key_file: "/path/to/agent/private/key.pem" + } + } + } + } + + external_command: { + args: "/usr/local/bin/some_other_enactment_cmd" + # Use the protobuf binary format for encoding both stdin and stdout messages. + proto_format: WIRE + } + } +} +``` + +See the documentation in [config.proto](internal/configpb/config.proto) for more details on the +available options. You can use the `--dry-run` flag to check that your configuration is valid: + +```bash +bazel run //agent/cmd/agent -- --log-level trace --config "$PWD/config.textproto" --dry-run +INFO: Invocation ID: d8a4b02f-1e01-47cb-bd26-5366704165af +INFO: Analyzed target //agent/cmd/agent:agent (0 packages loaded, 0 targets configured). +INFO: Found 1 target... +Target //agent/cmd/agent:agent up-to-date: + bazel-bin/agent/cmd/agent/agent_/agent +INFO: Elapsed time: 1.768s, Critical Path: 1.56s +INFO: 3 processes: 1 internal, 2 linux-sandbox. +INFO: Build completed successfully, 3 total actions +INFO: Running command line: bazel-bin/agent/cmd/agent/agent_/agent --log-level trace --config /path/to/config.textproto --format text --dry-run +2023-04-19 11:57:01AM INF config is valid +``` + +### Authentication + +The agent uses signed [JSON Web Tokens (JWTs)](https://www.rfc-editor.org/rfc/rfc7519) to +authenticate with the CDPI service. The JWT needs to be signed using an RSA private key with a +corresponding public key that's been shared - inside of a self-signed x509 certificate - with the +Aalyria team. + +#### Creating a test keypair + +For testing purposes, you can generate a valid key using the `openssl` tool: + +```bash +# generate a private key of size 4096 and save it to agent_priv_key.pem +openssl genrsa -out agent_priv_key.pem 4096 +# extract the public key and save it to an x509 certificate named +# agent_pub_key.cer (with an expiration far into the future) +openssl req -new -x509 -key agent_priv_key.pem -out agent_pub_key.cer -days 36500 +``` + +### Starting the agent + +Assuming you've saved your configuration in a file called `config.textproto`, you can use the +following command to start the agent: + +```bash +bazel run //agent/cmd/agent -- --config "$PWD/my_config.textproto" --log-level debug +``` + +NOTE: `bazel run` changes the working directory of the process, so you'll need +to use absolute paths to point to the config file. + +If the agent was able to authenticate correctly, you should see something like this appear as output +(requires `--log-level` be "debug" or "trace"): + +``` +2023-04-18 08:44:48PM INF starting agent +2023-04-18 08:44:48PM DBG node controller starting nodeID=Atlantis-groundstation +``` + +## Next steps + +### Writing a custom extproc enactment backend + +Writing a custom enactment backend using the `agent` is relatively simple as the agent takes care of +the CDPI protocol details, including timing and error reporting. When the agent receives a scheduled +control update, it invokes the configured external process, writes the incoming +`ScheduledControlUpdate` message in the encoding format of your choice to the process's stdin, and +optionally reads a new `ControlPlaneState` message (using the same encoding format) from the +process's stdout. + +- If nothing is written to stdout and the process terminates with an exit code of 0, the enactment + is considered successful and the node state is assumed to have been updated to match the change. + +- If anything goes wrong during the enactment (indicated by a non-zero exit code), the process's + stderr and exit code are combined to form a gRPC status which is conveyed back to the CDPI + endpoint as the (failing) result of the enactment. + +Since the external process only needs to be able to encode and decode JSON, it's trivial to write +the platform-specific logic in whatever language best suits the task. Included in this repo are some +sample programs that demonstrate basic error handling and message parsing in different languages: + + + +- examples/enact_flow_forward_updates.py: A python script that reads the input messages as ad-hoc + JSON, implements some basic error handling, and demonstrates how one might go about enacting flow + updates (the actual logic for forwarding packets is left as an exercise for the reader). + +## Operational notes + +### Time synchronization + +All agent implementations and instantiations MUST have some means to maintain time synchronization. +Ideally an agent's time would be synchronzied to the same ultimate source(s) used by Spacetime itself, though this is not a strict requirement. + +RECOMMENDED time synchronization mechanisms include: +* GPS or another Global Navigation Satellite System (GNSS) PNT service +* Network Time Protocol v4 ([NTPv4](https://www.rfc-editor.org/rfc/rfc5905.html)), ideally with Network Time Security ([NTS](https://www.rfc-editor.org/rfc/rfc8915.html)) +* IEEE Precision Time Protocol ([IEEE 1588-2019](https://ieeexplore.ieee.org/document/9120376)) + +Some experimental time synchronization mechanisms include: +* Network Time Protocol v5 ([draft](https://datatracker.ietf.org/doc/draft-ietf-ntp-ntpv5/)) +* roughtime ([draft](https://datatracker.ietf.org/doc/draft-ietf-ntp-roughtime/)) + +An agent SHOULD maintain a sub-second difference from its chosen time source(s), and all RECOMMENDED time synchronizations can in principle achieve much better accuracies. +The exact requirements and realistically achievable accuracies, however, are specific to a given deployment scenario. + +If an agent instance or its underlying platform cannot maintain adequate time synchronization, for a mission-specific definition of "adequate", then it MUST do one of the following: +* include notification of the loss of adequate time synchronization with any reported telemetry, or +* not report telemetry (information is too untrustworthy to be useful). + +An agent MAY continue to enact Scheduling API commands, especially if doing so might lead to restoring adequate time synchronization (e.g. restoration of a data plane connection over which a networked time protocol's messages are forwarded). diff --git a/agent/agent.go b/agent/agent.go new file mode 100644 index 0000000..0615512 --- /dev/null +++ b/agent/agent.go @@ -0,0 +1,212 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package agent provides a CDPI agent implementation. +package agent + +import ( + "context" + "errors" + "expvar" + "fmt" + "sync" + + "aalyria.com/spacetime/agent/enactment" + "aalyria.com/spacetime/agent/internal/task" + "aalyria.com/spacetime/agent/telemetry" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + "google.golang.org/grpc" +) + +var ( + statsMap *expvar.Map + statsMapMu = &sync.Mutex{} +) + +func init() { + statsMap = expvar.NewMap("agent") +} + +var ( + errNoClock = errors.New("no clock provided (see WithClock)") + errNoNodes = errors.New("no nodes configured (see WithNode)") + errNoActiveServices = errors.New("no services configured for node (see WithEnactmentBackend and WithTelemetryBackend)") +) + +// AgentOption provides a well-typed and sound mechanism to configure an Agent. +type AgentOption interface{ apply(*Agent) } + +// agentOptFunc is a shorthand for creating simple AgentOptions. +type agentOptFunc func(*Agent) + +func (fn agentOptFunc) apply(a *Agent) { fn(a) } + +// NodeOption provides a well-typed and sound mechanism to configure an +// individual node that an Agent will manage. +type NodeOption interface{ apply(n *node) } + +// nodeOptFunc is a shorthand for creating simple NodeOptions. +type nodeOptFunc func(*node) + +func (fn nodeOptFunc) apply(n *node) { fn(n) } + +// Agent is a CDPI agent that coordinates change requests across multiple +// nodes. +type Agent struct { + clock clockwork.Clock + nodes map[string]*node +} + +// NewAgent creates a new Agent configured with the provided options. +func NewAgent(opts ...AgentOption) (*Agent, error) { + a := &Agent{ + nodes: map[string]*node{}, + } + + for _, opt := range opts { + opt.apply(a) + } + + return a, a.validate() +} + +func (a *Agent) validate() error { + errs := []error{} + if a.clock == nil { + errs = append(errs, errNoClock) + } + if len(a.nodes) == 0 { + errs = append(errs, errNoNodes) + } + for _, n := range a.nodes { + if !n.enactmentsEnabled && !n.telemetryEnabled { + errs = append(errs, fmt.Errorf("node %q has no services enabled: %w", n.id, errNoActiveServices)) + } + } + return errors.Join(errs...) +} + +// WithClock configures the Agent to use the provided clock. +func WithClock(clock clockwork.Clock) AgentOption { + return agentOptFunc(func(a *Agent) { + a.clock = clock + }) +} + +// WithRealClock configures the Agent to use a real clock. +func WithRealClock() AgentOption { + return WithClock(clockwork.NewRealClock()) +} + +// WithNode configures a network node for the agent to represent. +func WithNode(id string, opts ...NodeOption) AgentOption { + n := &node{id: id} + for _, f := range opts { + f.apply(n) + } + return agentOptFunc(func(a *Agent) { + a.nodes[id] = n + }) +} + +type node struct { + ed enactment.Driver + td telemetry.Driver + id string + priority uint32 + + enactmentEndpoint, telemetryEndpoint string + enactmentsEnabled, telemetryEnabled bool + enactmentDialOpts, telemetryDialOpts []grpc.DialOption +} + +// WithEnactmentDriver configures the [enactment.Driver] for the given Node. +func WithEnactmentDriver(endpoint string, d enactment.Driver, dialOpts ...grpc.DialOption) NodeOption { + return nodeOptFunc(func(n *node) { + n.ed = d + n.enactmentEndpoint = endpoint + n.enactmentDialOpts = dialOpts + n.enactmentsEnabled = true + }) +} + +// WithTelemetryDriver configures the [telemetry.Driver] for the given Node. +func WithTelemetryDriver(endpoint string, d telemetry.Driver, dialOpts ...grpc.DialOption) NodeOption { + return nodeOptFunc(func(n *node) { + n.td = d + n.telemetryEndpoint = endpoint + n.telemetryDialOpts = dialOpts + n.telemetryEnabled = true + }) +} + +// Run starts the Agent and blocks until a fatal error is encountered or all +// node controllers terminate. +func (a *Agent) Run(ctx context.Context) error { + agentMap := &expvar.Map{} + agentMap.Init() + + statKey := fmt.Sprintf("%p", a) + + statsMapMu.Lock() + statsMap.Set(statKey, agentMap) + statsMapMu.Unlock() + + defer func() { + statsMapMu.Lock() + statsMap.Delete(statKey) + statsMapMu.Unlock() + }() + + // We can't use an errgroup here because we explicitly want all the errors, + // not just the first one. + // + // TODO: switch this to sourcegraph's conc library + errCh := make(chan error) + if err := a.start(ctx, agentMap, errCh); err != nil { + return err + } + + errs := []error{} + for err := range errCh { + if errs = append(errs, err); len(errs) == len(a.nodes) { + break + } + } + return errors.Join(errs...) +} + +func (a *Agent) start(ctx context.Context, agentMap *expvar.Map, errCh chan error) error { + for _, n := range a.nodes { + ctx, done := context.WithCancel(ctx) + + nc, err := a.newNodeController(n, done) + if err != nil { + return fmt.Errorf("node %q: %w", n.id, err) + } + agentMap.Set(n.id, expvar.Func(nc.Stats)) + + srv := task.Task(nc.run). + WithStartingStoppingLogs("node controller", zerolog.DebugLevel). + WithLogField("nodeID", n.id). + WithSpanAttributes(attribute.String("aalyria.nodeID", n.id)). + WithNewSpan("node_controller") + + go func() { errCh <- srv(ctx) }() + } + return nil +} diff --git a/agent/agent_test.go b/agent/agent_test.go new file mode 100644 index 0000000..08651a2 --- /dev/null +++ b/agent/agent_test.go @@ -0,0 +1,50 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "errors" + "testing" +) + +func TestAgentValidation_noOptions(t *testing.T) { + t.Parallel() + _, err := NewAgent() + if err == nil { + t.Errorf("expected NewAgent with no options to be invalid") + } +} + +func TestAgentValidation_noNodes(t *testing.T) { + t.Parallel() + _, err := NewAgent(WithRealClock()) + + if !errors.Is(err, errNoNodes) { + t.Errorf("expected NewAgent with no nodes to cause %s, but got %v error instead", errNoNodes, err) + } +} + +func TestAgentValidation_noServices(t *testing.T) { + t.Parallel() + _, err := NewAgent( + WithRealClock(), + WithNode("a"), + WithNode("b"), + ) + + if !errors.Is(err, errNoActiveServices) { + t.Errorf("expected NewAgent with no nodes to cause %s, but got %v error instead", errNoActiveServices, err) + } +} diff --git a/agent/cmd/agent/BUILD b/agent/cmd/agent/BUILD new file mode 100644 index 0000000..25d7f52 --- /dev/null +++ b/agent/cmd/agent/BUILD @@ -0,0 +1,31 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_binary( + name = "agent", + embed = [":agent_lib"], + pure = "on", + static = "on", +) + +go_library( + name = "agent_lib", + srcs = ["agent.go"], + importpath = "aalyria.com/spacetime/agent/cmd/agent", + deps = ["//agent/internal/agentcli"], +) diff --git a/agent/cmd/agent/agent.go b/agent/cmd/agent/agent.go new file mode 100644 index 0000000..c16267d --- /dev/null +++ b/agent/cmd/agent/agent.go @@ -0,0 +1,36 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main provides a CDPI agent that is configured using a protobuf-based +// manifest. +package main + +import ( + "context" + "fmt" + "os" + + "aalyria.com/spacetime/agent/internal/agentcli" +) + +func main() { + if err := (agentcli.AgentConf{ + AppName: "agent", + Handles: agentcli.DefaultHandles(), + Providers: []agentcli.Provider{}, + }).Run(context.Background(), os.Args[0], os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "fatal error: %v\n", err) + os.Exit(2) + } +} diff --git a/agent/cmd/prom2spacetime/BUILD b/agent/cmd/prom2spacetime/BUILD new file mode 100644 index 0000000..c2d91c6 --- /dev/null +++ b/agent/cmd/prom2spacetime/BUILD @@ -0,0 +1,38 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "prom2spacetime_lib", + srcs = ["prom2spacetime.go"], + importpath = "aalyria.com/spacetime/agent/cmd/prom2spacetime", + deps = [ + "//agent/telemetry/prometheus", + "//api/common:common_go_proto", + "@org_golang_google_protobuf//encoding/protojson", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//encoding/protowire", + "@org_golang_google_protobuf//proto", + ], +) + +go_binary( + name = "prom2spacetime", + embed = [":prom2spacetime_lib"], + pure = "on", + static = "on", +) diff --git a/agent/cmd/prom2spacetime/prom2spacetime.go b/agent/cmd/prom2spacetime/prom2spacetime.go new file mode 100644 index 0000000..6b2679c --- /dev/null +++ b/agent/cmd/prom2spacetime/prom2spacetime.go @@ -0,0 +1,109 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "os" + "time" + + "aalyria.com/spacetime/agent/telemetry/prometheus" + apipb "aalyria.com/spacetime/api/common" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/encoding/protowire" + "google.golang.org/protobuf/proto" +) + +func writeProtoAsText(w io.Writer, m proto.Message) error { + _, err := w.Write(append([]byte(prototext.Format(m)), []byte{'\r', '\n'}...)) + return err +} + +func writeProtoAsJSON(w io.Writer, m proto.Message) error { + _, err := w.Write(append([]byte(protojson.MarshalOptions{Indent: " ", EmitDefaultValues: true}.Format(m)), []byte{'\r', '\n'}...)) + return err +} + +func writeProtoAsWire(w io.Writer, m proto.Message) error { + // TODO: replace with the encoding/protodelim package once a new + // version of the protobuf package gets cut + // https://github.com/protocolbuffers/protobuf-go/commit/fb0abd915897428ccfdd6b03b48ad8219751ee54 + msgBytes, err := proto.Marshal(m) + if err != nil { + return err + } + + sizeBytes := protowire.AppendVarint(nil, uint64(len(msgBytes))) + if _, err = w.Write(msgBytes); err != nil { + return err + } + if _, err = w.Write(sizeBytes); err != nil { + return err + } + return nil +} + +func run(ctx context.Context) error { + fs := flag.NewFlagSet("prom2spacetime", flag.ContinueOnError) + var ( + reportFormat = fs.String("format", "text", "The format to print the NetworkStatsReport in (text, wire, or json)") + exporterURL = fs.String("exporter-url", "", "The full URL of the prometheus exporter's metrics page (typically /metrics)") + nodeID = fs.String("node-id", "", "The node ID to use in the NetworkStatsReport") + scrapeInterval = fs.Duration("scrape-interval", 10*time.Second, "The frequency in which NetworkStatsReports are generated") + ) + + if err := fs.Parse(os.Args[1:]); err != nil { + return err + } + if *exporterURL == "" { + return errors.New("missing exporter-url") + } + if *nodeID == "" { + return errors.New("missing node-id") + } + var writeTo func(io.Writer, proto.Message) error + switch rf := *reportFormat; rf { + case "text": + writeTo = writeProtoAsText + case "json": + writeTo = writeProtoAsJSON + case "wire": + writeTo = writeProtoAsWire + default: + return fmt.Errorf("unknown format: %q", rf) + } + + return prometheus.NewScraper(prometheus.ScraperConfig{ + ExporterURL: *exporterURL, + NodeID: *nodeID, + ScrapeInterval: *scrapeInterval, + Callback: func(r *apipb.NetworkStatsReport) error { + return writeTo(os.Stdout, r) + }, + }).Start(ctx) +} + +func main() { + if err := run(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "fatal error: %s\n", err) + os.Exit(2) + } +} diff --git a/agent/common_test.go b/agent/common_test.go new file mode 100644 index 0000000..864c48f --- /dev/null +++ b/agent/common_test.go @@ -0,0 +1,66 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/rs/zerolog" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/testing/protocmp" +) + +func baseContext(t *testing.T) context.Context { + log := zerolog.New(zerolog.NewTestWriter(t)).With().Timestamp().Stack().Caller().Logger() + return log.WithContext(context.Background()) +} + +func newAgent(t *testing.T, opts ...AgentOption) *Agent { + t.Helper() + + a, err := NewAgent(opts...) + if err != nil { + t.Fatalf("error creating agent: %s", err) + } + return a +} + +func check(t *testing.T, err error) { + t.Helper() + + if err != nil { + t.Fatal(err) + } +} + +func assertProtosEqual(t *testing.T, want, got interface{}) { + t.Helper() + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("proto mismatch: (-want +got):\n%s", diff) + t.FailNow() + } +} + +var rpcCanceledError = status.FromContextError(context.Canceled).Err() + +func checkErrIsDueToCanceledContext(t *testing.T, err error) { + if !errors.Is(err, context.Canceled) && !errors.Is(err, rpcCanceledError) { + t.Error("unexpected error:", err) + } +} diff --git a/agent/enactment/BUILD b/agent/enactment/BUILD new file mode 100644 index 0000000..5ab2990 --- /dev/null +++ b/agent/enactment/BUILD @@ -0,0 +1,24 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "enactment", + srcs = ["enactment.go"], + importpath = "aalyria.com/spacetime/agent/enactment", + deps = ["//api/scheduling/v1alpha:scheduling_go_grpc"], +) diff --git a/agent/enactment/enactment.go b/agent/enactment/enactment.go new file mode 100644 index 0000000..4ec8784 --- /dev/null +++ b/agent/enactment/enactment.go @@ -0,0 +1,38 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enactment + +import ( + "context" + + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" +) + +// Driver is the component that takes a ScheduledControlUpdate message for a +// given node and returns the new state for that node. If the error returned +// implements the gRPCStatus interface, the appropriate status will be used. +type Driver interface { + // Init initializes the driver. + Init(context.Context) error + // Dispatch handles the provided [schedpb.CreateEntryRequest]. Drivers are + // expected to handle retries. + Dispatch(context.Context, *schedpb.CreateEntryRequest) error + // Stats returns internal statistics for the driver in an unstructured + // form. The results are exposed as a JSON endpoint via the pprof server, + // if it's configured. + Stats() any + // Close closes the driver. Reusing a closed driver is a fatal error. + Close() error +} diff --git a/agent/enactment/extproc/BUILD b/agent/enactment/extproc/BUILD new file mode 100644 index 0000000..a515c91 --- /dev/null +++ b/agent/enactment/extproc/BUILD @@ -0,0 +1,40 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "extproc", + srcs = ["extproc.go"], + importpath = "aalyria.com/spacetime/agent/enactment/extproc", + deps = [ + "//agent/enactment", + "//agent/internal/extprocs", + "//agent/internal/protofmt", + "//api/scheduling/v1alpha:scheduling_go_grpc", + "@com_github_rs_zerolog//:zerolog", + ], +) + +go_test( + name = "extproc_test", + srcs = ["extproc_test.go"], + embed = [":extproc"], + deps = [ + "//agent/internal/protofmt", + "//api/scheduling/v1alpha:scheduling_go_grpc", + ], +) diff --git a/agent/enactment/extproc/extproc.go b/agent/enactment/extproc/extproc.go new file mode 100644 index 0000000..d90226f --- /dev/null +++ b/agent/enactment/extproc/extproc.go @@ -0,0 +1,79 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package extproc provides an enactment.Backend implementation that relies on +// an external process to enact changes. +// +// It works by piping incoming change requests to an external process and +// reading the new state from the process's stdout. Requests are +// single-threaded per-node, but if the backend is registered for multiple +// nodes there may be multiple processes invoked in parallel. If the process +// returns with a non-zero exit code within the range of GRPC status codes +// (https://pkg.go.dev/google.golang.org/grpc/codes#Code) then the appropriate +// status will be returned, otherwise the errors will be translated into a +// generic Unknown status (Code = 2). +package extproc + +import ( + "bytes" + "context" + "fmt" + "os/exec" + + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" + "aalyria.com/spacetime/agent/enactment" + "aalyria.com/spacetime/agent/internal/extprocs" + "aalyria.com/spacetime/agent/internal/protofmt" + "github.com/rs/zerolog" +) + +type driver struct { + args []string + protoFmt protofmt.Format +} + +func New(args []string, format protofmt.Format) enactment.Driver { + return &driver{args: args, protoFmt: format} +} + +func (ed *driver) Close() error { return nil } +func (ed *driver) Init(context.Context) error { return nil } +func (ed *driver) Stats() any { + return struct { + Type string + Args []string + Format string + }{ + Type: fmt.Sprintf("%T", ed), + Args: ed.args, + Format: ed.protoFmt.String(), + } +} + +func (ed *driver) Dispatch(ctx context.Context, req *schedpb.CreateEntryRequest) error { + log := zerolog.Ctx(ctx).With().Str("driver", "extproc").Logger() + + js, err := ed.protoFmt.Marshal(req) + if err != nil { + return fmt.Errorf("marshalling proto as %s: %w", ed.protoFmt, err) + } + log.Trace().Strs("args", ed.args).Msg("running enactment command") + cmd := exec.CommandContext(ctx, ed.args[0], ed.args[1:]...) + cmd.Stdin = bytes.NewBuffer(js) + + if err := cmd.Run(); err != nil { + return extprocs.CommandError(err) + } + return nil +} diff --git a/agent/enactment/extproc/extproc_test.go b/agent/enactment/extproc/extproc_test.go new file mode 100644 index 0000000..d59448c --- /dev/null +++ b/agent/enactment/extproc/extproc_test.go @@ -0,0 +1,36 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extproc + +import ( + "context" + "testing" + + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" + "aalyria.com/spacetime/agent/internal/protofmt" +) + +func TestDispatchEmptyOutput(t *testing.T) { + for _, format := range []protofmt.Format{protofmt.JSON, protofmt.Wire, protofmt.Text} { + t.Run(format.String(), func(t *testing.T) { + eb := New([]string{"/bin/true"}, format) + + if err := eb.Dispatch(context.Background(), &schedpb.CreateEntryRequest{}); err != nil { + t.Errorf("unexpected error from /bin/true command: %v", err) + return + } + }) + } +} diff --git a/agent/enactment/netlink/BUILD b/agent/enactment/netlink/BUILD new file mode 100644 index 0000000..7202c58 --- /dev/null +++ b/agent/enactment/netlink/BUILD @@ -0,0 +1,50 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# @com_github_vishvananda_netlink//:netlink + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "netlink", + srcs = [ + "errors.go", + "netlink.go", + ], + importpath = "aalyria.com/spacetime/agent/enactment/netlink", + deps = [ + "//api/scheduling/v1alpha:scheduling_go_grpc", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + "@com_github_vishvananda_netlink//:netlink", + "@org_golang_google_protobuf//proto", + "@org_golang_x_sys//unix", + ], +) + +go_test( + name = "netlink_test", + srcs = ["netlink_test.go"], + embed = [":netlink"], + deps = [ + "//api/common:common_go_proto", + "//api/scheduling/v1alpha:scheduling_go_grpc", + "@com_github_google_go_cmp//cmp", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_vishvananda_netlink//:netlink", + "@org_golang_google_protobuf//testing/protocmp", + ], +) diff --git a/agent/enactment/netlink/container_test/BUILD b/agent/enactment/netlink/container_test/BUILD new file mode 100644 index 0000000..5b55217 --- /dev/null +++ b/agent/enactment/netlink/container_test/BUILD @@ -0,0 +1,70 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# @com_github_vishvananda_netlink//:netlink + +load("@container_structure_test//:defs.bzl", "container_structure_test") +load("@rules_go//go:def.bzl", "go_binary", "go_library") +load("@rules_oci//oci:defs.bzl", "oci_image") +load("@rules_pkg//:pkg.bzl", "pkg_tar") + +package(default_visibility = ["//visibility:public"]) + +go_binary( + name = "netlink_exercise", + embed = [":netlink_exercise_lib"], + pure = "on", + static = "on", +) + +go_library( + name = "netlink_exercise_lib", + srcs = ["netlink_exercise.go"], + importpath = "aalyria.com/spacetime/agent/enactment/netlink/container_test", + deps = [ + "//agent/enactment/netlink", + "//api/scheduling/v1alpha:scheduling_go_grpc", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_vishvananda_netlink//:netlink", + ], +) + +pkg_tar( + name = "netlink_exercise_tar", + srcs = [ + ":debug.sh", + ":netlink_exercise", + ], +) + +oci_image( + name = "netlink_exercise_image", + base = "@alpine_base", + tars = [":netlink_exercise_tar"], + user = "root", +) + +# To make this work in cloudtop (due to docker socket permissions errors) I had to: +# sudo usermod -aG docker $USER +# reboot cloudtop (thanks Ciaran!) +container_structure_test( + name = "netlink_exercise_tests", + configs = [":netlink_exercise_tests.yaml"], + driver = "docker", + image = ":netlink_exercise_image", + tags = [ + "manual", + "no_ci_pipeline", + ], +) diff --git a/agent/enactment/netlink/container_test/debug.sh b/agent/enactment/netlink/container_test/debug.sh new file mode 100644 index 0000000..ddd5519 --- /dev/null +++ b/agent/enactment/netlink/container_test/debug.sh @@ -0,0 +1,23 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/bin/sh + +ip link add veth1 type veth peer name veth2 +ip addr add 192.168.1.1/24 dev veth0 +ip addr add 192.168.1.2/24 dev veth1 +ip link set veth0 up +ip link set veth1 up +ip link show +ip addr show \ No newline at end of file diff --git a/agent/enactment/netlink/container_test/netlink_exercise.go b/agent/enactment/netlink/container_test/netlink_exercise.go new file mode 100644 index 0000000..e1de13f --- /dev/null +++ b/agent/enactment/netlink/container_test/netlink_exercise.go @@ -0,0 +1,344 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/jonboulle/clockwork" + vnl "github.com/vishvananda/netlink" + + "aalyria.com/spacetime/agent/enactment/netlink" + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" +) + +const ( + rtTableID = 252 + rtTableLookupPriority = 25200 +) + +// setupInterface creates a dummy interface in the containter environment +// which can be used to simulate the behaviour of real Linux network interfaces +func setupInterface(nlHandle *vnl.Handle, ifaceIndex int, ifaceName string, ifaceIP string) { + // Generate synthetic link environment for test + link := vnl.Dummy{LinkAttrs: vnl.LinkAttrs{Index: ifaceIndex, Name: ifaceName}} + err := nlHandle.LinkAdd(&link) + if err != nil { + log.Fatalf("nlHandle.LinkAdd(Dummy(%s)) failed with: %s", ifaceName, err) + } + addr, err := vnl.ParseAddr(ifaceIP) + if err != nil { + log.Fatalf("vnl.ParseAddr(%s) failure: %s", ifaceIP, err) + } + err = nlHandle.AddrAdd(&link, addr) + if err != nil { + log.Fatalf("nlHandle.AddrAdd(Dummy(%s), addr) failed: %s", ifaceName, err) + } + + err = nlHandle.LinkSetUp(&link) + if err != nil { + log.Fatalf("nlHandle.LinkSetUp(Dummy(%s)) failed: %s", ifaceName, err) + } +} + +// prepopulateSpacetimeTableWithRoutes enters two "potentially conflicting routes to the route table: , +// which are to be deleted by the netlink enactment backend before it starts adding routes to that table +func prepopulateSpacetimeTableWithRoutes(tableID int) { + prepopRoutes := []string{"111.222.111.222/32", "222.111.222.111/32"} + + fmt.Printf("Attempting to prepopulate table %v with routes: %v\n", tableID, prepopRoutes) + for _, dest := range prepopRoutes { + cmd := exec.Command("ip", "route", "add", dest, "via", "192.168.200.1", "dev", "ens224", "table", strconv.Itoa(tableID)) + _, err := cmd.Output() + if err != nil { + log.Fatalf("Error prepopRoutes in table %v for destination %v with error: %v\n", tableID, dest, err) + } + + } + + fmt.Printf("Successfully prepopulated table %v with routes %v\n", tableID, prepopRoutes) +} + +// checkNetlinkIpRulePriorityJumpsToTable checks that the netlink enactmnent +// backedn config installed the expected ip rule at expected priority. The +// test YAML checks the output for a jump to a known table (ick). +// +// N.B.: rather than run "ip -X rule show priority NNN" this code walks the +// output of "ip -X rule show" looking for a line that begins with "NNN:". +// This is due to limitations with the stripped down /sbin/ip command in the +// container image use for this test. +// +// Checks both IPv4 and IPv6 rules. +func checkNetlinkIpRulePriorityJumpsToTable(priority int) { + ipVersions := []string{"-4", "-6"} + + for _, ipVersion := range ipVersions { + cmd := exec.Command("ip", ipVersion, "rule", "show") + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("Error running '%v': %v (%v)\n", cmd.String(), strings.TrimSpace(err.Error()), string(output)) + } else { + linePrefix := fmt.Sprintf("%v:", strconv.Itoa(priority)) + for _, line := range strings.Split(string(output), "\n") { + if strings.HasPrefix(line, linePrefix) { + fmt.Printf("ip %v rule: %v\n", ipVersion, line) + } + } + } + } +} + +// formatFlowState implements a consistent string format for +// ControlPlaneState messages contain only a FlowState field. +func formatDriverState(d *netlink.Driver) string { + data, err := json.Marshal(d.Stats()) + if err != nil { + panic(fmt.Errorf("failed to marshal driver state as JSON: %w", err)) + } + return string(data) +} + +// printTestState outputs a string in format to be consumed by the test runner +// and compared against expected output in the YAML test definition file. +func printTestState(testName string, eb *netlink.Driver, err error) { + fmt.Printf("%v %v, err: %v\n", testName, formatDriverState(eb), err) +} + +func main() { + nlHandle, err := vnl.NewHandle(vnl.FAMILY_ALL) + if err != nil { + log.Fatalf("netlink.NewHandle(\"test_namespace\", vnl.FAMILY_V4) failed with: %s", err) + } + + setupInterface(nlHandle, 1000, "ens161", "192.168.100.2/24") // setting up the OneWeb control plane interface + setupInterface(nlHandle, 1001, "ens224", "192.168.200.2/24") // setting up the OneWeb SD-One data plane interface + setupInterface(nlHandle, 1002, "ens256", "192.168.1.2/24") // setting up the Viasat interface + setupInterface(nlHandle, 1003, "ens160", "172.16.51.1/24") // setting up the ground mgmt interface + + ctx := context.Background() + config := netlink.DefaultConfig(ctx, nlHandle, rtTableID, rtTableLookupPriority) + config.Clock = clockwork.NewFakeClockAt(time.Date(1981, time.November, 0, 0, 0, 0, 0, time.UTC)) + + prepopulateSpacetimeTableWithRoutes(rtTableID) + + eb := netlink.New(config) + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + if err := eb.Init(ctx); err != nil { + log.Fatal("Failed to invoke Init() for enactment backend") + } + + checkNetlinkIpRulePriorityJumpsToTable(rtTableLookupPriority) + + testName := "TST1" + fmt.Printf("\nImplement zulu1 FlowRuleID for destination 104.198.75.23\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + Dev: "ens224", + Via: "192.168.200.1", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST2" + fmt.Printf("\nImplement zulu2 FlowRuleID for destination 104.198.75.23. Zulu1 already implements the route so zulu2 FlowRuleID is just cached\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu2", + Seqno: 2, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + Dev: "ens224", + Via: "192.168.200.1", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST3" + fmt.Printf("\nManually delete the route to 104.198.75.23 designated by zulu1 and zulu2\n\n") + cmd := exec.Command("ip", "route", "del", "104.198.75.23", "table", strconv.Itoa(rtTableID)) + + output, err := cmd.Output() + if err != nil { + fmt.Println("Error:", err) + return + } + + fmt.Printf("\nImplement zulu3 FlowRuleID for destination 104.198.75.23. Without syncing, agent would think this is already implemented, and the route ADD would be skipped, leaving us without a route\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu3", + Seqno: 3, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + Dev: "ens224", + Via: "192.168.200.1", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST4" + fmt.Printf("\nImplement dbvr2 FlowRuleID to destination 34.135.90.47 (different route to zuluX)\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "dvbr2", + Seqno: 4, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "216.58.201.110", + To: "34.135.90.47/32", + Dev: "ens224", + Via: "192.168.200.1", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST5" + fmt.Printf("\nAttempt to delete FlowRuleID zulu1 (non-existent currently)\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu1", + Seqno: 5, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "216.58.201.110", + To: "34.135.90.47/32", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST6" + fmt.Printf("\nSpacetime reprovisions zulu1. Because zulu3 already implemented the route, zulu1's ADD is skipped but the FlowRuleID is cached") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu1", + Seqno: 6, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + Dev: "ens224", + Via: "192.168.200.1", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST7" + fmt.Printf("\nSpacetime reprovisions zulu2. Because zulu1 and zulu3 already implement the route, zulu2's ADD is skipped but the FlowRuleID is cached") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu2", + Seqno: 7, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + Dev: "ens224", + Via: "192.168.200.1", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST8" + fmt.Printf("\nSpacetime provisions DELETE for zulu1. Because zulu2 and zulu3 still represent the route, the actual route is kept\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu1", + Seqno: 8, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST9" + fmt.Printf("\nSpacetime provisions DELETE for zulu2. Because zulu3 still represents the route, the actual route is kept\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu2", + Seqno: 9, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + }, + }, + }) + + printTestState(testName, eb, err) + + testName = "TST10" + fmt.Printf("\nSpacetime provisions DELETE for zulu3. Since this is the last FlowRuleID representing the route to destination 104.198.75.23, the route too is deleted\n") + + err = eb.Dispatch(ctx, &schedpb.CreateEntryRequest{ + Id: "zulu3", + Seqno: 10, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "216.58.201.110", + To: "104.198.75.23/32", + }, + }, + }) + + printTestState(testName, eb, err) + + cmd = exec.Command("ip", "route", "show", "table", strconv.Itoa(rtTableID)) + + output, err = cmd.Output() + if err != nil { + fmt.Println("Error:", err) + return + } + + fmt.Printf("Last implemented route is FlowRuleID dbvr2 to destination 34.135.90.47\n%v", string(output)) +} diff --git a/agent/enactment/netlink/container_test/netlink_exercise_tests.yaml b/agent/enactment/netlink/container_test/netlink_exercise_tests.yaml new file mode 100644 index 0000000..6fe93b7 --- /dev/null +++ b/agent/enactment/netlink/container_test/netlink_exercise_tests.yaml @@ -0,0 +1,50 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +schemaVersion: "2.0.0" + +containerRunOptions: + user: "root" # set the --user/-u flag + privileged: true # set the --privileged flag (default: false) + # capabilities: # Add list of Linux capabilities (--cap-add) + # - NET_BIND_SERVICE + +globalEnvVars: + - key: "VIRTUAL_ENV" + value: "/env" + - key: "PATH" + value: "/env/bin:$PATH" + +commandTests: + - name: "netlink exercise" + setup: [ + ["apk", "add", "gcompat"], + ["ln", "-sf", "/lib/libgcompat.so.0", "/lib/libresolv.so.2"], + ] + command: "/netlink_exercise" + #args: ["add", "libcap"] + expectedOutput: + - "\\bip -4 rule: 25200:\\s*from all lookup 252\\b" + - "\\bip -6 rule: 25200:\\s*from all lookup 252\\b" + - "TST1 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"zulu1\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":1,\"RoutesDeletedCount\":0}, err: \n" + - "TST2 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"zulu1\",\"zulu2\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":2,\"RoutesDeletedCount\":0}, err: \n" + - "TST3 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"zulu1\",\"zulu2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":3,\"RoutesDeletedCount\":0}, err: \n" + - "TST4 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\",\"zulu1\",\"zulu2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":4,\"RoutesDeletedCount\":0}, err: \n" + - "TST5 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\",\"zulu1\",\"zulu2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":4,\"RoutesDeletedCount\":1}, err: attempted to DELETE unknown route: zulu1\n" + - "TST6 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\",\"zulu1\",\"zulu2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":5,\"RoutesDeletedCount\":1}, err: \n" + - "TST7 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\",\"zulu1\",\"zulu2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":6,\"RoutesDeletedCount\":1}, err: \n" + - "TST8 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\",\"zulu2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":6,\"RoutesDeletedCount\":2}, err: \n" + - "TST9 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\",\"zulu3\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":6,\"RoutesDeletedCount\":3}, err: \n" + - "TST10 {\"InitCount\":1,\"LastError\":\"\",\"InstalledRoutes\":\\[\"dvbr2\"\\],\"EnactmentFailureCount\":0,\"RoutesSetCount\":6,\"RoutesDeletedCount\":4}, err: \n" + - "\\b34.135.90.47 via 192.168.200.1 dev ens224 metric -373334401\\b" diff --git a/agent/enactment/netlink/errors.go b/agent/enactment/netlink/errors.go new file mode 100644 index 0000000..b06afb0 --- /dev/null +++ b/agent/enactment/netlink/errors.go @@ -0,0 +1,144 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netlink + +import ( + "fmt" + + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" + "google.golang.org/protobuf/proto" +) + +// NoChangeSpecifiedError indicates that there is no Change supplied in the apipb.SCU +type NoChangeSpecifiedError struct { + req *schedpb.CreateEntryRequest +} + +func (e *NoChangeSpecifiedError) Error() string { + return fmt.Sprintf("CreateEntryRequest received with no ConfigurationChange specified: %v", e.req) +} + +func (e *NoChangeSpecifiedError) Is(err error) bool { + if typedErr, ok := err.(*NoChangeSpecifiedError); ok { + return proto.Equal(typedErr.req, e.req) + } + return false +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +// UnsupportedUpdateError indicates the UpdateType specified in the apipb.SCU is unsupported +type UnsupportedUpdateError struct { + req *schedpb.CreateEntryRequest +} + +func (e *UnsupportedUpdateError) Error() string { + return fmt.Sprintf( + "unsupported update type %T on update id %s", + e.req.GetConfigurationChange(), e.req.GetId()) +} + +func (e *UnsupportedUpdateError) Is(err error) bool { + if typedErr, ok := err.(*UnsupportedUpdateError); ok { + return proto.Equal(e.req, typedErr.req) + } + return false +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +// UnknownRouteDeleteError indicates an unknown FlowRule is attempted to be deleted +type UnknownRouteDeleteError struct { + changeID string +} + +func (e *UnknownRouteDeleteError) Error() string { + return fmt.Sprintf("attempted to DELETE unknown route: %s", e.changeID) +} + +func (e *UnknownRouteDeleteError) Is(err error) bool { + if typedErr, ok := err.(*UnknownRouteDeleteError); ok { + return typedErr.changeID == e.changeID + } + return false +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +// UnrecognizedRouteUpdateOperationError indicates an unrecognized FlowUpdate Operation is attempted to be applied +type UnrecognizedRouteUpdateOperationError struct { + operation *schedpb.CreateEntryRequest +} + +func (e *UnrecognizedRouteUpdateOperationError) Error() string { + return fmt.Sprintf("attempted unrecognized CreateEntryRequest operation (%s)", e.operation) +} + +func (e *UnrecognizedRouteUpdateOperationError) Is(err error) bool { + if typedErr, ok := err.(*UnrecognizedRouteUpdateOperationError); ok { + return e.operation == typedErr.operation + } + return false +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +// IPv4FormattingError indicates an erroneously formatted IPv4 address was passed +type IPv4Entry string + +const ( + SrcIpRange_Ip IPv4Entry = "SrcIpRange" + DstIpRange_Ip IPv4Entry = "DstIpRange" + NextHopIp_Ip IPv4Entry = "NextHopIp" +) + +type IPv4FormattingError struct { + ipv4 string + sourceField IPv4Entry +} + +func (e IPv4FormattingError) Error() string { + return fmt.Sprintf("attempted using wrongly formatted IPv4 address/range (%s) for %s field", e.ipv4, e.sourceField) +} + +func (e IPv4FormattingError) Is(err error) bool { + if typedErr, ok := err.(IPv4FormattingError); ok { + return typedErr.sourceField == e.sourceField && typedErr.ipv4 == e.ipv4 + } + return false +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +// OutInterfaceIdxError indicates an erroneously supplied outbound network interface +type OutInterfaceIdxError struct { + wrongIface string + sourceError error +} + +func (e OutInterfaceIdxError) Error() string { + return fmt.Sprintf("attempted using erroneous interface (%s): %v", e.wrongIface, e.sourceError) +} + +func (e OutInterfaceIdxError) Unwrap() error { + return e.sourceError +} + +func (e OutInterfaceIdxError) Is(err error) bool { + if typedErr, ok := err.(OutInterfaceIdxError); ok { + return typedErr.wrongIface == e.wrongIface + } + return false +} diff --git a/agent/enactment/netlink/netlink.go b/agent/enactment/netlink/netlink.go new file mode 100644 index 0000000..f50b447 --- /dev/null +++ b/agent/enactment/netlink/netlink.go @@ -0,0 +1,498 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netlink + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "slices" + "sync" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + vnl "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" + + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" +) + +const ( + defaultRtTableID = 252 // a5a + defaultRtTableLookupPriority = 25200 // a5a * 100 +) + +type routeToRuleID struct { + route vnl.Route + ruleIDs map[string]*schedpb.SetRoute +} + +type Driver struct { + // mu protects the backend's map fields from concurrent mutation. + mu *sync.Mutex + + // routesToRuleIDs is a list-formatted mapping of route keys to a set of + // rule ID strings. For every controlled route, there will be an entry in + // this list. It will have a non-empty list of ruleIDs associated with it. + // The entire structure and all its contents are protected by the + // [backend.mu]. + routesToRuleIDs []*routeToRuleID + + config Config + + stats *exportedStats +} + +type exportedStats struct { + InitCount int + LastError string + InstalledRoutes []string + EnactmentFailureCount int + RoutesSetCount, RoutesDeletedCount int +} + +// Config provides configuration and dependency injection parameters for backend +type Config struct { + // Clock to support repeatable unit or container testing + Clock clockwork.Clock + + // The route table number in which destination routes will be managed. + RtTableID int + + // The Linux PBR priority to use for the lookup into |RtTableID|. + RtTableLookupPriority int + + // GetLinkIDByName returns the ID of the provided interface. + GetLinkIDByName func(interfaceID string) (int, error) + + // RouteList fetches a list of installed routes. + // TODO: Evaluate whether this is still needed + RouteList func() ([]vnl.Route, error) + + // RouteListFiltered fetches a list of installed routes matching a filter. + RouteListFiltered func(int, *vnl.Route, uint64) ([]vnl.Route, error) + + // RouteAdd inserts new routes. + RouteAdd func(*vnl.Route) error + + // RouteDel removes the provided route. + RouteDel func(*vnl.Route) error + + // RuleAdd is called during the backend Init() process. + RuleAdd func(*vnl.Rule) error +} + +// DefaultConfig generates a nominal Config for New(). +// Pass in a Netlink *Handle with the specified namespace, like so: +// nlHandle, err := vnl.NewHandle(vnl.FAMILY_V4) +// config := DefaultConfig(nlHandle) +func DefaultConfig(ctx context.Context, nlHandle *vnl.Handle, rtTableID int, rtTableLookupPriority int) Config { + log := zerolog.Ctx(ctx).With().Str("backend", "netlink").Logger() + + if rtTableID <= 0 { + rtTableID = defaultRtTableID + } + if rtTableLookupPriority <= 0 { + rtTableLookupPriority = defaultRtTableLookupPriority + } + + return Config{ + Clock: clockwork.NewRealClock(), + + RtTableID: rtTableID, + + RtTableLookupPriority: rtTableLookupPriority, + + GetLinkIDByName: func(interfaceID string) (n int, err error) { + defer func() { log.Debug().Msgf("GetLinkIDByName(%s) returned (%d, %v)", interfaceID, n, err) }() + + link, err := nlHandle.LinkByName(interfaceID) + if err != nil { + return 0, fmt.Errorf("failed GetLinkIDByName(%s): %w", interfaceID, err) + } + return link.Attrs().Index, nil + }, + + // TODO: Evaluate whether this is still needed + RouteList: func() (routes []vnl.Route, err error) { + defer func() { log.Debug().Msgf("RouteList() returned (%v, %v)", routes, err) }() + + // TODO: FAMILY_ALL. + routes, err = nlHandle.RouteList(nil, vnl.FAMILY_V4) + if err != nil { + return nil, fmt.Errorf("failed RouteList(): %w)", err) + } + return routes, nil + }, + + RouteListFiltered: func(family int, filter *vnl.Route, filterMask uint64) (routes []vnl.Route, err error) { + defer func() { log.Debug().Msgf("RouteListFiltered() returned (%v, %v)", routes, err) }() + + // TODO: FAMILY_ALL. + routes, err = nlHandle.RouteListFiltered(family, filter, filterMask) + if err != nil { + return nil, fmt.Errorf("failed RouteList(): %w)", err) + } + return routes, nil + }, + + RouteAdd: func(route *vnl.Route) (err error) { + defer func() { log.Debug().Msgf("RouteAdd(%+v) returned %v", route, err) }() + + return nlHandle.RouteAdd(route) + }, + + RouteDel: func(route *vnl.Route) (err error) { + defer func() { log.Debug().Msgf("RouteDel(%+v) returned %v", route, err) }() + + return nlHandle.RouteDel(route) + }, + + RuleAdd: func(rule *vnl.Rule) error { + err := nlHandle.RuleAdd(rule) + if err != nil { + return fmt.Errorf("failed RuleAdd(%v): %w)", rule.String(), err) + } + return nil + }, + } +} + +// routeListFilteredByTableID is a helper function to return all routes from table +func (b *Driver) routeListFilteredByTableID() ([]vnl.Route, error) { + ret, err := b.config.RouteListFiltered(vnl.FAMILY_V4, &vnl.Route{Table: b.config.RtTableID}, vnl.RT_FILTER_TABLE) + return ret, err +} + +// flushExistingRoutesInSpacetimeTable deletes all routes located in the Spacetime route table +// TODO: Should we returns errors here? +func (b *Driver) flushExistingRoutesInSpacetimeTable() error { + implRoutes, err := b.routeListFilteredByTableID() + if err != nil { + return err + } + + errs := []error{} + for _, route := range implRoutes { + err := b.config.RouteDel(&route) + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +// New is a constructor function which allows you to supply the Config as well +// as a map of any already implemented routes. Before it starts managing +// routes, it flushes the route table which is dedicated to +// Spacetime activities +func New(config Config) *Driver { + return &Driver{ + mu: &sync.Mutex{}, + routesToRuleIDs: []*routeToRuleID{}, + config: config, + stats: &exportedStats{}, + } +} + +func (b *Driver) Close() error { return nil } + +func (b *Driver) Init(ctx context.Context) error { + b.mu.Lock() + defer b.mu.Unlock() + + b.stats.InitCount++ + + if err := b.flushExistingRoutesInSpacetimeTable(); err != nil { + return fmt.Errorf("flushExistingRoutesInSpacetimeTable: %w", err) + } + + for _, family := range []int{vnl.FAMILY_V4, vnl.FAMILY_V6} { + rule := vnl.NewRule() + rule.Priority = b.config.RtTableLookupPriority + rule.Family = family + rule.Table = b.config.RtTableID + // TODO: EEXISTS is okay, if the existing rule is the same. + if err := b.config.RuleAdd(rule); err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msgf("RuleAdd failed (do not be [too] alarmed by EEXIST)") + } + } + + b.stats.LastError = "" + b.stats.InstalledRoutes = []string{} + b.stats.EnactmentFailureCount = 0 + b.stats.RoutesSetCount = 0 + b.stats.RoutesDeletedCount = 0 + + return nil +} + +func (b *Driver) Stats() interface{} { + b.mu.Lock() + defer b.mu.Unlock() + + return b.stats +} + +// isRouteUpdate validates that the CreateEntryRequest.ConfigurationChange is +// of the supported type (SetRoute | DeleteRoute). +func isRouteUpdate(req *schedpb.CreateEntryRequest) error { + switch t := req.GetConfigurationChange().(type) { + case *schedpb.CreateEntryRequest_DeleteRoute, *schedpb.CreateEntryRequest_SetRoute: + return nil + + default: + // The update is not for routes + return fmt.Errorf("unimplemented CreateEntryRequest.ConfigurationChange variant: %T", t) + } +} + +func (b *Driver) buildNetlinkRoute(cc *schedpb.SetRoute) (*vnl.Route, error) { + var protocol uint32 = 0 + + outIfaceIdx, err := b.config.GetLinkIDByName(cc.Dev) + if err != nil { + return nil, err + } + _, dst, err := net.ParseCIDR(cc.To) + if err != nil || dst.IP.To4() == nil { + return nil, &IPv4FormattingError{ipv4: cc.To, sourceField: DstIpRange_Ip} + } + nextHopIP := net.ParseIP(cc.Via) + if nextHopIP == nil { + return nil, &IPv4FormattingError{ipv4: cc.Via, sourceField: DstIpRange_Ip} + } + + return &vnl.Route{ + LinkIndex: outIfaceIdx, + // ILinkIndex: Unused, + Scope: vnl.SCOPE_UNIVERSE, + Dst: dst, + // Src: src, + Gw: nextHopIP, + // Multipath: Unused, + Protocol: vnl.RouteProtocol(protocol), + // Priority: Unused, + Family: unix.AF_INET, // IPv4 + Table: b.config.RtTableID, + Type: unix.RTN_UNICAST, + // Tos: Unused, + // Flags: Unused, + // MPLSDst: Unused, + // NewDst: Unused, + // Encap: Unused, + // Via: Unused, + // Realm: Unused, + // MTU: Unused, + // Window: Unused, + // Rtt: Unused, + // Remaining are TCP related and will be omitted for brevity + }, nil +} + +// syncCachedRoutes is invoked at the beginning of every operation in order to align the in-memory cache with the real +// state of the Spacetime routing table on the host. In cases where a Spacetime-managed route has been deleted by a party +// or process other than Spacetime, this is caught here and the in-memory cache is updated to reflect it. This updated +// representation of the underlying host is eventually returned to Spacetime, which reprovisions the missing routes if +// neccessary +func (b *Driver) syncCachedRoutes(log zerolog.Logger) error { + b.mu.Lock() + defer b.mu.Unlock() + + const filterMask = vnl.RT_FILTER_TABLE + vnl.RT_FILTER_DST + vnl.RT_FILTER_GW + vnl.RT_FILTER_OIF + + errs := []error{} + b.routesToRuleIDs = slices.DeleteFunc(b.routesToRuleIDs, func(rt *routeToRuleID) bool { + matchingRoutes, err := b.config.RouteListFiltered(vnl.FAMILY_V4, &rt.route, filterMask) + if err != nil { + errs = append(errs, fmt.Errorf("syncCachedRoutes() failed fetching installed routes: %w", err)) + return false + } + + switch len(matchingRoutes) { + case 0: + log.Warn().Msgf("syncCachedRoutes() found a cached route which isn't implemented in Host: %v", rt.route) + return true + case 1: + // Expectation is that there will be at most one route with this + // signature. If we get here we're good + return false + default: + errs = append(errs, fmt.Errorf("syncCachedRoutes() returned an unexpected number of routes (%d) matching route (%v)", len(matchingRoutes), rt.route)) + return false + } + }) + return errors.Join(errs...) +} + +func (b *Driver) addRoute(route *vnl.Route) error { + if err := b.config.RouteAdd(route); err != nil { + return fmt.Errorf("RouteAdd(%v): %w", route, err) + } + return nil +} + +func routeEqual(routeA, routeB *vnl.Route) bool { + return (routeA.Dst == routeB.Dst && routeA.Gw.Equal(routeB.Gw) && routeA.LinkIndex == routeB.LinkIndex) +} + +func (b *Driver) deleteRoute(route *vnl.Route) error { + if err := b.config.RouteDel(route); err != nil { + return fmt.Errorf("RouteDel(%v): %w", route, err) + } + return nil +} + +func (b *Driver) Dispatch(ctx context.Context, req *schedpb.CreateEntryRequest) error { + log := zerolog.Ctx(ctx).With().Str("backend", "netlink").Logger() + + err := b.syncCachedRoutes(log) + if err != nil { + return fmt.Errorf("Failed to sync cached routes with implemented routes, with err: %w\n", err) + } + + if req.ConfigurationChange == nil { + return &NoChangeSpecifiedError{req} + } + + changeID := req.GetId() + + switch cc := req.ConfigurationChange.(type) { + case *schedpb.CreateEntryRequest_SetRoute: + route, needsAdd, err := func() (*vnl.Route, bool, error) { + b.mu.Lock() + defer b.mu.Unlock() + + b.stats.RoutesSetCount++ + + route, err := b.buildNetlinkRoute(cc.SetRoute) + if err != nil { + return nil, false, err + } + + needsAdd := true + for _, r := range b.routesToRuleIDs { + if r.route.Equal(*route) { + log.Debug().Msgf("skipping adding route %q because route is already installed", changeID) + needsAdd = false + r.ruleIDs[changeID] = cc.SetRoute + } + } + return route, needsAdd, nil + }() + + if err != nil { + return fmt.Errorf("SetRoute with ID %q failed: %w", changeID, err) + } + + if needsAdd { + // Then add to the system via Netlink + routeWithPriority := *route + routeWithPriority.Priority = int(math.MaxUint32 - uint32(b.config.Clock.Now().Unix())) + if err := b.addRoute(&routeWithPriority); err != nil { + return fmt.Errorf("SetRoute with ID %q failed with RTNETLINK-sourced error: %w", changeID, err) + } + } + + b.mu.Lock() + if needsAdd { + b.routesToRuleIDs = append(b.routesToRuleIDs, &routeToRuleID{ + route: *route, + ruleIDs: map[string]*schedpb.SetRoute{changeID: cc.SetRoute}, + }) + } + b.stats.InstalledRoutes = append(b.stats.InstalledRoutes, changeID) + slices.Sort(b.stats.InstalledRoutes) + b.stats.InstalledRoutes = slices.Compact(b.stats.InstalledRoutes) + + b.mu.Unlock() + + case *schedpb.CreateEntryRequest_DeleteRoute: + route, needsDelete, err := func() (route *vnl.Route, needsDelete bool, err error) { + b.mu.Lock() + defer b.mu.Unlock() + + b.stats.RoutesDeletedCount++ + + // Sanity check delete operation + var routeToDelete *schedpb.SetRoute + findRuleProtoLoop: + for _, rt := range b.routesToRuleIDs { + for rID, pb := range rt.ruleIDs { + if rID == changeID { + routeToDelete = pb + break findRuleProtoLoop + } + } + } + + if routeToDelete == nil { + return nil, false, &UnknownRouteDeleteError{changeID} + } + + route, err = b.buildNetlinkRoute(routeToDelete) + if err != nil { + return nil, false, fmt.Errorf("building netlink route (route=%v): %w", routeToDelete, err) + } + + for _, rt := range b.routesToRuleIDs { + if _, ok := rt.ruleIDs[changeID]; !ok { + continue + } + if !rt.route.Equal(*route) { + continue + } + needsDelete = needsDelete || len(rt.ruleIDs) == 1 + } + + return route, needsDelete, nil + }() + if err != nil { + return err + } + + if needsDelete { + // First attempt to remove from the system via Netlink + // If this fails, we do not want to delete the stateful store of routes (which we do next) + if err := b.deleteRoute(route); err != nil { + return fmt.Errorf("DeleteRoute with ID %q failed with RTNETLINK-sourced error: %w", changeID, err) + } + } + + b.mu.Lock() + b.routesToRuleIDs = slices.DeleteFunc(b.routesToRuleIDs, func(rt *routeToRuleID) bool { + delete(rt.ruleIDs, changeID) + if len(rt.ruleIDs) > 0 { + return false + } + + log.Debug().Msgf("deleting route %v because the last change that referenced it (%q) has been deleted", route, changeID) + return true + }) + b.stats.InstalledRoutes = slices.DeleteFunc(b.stats.InstalledRoutes, func(s string) bool { return s == changeID }) + b.mu.Unlock() + + default: + log.Warn().Msgf("received CreateEntryRequest with unsupported update type: %v", err) + return &UnsupportedUpdateError{req} + } + + log.Info().Msgf("Successfully implemented CreateEntryRequest with ID %s and action: %T", req.Id, req.GetConfigurationChange()) + + return nil +} diff --git a/agent/enactment/netlink/netlink_test.go b/agent/enactment/netlink/netlink_test.go new file mode 100644 index 0000000..64c1eae --- /dev/null +++ b/agent/enactment/netlink/netlink_test.go @@ -0,0 +1,435 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netlink + +import ( + "context" + "maps" + "net" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/jonboulle/clockwork" + vnl "github.com/vishvananda/netlink" + "google.golang.org/protobuf/testing/protocmp" + + apipb "aalyria.com/spacetime/api/common" + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" +) + +func TestNetlink(t *testing.T) { + // t.Parallel() + + type routesError struct { + routes []vnl.Route + err error + } + + type linkIdxError struct { + idx int + err error + } + + type testCase struct { + name string + preInstalledFlowRules map[string]*apipb.FlowRule + routeList []routesError + routeAdd []error + routeDel []error + getLinkIDByName []linkIdxError + configChanges []*schedpb.CreateEntryRequest + wantIDs []map[string]*schedpb.SetRoute + wantErrors []error + } + + _, dst, err := net.ParseCIDR("192.168.1.0/24") + if err != nil { + t.Fatalf("failed net.ParseIP(dstString=%s)", "192.168.1.0/24") + } + + testCases := []testCase{ + { + name: "add route", + routeList: []routesError{{ + routes: []vnl.Route{{ + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }}, + err: nil, + }}, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "rule1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + Via: "192.168.1.1", + Dev: "foo", + }, + }, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{ + { + "rule1": &schedpb.SetRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + Via: "192.168.1.1", + Dev: "foo", + }, + }, + }, + wantErrors: []error{nil}, + }, + { + name: "add and remove route", + routeList: []routesError{ + { + routes: []vnl.Route{{ + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }}, + err: nil, + }, + { + routes: []vnl.Route{}, + err: nil, + }, + }, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}, {idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "rule1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + Dev: "foo", + Via: "192.168.1.1", + }, + }, + }, + { + Id: "rule1", + Seqno: 2, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + }, + }, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{ + { + "rule1": &schedpb.SetRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + Dev: "foo", + Via: "192.168.1.1", + }, + }, + {}, + }, + wantErrors: []error{nil, nil}, + }, + { + name: "remove existing route", + routeList: []routesError{ + { + routes: []vnl.Route{ + { + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }, + }, + err: nil, + }, + { + err: nil, + }, + }, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}, {idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "rule1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + Dev: "foo", + Via: "192.168.1.1", + }, + }, + }, + { + Id: "rule1", + Seqno: 2, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + }, + }, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{ + { + "rule1": &schedpb.SetRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + Dev: "foo", + Via: "192.168.1.1", + }, + }, + {}, + }, + wantErrors: []error{nil, nil}, + }, + { + name: "remove nonexisting route", + routeList: []routesError{ + { + routes: []vnl.Route{ + { + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }, + }, + err: nil, + }, + }, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "rule1", + Seqno: 2, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteRoute{ + DeleteRoute: &schedpb.DeleteRoute{ + From: "192.168.2.2", + To: "192.168.1.0/24", + }, + }, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{{}}, + wantErrors: []error{&UnknownRouteDeleteError{"rule1"}}, + }, + { + name: "attempt to perform ScheduledControlUpdate with no Change", + routeList: []routesError{ + { + routes: []vnl.Route{ + { + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }, + }, + err: nil, + }, + }, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "rule1", + Seqno: 1, + ConfigurationChange: nil, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{{}}, + wantErrors: []error{&NoChangeSpecifiedError{&schedpb.CreateEntryRequest{Id: "rule1", Seqno: 1}}}, + }, + { + name: "attempt to perform unsupported UpdateBeam", + routeList: []routesError{ + { + routes: []vnl.Route{ + { + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }, + }, + err: nil, + }, + }, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "beam_update_1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_UpdateBeam{ + UpdateBeam: &schedpb.UpdateBeam{}, + }, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{{}}, + wantErrors: []error{ + &UnsupportedUpdateError{ + req: &schedpb.CreateEntryRequest{ + Id: "beam_update_1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_UpdateBeam{ + UpdateBeam: &schedpb.UpdateBeam{}, + }, + }, + }, + }, + }, + { + name: "attempt to perform unsupported DeleteBeam", + routeList: []routesError{ + { + routes: []vnl.Route{ + { + Src: net.ParseIP("192.168.2.2"), + Dst: dst, + Gw: net.ParseIP("192.168.1.1"), + LinkIndex: 7, + }, + }, + err: nil, + }, + }, + routeAdd: []error{nil}, + routeDel: []error{nil}, + getLinkIDByName: []linkIdxError{{idx: 7, err: nil}}, + configChanges: []*schedpb.CreateEntryRequest{ + { + Id: "beam_update_1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteBeam{ + DeleteBeam: &schedpb.DeleteBeam{}, + }, + }, + }, + wantIDs: []map[string]*schedpb.SetRoute{{}}, + wantErrors: []error{ + &UnsupportedUpdateError{ + req: &schedpb.CreateEntryRequest{ + Id: "beam_update_1", + Seqno: 1, + ConfigurationChange: &schedpb.CreateEntryRequest_DeleteBeam{ + DeleteBeam: &schedpb.DeleteBeam{}, + }, + }, + }, + }, + }, + } + + // Execute table driven test cases + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + // t.Parallel() + + t.Logf("==== Unit test: %s", tc.name) + + routeListIdx := 0 + routeAddIdx := 0 + routeDelIdx := 0 + getLinkIdxByNameIdx := 0 + + config := Config{ + Clock: clockwork.NewFakeClock(), + RtTableID: 252, + RtTableLookupPriority: 25200, + GetLinkIDByName: func(interface_id string) (int, error) { + rv := tc.getLinkIDByName[getLinkIdxByNameIdx/2] + getLinkIdxByNameIdx += 1 + return rv.idx, rv.err + }, + RouteList: func() ([]vnl.Route, error) { + rv := tc.routeList[routeListIdx] + routeListIdx += 1 + return rv.routes, rv.err + }, + RouteListFiltered: func(int, *vnl.Route, uint64) (routes []vnl.Route, err error) { + rv := tc.routeList[routeListIdx/2] // Because now this function is invoked twice for every operation (due to syncCachedRoutes) + routeListIdx += 1 + return rv.routes, rv.err + }, + RouteAdd: func(*vnl.Route) error { + rv := tc.routeAdd[routeAddIdx] + routeAddIdx += 1 + return rv + }, + RouteDel: func(*vnl.Route) error { + rv := tc.routeDel[routeDelIdx] + routeDelIdx += 1 + return rv + }, + RuleAdd: func(*vnl.Rule) error { + return nil + }, + } + backend := New(config) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for i, cc := range tc.configChanges { + err := backend.Dispatch(ctx, cc) + switch wantErr := tc.wantErrors[i]; { + case err == nil && wantErr != nil: + t.Fatalf("test %q update #%d expected error (%v), got nil", tc.name, i, wantErr) + case err != nil && err.Error() != wantErr.Error(): + t.Fatalf("test %q update #%d error mismatch; wanted %v, got %v", tc.name, i, wantErr, err) + } + + gotIDs := map[string]*schedpb.SetRoute{} + backend.mu.Lock() + for _, rtr := range backend.routesToRuleIDs { + maps.Insert(gotIDs, maps.All(rtr.ruleIDs)) + } + backend.mu.Unlock() + + if diff := cmp.Diff(tc.wantIDs[i], gotIDs, protocmp.Transform()); diff != "" { + t.Fatalf("test %q update %d unexpected message (-want +got):\n%s", tc.name, i, diff) + } + + } + }) + } +} diff --git a/agent/enactment_service.go b/agent/enactment_service.go new file mode 100644 index 0000000..3817f8a --- /dev/null +++ b/agent/enactment_service.go @@ -0,0 +1,447 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "cmp" + "context" + "fmt" + "slices" + "time" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + apipb "aalyria.com/spacetime/api/common" + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" + "aalyria.com/spacetime/agent/enactment" + "aalyria.com/spacetime/agent/internal/channels" + "aalyria.com/spacetime/agent/internal/loggable" + "aalyria.com/spacetime/agent/internal/task" +) + +// we keep already attempted enactments in memory for this long to help resolve +// issues where the CDPI server might try and update a request after it's been +// applied. +const attemptedUpdateKeepAliveTimeout = 1 * time.Minute + +func OK() *status.Status { return status.New(codes.OK, "") } + +type enactmentService struct { + sc schedpb.SchedulingClient + ed enactment.Driver + clock clockwork.Clock + initState *apipb.ControlPlaneState + nodeID string + + scheduleManipulationToken string + schedMgr *scheduleManager + reqsFromController chan *schedpb.ReceiveRequestsMessageFromController + rspsToController chan *schedpb.ReceiveRequestsMessageToController + dispatchTimer chan string + enactmentResults chan *enactmentResult +} + +func (nc *nodeController) newEnactmentService(sc schedpb.SchedulingClient, ed enactment.Driver, manipToken string) *enactmentService { + return &enactmentService{ + ed: ed, + sc: sc, + clock: nc.clock, + nodeID: nc.id, + initState: nc.initState, + scheduleManipulationToken: manipToken, + schedMgr: newScheduleManager(), + reqsFromController: make(chan *schedpb.ReceiveRequestsMessageFromController), + rspsToController: make(chan *schedpb.ReceiveRequestsMessageToController), + dispatchTimer: make(chan string), + enactmentResults: make(chan *enactmentResult), + } +} + +func (es *enactmentService) Stats() interface{} { + return struct { + Driver any + Schedule *scheduleManager + ScheduleManipulationToken string + }{ + Driver: es.ed.Stats(), + Schedule: es.schedMgr, + ScheduleManipulationToken: es.scheduleManipulationToken, + } +} + +type enactmentResult struct { + id string + err error + tStamp time.Time +} + +type scheduleManager struct { + LastFinalize time.Time + Entries map[string]*scheduleEvent +} + +func newScheduleManager() *scheduleManager { + return &scheduleManager{ + LastFinalize: time.Time{}, + Entries: map[string]*scheduleEvent{}, + } +} + +type scheduleEvent struct { + DeletePending bool + timer clockwork.Timer + StartTime time.Time + EndTime time.Time + Error error + Req *schedpb.ReceiveRequestsMessageFromController +} + +func (sm *scheduleManager) createEntry(timer clockwork.Timer, req *schedpb.ReceiveRequestsMessageFromController) { + sm.Entries[req.GetCreateEntry().Id] = &scheduleEvent{ + timer: timer, + Req: req, + } +} + +func (sm *scheduleManager) deleteEntry(id string) { + // DeleteEntry has been called; clean up on aisle five. + entry, ok := sm.Entries[id] + if !ok { + // It's ok to delete entries that don't exist. + return + } + + if entry.EndTime.Before(entry.StartTime) { + // The enactment backend has been called but has not yet completed. + entry.DeletePending = true + return + } + + entry.timer.Stop() + delete(sm.Entries, id) +} + +func (sm *scheduleManager) finalizeEntries(upTo time.Time) { + // TODO: + // for each entry: + // if entry.time.Before(upTo) and enacted [and return code captured]: + // delete entry + sm.LastFinalize = upTo +} + +func (sm *scheduleManager) recordResult(result *enactmentResult) { + entry, ok := sm.Entries[result.id] + if !ok { + // Entry somehow deleted after being kicked off. + // TODO: log (will want ctx here) + return + } + + entry.Error = result.err + entry.EndTime = result.tStamp + + if entry.DeletePending { + // A DeleteEntryRequest arrived after the request had + // already been Dispatch()-ed. + sm.deleteEntry(result.id) + return + } + // TODO: somehow relay info to controler. +} + +func (es *enactmentService) run(ctx context.Context) error { + g, ctx := errgroup.WithContext(ctx) + + if err := es.ed.Init(ctx); err != nil { + return fmt.Errorf("%T.Init() failed: %w", es.ed, err) + } + + schedulingRequestStream, err := es.sc.ReceiveRequests(ctx) + if err != nil { + return fmt.Errorf("error invoking the Scheduling ReceiveRequests interface: %w", err) + } + + g.Go(channels.NewSource(es.rspsToController).ForwardTo(schedulingRequestStream.Send). + WithStartingStoppingLogs("toController", zerolog.TraceLevel). + WithLogField("task", "send"). + WithNewSpan("sendLoop"). + WithPanicCatcher(). + WithCtx(ctx)) + + g.Go(channels.NewSink(es.reqsFromController).FillFrom(schedulingRequestStream.Recv). + WithStartingStoppingLogs("fromController", zerolog.TraceLevel). + WithLogField("task", "recv"). + WithPanicCatcher(). + WithCtx(ctx)) + + g.Go(task.Task(es.mainScheduleLoop). + WithStartingStoppingLogs("mainScheduleLoop", zerolog.TraceLevel). + WithLogField("task", "main"). + WithNewSpan("mainScheduleLoop"). + WithPanicCatcher(). + WithCtx(ctx)) + + // Send an immediate Reset to convey a new schedule_manipulation_token, followed + // by a Hello. + g.Go(func() error { + reset := es.schedReset() + zerolog.Ctx(ctx).Debug().Object("reset", loggable.Proto(reset)).Msg("sending reset") + if _, err := es.sc.Reset(ctx, reset); err != nil { + return err + } + + hello := es.schedHello() + zerolog.Ctx(ctx).Debug().Object("hello", loggable.Proto(hello)).Msg("sending hello") + select { + case es.rspsToController <- hello: + break + case <-ctx.Done(): + return context.Cause(ctx) + } + + return nil + }) + + return g.Wait() +} + +func (es *enactmentService) schedHello() *schedpb.ReceiveRequestsMessageToController { + return &schedpb.ReceiveRequestsMessageToController{ + Hello: &schedpb.ReceiveRequestsMessageToController_Hello{ + AgentId: es.nodeID, + }, + } +} + +func (es *enactmentService) schedReset() *schedpb.ResetRequest { + return &schedpb.ResetRequest{ + AgentId: es.nodeID, + ScheduleManipulationToken: es.scheduleManipulationToken, + } +} + +func (es *enactmentService) mainScheduleLoop(ctx context.Context) error { + var nextSeqno uint64 = 1 + pendingRequests := []*schedpb.ReceiveRequestsMessageFromController{} + + for { + select { + // [0] Handle context shutdown. + case <-ctx.Done(): + return context.Cause(ctx) + // [1] Process requests from the controller. + case req := <-es.reqsFromController: + // Check for correct schedule manipulation token, if present. + token, ok := getScheduleManipulationToken(req) + if ok && token != es.scheduleManipulationToken { + err := es.sendResponse(ctx, req.RequestId, status.Newf( + codes.FailedPrecondition, + "received schedule manipulation token %q is not current (%s)", token, es.scheduleManipulationToken)) + if err != nil { + return err + } + // Carry on; we may sync up at some point in the future. + continue + } + + _, ok = getSeqno(req) + if !ok { + // Presently, all requests supported here have a sequence number. + err := es.sendResponse(ctx, req.RequestId, status.Newf( + codes.Unimplemented, + "unable to handle request %q which has no seqno", req.RequestId)) + if err != nil { + return err + } + // Carry on awaiting a message we can understand. + continue + } + // Sort requests by seqno. + pendingRequests = append(pendingRequests, req) + slices.SortFunc(pendingRequests, func(l, r *schedpb.ReceiveRequestsMessageFromController) int { + return cmp.Compare(mustSeqno(l), mustSeqno(r)) + }) + + // Process any old requests followed by all next expected requests. + // Any requests sequenced after the next expected are blocked until + // missing requests arrive. + for len(pendingRequests) > 0 { + req = pendingRequests[0] + seqno := mustSeqno(req) + if seqno > nextSeqno { + zerolog.Ctx(ctx).Info(). + Uint64("seqno.this", seqno). + Uint64("seqno.nextExpected", nextSeqno). + Msg("seqno greater than next expected; delaying scheduling") + break + } + status := es.handleSchedulingRequest(ctx, req) + err := es.sendResponse(ctx, req.RequestId, status) + if err != nil { + return err + } + pendingRequests = pendingRequests[1:] + if seqno == nextSeqno { + nextSeqno++ + } + } + // [2] Handle timer firing to launch a call to the backend. + case id := <-es.dispatchTimer: + entry, ok := es.schedMgr.Entries[id] + if !ok { + // Likely a DeleteEntry() racing with the Timer firing. + continue + } + if entry.StartTime.After(entry.EndTime) { + zerolog.Ctx(ctx).Warn().Object("req", loggable.Proto(entry.Req)).Msg("Dispatch already in progress") + continue + } + if entry.EndTime.After(entry.StartTime) { + zerolog.Ctx(ctx).Warn().Object("req", loggable.Proto(entry.Req)).Msg("Dispatch already completed") + continue + } + entry.StartTime = es.clock.Now() + go func() { + result := &enactmentResult{ + id: id, + } + result.err = es.ed.Dispatch(ctx, entry.Req.GetCreateEntry()) + result.tStamp = es.clock.Now() + + select { + case <-ctx.Done(): + return + case es.enactmentResults <- result: + } + }() + // [3] Note the return code (and other results) from a backend call. + case result := <-es.enactmentResults: + zerolog.Ctx(ctx).Debug(). + Str("result.id", result.id). + AnErr("result.err", result.err). + Time("result.tStamp", result.tStamp). + Msg("recv'd Dispatch result") + es.schedMgr.recordResult(result) + } + } +} + +func (es *enactmentService) sendResponse(ctx context.Context, id int64, status *status.Status) error { + resp := &schedpb.ReceiveRequestsMessageToController{ + Response: &schedpb.ReceiveRequestsMessageToController_Response{ + RequestId: id, + Status: status.Proto(), + }, + } + + select { + case <-ctx.Done(): + return context.Cause(ctx) + case es.rspsToController <- resp: + zerolog.Ctx(ctx).Debug().Object("response", loggable.Proto(resp)).Msg("sending response") + return nil + } +} + +func (es *enactmentService) handleSchedulingRequest(ctx context.Context, req *schedpb.ReceiveRequestsMessageFromController) *status.Status { + switch req.Request.(type) { + case *schedpb.ReceiveRequestsMessageFromController_CreateEntry: + id := req.GetCreateEntry().Id + if _, ok := es.schedMgr.Entries[id]; ok { + // Ignore repeated scheduling of the same entry ID. + zerolog.Ctx(ctx).Debug().Str("entry ID", id).Msg("ignoring scheduling attempt of duplicate entry") + return OK() + } + delay := req.GetCreateEntry().Time.AsTime().Sub(es.clock.Now()) + timer := es.clock.AfterFunc(delay, func() { + select { + case <-ctx.Done(): + return + case es.dispatchTimer <- id: + } + }) + es.schedMgr.createEntry(timer, req) + return OK() + + case *schedpb.ReceiveRequestsMessageFromController_DeleteEntry: + es.schedMgr.deleteEntry(req.GetDeleteEntry().Id) + return OK() + + case *schedpb.ReceiveRequestsMessageFromController_Finalize: + upTo := req.GetFinalize().UpTo.AsTime() + if !upTo.After(es.schedMgr.LastFinalize) { + return status.Newf( + codes.FailedPrecondition, + "received finalize up_to %d <= latest finalize up_to (%d)", + upTo.UnixNano(), + es.schedMgr.LastFinalize.UnixNano()) + } + + es.schedMgr.finalizeEntries(upTo) + return OK() + + default: + return status.New(codes.Unimplemented, "unknown ReceiveRequestsFromController request type") + } +} + +func getScheduleManipulationToken(req *schedpb.ReceiveRequestsMessageFromController) (string, bool) { + switch req.Request.(type) { + case *schedpb.ReceiveRequestsMessageFromController_CreateEntry: + return req.GetCreateEntry().ScheduleManipulationToken, true + case *schedpb.ReceiveRequestsMessageFromController_DeleteEntry: + return req.GetDeleteEntry().ScheduleManipulationToken, true + case *schedpb.ReceiveRequestsMessageFromController_Finalize: + return req.GetFinalize().ScheduleManipulationToken, true + default: + return "", false + } +} + +func getSeqno(req *schedpb.ReceiveRequestsMessageFromController) (uint64, bool) { + switch req.Request.(type) { + case *schedpb.ReceiveRequestsMessageFromController_CreateEntry: + return req.GetCreateEntry().Seqno, true + case *schedpb.ReceiveRequestsMessageFromController_DeleteEntry: + return req.GetDeleteEntry().Seqno, true + case *schedpb.ReceiveRequestsMessageFromController_Finalize: + return req.GetFinalize().Seqno, true + default: + return 0, false + } +} + +func mustSeqno(req *schedpb.ReceiveRequestsMessageFromController) uint64 { + seqno, ok := getSeqno(req) + if !ok { + panic(fmt.Errorf("failed to get seqno when required; id %d", req.RequestId)) + } + return seqno +} + +func getId(req *schedpb.ReceiveRequestsMessageFromController) (string, bool) { + switch req.Request.(type) { + case *schedpb.ReceiveRequestsMessageFromController_CreateEntry: + return req.GetCreateEntry().Id, true + case *schedpb.ReceiveRequestsMessageFromController_DeleteEntry: + return req.GetDeleteEntry().Id, true + default: + return "", false + } +} diff --git a/agent/enactment_test.go b/agent/enactment_test.go new file mode 100644 index 0000000..a6091e8 --- /dev/null +++ b/agent/enactment_test.go @@ -0,0 +1,448 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "context" + "errors" + "fmt" + "net" + "sync" + "testing" + "time" + + afpb "aalyria.com/spacetime/api/cdpi/v1alpha" + apipb "aalyria.com/spacetime/api/common" + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" + "aalyria.com/spacetime/agent/internal/channels" + "aalyria.com/spacetime/agent/internal/task" + + "github.com/google/go-cmp/cmp" + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + startTime = time.Now() + + testCases = []testCase{ + { + desc: "check Scheduling API session begins with reset and hello", + nodes: []testNode{{id: "node-a"}}, + test: func(ctx context.Context, f *testFixture) { + f.expectSchedulingReset(ctx, "node-a") + f.expectSchedulingHello(ctx, "node-a") + }, + }, + { + desc: "check basic Scheduling API CreateEntry yields backend Dispatch", + nodes: []testNode{{id: "node-a"}}, + test: func(ctx context.Context, f *testFixture) { + token := f.expectSchedulingReset(ctx, "node-a") + f.expectSchedulingHello(ctx, "node-a") + + var requestId int64 = 0 + nextRequestId := func() int64 { + rval := requestId + requestId++ + return rval + } + + var seqno uint64 = 1 + nextSeqno := func() uint64 { + rval := seqno + seqno++ + return rval + } + + // send CreateEntry:SetRoute + createEntrySetRoute := &schedpb.CreateEntryRequest{ + ScheduleManipulationToken: token, + Seqno: nextSeqno(), + Id: "create-route-1", + Time: timestamppb.New(startTime), + ConfigurationChange: &schedpb.CreateEntryRequest_SetRoute{ + SetRoute: &schedpb.SetRoute{ + To: "2001:db8:1::/48", + Dev: "eth0", + Via: "fe80::1", + }, + }, + } + reqScheduleCreateEntrySetRoute := &schedpb.ReceiveRequestsMessageFromController{ + RequestId: nextRequestId(), + Request: &schedpb.ReceiveRequestsMessageFromController_CreateEntry{ + CreateEntry: createEntrySetRoute, + }, + } + f.sendSchedulingRequest(ctx, "node-a", reqScheduleCreateEntrySetRoute) + + req := f.waitForDispatchRequestFromController(ctx) + if diff := cmp.Diff(createEntrySetRoute, req, protocmp.Transform()); diff != "" { + f.t.Errorf("waitForRequestFromController(): payload proto mismatch: (-want +got):\n%s", diff) + return + } + }, + }, + } +) + +func TestEnactments(t *testing.T) { + t.Parallel() + + for _, tc := range testCases { + tc := tc + + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + tc.runTest(t) + }) + } +} + +func (tc *testCase) runTest(t *testing.T) { + ctx, cancel := context.WithCancel(baseContext(t)) + g, ctx := errgroup.WithContext(ctx) + defer func() { + cancel() + checkErrIsDueToCanceledContext(t, g.Wait()) + }() + + enact := newDelegatingBackend() + defer enact.checkNoUnhandledUpdates(t) + + clock := clockwork.NewFakeClockAt(startTime) + srv := newServer( + zerolog.Ctx(ctx).With().Str("role", "test server").Logger().WithContext(ctx), tc.nodes) + srvAddr := srv.start(ctx, t, g) + + opts := []AgentOption{WithClock(clock)} + for _, n := range tc.nodes { + opts = append(opts, WithNode(n.id, + WithEnactmentDriver(srvAddr, enact, grpc.WithTransportCredentials(insecure.NewCredentials())))) + } + agent := newAgent(t, opts...) + + g.Go(func() error { return agent.Run(ctx) }) + tc.test(ctx, &testFixture{t: t, srv: srv, eb: enact, clock: clock}) +} + +type delegatingBackend struct { + m map[string]backendFn + errs []error + reqs chan *schedpb.CreateEntryRequest +} + +type backendFn func(context.Context, *apipb.ScheduledControlUpdate) (*apipb.ControlPlaneState, error) + +func newDelegatingBackend() *delegatingBackend { + return &delegatingBackend{ + m: map[string]backendFn{}, + errs: []error{}, + reqs: make(chan *schedpb.CreateEntryRequest), + } +} + +func (d *delegatingBackend) Init(context.Context) error { return nil } +func (d *delegatingBackend) Close() error { return nil } +func (d *delegatingBackend) Stats() interface{} { return nil } + +func (d *delegatingBackend) setBackendForUpdateID(updateID string, b backendFn) { + d.m[updateID] = b +} + +func (d *delegatingBackend) Apply(ctx context.Context, scu *apipb.ScheduledControlUpdate) (*apipb.ControlPlaneState, error) { + uid := scu.GetUpdateId() + if b, ok := d.m[uid]; ok { + return b(ctx, scu) + } + + err := fmt.Errorf("no delegate for update with ID %s", uid) + d.errs = append(d.errs, err) + return nil, err +} + +func (d *delegatingBackend) checkNoUnhandledUpdates(t *testing.T) { + if err := errors.Join(d.errs...); err != nil { + t.Errorf("delegatingBackend encountered error(s): %s", err) + } +} + +func (d *delegatingBackend) Dispatch(ctx context.Context, req *schedpb.CreateEntryRequest) error { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case d.reqs <- req: + return nil + } +} + +type server struct { + afpb.UnimplementedCdpiServer + ctx context.Context + mu *sync.Mutex + updateCond *sync.Cond + + schedpb.UnimplementedSchedulingServer + agents map[string]*agentSchedulingStream + tokens map[string]chan string +} + +type agentSchedulingStream struct { + stream *schedpb.Scheduling_ReceiveRequestsServer + toController chan *schedpb.ReceiveRequestsMessageToController + fromController chan *schedpb.ReceiveRequestsMessageFromController +} + +func newServer(ctx context.Context, nodes []testNode) *server { + agents := map[string]*agentSchedulingStream{} + tokens := map[string]chan string{} + for _, n := range nodes { + agents[n.id] = &agentSchedulingStream{ + toController: make(chan *schedpb.ReceiveRequestsMessageToController), + fromController: make(chan *schedpb.ReceiveRequestsMessageFromController), + } + tokens[n.id] = make(chan string) + } + + mu := &sync.Mutex{} + return &server{ + mu: mu, + updateCond: sync.NewCond(mu), + ctx: ctx, + agents: agents, + tokens: tokens, + } +} + +func (s *server) start(ctx context.Context, t *testing.T, g *errgroup.Group) string { + nl, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0") + if err != nil { + t.Fatalf("error starting tcp listener: %s", err) + } + + grpcSrv := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) + schedpb.RegisterSchedulingServer(grpcSrv, s) + + g.Go(task.Task(func(ctx context.Context) error { + return grpcSrv.Serve(nl) + }).WithCtx(ctx)) + + g.Go(task.Task(func(ctx context.Context) error { + <-ctx.Done() + grpcSrv.GracefulStop() + return nil + }).WithCtx(ctx)) + + return nl.Addr().String() +} + +func (s *server) ReceiveRequests(stream schedpb.Scheduling_ReceiveRequestsServer) error { + hello, err := stream.Recv() + if err != nil { + return err + } + + nid := hello.GetHello().GetAgentId() + + s.mu.Lock() + ss, ok := s.agents[nid] + s.mu.Unlock() + if !ok { + return fmt.Errorf("unknown node connecting: %q", nid) + } + + defer close(ss.fromController) + + select { + case <-s.ctx.Done(): + return context.Canceled + case ss.toController <- hello: + } + + return task.Group( + channels.NewSink(ss.toController).FillFrom(stream.Recv). + WithStartingStoppingLogs("server_stream", zerolog.TraceLevel). + WithLogField("nodeID", nid). + WithLogField("direction", "server.Recv"), + channels.NewSource(ss.fromController).ForwardTo(stream.Send). + WithStartingStoppingLogs("server_stream", zerolog.TraceLevel). + WithLogField("nodeID", nid). + WithLogField("direction", "server.Send"), + )(s.ctx) +} + +func (s *server) Reset(ctx context.Context, reset *schedpb.ResetRequest) (*emptypb.Empty, error) { + nid := reset.GetAgentId() + + s.mu.Lock() + ch, ok := s.tokens[nid] + if !ok { + return nil, fmt.Errorf("unknown node reseting: %q", nid) + } + s.mu.Unlock() + go func() { + select { + case ch <- reset.GetScheduleManipulationToken(): + } + }() + return &emptypb.Empty{}, nil +} + +func (s *server) RecvFromNode(ctx context.Context, nid string) (*schedpb.ReceiveRequestsMessageToController, error) { + ss, ok := s.agents[nid] + if !ok { + return nil, fmt.Errorf("RecvFromNode(%q): unknown node provided", nid) + } + + select { + case rsp := <-ss.toController: + return rsp, nil + case <-ctx.Done(): + return nil, context.Cause(ctx) + } +} + +func (s *server) SendToNode(ctx context.Context, nid string, msg *schedpb.ReceiveRequestsMessageFromController) error { + ss, ok := s.agents[nid] + if !ok { + return fmt.Errorf("SendToNode(%q): unknown node provided", nid) + } + + select { + case ss.fromController <- msg: + return nil + case <-ctx.Done(): + return context.Cause(ctx) + } +} + +func (s *server) WaitForSchedulingReset(ctx context.Context, nid string) (string, error) { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + s.mu.Lock() + ch, ok := s.tokens[nid] + if !ok { + return "", fmt.Errorf("unknown node reseting: %q", nid) + } + s.mu.Unlock() + select { + case token := <-ch: + return token, nil + case <-ctx.Done(): + return "", context.Cause(ctx) + } +} + +func (s *server) WaitForSchedulingHello(ctx context.Context, nid string) (*schedpb.ReceiveRequestsMessageToController_Hello, error) { + hello, err := s.RecvFromNode(ctx, nid) + if err != nil { + return nil, err + } + return hello.GetHello(), nil +} + +func mustMarshal(t *testing.T, m proto.Message) []byte { + t.Helper() + + b, err := proto.Marshal(m) + if err != nil { + t.Fatalf("mustMarshal(%#v): %s", m, err) + } + return b +} + +// testFixture holds references to the various moving parts involved in an +// agent test and provides a high-level vocabulary for expressing test steps. +type testFixture struct { + t *testing.T + srv *server + eb *delegatingBackend + clock clockwork.FakeClock +} + +type testNode struct { + id string + priority uint32 +} + +type testCase struct { + test func(context.Context, *testFixture) + desc string + nodes []testNode +} + +func (f *testFixture) advanceClock(ctx context.Context, dur time.Duration) { + zerolog.Ctx(ctx).Debug().Dur("duration", dur).Msg("advancing clock") + f.clock.Advance(dur) +} + +func (f *testFixture) expectSchedulingReset(ctx context.Context, nid string) string { + token, err := f.srv.WaitForSchedulingReset(ctx, nid) + if err != nil { + f.t.Errorf("WaitForSchedulingReset(%q): %s", nid, err) + f.t.FailNow() + return "" + } + + if token == "" { + f.t.Errorf("WaitForSchedulingReset(%q): empty schedule manipulation token", nid) + f.t.FailNow() + return "" + } + + zerolog.Ctx(ctx).Debug().Str("nodeID", nid).Msg("WaitForSchedulingReset() succeeded") + return token +} + +func (f *testFixture) expectSchedulingHello(ctx context.Context, nid string) { + hello, err := f.srv.WaitForSchedulingHello(ctx, nid) + if err != nil { + f.t.Errorf("WaitForSchedulingHello(%q): %s", nid, err) + f.t.FailNow() + } + + if hello.AgentId != nid { + f.t.Errorf("WaitForSchedulingHello(%q): unexpected agent ID %q", nid, hello.AgentId) + f.t.FailNow() + } + + zerolog.Ctx(ctx).Debug().Str("nodeID", nid).Msg("WaitForSchedulingHello() succeeded") +} + +func (f *testFixture) sendSchedulingRequest(ctx context.Context, nid string, req *schedpb.ReceiveRequestsMessageFromController) { + err := f.srv.SendToNode(ctx, nid, req) + if err != nil { + f.t.Errorf("sendSchedulingRequest(%q): %s", nid, err) + f.t.FailNow() + } +} + +func (f *testFixture) waitForDispatchRequestFromController(ctx context.Context) *schedpb.CreateEntryRequest { + select { + case <-ctx.Done(): + return nil + case req := <-f.eb.reqs: + return req + } +} diff --git a/agent/examples/enact_flow_forward_updates.py b/agent/examples/enact_flow_forward_updates.py new file mode 100644 index 0000000..e2b7b43 --- /dev/null +++ b/agent/examples/enact_flow_forward_updates.py @@ -0,0 +1,132 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A simple example enactment backend written in Python. +import json +import sys +import enum + +from typing import Any + + +NODE_ID = "esa-5g6g-hub" + + +class StatusCode(enum.Enum): + """An enum mirroring the gRPC status codes. + + https://grpc.github.io/grpc/core/md_doc_statuscodes.html + """ + + OK = 0 + CANCELLED = 1 + UNKNOWN = 2 + INVALID_ARGUMENT = 3 + DEADLINE_EXCEEDED = 4 + NOT_FOUND = 5 + ALREADY_EXISTS = 6 + PERMISSION_DENIED = 7 + RESOURCE_EXHAUSTED = 8 + FAILED_PRECONDITION = 9 + ABORTED = 10 + OUT_OF_RANGE = 11 + UNIMPLEMENTED = 12 + INTERNAL = 13 + UNAVAILABLE = 14 + DATA_LOSS = 15 + UNAUTHENTICATED = 16 + + +class StatusCodeException(Exception): + def __init__(self, code: StatusCode, msg: str = ""): + super().__init__(msg) + self.code = code + + +def check_preconditions(req: dict[str, Any]): + requested_node_id = req.get("nodeId") + if requested_node_id != NODE_ID: + raise StatusCodeException( + StatusCode.INVALID_ARGUMENT, f"unknown node {requested_node_id}" + ) + + flow_update = req.get("change", {}).get("flowUpdate", {}) + if not flow_update: + raise StatusCodeException( + StatusCode.UNIMPLEMENTED, + "this agent only handles FlowUpdate change requests", + ) + + op = flow_update.get("operation", "UNKNOWN") + if op not in ["ADD", "DELETE"]: + raise StatusCodeException( + StatusCode.INVALID_ARGUMENT, f"unknown operation {op}" + ) + + rule = flow_update.get("rule") + if not rule.get("classifier"): + raise StatusCodeException( + StatusCode.INVALID_ARGUMENT, "no packet classifier provided" + ) + + if not all( + [ + act.get("actionType", {}).get("forward") + for bucket in rule["action_bucket"] + for act in bucket["action"] + ] + ): + raise StatusCodeException( + StatusCode.UNIMPLEMENTED, + "this agent only handles 'forward' actions", + ) + + +def process(req: dict[str, Any]): + check_preconditions(req) + + flow_update = req["change"]["flowUpdate"] + rule_id = flow_update["flowRuleId"] + rule = flow_update["rule"] + is_add = rule["operation"] == "ADD" + packet_classifier = rule["classifier"] + fn = add_forwarding_rule if is_add else delete_forwarding_rule + + for bucket in rule["actionBucket"]: + for action in bucket["action"]: + out_iface_id = action["actionType"]["forward"]["outInterfaceId"] + fn(id=rule_id, classifier=packet_classifier, out_iface=out_iface_id) + + +def add_forwarding_rule(id: str, classifier: dict[str, Any], out_iface: str): + """The logic to add a forwarding rule goes here.""" + pass + + +def delete_forwarding_rule(id: str, classifier: dict[str, Any], out_iface: str): + """The logic to delete a forwarding rule goes here.""" + pass + + +def main(): + try: + process(json.load(sys.stdin)) + # (optional) write the new JSON-encoded ControlPlaneState to stdout + except StatusCodeException as sce: + sys.stderr.write(str(sce)) + sys.exit(sce.code.value) + + +if __name__ == "__main__": + main() diff --git a/agent/examples/flow_update_request.json b/agent/examples/flow_update_request.json new file mode 100644 index 0000000..53160d0 --- /dev/null +++ b/agent/examples/flow_update_request.json @@ -0,0 +1,26 @@ +{ + "nodeId": "my-network-hub", + "updateId": "574fc796-128d-490a-b6d3-36be4c20e92c", + "timeToEnact": "2023-05-22T18:21:38.922296442Z", + "change": { + "beamUpdate": { + "beamTaskId": "45h8k", + "operation": "ADD", + "sourceInterfaceId": "lo0", + "interfaceId": { + "nodeId": "my-network-hub", + "interfaceId": "lo0" + }, + "targetInterfaceId": "lo1", + "targetId": { + "nodeId": "my-network-satellite", + "interfaceId": "lo1" + }, + "signalInfo": { + "modeledPowerAtReceiverOutputDbw": 0 + }, + "perInterfaceSequenceNumber": "1684779683553804", + "establishmentTimeout": "40s" + } + } +} diff --git a/agent/internal/agentcli/BUILD b/agent/internal/agentcli/BUILD new file mode 100644 index 0000000..c1cbb4e --- /dev/null +++ b/agent/internal/agentcli/BUILD @@ -0,0 +1,69 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "agentcli", + srcs = [ + "agentcli.go", + "netlink_linux.go", + "netlink_other.go", + ], + importpath = "aalyria.com/spacetime/agent/internal/agentcli", + deps = [ + "//agent", + "//agent/enactment", + "//agent/enactment/extproc", + "//agent/internal/configpb:configpb_go_proto", + "//agent/internal/protofmt", + "//agent/internal/task", + "//agent/telemetry", + "//agent/telemetry/extproc", + "//auth", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + "@io_opentelemetry_go_contrib_instrumentation_google_golang_org_grpc_otelgrpc//:otelgrpc", + "@io_opentelemetry_go_otel//attribute", + "@io_opentelemetry_go_otel//propagation", + "@io_opentelemetry_go_otel//semconv/v1.25.0:v1_25_0", + "@io_opentelemetry_go_otel_exporters_otlp_otlptrace_otlptracegrpc//:otlptracegrpc", + "@io_opentelemetry_go_otel_sdk//resource", + "@io_opentelemetry_go_otel_sdk//trace", + "@io_opentelemetry_go_otel_trace//noop", + "@org_golang_google_grpc//:grpc", + "@org_golang_google_grpc//backoff", + "@org_golang_google_grpc//channelz/service", + "@org_golang_google_grpc//credentials", + "@org_golang_google_grpc//credentials/insecure", + "@org_golang_google_grpc//encoding/gzip", + "@org_golang_google_grpc//keepalive", + "@org_golang_google_protobuf//types/known/anypb", + "@org_golang_x_sync//errgroup", + ] + select({ + "@rules_go//go/platform:android": [ + "//agent/enactment/netlink", + "//agent/telemetry/netlink", + "@com_github_vishvananda_netlink//:netlink", + ], + "@rules_go//go/platform:linux": [ + "//agent/enactment/netlink", + "//agent/telemetry/netlink", + "@com_github_vishvananda_netlink//:netlink", + ], + "//conditions:default": [], + }), +) diff --git a/agent/internal/agentcli/agentcli.go b/agent/internal/agentcli/agentcli.go new file mode 100644 index 0000000..070173b --- /dev/null +++ b/agent/internal/agentcli/agentcli.go @@ -0,0 +1,579 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package agentcli provides a CDPI agent that is configured using a +// protobuf-based manifest. +package agentcli + +import ( + "bytes" + "cmp" + "context" + "crypto/x509" + "errors" + "flag" + "fmt" + "io" + "net" + "net/http" + _ "net/http/pprof" + "os" + "os/signal" + "strings" + "time" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + otelsdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + oteltracenoop "go.opentelemetry.io/otel/trace/noop" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + channelz "google.golang.org/grpc/channelz/service" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding/gzip" + "google.golang.org/grpc/keepalive" + "google.golang.org/protobuf/types/known/anypb" + + agent "aalyria.com/spacetime/agent" + "aalyria.com/spacetime/agent/enactment" + enact_extproc "aalyria.com/spacetime/agent/enactment/extproc" + "aalyria.com/spacetime/agent/internal/configpb" + "aalyria.com/spacetime/agent/internal/protofmt" + "aalyria.com/spacetime/agent/internal/task" + "aalyria.com/spacetime/agent/telemetry" + telemetry_extproc "aalyria.com/spacetime/agent/telemetry/extproc" + "aalyria.com/spacetime/auth" +) + +// Handles are abstractions over impure, external resources like time and stdio +// streams. +type Handles interface { + Clock() clockwork.Clock + Stdout() io.Writer + Stderr() io.Writer +} + +type realHandles struct { + clock clockwork.Clock +} + +func (rh realHandles) Clock() clockwork.Clock { return rh.clock } +func (rh realHandles) Stdout() io.Writer { return os.Stdout } +func (rh realHandles) Stderr() io.Writer { return os.Stderr } + +// DefaultHandles returns a Handles implementation that delegates to the +// standard implementations for external resources. +func DefaultHandles() Handles { return realHandles{clock: clockwork.NewRealClock()} } + +// Provider provides a dynamic way of registering enactment or telemetry +// driver providers. If the provided configuration isn't of the appropriate +// type, the factory function is expected to return nil, [ErrUnknownConfigProto]. +type Provider interface { + EnactmentDriver(_ context.Context, _ Handles, nodeID string, conf *anypb.Any) (enactment.Driver, error) + TelemetryDriver(_ context.Context, _ Handles, nodeID string, conf *anypb.Any) (telemetry.Driver, error) +} + +// ErrUnknownConfigProto is the error a [Provider] should return if the +// provided `anypb.Any` is of an unknown type. +var ErrUnknownConfigProto = errors.New("unknown config proto") + +type UnsupportedEnactmentDriver struct{} + +func (*UnsupportedEnactmentDriver) EnactmentDriver(_ context.Context, _ Handles, _ string, _ *anypb.Any) (enactment.Driver, error) { + return nil, ErrUnknownConfigProto +} + +type UnsupportedTelemetryDriver struct{} + +func (*UnsupportedTelemetryDriver) TelemetryDriver(_ context.Context, _ Handles, _ string, _ *anypb.Any) (telemetry.Driver, error) { + return nil, ErrUnknownConfigProto +} + +type AgentConf struct { + AppName string + Handles Handles + Providers []Provider +} + +func (ac AgentConf) Run(ctx context.Context, appName string, args []string) (err error) { + var log zerolog.Logger + if os.Getenv("TERM") != "" { + log = zerolog.New(zerolog.ConsoleWriter{Out: ac.Handles.Stderr(), TimeFormat: "2006-01-02 03:04:05PM"}) + } else { + log = zerolog.New(ac.Handles.Stderr()) + } + ctx = log.With().Timestamp().Logger().WithContext(ctx) + + fs := flag.NewFlagSet(appName, flag.ContinueOnError) + fs.SetOutput(ac.Handles.Stderr()) + fs.Usage = func() { + w := fs.Output() + fmt.Fprintf(w, "Usage: %s [options]\n", appName) + fmt.Fprint(w, "\nOptions:\n") + fs.PrintDefaults() + } + + confPath := fs.String("config", "", "The path to a protobuf representation of the agent's configuration (an AgentParams message).") + protoFormat := fs.String("format", "text", "The format (one of text, wire, or json) to read the configuration as.") + dryRunOnly := fs.Bool("dry-run", false, "Just validate the config, don't start the agent. Exits with a non-zero return code if the config is invalid.") + logLevel := logLevelFlag(zerolog.InfoLevel) + fs.Var(&logLevel, "log-level", "The log level (one of disabled, warn, panic, info, fatal, error, debug, or trace) to use.") + if err := fs.Parse(args); err == flag.ErrHelp { + fs.Usage() + return nil + } else if err != nil { + return err + } + + params, err := readParams(*confPath, *protoFormat) + if err != nil { + return err + } + log = (*zerolog.Ctx(ctx)).Level(zerolog.Level(logLevel)) + ctx = log.WithContext(ctx) + if *dryRunOnly { + log.Info().Msg("config is valid") + return nil + } + + ctx, shutdownTracer, err := injectTracer(ctx, params) + if err != nil { + return err + } + defer shutdownTracer() + + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + defer stop() + + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + if err := runChannelzServer(ctx, params); err != nil { + return fmt.Errorf("running channelz server: %w", err) + } + return nil + }) + g.Go(func() error { + if err := runPprofServer(ctx, params); err != nil { + return fmt.Errorf("running pprof server: %w", err) + } + return nil + }) + g.Go(func() error { + if err := ac.runAgent(ctx, params); err != nil { + return fmt.Errorf("running agent: %w", err) + } + return nil + }) + return g.Wait() +} + +const defaultMinConnectTimeout = 20 * time.Second + +// logLevelFlag is a flag.Value implementation for the zerolog.Level type. +type logLevelFlag zerolog.Level + +func (l *logLevelFlag) String() string { + return fmt.Sprintf("%q", zerolog.Level(*l).String()) +} + +func (l *logLevelFlag) Set(value string) error { + level, err := zerolog.ParseLevel(value) + if err != nil { + return err + } + + *l = logLevelFlag(level) + return nil +} + +func readParams(confPath, protoFormat string) (*configpb.AgentParams, error) { + if confPath == "" { + return nil, errors.New("no config (--config) provided") + } + pf, err := protofmt.FromString(protoFormat) + if err != nil { + return nil, fmt.Errorf("bad --format: %w", err) + } + + confData, err := os.ReadFile(confPath) + if err != nil { + return nil, err + } else if len(confData) == 0 { + return nil, errors.New("empty config (--config) provided") + } + + conf := &configpb.AgentParams{} + if err = pf.Unmarshal(confData, conf); err != nil { + return nil, fmt.Errorf("unmarshalling config proto: %w", err) + } + + return conf, err +} + +func injectTracer(ctx context.Context, params *configpb.AgentParams) (newCtx context.Context, shutdown func(), err error) { + endpoint := params.GetObservabilityParams().GetOtelCollectorEndpoint() + if endpoint == "" { + return task.InjectTracerProvider(ctx, oteltracenoop.NewTracerProvider()), func() {}, nil + } + + res, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("cdpi-agent"), + semconv.ServiceNamespaceKey.String("spacetime"), + semconv.ServiceVersion("v0.1.0"), + ), + ) + if err != nil { + return nil, nil, fmt.Errorf("creating tracer resources: %w", err) + } + + exporterOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(endpoint)} + if params.GetObservabilityParams().GetUseInsecureConnectionForOtelCollector() { + exporterOpts = append(exporterOpts, otlptracegrpc.WithInsecure()) + } + + exporter, err := otlptracegrpc.New(ctx, exporterOpts...) + if err != nil { + return nil, nil, err + } + tracerProvider := otelsdktrace.NewTracerProvider( + otelsdktrace.WithResource(res), + otelsdktrace.WithBatcher(exporter)) + + ctx = task.InjectTracerProvider(ctx, tracerProvider) + shutdown = func() { + if tpErr := tracerProvider.Shutdown(ctx); tpErr != nil { + (*zerolog.Ctx(ctx)).Error().Err(tpErr).Msg("error shutting down trace provider") + } + } + return ctx, shutdown, nil +} + +func getPrivateKey(ss *configpb.SigningStrategy) (io.Reader, error) { + switch ss.Type.(type) { + case *configpb.SigningStrategy_PrivateKeyBytes: + return bytes.NewBuffer(ss.GetPrivateKeyBytes()), nil + + case *configpb.SigningStrategy_PrivateKeyFile: + b, err := os.ReadFile(ss.GetPrivateKeyFile()) + if err != nil { + return nil, err + } + return bytes.NewBuffer(b), nil + + default: + return nil, errors.New("no signing strategy provided") + } +} + +func getProtoFmt(pfpb configpb.NetworkNode_ExternalCommand_ProtoFormat) protofmt.Format { + switch pfpb.Enum() { + case configpb.NetworkNode_ExternalCommand_JSON.Enum(): + return protofmt.JSON + case configpb.NetworkNode_ExternalCommand_TEXT.Enum(): + return protofmt.Text + case configpb.NetworkNode_ExternalCommand_WIRE.Enum(): + return protofmt.Wire + + case configpb.NetworkNode_ExternalCommand_PROTO_FORMAT_UNSPECIFIED.Enum(): + fallthrough + default: + return protofmt.JSON + } +} + +func getDialOpts(ctx context.Context, connParams *configpb.ConnectionParams, clock clockwork.Clock) ([]grpc.DialOption, error) { + tracerProvider, _ := task.ExtractTracerProvider(ctx) + + dialOpts := []grpc.DialOption{ + grpc.WithStreamInterceptor( + otelgrpc.StreamClientInterceptor( + otelgrpc.WithTracerProvider(tracerProvider), + otelgrpc.WithPropagators(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})), + )), + grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)), + grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: cmp.Or(connParams.GetKeepalivePeriod().AsDuration(), 30*time.Second), + PermitWithoutStream: true, + }), + } + + backoffParams := connParams.GetBackoffParams() + grpcBackoff := backoff.DefaultConfig + + if baseDelay := backoffParams.GetBaseDelay().AsDuration(); baseDelay > 0 { + grpcBackoff.BaseDelay = baseDelay + } + if maxDelay := backoffParams.GetBaseDelay().AsDuration(); maxDelay > 0 { + grpcBackoff.MaxDelay = maxDelay + } + + grpcConnParams := grpc.ConnectParams{Backoff: grpcBackoff, MinConnectTimeout: defaultMinConnectTimeout} + if minConnectTimeout := connParams.GetMinConnectTimeout().AsDuration(); minConnectTimeout > 0 { + grpcConnParams.MinConnectTimeout = minConnectTimeout + } + + switch connParams.GetTransportSecurity().GetType().(type) { + case *configpb.ConnectionParams_TransportSecurity_Insecure: + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + + case *configpb.ConnectionParams_TransportSecurity_SystemCertPool: + cp, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("reading system tls cert pool: %w", err) + } + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(cp, ""))) + + default: + return nil, errors.New("no transport security selection provided") + } + + dialOpts = append(dialOpts, grpc.WithConnectParams(grpcConnParams)) + + switch authStrat := connParams.GetAuthStrategy(); authStrat.Type.(type) { + case *configpb.AuthStrategy_None: + // ¯\_(ツ)_/¯ + + case *configpb.AuthStrategy_Jwt_: + jwtSpec := authStrat.GetJwt() + pkeySrc, err := getPrivateKey(jwtSpec.GetSigningStrategy()) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(connParams.GetEndpointUri()) + if err != nil { + return nil, fmt.Errorf("parsing %q: %w", connParams.GetEndpointUri(), err) + } + + creds, err := auth.NewCredentials(ctx, auth.Config{ + Clock: clock, + Email: jwtSpec.GetEmail(), + PrivateKeyID: jwtSpec.GetPrivateKeyId(), + PrivateKey: pkeySrc, + Host: host, + }) + if err != nil { + return nil, fmt.Errorf("generating authorization JWT: %w", err) + } + dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(creds)) + + default: + return nil, errors.New("no auth_strategy provided") + } + + return dialOpts, nil +} + +func (ac *AgentConf) getNodeOpts(ctx context.Context, node *configpb.NetworkNode, clock clockwork.Clock) (nodeOpts []agent.NodeOption, err error) { + // EndpointUri should be in the format `hostname[:port]`, but we want to backward support configs that used the dns:/// prefix. + const dnsSchema = "dns:///" + if cp := node.GetEnactmentDriver().GetConnectionParams(); cp != nil { + cp.EndpointUri = strings.TrimPrefix(cp.EndpointUri, dnsSchema) + } + if cp := node.GetTelemetryDriver().GetConnectionParams(); cp != nil { + cp.EndpointUri = strings.TrimPrefix(cp.EndpointUri, dnsSchema) + } + +enactmentSwitch: + switch conf := node.GetEnactmentDriver().GetType().(type) { + case *configpb.NetworkNode_EnactmentDriver_ExternalCommand: + dialOpts, err := getDialOpts(ctx, node.EnactmentDriver.GetConnectionParams(), clock) + if err != nil { + return nil, err + } + + enactCmd := node.GetEnactmentDriver().GetExternalCommand() + ed := enact_extproc.New(enactCmd.GetArgs(), getProtoFmt(enactCmd.GetProtoFormat())) + + nodeOpts = append(nodeOpts, agent.WithEnactmentDriver(node.GetEnactmentDriver().GetConnectionParams().EndpointUri, ed, dialOpts...)) + + case *configpb.NetworkNode_EnactmentDriver_Netlink: + dialOpts, err := getDialOpts(ctx, node.EnactmentDriver.GetConnectionParams(), clock) + if err != nil { + return nil, err + } + + ed, err := newNetlinkEnactmentDriver(ctx, clock, node.GetId(), conf.Netlink) + if err != nil { + return nil, err + } + nodeOpts = append(nodeOpts, agent.WithEnactmentDriver(node.GetEnactmentDriver().GetConnectionParams().EndpointUri, ed, dialOpts...)) + + case *configpb.NetworkNode_EnactmentDriver_Dynamic: + dialOpts, err := getDialOpts(ctx, node.EnactmentDriver.GetConnectionParams(), clock) + if err != nil { + return nil, err + } + + for _, p := range ac.Providers { + ed, err := p.EnactmentDriver(ctx, ac.Handles, node.GetId(), conf.Dynamic) + if errors.Is(err, ErrUnknownConfigProto) { + continue + } else if err != nil { + return nil, err + } + + nodeOpts = append(nodeOpts, agent.WithEnactmentDriver(node.GetEnactmentDriver().GetConnectionParams().EndpointUri, ed, dialOpts...)) + break enactmentSwitch + } + + return nil, fmt.Errorf("no provider recognized proto of type %s for node %s", conf.Dynamic.GetTypeUrl(), node.GetId()) + } + +telemetrySwitch: + switch conf := node.GetTelemetryDriver().GetType().(type) { + case *configpb.NetworkNode_TelemetryDriver_ExternalCommand: + dialOpts, err := getDialOpts(ctx, node.TelemetryDriver.GetConnectionParams(), clock) + if err != nil { + return nil, err + } + + telCmd := node.GetTelemetryDriver().GetExternalCommand() + td, err := telemetry_extproc.NewDriver(telCmd.GetCommand().GetArgs(), getProtoFmt(telCmd.GetCommand().GetProtoFormat()), telCmd.GetCollectionPeriod().AsDuration()) + if err != nil { + return nil, err + } + nodeOpts = append(nodeOpts, agent.WithTelemetryDriver(node.GetTelemetryDriver().GetConnectionParams().EndpointUri, td, dialOpts...)) + + case *configpb.NetworkNode_TelemetryDriver_Netlink: + dialOpts, err := getDialOpts(ctx, node.TelemetryDriver.GetConnectionParams(), clock) + if err != nil { + return nil, err + } + + td, err := newNetlinkTelemetryDriver(ctx, clock, node.GetId(), conf.Netlink) + if err != nil { + return nil, err + } + nodeOpts = append(nodeOpts, agent.WithTelemetryDriver(node.GetTelemetryDriver().GetConnectionParams().EndpointUri, td, dialOpts...)) + + case *configpb.NetworkNode_TelemetryDriver_Dynamic: + dialOpts, err := getDialOpts(ctx, node.TelemetryDriver.GetConnectionParams(), clock) + if err != nil { + return nil, err + } + + for _, p := range ac.Providers { + td, err := p.TelemetryDriver(ctx, ac.Handles, node.GetId(), conf.Dynamic) + if errors.Is(err, ErrUnknownConfigProto) { + continue + } else if err != nil { + return nil, err + } + + nodeOpts = append(nodeOpts, agent.WithTelemetryDriver(node.GetTelemetryDriver().GetConnectionParams().EndpointUri, td, dialOpts...)) + break telemetrySwitch + } + return nil, fmt.Errorf("no provider recognized proto of type %s for node %s", conf.Dynamic.GetTypeUrl(), node.GetId()) + } + + return nodeOpts, nil +} + +func runPprofServer(ctx context.Context, params *configpb.AgentParams) error { + addr := params.GetObservabilityParams().GetPprofAddress() + if addr == "" { + return nil + } + + lis, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) + if err != nil { + return err + } + + srv := &http.Server{ + BaseContext: func(_ net.Listener) context.Context { return ctx }, + Handler: http.DefaultServeMux, + } + log := zerolog.Ctx(ctx).With().Str("addr", lis.Addr().String()).Logger() + log.Info().Msg("starting pprof server") + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return srv.Serve(lis) + }) + g.Go(func() error { + <-ctx.Done() + log.Debug().Msg("stopping pprof server") + return srv.Shutdown(ctx) + }) + if err := g.Wait(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +func runChannelzServer(ctx context.Context, params *configpb.AgentParams) error { + addr := params.GetObservabilityParams().GetChannelzAddress() + if addr == "" { + return nil + } + + lis, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) + if err != nil { + return err + } + + srv := grpc.NewServer( + grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()), + grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()), + ) + channelz.RegisterChannelzServiceToServer(srv) + + log := zerolog.Ctx(ctx).With().Str("addr", lis.Addr().String()).Logger() + log.Info().Msg("starting channelz server") + + g, ctx := errgroup.WithContext(ctx) + g.Go(task.Task(func(ctx context.Context) error { + return srv.Serve(lis) + }).WithSpanAttributes(attribute.String("channelz.address", lis.Addr().String())).WithCtx(ctx)) + g.Go(func() error { + <-ctx.Done() + log.Debug().Msg("stopping channelz server") + srv.GracefulStop() + return nil + }) + return g.Wait() +} + +func (ac *AgentConf) runAgent(ctx context.Context, params *configpb.AgentParams) error { + clock := clockwork.NewRealClock() + + agentOpts := []agent.AgentOption{agent.WithClock(clock)} + + for _, node := range params.GetNetworkNodes() { + nodeOpts, err := ac.getNodeOpts(ctx, node, clock) + if err != nil { + return fmt.Errorf("node %s: %w", node.Id, err) + } + agentOpts = append(agentOpts, agent.WithNode(node.GetId(), nodeOpts...)) + } + + a, err := agent.NewAgent(agentOpts...) + if err != nil { + return err + } + + zerolog.Ctx(ctx).Info().Msg("starting agent") + return a.Run(ctx) +} diff --git a/agent/internal/agentcli/netlink_linux.go b/agent/internal/agentcli/netlink_linux.go new file mode 100644 index 0000000..bd07e73 --- /dev/null +++ b/agent/internal/agentcli/netlink_linux.go @@ -0,0 +1,54 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux + +package agentcli + +import ( + "context" + "fmt" + + "aalyria.com/spacetime/agent/enactment" + enact_netlink "aalyria.com/spacetime/agent/enactment/netlink" + "aalyria.com/spacetime/agent/internal/configpb" + "aalyria.com/spacetime/agent/telemetry" + telemetry_netlink "aalyria.com/spacetime/agent/telemetry/netlink" + + "github.com/jonboulle/clockwork" + vnl "github.com/vishvananda/netlink" +) + +func newNetlinkEnactmentDriver(ctx context.Context, clock clockwork.Clock, nodeID string, conf *configpb.NetworkNode_NetlinkEnactment) (enactment.Driver, error) { + nlHandle, err := vnl.NewHandle(vnl.FAMILY_ALL) + if err != nil { + return nil, fmt.Errorf("creating new netlink handle for enactments: %w", err) + } + + return enact_netlink.New( + enact_netlink.DefaultConfig( + ctx, + nlHandle, + int(conf.GetRouteTableId()), + int(conf.GetRouteTableLookupPriority()))), nil +} + +func newNetlinkTelemetryDriver(ctx context.Context, clock clockwork.Clock, nodeID string, conf *configpb.NetworkNode_NetlinkTelemetry) (telemetry.Driver, error) { + interfaceIDs := make([]string, 0, len(conf.GetMonitoredInterfaces())) + for _, mi := range conf.GetMonitoredInterfaces() { + interfaceIDs = append(interfaceIDs, mi.GetInterfaceId()) + } + + return telemetry_netlink.NewDriver(clock, interfaceIDs, vnl.LinkByName, conf.GetCollectionPeriod().AsDuration()) +} diff --git a/agent/internal/agentcli/netlink_other.go b/agent/internal/agentcli/netlink_other.go new file mode 100644 index 0000000..1684ee7 --- /dev/null +++ b/agent/internal/agentcli/netlink_other.go @@ -0,0 +1,36 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !linux + +package agentcli + +import ( + "context" + "fmt" + + "aalyria.com/spacetime/agent/enactment" + "aalyria.com/spacetime/agent/internal/configpb" + "aalyria.com/spacetime/agent/telemetry" + + "github.com/jonboulle/clockwork" +) + +func newNetlinkEnactmentDriver(ctx context.Context, clock clockwork.Clock, nodeID string, conf *configpb.NetworkNode_NetlinkEnactment) (enactment.Driver, error) { + return nil, fmt.Errorf("netlink enactment driver is unavailable on this platform") +} + +func newNetlinkTelemetryDriver(ctx context.Context, clock clockwork.Clock, nodeID string, conf *configpb.NetworkNode_NetlinkTelemetry) (telemetry.Driver, error) { + return nil, fmt.Errorf("netlink telemetry driver is unavailable on this platform") +} diff --git a/agent/internal/channels/BUILD b/agent/internal/channels/BUILD new file mode 100644 index 0000000..ae35c18 --- /dev/null +++ b/agent/internal/channels/BUILD @@ -0,0 +1,27 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "channels", + srcs = ["channels.go"], + importpath = "aalyria.com/spacetime/agent/internal/channels", + deps = [ + "//agent/internal/task", + "@com_github_rs_zerolog//:zerolog", + ], +) diff --git a/agent/internal/channels/channels.go b/agent/internal/channels/channels.go new file mode 100644 index 0000000..a04726a --- /dev/null +++ b/agent/internal/channels/channels.go @@ -0,0 +1,103 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package channels provide some simple adapters to facilitate common +// read/write patterns. +package channels + +import ( + "context" + + "aalyria.com/spacetime/agent/internal/task" + + "github.com/rs/zerolog" +) + +type Source[T any] <-chan T +type Sink[T any] chan<- T +type Receiver[T any] func() (T, error) +type Sender[T any] func(T) error + +// NewSource takes a readable channel and returns a Source, which can be used +// to chain common transformations fluently. +func NewSource[T any](c <-chan T) Source[T] { return Source[T](c) } + +// NewSink takes a writable channel and returns a Sink, which can be used to +// chain common transformations fluently. +func NewSink[T any](c chan<- T) Sink[T] { return Sink[T](c) } + +func (s Sink[T]) FillFrom(recv Receiver[T]) task.Task { + return task.Task(func(ctx context.Context) error { + log := zerolog.Ctx(ctx) + msg, err := recv() + if err != nil { + return err + } + + select { + case <-ctx.Done(): + log.Warn().Msg("discarding received msg because context was cancelled") + return context.Cause(ctx) + case s <- msg: + } + + return nil + }). + WithNewSpan("channels.Sink.FillFrom"). + LoopUntilError() +} + +func (s Source[T]) ForwardTo(send Sender[T]) task.Task { + return task.Task(func(ctx context.Context) error { + select { + case <-ctx.Done(): + return context.Cause(ctx) + + case msg := <-s: + if err := send(msg); err != nil { + return err + } + } + return nil + }). + WithNewSpan("channels.Source.ForwardTo"). + LoopUntilError() +} + +type MapFn[A, B any] func(context.Context, A) (B, error) + +func MapBetween[A, B any](src Source[A], dst Sink[B], fn MapFn[A, B]) task.Task { + return task.Task(func(ctx context.Context) error { + var before A + select { + case <-ctx.Done(): + return context.Cause(ctx) + case before = <-src: + } + + after, err := fn(ctx, before) + if err != nil { + return err + } + + select { + case <-ctx.Done(): + return context.Cause(ctx) + case dst <- after: + } + return nil + }). + WithNewSpan("channels.MapBetween"). + LoopUntilError() +} diff --git a/agent/internal/configpb/BUILD b/agent/internal/configpb/BUILD new file mode 100644 index 0000000..a144aad --- /dev/null +++ b/agent/internal/configpb/BUILD @@ -0,0 +1,38 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "configpb_proto", + srcs = ["config.proto"], + deps = [ + "//api/common:common_proto", + "@protobuf//:any_proto", + "@protobuf//:duration_proto", + "@protobuf//:empty_proto", + ], +) + +go_proto_library( + name = "configpb_go_proto", + importpath = "aalyria.com/spacetime/agent/internal/configpb", + proto = ":configpb_proto", + deps = [ + "//api/common:common_go_proto", + ], +) diff --git a/agent/internal/configpb/config.proto b/agent/internal/configpb/config.proto new file mode 100644 index 0000000..3db5f25 --- /dev/null +++ b/agent/internal/configpb/config.proto @@ -0,0 +1,243 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Structured configuration for the reference SBI agent binary. + +syntax = "proto3"; + +package aalyria.spacetime.agent.cmd.agent; + +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/any.proto"; +import "api/common/control.proto"; + +option go_package = "aalyria.com/spacetime/agent/internal/configpb"; + +// A strategy for signing a JWT. +message SigningStrategy { + oneof type { + // The bytes of a PEM-encoded RSA private key in PKCS #1, ASN.1 DER form or + // in (unencrypted) PKCS #8, ASN.1 DER form. + bytes private_key_bytes = 1; + // The path to a PEM-encoded RSA private key in PKCS #1, ASN.1 DER form or + // in (unencrypted) PKCS #8, ASN.1 DER form. + string private_key_file = 2; + } +} + +message AuthStrategy { + // Jwt is a JSON web token. See https://jwt.io/introduction for more + // information. + message Jwt { + string email = 1; + string audience = 2; + string private_key_id = 3; + SigningStrategy signing_strategy = 4; + } + + oneof type { + // The specifications for a JWT that should be generated and signed by the + // agent. + Jwt jwt = 1; + + // No authentication options should be used. This is unlikely to work for + // you. + google.protobuf.Empty none = 2; + } +} + +message ConnectionParams { + message TransportSecurity { + oneof type { + // Don't use TLS, connect using plain-text HTTP/2. + google.protobuf.Empty insecure = 1; + + // Use the system certificate pool for TLS. + google.protobuf.Empty system_cert_pool = 2; + } + } + + // BackoffParams mirror the gRPC backoff parameters. See + // https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md for + // more details. + message BackoffParams { + // The amount of time to backoff after the first connection failure. + google.protobuf.Duration base_delay = 1; + // The factor with which to multiply backoffs after a failed retry. Should + // ideally be greater than 1. + double multiplier = 2; + // The factor with which backoffs are randomized. + double jitter = 3; + // The upper bound of backoff delay. + google.protobuf.Duration max_delay = 4; + } + + // The transport security options to use. Required. + TransportSecurity transport_security = 1; + // The "host[:port]" network endpoint the relevant service. Required. + string endpoint_uri = 2; + // The strategy to use for authorization. Required. + AuthStrategy auth_strategy = 3; + // Parameters to control the gRPC backoff behavior. Defaults to the standard + // gRPC backoff parameters described in + // https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md. + BackoffParams backoff_params = 4; + // The minimum amount of time to wait for a connection to complete. Defaults + // to 20 seconds. + google.protobuf.Duration min_connect_timeout = 5; + // The amount of time between keepalive pings on an open connection. Can't be + // lower than 10s. See + // https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md + google.protobuf.Duration keepalive_period = 6; +} + +// A node the agent represents. +message NetworkNode { + // An external command is a delegate process that handles implementing some + // aspect of the SBI contract. Exit codes within the range of the canonical + // gRPC status codes will be transformed (alongside any messages written to + // standard error) into an appropriate gRPC status. + // + // See https://pkg.go.dev/google.golang.org/grpc/codes#Code for the status + // code integer mappings. + message ExternalCommand { + enum ProtoFormat { + PROTO_FORMAT_UNSPECIFIED = 0; + + // Use the canonical protojson format for proto messages. This is the + // default. See https://protobuf.dev/programming-guides/proto3/#json + JSON = 1; + // Use the canonical text format for proto messages. See + // https://protobuf.dev/reference/protobuf/textformat-spec/ + // + // NOTE: Because text format protos are considered overly fragile for + // interchange purposes, its use isn't recommended in production. Prefer + // JSON or wire formats outside of development / debugging scenarios. See + // https://protobuf.dev/programming-guides/dos-donts/#never-use-text-format-messages-for-interchange + TEXT = 2; + // Use the binary wire format for proto messages. See + // https://protobuf.dev/programming-guides/encoding/ + WIRE = 3; + } + + // The arguments (starting with the executable) to execute. + repeated string args = 1; + // The format to write and read proto messages in. Defaults to JSON. + ProtoFormat proto_format = 2; + } + + // NetlinkEnactment does not yet require any specialty fields, but may in the future. + message NetlinkEnactment { + // The route table number in which destination routes will be managed. + int32 route_table_id = 1; + + // The Linux PBR priority to use for looking up routes in table + // |route_table_id| (above). + int32 route_table_lookup_priority = 2; + } + + // An EnactmentDriver is responsible for processing incoming enactments for + // a given node. + message EnactmentDriver { + ConnectionParams connection_params = 1; + + oneof type { + // Use an external command to process enactments. The command will + // receive a ScheduledControlUpdate message over standard input. The + // command may optionally write a new ControlPlaneState message to + // standard out. + ExternalCommand external_command = 2; + + // Use the Linux-only netlink APIs to process enactments. + NetlinkEnactment netlink = 3; + + // Use an agent-specific driver to process enactments. + google.protobuf.Any dynamic = 4; + } + } + + message ExternalCommandTelemetry { + // How often the metrics will be gathered and reported. + google.protobuf.Duration collection_period = 1; + + ExternalCommand command = 2; + } + + message MonitoredInterface { + // The interface_id as specified by the NetworkNode's NetworkInterface. + string interface_id = 1; + } + + message NetlinkTelemetry { + // How often the metrics will be gathered and reported. + google.protobuf.Duration collection_period = 2; + + repeated MonitoredInterface monitored_interfaces = 1; + } + + // A TelemetryDriver is responsible for generating telemetry reports for a + // given node. + message TelemetryDriver { + ConnectionParams connection_params = 1; + + oneof type { + // Use an external command to handle telemetry updates. The command + // should write a NetworkStatsReport message in the appropriate format to + // standard out when executed. + ExternalCommandTelemetry external_command = 2; + + // Use the Linux-only netlink APIs to gather telemetry. + NetlinkTelemetry netlink = 3; + + // Use an agent-specific driver to gather telemetry. + google.protobuf.Any dynamic = 4; + } + } + + // The node's ID. + string id = 1; + + EnactmentDriver enactment_driver = 2; + TelemetryDriver telemetry_driver = 3; +} + +message ObservabilityParams { + // The OTEL gRPC collector endpoint the exporter will connect to. If blank, + // no exporter will be configured. + string otel_collector_endpoint = 1; + // Whether to connect to the OTEL gRPC collector over a plaintext (insecure) + // connection. Corresponds to the "WithInsecure" exporter option + // https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc#WithInsecure + bool use_insecure_connection_for_otel_collector = 4; + + // Channelz is a gRPC introspection service that can aid in debugging and + // understanding gRPC behavior. See + // https://grpc.io/blog/a-short-introduction-to-channelz/ and + // https://github.com/grpc/proposal/blob/master/A14-channelz.md for more + // details. + // + // The address to start the channelz server on. If blank, the channelz + // server will not be started. + string channelz_address = 2; + + // The address to start Go's standard net/http/pprof server on. If blank, the + // pprof server will not be started. + string pprof_address = 3; +} + +message AgentParams { + ObservabilityParams observability_params = 2; + repeated NetworkNode network_nodes = 3; +} diff --git a/agent/internal/extprocs/BUILD b/agent/internal/extprocs/BUILD new file mode 100644 index 0000000..3c14edd --- /dev/null +++ b/agent/internal/extprocs/BUILD @@ -0,0 +1,37 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "extprocs", + srcs = ["extprocs.go"], + importpath = "aalyria.com/spacetime/agent/internal/extprocs", + deps = [ + "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//status", + ], +) + +go_test( + name = "extprocs_test", + srcs = ["extprocs_test.go"], + embed = [":extprocs"], + deps = [ + "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//status", + ], +) diff --git a/agent/internal/extprocs/extprocs.go b/agent/internal/extprocs/extprocs.go new file mode 100644 index 0000000..656c131 --- /dev/null +++ b/agent/internal/extprocs/extprocs.go @@ -0,0 +1,56 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package extprocs provides common utilities shared between the extproc +// backends. +package extprocs + +import ( + "errors" + "fmt" + "os/exec" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// The first invalid codes.Code value. Used to coerce exit codes into +// reasonable gRPC codes. This should match the constants in the gRPC codes +// package: +// https://github.com/grpc/grpc-go/blob/fe39661ffe8a83227c5c40591f335176aa7e5153/codes/codes.go#L195 +const maxCode = 17 + +// CommandError turns an *exec.ExitError into a gRPC-flavored error using the +// command's exit code as the gRPC code (if valid) and the command's stderr (or +// an excerpt from it) as the error message. If the provided error is not an +// *exec.ExitError then the error is wrapped with a ready-to-display message. +func CommandError(err error) error { + if err != nil { + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) { + return fmt.Errorf("running command: %w", err) + } + + c := codes.Code(exitErr.ExitCode()) + if c >= maxCode { + c = codes.Unknown + } + + return status.Error(c, strings.TrimSpace(string(exitErr.Stderr))) + } + + // shouldn't happen + return nil +} diff --git a/agent/internal/extprocs/extprocs_test.go b/agent/internal/extprocs/extprocs_test.go new file mode 100644 index 0000000..e621b0c --- /dev/null +++ b/agent/internal/extprocs/extprocs_test.go @@ -0,0 +1,75 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extprocs + +import ( + "errors" + "fmt" + "os/exec" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type grpcError interface { + GRPCStatus() *status.Status +} + +func TestCommandError_compliantCommand(t *testing.T) { + _, err := exec.Command("/bin/sh", "-c", `echo >&2 "my error message"; exit 3;`).Output() + cmdErr := CommandError(err) + se, ok := cmdErr.(grpcError) + if !ok { + t.Errorf("expected CommandError to convert %v into a gRPC error, but got %v", err, cmdErr) + } + + st := se.GRPCStatus() + if want := codes.InvalidArgument; st.Code() != want { + t.Errorf("expected CommandError to have a code of %v, but got %v", want, st.Code()) + } + if want := "my error message"; st.Message() != want { + t.Errorf("expected CommandError to have a message of %q, but got %q", want, st.Message()) + } +} + +func TestCommandError_unknownError(t *testing.T) { + _, err := exec.Command("/bin/sh", "-c", fmt.Sprintf(`echo >&2 "some error message"; exit %d;`, maxCode+1)).Output() + cmdErr := CommandError(err) + se, ok := cmdErr.(grpcError) + if !ok { + t.Errorf("expected CommandError to convert %v into a gRPC error, but got %v", err, cmdErr) + } + + st := se.GRPCStatus() + if want := codes.Unknown; st.Code() != want { + t.Errorf("expected CommandError to have a code of %v, but got %v", want, st.Code()) + } + if want := "some error message"; st.Message() != want { + t.Errorf("expected CommandError to have a message of %q, but got %q", want, st.Message()) + } +} + +func TestCommandError_notAnExitError(t *testing.T) { + err := errors.New("some non-exit error") + cmdErr := CommandError(err) + _, ok := cmdErr.(grpcError) + if ok { + t.Errorf("expected CommandError NOT to convert %v into a gRPC error, but got %v", err, cmdErr) + } + if want := "running command: some non-exit error"; cmdErr.Error() != want { + t.Errorf("expected CommandError to return %q, but got %q", want, cmdErr.Error()) + } +} diff --git a/agent/internal/loggable/BUILD b/agent/internal/loggable/BUILD new file mode 100644 index 0000000..5d1d1c2 --- /dev/null +++ b/agent/internal/loggable/BUILD @@ -0,0 +1,41 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "loggable", + srcs = ["loggable.go"], + importpath = "aalyria.com/spacetime/agent/internal/loggable", + deps = [ + "@com_github_rs_zerolog//:zerolog", + "@org_golang_google_protobuf//proto", + ], +) + +go_test( + name = "loggable_test", + size = "small", + srcs = ["loggable_test.go"], + embed = [":loggable"], + deps = [ + "@com_github_google_go_cmp//cmp", + "@com_github_rs_zerolog//:zerolog", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/durationpb", + "@org_golang_google_protobuf//types/known/structpb", + ], +) diff --git a/agent/internal/loggable/loggable.go b/agent/internal/loggable/loggable.go new file mode 100644 index 0000000..39e0abd --- /dev/null +++ b/agent/internal/loggable/loggable.go @@ -0,0 +1,36 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package loggable provides some adaptors for logging Spacetime domain objects +// using the zerolog library. +package loggable + +import ( + "github.com/rs/zerolog" + "google.golang.org/protobuf/proto" +) + +// Returns a zerolog adaptor for the given proto. Since marshalling uses +// reflection (both proto reflection and runtime reflection), this should +// probably only be used for trace or debug logging or in codepaths where +// performance is not a concern. +func Proto(m proto.Message) zerolog.LogObjectMarshaler { + return protoObject{m} +} + +type protoObject struct{ proto.Message } + +func (p protoObject) MarshalZerologObject(ev *zerolog.Event) { + ev.Interface("proto", p.Message.ProtoReflect().Interface()) +} diff --git a/agent/internal/loggable/loggable_test.go b/agent/internal/loggable/loggable_test.go new file mode 100644 index 0000000..205f878 --- /dev/null +++ b/agent/internal/loggable/loggable_test.go @@ -0,0 +1,117 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loggable + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/rs/zerolog" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/structpb" +) + +func encode(key string, m proto.Message) []byte { + buf := &bytes.Buffer{} + logger := zerolog.New(buf) + logger.Info(). + Object("msg", Proto(m)). + Msg("testing proto marshaller") + return buf.Bytes() +} + +func TestSimpleProto(t *testing.T) { + dur := &durationpb.Duration{Seconds: 12} + + bytes := encode("msg", dur) + + var got struct { + LogMsg struct { + Proto struct { + Seconds int64 `json:"seconds"` + } `json:"proto"` + } `json:"msg"` + } + if err := json.Unmarshal(bytes, &got); err != nil { + t.Error("unmarshalling error", err) + } + if got.LogMsg.Proto.Seconds != dur.GetSeconds() { + t.Errorf("(%s) got %#v, wanted %v", string(bytes), got, dur) + } +} + +func TestStructProto(t *testing.T) { + orig := map[string]interface{}{ + "firstName": "John", + "lastName": "Smith", + "isAlive": true, + "age": float64(27), + "address": map[string]interface{}{ + "streetAddress": "21 2nd Street", + "city": "New York", + "state": "NY", + "postalCode": "10021-3100", + }, + "phoneNumbers": []interface{}{ + map[string]interface{}{ + "type": "home", + "number": "212 555-1234", + }, + map[string]interface{}{ + "type": "office", + "number": "646 555-4567", + }, + }, + "children": []interface{}{}, + "spouse": nil, + } + + m, err := structpb.NewStruct(orig) + if err != nil { + t.Fatal("error creating struct from map[string]interface:", err) + } + + gotPayload := map[string]interface{}{} + if err := json.Unmarshal(encode("msg", m), &gotPayload); err != nil { + t.Error("unmarshalling error", err) + } + if diff := cmp.Diff(orig, gotPayload["msg"].(map[string]interface{})["proto"]); diff != "" { + t.Errorf("mismatch: (-want +got):\n%s", diff) + t.FailNow() + } +} + +func TestStructProtoWithExtraSpaces(t *testing.T) { + orig := map[string]interface{}{ + "a field with carriage returns": "Hello\rWorld", + } + + m, err := structpb.NewStruct(orig) + if err != nil { + t.Fatal("error creating struct from map[string]interface:", err) + } + payload := map[string]interface{}{} + if err := json.Unmarshal(encode("msg", m), &payload); err != nil { + t.Error("unmarshalling error", err) + } + got := payload["msg"].(map[string]interface{})["proto"] + if diff := cmp.Diff(orig, got); diff != "" { + t.Errorf("mismatch: (-want +got):\n%s", diff) + t.FailNow() + } +} diff --git a/agent/internal/protofmt/BUILD b/agent/internal/protofmt/BUILD new file mode 100644 index 0000000..174a3ae --- /dev/null +++ b/agent/internal/protofmt/BUILD @@ -0,0 +1,34 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "protofmt", + srcs = ["protofmt.go"], + importpath = "aalyria.com/spacetime/agent/internal/protofmt", + deps = [ + "@org_golang_google_protobuf//encoding/protojson", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + ], +) + +go_test( + name = "protofmt_test", + srcs = ["protofmt_test.go"], + embed = [":protofmt"], +) diff --git a/agent/internal/protofmt/protofmt.go b/agent/internal/protofmt/protofmt.go new file mode 100644 index 0000000..19f1813 --- /dev/null +++ b/agent/internal/protofmt/protofmt.go @@ -0,0 +1,84 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protofmt + +import ( + "fmt" + "strings" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" +) + +type Format int + +const ( + JSON Format = iota + Wire + Text +) + +func FromString(s string) (Format, error) { + switch strings.ToLower(s) { + case "text": + return Text, nil + case "json": + return JSON, nil + case "wire": + return Wire, nil + default: + return 0, fmt.Errorf("unknown proto format: %q", s) + } +} + +func (pf Format) String() string { + switch pf { + case JSON: + return "JSON" + case Text: + return "text" + case Wire: + return "wire" + default: + return "unknown" + } +} + +func (pf Format) Marshal(m proto.Message) ([]byte, error) { + switch pf { + case JSON: + return protojson.Marshal(m) + case Text: + return prototext.Marshal(m) + case Wire: + return proto.Marshal(m) + default: + return nil, fmt.Errorf("marshal: unknown format: %v", pf) + } +} + +func (pf Format) Unmarshal(data []byte, m proto.Message) error { + switch pf { + case JSON: + return protojson.Unmarshal(data, m) + case Text: + return prototext.Unmarshal(data, m) + case Wire: + return proto.Unmarshal(data, m) + default: + return fmt.Errorf("unmarshal: unknown format: %v", pf) + } +} diff --git a/agent/internal/protofmt/protofmt_test.go b/agent/internal/protofmt/protofmt_test.go new file mode 100644 index 0000000..693f64e --- /dev/null +++ b/agent/internal/protofmt/protofmt_test.go @@ -0,0 +1,52 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package protofmt + +import ( + "fmt" + "testing" +) + +func TestString_roundTripping(t *testing.T) { + for _, n := range []Format{JSON, Wire, Text} { + t.Run(n.String(), func(t *testing.T) { + pf, err := FromString(n.String()) + if err != nil { + t.Errorf("got unexpected error from FromString(%q): %v", n.String(), err) + return + } + + if n != pf { + t.Errorf("expected round-tripping %v to be the same, but got %v", n, pf) + } + }) + } +} + +func TestFromString(t *testing.T) { + for s, f := range map[string]Format{"json": JSON, "JSON": JSON, "text": Text, "wire": Wire} { + t.Run(fmt.Sprintf("%q -> %v", s, f), func(t *testing.T) { + pf, err := FromString(s) + if err != nil { + t.Errorf("got unexpected error from FromString(%q): %v", s, err) + return + } + + if f != pf { + t.Errorf("expected FromString(%q) to return %v, but got %v", s, f, pf) + } + }) + } +} diff --git a/agent/internal/task/BUILD b/agent/internal/task/BUILD new file mode 100644 index 0000000..c7c1a06 --- /dev/null +++ b/agent/internal/task/BUILD @@ -0,0 +1,42 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "task", + srcs = ["task.go"], + importpath = "aalyria.com/spacetime/agent/internal/task", + deps = [ + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + "@io_opentelemetry_go_otel//attribute", + "@io_opentelemetry_go_otel//codes", + "@io_opentelemetry_go_otel_trace//:trace", + "@org_golang_x_sync//errgroup", + ], +) + +go_test( + name = "task_test", + size = "small", + srcs = ["task_test.go"], + embed = [":task"], + deps = [ + "@io_opentelemetry_go_otel_sdk//trace", + "@io_opentelemetry_go_otel_trace//noop", + ], +) diff --git a/agent/internal/task/task.go b/agent/internal/task/task.go new file mode 100644 index 0000000..8d4baa5 --- /dev/null +++ b/agent/internal/task/task.go @@ -0,0 +1,277 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package task provides some useful helpers to express common tasks like +// retries and adding contextual information to a context-scoped logger. +package task + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "math" + "math/big" + "time" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + otelcodes "go.opentelemetry.io/otel/codes" + oteltrace "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" +) + +var errNoTasks = errors.New("no tasks provided") + +// Task is an abstraction over any context-bound, fallible activity. +type Task func(context.Context) error + +type RetryConfig struct { + Clock clockwork.Clock + MaxRetries int + BackoffDuration time.Duration + ErrIsFatal func(error) bool +} + +// Float64 generates a float64 between [0, 1). It uses the technique described +// in the comments on math/rand's Float64 implementation: +// https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/math/rand/rand.go;l=190;drc=c29444ef39a44ad56ddf7b3d2aa8a51df1163e04 +func Float64() float64 { + const maxMantissa = 1 << 53 + nBig, err := rand.Int(rand.Reader, big.NewInt(maxMantissa)) + if err != nil { + panic(err) + } + return float64(nBig.Int64() / maxMantissa) +} + +// WithRetries returns a new task that will retry the inner task +// according to the provided retryConfig. +func (t Task) WithRetries(rc RetryConfig) Task { + return func(ctx context.Context) error { + log := zerolog.Ctx(ctx) + var err error + + for retryCount := 0; rc.MaxRetries == 0 || retryCount <= rc.MaxRetries; retryCount++ { + select { + case <-ctx.Done(): + return context.Cause(ctx) + default: + } + + if err = t(ctx); err != nil { + if rc.ErrIsFatal != nil && rc.ErrIsFatal(err) { + return err + } + + // delayDur is within [0.5 * backoff, 1.5 * backoff] + randFact := Float64() - 0.5 + jitterMs := time.Millisecond * time.Duration( + math.Round(randFact*float64(rc.BackoffDuration.Milliseconds()))) + delayDur := rc.BackoffDuration + jitterMs + + log.Error(). + Err(err). + Dur("backoffDelay", delayDur). + Msg("error, retrying shortly") + + timer := rc.Clock.NewTimer(delayDur) + select { + case <-ctx.Done(): + if !timer.Stop() { + // drain the timer channel if we weren't able to stop it + <-timer.Chan() + } + + case <-timer.Chan(): + } + } + } + return err + } +} + +// WithLogContext returns a new task that will apply the provided function +// to the context-scoped logger before invoking the inner task. +func (t Task) WithLogContext(fn func(zerolog.Context) zerolog.Context) Task { + return func(ctx context.Context) error { + log := fn(zerolog.Ctx(ctx).With()).Logger() + return t(log.WithContext(ctx)) + } +} + +// WithLogField returns a new task that will add the provided key/value pair +// to the context-scoped logger before invoking the inner task. +func (t Task) WithLogField(k, v string) Task { + return t.WithLogContext(func(logctx zerolog.Context) zerolog.Context { + return logctx.Str(k, v) + }) +} + +// WithStartingStoppingLogs returns a new task that will log a "starting" +// message before and a "stopping" message after invoking the inner task. +func (t Task) WithStartingStoppingLogs(name string, lvl zerolog.Level) Task { + return func(ctx context.Context) error { + log := zerolog.Ctx(ctx) + + log.WithLevel(lvl).Msg(name + " starting") + err := t(ctx) + log.WithLevel(lvl).Err(err).Msg(name + " stopping") + + return err + } +} + +// WithCtx converts the inner task into a `func() error` that uses the provided +// context. +func (t Task) WithCtx(ctx context.Context) func() error { + return func() error { return t(ctx) } +} + +// AsThunk returns a no-argument, no-result function that calls the inner task +// and sets the provided `err` pointer to the returned error. Note that `err` +// will be overwritten regardless of the inner function result. +func (t Task) AsThunk(ctx context.Context, err *error) func() { + return func() { *err = t(ctx) } +} + +func Noop() Task { return func(_ context.Context) error { return nil } } + +// WithOtelTracerProvider returns a task that injects the provided +// TracerProvider into the context before invoking the inner task. +func (t Task) WithOtelTracerProvider(tp oteltrace.TracerProvider) Task { + return func(ctx context.Context) error { + return t(InjectTracerProvider(ctx, tp)) + } +} + +// WithOtelTracer returns a task that injects a tracer, created using the `pkg` +// argument, into the context before invoking the inner task. +func (t Task) WithOtelTracer(pkg string) Task { + return func(ctx context.Context) error { + log := zerolog.Ctx(ctx) + log.Trace().Msg("adding otel tracer to context") + + tp, ok := ExtractTracerProvider(ctx) + if !ok { + return fmt.Errorf("tracing requested, but no tracer provider present in context") + } + return t(InjectTracer(ctx, tp.Tracer(pkg))) + } +} + +// WithNewSpan starts a new trace span named `name`. +func (t Task) WithNewSpan(name string, opts ...oteltrace.SpanStartOption) Task { + return func(ctx context.Context) (err error) { + if tracer, ok := ExtractTracer(ctx); ok { + var span oteltrace.Span + ctx, span = tracer.Start(ctx, name, opts...) + defer func() { + if err != nil { + span.SetStatus(otelcodes.Error, err.Error()) + span.RecordError(err) + } + span.End() + }() + } + + return t(ctx) + } +} + +// WithSpanAttributes returns a task that sets the trace span attributes to +// `attrs` before invoking the inner task. +func (t Task) WithSpanAttributes(attrs ...attribute.KeyValue) Task { + return func(ctx context.Context) error { + span := oteltrace.SpanFromContext(ctx) + span.SetAttributes(attrs...) + + return t(ctx) + } +} + +func (t Task) WithPanicCatcher() Task { + return func(ctx context.Context) (err error) { + defer func() { + if val := recover(); val != nil { + err = fmt.Errorf("panic: %v", val) + } + }() + + return t(ctx) + } +} + +// Group takes a sequence of Tasks and returns a task that starts them all in +// separate goroutines and waits for them all to finish. The semantics mirror +// those of the errgroup package, so only the first non-nil error will be +// returned. +func Group(fns ...Task) Task { + if len(fns) == 0 { + return func(ctx context.Context) error { return errNoTasks } + } + + return func(ctx context.Context) error { + g, ctx := errgroup.WithContext(ctx) + for _, f := range fns { + g.Go(f.WithCtx(ctx)) + } + return g.Wait() + } +} + +// LoopUntilError returns a Task that runs the inner task in a loop until it +// returns a non-nil error. +func (t Task) LoopUntilError() Task { + return func(ctx context.Context) error { + for { + if err := t(ctx); err != nil { + return err + } + } + } +} + +type ( + tracerKey struct{} + tracerProviderKey struct{} +) + +// ExtractTracer extracts the otel tracer from the provided context. Use +// `InjectTracer` to prepare a context for use with this function. +func ExtractTracer(ctx context.Context) (oteltrace.Tracer, bool) { + t, ok := ctx.Value(tracerKey{}).(oteltrace.Tracer) + return t, ok +} + +// InjectTracer returns a new context with the provided Tracer injected. Use +// `ExtractTracer` to retrieve it. +func InjectTracer(ctx context.Context, t oteltrace.Tracer) context.Context { + return context.WithValue(ctx, tracerKey{}, t) +} + +// ExtractTracerProvider extracts the otel tracer provider from the provided +// context. Use `InjectTracerProvider` to prepare a context for use with this +// function. +func ExtractTracerProvider(ctx context.Context) (oteltrace.TracerProvider, bool) { + tp, ok := ctx.Value(tracerProviderKey{}).(oteltrace.TracerProvider) + return tp, ok +} + +// InjectTracerProvider returns a new context with the provided TracerProvider +// injected. Use `ExtractTracerProvider` to retrieve it. +func InjectTracerProvider(ctx context.Context, tp oteltrace.TracerProvider) context.Context { + return context.WithValue(ctx, tracerProviderKey{}, tp) +} diff --git a/agent/internal/task/task_test.go b/agent/internal/task/task_test.go new file mode 100644 index 0000000..362fd13 --- /dev/null +++ b/agent/internal/task/task_test.go @@ -0,0 +1,96 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package task + +import ( + "context" + "errors" + "reflect" + "testing" + + otelsdktrace "go.opentelemetry.io/otel/sdk/trace" + oteltracenoop "go.opentelemetry.io/otel/trace/noop" +) + +func TestContextGetters_Tracer(t *testing.T) { + want := oteltracenoop.NewTracerProvider().Tracer("task") + + ctx := InjectTracer(context.Background(), want) + got, ok := ExtractTracer(ctx) + if !ok { + t.Errorf("wasn't able to retrieve injected tracer") + return + } + + if !reflect.DeepEqual(want, got) { + t.Errorf("got tracer %#v, but wanted tracer %#v", got, want) + } +} + +func TestContextGetters_TracerProvider(t *testing.T) { + want := otelsdktrace.NewTracerProvider() + + ctx := InjectTracerProvider(context.Background(), want) + got, ok := ExtractTracerProvider(ctx) + if !ok { + t.Errorf("wasn't able to retrieve injected tracer") + return + } + + if want != got { + t.Errorf("got tracer %#v, but wanted tracer %#v", got, want) + } +} + +func TestAsThunk(t *testing.T) { + want := context.DeadlineExceeded + + var got error + Task(func(_ context.Context) error { return want }). + AsThunk(context.Background(), &got)() + + if got != want { + t.Errorf("AsThunk didn't set err correctly, got %v but wanted %v", got, want) + } +} + +func TestAsThunk_disregardsPreviousErr(t *testing.T) { + want := context.DeadlineExceeded + + got := context.Canceled + Task(func(_ context.Context) error { return want }). + AsThunk(context.Background(), &got)() + + if got != want { + t.Errorf("AsThunk didn't set err correctly, got %v but wanted %v", got, want) + } +} + +func TestGroup_emptyGroup(t *testing.T) { + err := Group()(context.Background()) + if err != errNoTasks { + t.Errorf("unexpected result from empty group, got %v but wanted %v", err, errNoTasks) + } +} + +func TestWithPanicCatcher(t *testing.T) { + badTask := func(_ context.Context) error { panic("bad") } + + got := Task(badTask).WithPanicCatcher()(context.Background()) + want := errors.New("panic: bad") + if got.Error() != want.Error() { + t.Errorf("expected result from WithPanicCatcher to be %v, but got %v", want, got) + } +} diff --git a/agent/internal/worker/BUILD b/agent/internal/worker/BUILD new file mode 100644 index 0000000..168fe79 --- /dev/null +++ b/agent/internal/worker/BUILD @@ -0,0 +1,35 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "worker", + srcs = ["worker.go"], + importpath = "aalyria.com/spacetime/agent/internal/worker", +) + +go_test( + name = "worker_test", + srcs = ["worker_test.go"], + embed = [":worker"], + deps = [ + "@com_github_google_go_cmp//cmp", + "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_jonboulle_clockwork//:clockwork", + "@org_golang_x_sync//errgroup", + ], +) diff --git a/agent/internal/worker/worker.go b/agent/internal/worker/worker.go new file mode 100644 index 0000000..47d0759 --- /dev/null +++ b/agent/internal/worker/worker.go @@ -0,0 +1,140 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package worker + +import ( + "context" + "sync" +) + +// Queue is something that can enqueue a work item for asynchronous processing. +type Queue[T any] interface { + Enqueue(T) bool +} + +// SerialQueue is an extremely basic Queue implementation that handles incoming +// work items one by one. +type SerialQueue[T any] struct { + mu sync.Mutex + workFn func(context.Context, T) error + ready chan struct{} + doneCh <-chan struct{} + pending []T +} + +type Pool interface { + Go(func() error) +} + +// NewSerialQueue creates a new serial queue that will handle incoming work +// items one by one. The single worker is stopped once the provided context is +// canceled. Depending on the `pool` implementation, if the `workFn` returns a +// non-nil error the context may be canceled. In both cases, any pending work +// items are lost. +func NewSerialQueue[T any](ctx context.Context, pool Pool, workFn func(context.Context, T) error) Queue[T] { + sq := &SerialQueue[T]{ + workFn: workFn, + mu: sync.Mutex{}, + ready: make(chan struct{}, 1), + doneCh: ctx.Done(), + } + + pool.Go(func() error { return sq.start(ctx) }) + + return sq +} + +func (q *SerialQueue[T]) start(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case <-q.ready: + } + + q.mu.Lock() + todo := q.pending + q.pending = nil + q.mu.Unlock() + + for _, next := range todo { + if err := q.workFn(ctx, next); err != nil { + return err + } + } + } +} + +// Add the provided `item` to the queue. Returns true if the item was added +// successfully. +func (q *SerialQueue[T]) Enqueue(item T) bool { + select { + case <-q.doneCh: + return false + default: + } + + q.mu.Lock() + q.pending = append(q.pending, item) + q.mu.Unlock() + + select { + case q.ready <- struct{}{}: + default: + } + return true +} + +// A MapQueue is a grouping of multiple queues. Items are sent to a queue +// determined by the `keyFn`. +type MapQueue[K comparable, T any] struct { + keyFn func(T) K + factory func(K) Queue[T] + mu sync.Mutex + qs map[K]Queue[T] + doneCh <-chan struct{} +} + +// NewMapQueue creates a new map queue that partitions incoming work items +// based on the provided `keyFn`. If the provided `ctx` is canceled, the entire +// set of queues will be canceled and pending work items may be lost. +// Additionally, the standard `errgroup` `pool` implementation will cancel the +// context if the `workFn` returns a non-nil error, which will also cause +// pending work items to be lost. +func NewMapQueue[K comparable, T any](ctx context.Context, pool Pool, keyFn func(T) K, workFn func(context.Context, T) error) *MapQueue[K, T] { + return &MapQueue[K, T]{ + keyFn: keyFn, + factory: func(_ K) Queue[T] { + return NewSerialQueue(ctx, pool, workFn) + }, + qs: make(map[K]Queue[T]), + mu: sync.Mutex{}, + doneCh: ctx.Done(), + } +} + +func (mq *MapQueue[K, T]) Enqueue(item T) bool { + key := mq.keyFn(item) + + mq.mu.Lock() + q, ok := mq.qs[key] + if !ok { + q = mq.factory(key) + mq.qs[key] = q + } + mq.mu.Unlock() + + return q.Enqueue(item) +} diff --git a/agent/internal/worker/worker_test.go b/agent/internal/worker/worker_test.go new file mode 100644 index 0000000..a69e739 --- /dev/null +++ b/agent/internal/worker/worker_test.go @@ -0,0 +1,103 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package worker + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/google/go-cmp/cmp" + "github.com/jonboulle/clockwork" + "golang.org/x/sync/errgroup" +) + +func TestMapQueue(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + defer g.Wait() + defer cancel() + + type input struct { + key string + duration time.Duration + payload int + } + + respCh := make(chan int) + keyFn := func(i input) string { return i.key } + workFn := func(_ context.Context, i input) error { + clock.Sleep(i.duration) + respCh <- i.payload + return nil + } + + mq := NewMapQueue(ctx, g, keyFn, workFn) + mq.Enqueue(input{key: "a", duration: 1 * time.Second, payload: 1}) + mq.Enqueue(input{key: "a", duration: 3 * time.Second, payload: 2}) + mq.Enqueue(input{key: "b", duration: 1 * time.Second, payload: 3}) + + // both the "a" and "b" 1s tasks should have been started in parallel + clock.BlockUntil(2) + clock.Advance(1 * time.Second) + + earlyResults := []int{<-respCh, <-respCh} + if diff := cmp.Diff([]int{1, 3}, earlyResults, cmpopts.SortSlices(func(l, r int) bool { + return l < r + })); diff != "" { + t.Errorf("earlyResults mismatch (-want +got):\n%s", diff) + } + + // The second, longer "a" task should now be started + clock.BlockUntil(1) + clock.Advance(3 * time.Second) + + want := 2 + if got := <-respCh; got != want { + t.Errorf("expected final job to finish with result %d, but got %d", want, got) + } +} + +func TestMapQueue_ctxCanceled(t *testing.T) { + clock := clockwork.NewFakeClock() + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + defer g.Wait() + defer cancel() + + type input struct { + key string + duration time.Duration + payload int + } + + respCh := make(chan int) + keyFn := func(i input) string { return i.key } + workFn := func(_ context.Context, i input) error { + clock.Sleep(i.duration) + respCh <- i.payload + return nil + } + + mq := NewMapQueue(ctx, g, keyFn, workFn) + + cancel() + if mq.Enqueue(input{key: "b", duration: 1 * time.Second, payload: 3}) != false { + t.Errorf("expected queue.Enqueue to return false after ctx was canceled") + } +} diff --git a/agent/node_controller.go b/agent/node_controller.go new file mode 100644 index 0000000..63be437 --- /dev/null +++ b/agent/node_controller.go @@ -0,0 +1,144 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "context" + "errors" + "fmt" + "time" + + "aalyria.com/spacetime/agent/internal/task" + apipb "aalyria.com/spacetime/api/common" + schedpb "aalyria.com/spacetime/api/scheduling/v1alpha" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" + + "github.com/google/uuid" + "github.com/jonboulle/clockwork" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// nodeController is the logical owner of a node and its various services +// (telemetry, enactments, etc.). +type nodeController struct { + // The node ID this controller is responsible for. + id string + // done is called when the controller should stop. + done func() + clock clockwork.Clock + initState *apipb.ControlPlaneState + services []task.Task + + enactmentStats func() interface{} + telemetryStats func() interface{} + + closers []func() error + + newToken func() string +} + +func (a *Agent) newNodeController(node *node, done func()) (*nodeController, error) { + nc := &nodeController{ + id: node.id, + done: done, + clock: a.clock, + enactmentStats: func() interface{} { return nil }, + telemetryStats: func() interface{} { return nil }, + newToken: uuid.NewString, + } + + rc := task.RetryConfig{ + BackoffDuration: 5 * time.Second, + ErrIsFatal: func(err error) bool { + switch c := status.Code(err); { + case c == codes.Unauthenticated, c == codes.Canceled, c == codes.Unimplemented, errors.Is(err, context.Canceled): + return true + default: + return false + } + }, + Clock: nc.clock, + } + + nc.services = []task.Task{} + if node.telemetryEnabled { + telemetryConn, err := grpc.NewClient(node.telemetryEndpoint, node.telemetryDialOpts...) + if err != nil { + return nil, fmt.Errorf("failed connecting to telemetry endpoint: %w", err) + } + nc.closers = append(nc.closers, telemetryConn.Close) + + telemetryClient := telemetrypb.NewTelemetryClient(telemetryConn) + + ts := nc.newTelemetryService(telemetryClient, node.td) + + nc.services = append(nc.services, task.Task(ts.run). + WithNewSpan("telemetry_service"). + WithLogField("service", "telemetry"). + WithRetries(rc). + WithPanicCatcher()) + + nc.telemetryStats = ts.Stats + } + + if node.enactmentsEnabled { + enactmentConn, err := grpc.NewClient(node.enactmentEndpoint, node.enactmentDialOpts...) + if err != nil { + return nil, fmt.Errorf("failed connecting to enactment endpoint: %w", err) + } + nc.closers = append(nc.closers, enactmentConn.Close) + + schedClient := schedpb.NewSchedulingClient(enactmentConn) + es := nc.newEnactmentService(schedClient, node.ed, nc.newToken()) + + nc.services = append(nc.services, task.Task(es.run). + WithNewSpan("enactment_service"). + WithLogField("service", "enactment"). + WithRetries(rc). + WithPanicCatcher()) + + nc.enactmentStats = es.Stats + } + + return nc, nil +} + +func (nc *nodeController) run(ctx context.Context) (resErr error) { + defer func() { + nc.done() + + errs := []error{resErr} + for _, c := range nc.closers { + errs = append(errs, c()) + } + resErr = errors.Join(errs...) + }() + + return task.Group(nc.services...).WithPanicCatcher()(ctx) +} + +type nodeControllerStats struct { + Enactment interface{} + Telemetry interface{} +} + +func (nc *nodeController) Stats() interface{} { + return nodeControllerStats{ + Enactment: nc.enactmentStats(), + Telemetry: nc.telemetryStats(), + } +} diff --git a/agent/telemetry/BUILD b/agent/telemetry/BUILD new file mode 100644 index 0000000..1a675c2 --- /dev/null +++ b/agent/telemetry/BUILD @@ -0,0 +1,45 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "telemetry", + srcs = [ + "periodic_driver.go", + "telemetry.go", + ], + importpath = "aalyria.com/spacetime/agent/telemetry", + deps = [ + "//api/telemetry:telemetry_go_grpc", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + ], +) + +go_test( + name = "telemetry_test", + srcs = ["periodic_driver_test.go"], + embed = [":telemetry"], + deps = [ + "//api/telemetry:telemetry_go_grpc", + "@com_github_google_go_cmp//cmp", + "@com_github_jonboulle_clockwork//:clockwork", + "@org_golang_google_protobuf//testing/protocmp", + "@org_golang_google_protobuf//types/known/timestamppb", + "@org_golang_x_sync//errgroup", + ], +) diff --git a/agent/telemetry/extproc/BUILD b/agent/telemetry/extproc/BUILD new file mode 100644 index 0000000..71cefa5 --- /dev/null +++ b/agent/telemetry/extproc/BUILD @@ -0,0 +1,32 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "extproc", + srcs = ["extproc.go"], + importpath = "aalyria.com/spacetime/agent/telemetry/extproc", + deps = [ + "//agent/internal/extprocs", + "//agent/internal/loggable", + "//agent/internal/protofmt", + "//agent/telemetry", + "//api/telemetry:telemetry_go_grpc", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + ], +) diff --git a/agent/telemetry/extproc/extproc.go b/agent/telemetry/extproc/extproc.go new file mode 100644 index 0000000..eb02f66 --- /dev/null +++ b/agent/telemetry/extproc/extproc.go @@ -0,0 +1,83 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package extproc provides a telemetry.Backend implementation that relies on +// an external process to generate telemetry reports in the specified form. +package extproc + +import ( + "context" + "errors" + "fmt" + "os/exec" + "time" + + "aalyria.com/spacetime/agent/internal/extprocs" + "aalyria.com/spacetime/agent/internal/loggable" + "aalyria.com/spacetime/agent/internal/protofmt" + "aalyria.com/spacetime/agent/telemetry" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" +) + +var errEmptyReport = errors.New("command generated an empty response") + +type reportGenerator struct { + args []string + protoFmt protofmt.Format +} + +func NewDriver(args []string, format protofmt.Format, collectionPeriod time.Duration) (telemetry.Driver, error) { + return telemetry.NewPeriodicDriver(&reportGenerator{ + args: args, + protoFmt: format, + }, clockwork.NewRealClock(), collectionPeriod) +} + +func (rg *reportGenerator) Stats() interface{} { + return struct { + Type string + Args []string + Format string + }{ + Type: fmt.Sprintf("%T", rg), + Args: rg.args, + Format: rg.protoFmt.String(), + } +} + +func (rg *reportGenerator) GenerateReport(ctx context.Context, nodeID string) (*telemetrypb.ExportMetricsRequest, error) { + log := zerolog.Ctx(ctx).With().Str("driver", "extproc").Logger() + + log.Trace().Strs("args", rg.args).Msg("running telemetry command") + // nosemgrep: dangerous-exec-command + cmd := exec.CommandContext(ctx, rg.args[0], rg.args[1:]...) + reportData, err := cmd.Output() + if err != nil { + return nil, extprocs.CommandError(err) + } + + if len(reportData) == 0 { + return nil, errEmptyReport + } + + report := &telemetrypb.ExportMetricsRequest{} + if err = rg.protoFmt.Unmarshal(reportData, report); err != nil { + return nil, fmt.Errorf("unmarshalling command output into report proto: %w", err) + } + log.Trace().Interface("state", loggable.Proto(report)).Msg("command generated report") + return report, nil +} diff --git a/agent/telemetry/netlink/BUILD b/agent/telemetry/netlink/BUILD new file mode 100644 index 0000000..0481785 --- /dev/null +++ b/agent/telemetry/netlink/BUILD @@ -0,0 +1,51 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "netlink", + srcs = ["netlink.go"], + importpath = "aalyria.com/spacetime/agent/telemetry/netlink", + deps = [ + "//agent/telemetry", + "//api/common:common_go_proto", + "//api/telemetry:telemetry_go_grpc", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_rs_zerolog//:zerolog", + "@com_github_vishvananda_netlink//:netlink", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/timestamppb", + ], +) + +go_test( + name = "netlink_test", + srcs = ["netlink_test.go"], + embed = [":netlink"], + deps = [ + "//api/common:common_go_proto", + "//api/telemetry:telemetry_go_grpc", + "@com_github_google_go_cmp//cmp", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_vishvananda_netlink//:netlink", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//testing/protocmp", + "@org_golang_google_protobuf//types/known/timestamppb", + ], +) diff --git a/agent/telemetry/netlink/container_test/BUILD b/agent/telemetry/netlink/container_test/BUILD new file mode 100644 index 0000000..275951b --- /dev/null +++ b/agent/telemetry/netlink/container_test/BUILD @@ -0,0 +1,67 @@ +# Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@container_structure_test//:defs.bzl", "container_structure_test") +load("@rules_go//go:def.bzl", "go_binary", "go_library") +load("@rules_oci//oci:defs.bzl", "oci_image") +load("@rules_pkg//:pkg.bzl", "pkg_tar") + +package(default_visibility = ["//visibility:public"]) + +go_binary( + name = "netlink_telemetry", + embed = [":netlink_telemetry_lib"], + pure = "on", + static = "on", +) + +go_library( + name = "netlink_telemetry_lib", + srcs = ["netlink_telemetry.go"], + importpath = "aalyria.com/spacetime/agent/telemetry/netlink/container_test", + deps = [ + "//agent/telemetry/netlink", + "//api/telemetry:telemetry_go_grpc", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_vishvananda_netlink//:netlink", + "@org_golang_x_sync//errgroup", + ], +) + +pkg_tar( + name = "netlink_telemetry_tar", + srcs = [ + ":netlink_telemetry", + ], +) + +oci_image( + name = "netlink_telemetry_image", + base = "@alpine_base", + tars = [":netlink_telemetry_tar"], + user = "root", +) + +# You'll need sudoless docker to run this: +# https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user +container_structure_test( + name = "netlink_telemetry_tests", + configs = [":netlink_telemetry_tests.yaml"], + driver = "docker", + image = ":netlink_telemetry_image", + tags = [ + "manual", + "no_ci_pipeline", + ], +) diff --git a/agent/telemetry/netlink/container_test/netlink_telemetry.go b/agent/telemetry/netlink/container_test/netlink_telemetry.go new file mode 100644 index 0000000..7639be3 --- /dev/null +++ b/agent/telemetry/netlink/container_test/netlink_telemetry.go @@ -0,0 +1,185 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This test does a quick sanity check that the byte and packet counters provided by a netlink +// telemetry backend go up when traffic flows over an interface. +// +// It does this by: +// 1. creates an echo server that listens on the loopback address +// 2. generates a telemetry report for the loopback interface +// 3. hits the echo server to generate some traffic on the loopback interface +// 4. generates another telemetry report for the loopback interface +// 5. checks that numbers went up +package main + +import ( + "context" + "fmt" + "io" + "log" + "net" + "time" + + "github.com/jonboulle/clockwork" + vnl "github.com/vishvananda/netlink" + "golang.org/x/sync/errgroup" + + "aalyria.com/spacetime/agent/telemetry/netlink" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" +) + +type echoServer struct { + ln net.Listener + listening chan struct{} + closer chan struct{} + closed chan struct{} +} + +func newEchoServer(listenAddr string) (*echoServer, error) { + ln, err := net.Listen("tcp", listenAddr) + if err != nil { + return nil, fmt.Errorf("net.Listen: %w", err) + } + return &echoServer{ + ln: ln, + listening: make(chan struct{}), + closer: make(chan struct{}), + closed: make(chan struct{}), + }, nil +} + +func (es *echoServer) listen() { + close(es.listening) + defer close(es.closed) + + for { + conn, err := es.ln.Accept() + if err != nil { + select { + case <-es.closer: + return + default: + log.Fatalf("echo server ln.Accept: %s", err) + } + } + func() { + defer conn.Close() + + if _, err = io.Copy(conn, conn); err != nil { + log.Fatalf("echo server io.Copy: %s", err) + } + }() + } +} + +func (es *echoServer) run(ctx context.Context) error { + go es.listen() + + var err error + select { + case <-ctx.Done(): + err = ctx.Err() + case <-es.closer: + } + + es.ln.Close() + <-es.closed + return err +} + +func (es *echoServer) close() { + close(es.closer) +} + +// genTraffic writes an arbitrary string to the given address +func genTraffic(addr string) error { + conn, err := net.Dial("tcp", addr) + if err != nil { + return fmt.Errorf("Dial: %w", err) + } + defer conn.Close() + + if _, err = conn.Write([]byte("Lions and tigers and bears, oh my!")); err != nil { + return fmt.Errorf("Write: %w", err) + } + + return nil +} + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + g, ctx := errgroup.WithContext(ctx) + + func() { + echoAddr := "127.0.0.1:8080" + echoServer, err := newEchoServer(echoAddr) + if err != nil { + log.Fatalf("newEchoServer: %v", err) + } + g.Go(func() error { + return echoServer.run(ctx) + }) + defer echoServer.close() + + select { + case <-echoServer.listening: + case <-ctx.Done(): + log.Fatalf("echo server not ready in time") + } + + clock := clockwork.NewFakeClock() + interfaceIDs := []string{"lo"} + collectionPeriod := 1 * time.Second + driver, err := netlink.NewDriver(clock, interfaceIDs, vnl.LinkByName, collectionPeriod) + if err != nil { + log.Fatalf("NewDriver: %v", err) + } + + reportedMetrics := make(chan *telemetrypb.ExportMetricsRequest) + g.Go(func() error { + return driver.Run(ctx, "node_id", func(report *telemetrypb.ExportMetricsRequest) error { + reportedMetrics <- report + return nil + }) + }) + + firstReport := <-reportedMetrics + log.Printf("first report: %v", firstReport) + + if err := genTraffic(echoAddr); err != nil { + log.Fatalf("genTraffic: %s", err) + } + clock.BlockUntil(1) + clock.Advance(collectionPeriod) + secondReport := <-reportedMetrics + log.Printf("second report: %v", secondReport) + + firstStats := firstReport.GetInterfaceMetrics()[0].GetStandardInterfaceStatisticsDataPoints()[0] + secondStats := secondReport.GetInterfaceMetrics()[0].GetStandardInterfaceStatisticsDataPoints()[0] + + if secondStats.TxPackets > firstStats.TxPackets && + secondStats.RxPackets > firstStats.RxPackets && + secondStats.TxBytes > firstStats.TxBytes && + secondStats.RxBytes > firstStats.RxBytes { + fmt.Printf("PASS: Byte and packet counters all went up") + } else { + fmt.Printf("FAIL: Byte and packet counters did not all go up") + } + }() + + cancel() + g.Wait() +} diff --git a/agent/telemetry/netlink/container_test/netlink_telemetry_tests.yaml b/agent/telemetry/netlink/container_test/netlink_telemetry_tests.yaml new file mode 100644 index 0000000..4940013 --- /dev/null +++ b/agent/telemetry/netlink/container_test/netlink_telemetry_tests.yaml @@ -0,0 +1,21 @@ +# Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +schemaVersion: "2.0.0" + +commandTests: + - name: "netlink telemetry" + command: "/netlink_telemetry" + expectedOutput: + - "PASS: Byte and packet counters all went up" diff --git a/agent/telemetry/netlink/netlink.go b/agent/telemetry/netlink/netlink.go new file mode 100644 index 0000000..4d9adcd --- /dev/null +++ b/agent/telemetry/netlink/netlink.go @@ -0,0 +1,116 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netlink + +import ( + "context" + "errors" + "time" + + "aalyria.com/spacetime/agent/telemetry" + apipb "aalyria.com/spacetime/api/common" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" + + "github.com/jonboulle/clockwork" + "github.com/rs/zerolog" + vnl "github.com/vishvananda/netlink" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var errNoStats = errors.New("could not generate stats for any interface") + +type reportGenerator struct { + clock clockwork.Clock + interfaceIDs []string + linkByName func(string) (vnl.Link, error) +} + +func NewDriver( + clock clockwork.Clock, + interfaceIDs []string, + linkByName func(string) (vnl.Link, error), + collectionPeriod time.Duration, +) (telemetry.Driver, error) { + return telemetry.NewPeriodicDriver(&reportGenerator{ + clock: clock, + interfaceIDs: interfaceIDs, + linkByName: linkByName, + }, clock, collectionPeriod) +} + +func (rg *reportGenerator) Stats() any { return nil } + +func (rg *reportGenerator) GenerateReport(ctx context.Context, nodeID string) (*telemetrypb.ExportMetricsRequest, error) { + log := zerolog.Ctx(ctx).With().Str("backend", "netlink").Logger() + // NOTE: This assumes the netlink stats are returned fast enough that we can use the same + // timestamp for all metrics. + ts := rg.clock.Now() + + interfaceMetrics := []*telemetrypb.InterfaceMetrics{} + + for _, interfaceID := range rg.interfaceIDs { + textNetIfaceID, err := prototext.Marshal(&apipb.NetworkInterfaceId{ + NodeId: proto.String(nodeID), + InterfaceId: proto.String(interfaceID), + }) + if err != nil { + log.Err(err).Msgf("marshalling textproto interface ID") + continue + } + + link, err := rg.linkByName(interfaceID) + if err != nil { + log.Warn().Err(err).Msgf("error retrieving link for interface %s", interfaceID) + continue + } + + attrs := link.Attrs() + if attrs == nil { + log.Warn().Msgf("link has not attrs for interface %s", interfaceID) + continue + } + + stats := attrs.Statistics + if stats == nil { + log.Warn().Msgf("link attrs have no stats for interface %s", interfaceID) + continue + } + + interfaceMetrics = append(interfaceMetrics, &telemetrypb.InterfaceMetrics{ + InterfaceId: string(textNetIfaceID), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + Time: timestamppb.New(ts), + RxPackets: int64(stats.RxPackets), + TxPackets: int64(stats.TxPackets), + RxBytes: int64(stats.RxBytes), + TxBytes: int64(stats.TxBytes), + TxErrors: int64(stats.TxErrors), + RxErrors: int64(stats.RxErrors), + RxDropped: int64(stats.RxDropped), + TxDropped: int64(stats.TxDropped), + }}, + }) + } + + if len(interfaceMetrics) == 0 { + return nil, errNoStats + } + + return &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: interfaceMetrics, + }, nil +} diff --git a/agent/telemetry/netlink/netlink_test.go b/agent/telemetry/netlink/netlink_test.go new file mode 100644 index 0000000..ca88453 --- /dev/null +++ b/agent/telemetry/netlink/netlink_test.go @@ -0,0 +1,373 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package netlink + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/jonboulle/clockwork" + vnl "github.com/vishvananda/netlink" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + commonpb "aalyria.com/spacetime/api/common" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" +) + +func textNetworkIfaceID(id *commonpb.NetworkInterfaceId) string { + text, _ := prototext.Marshal(id) + return string(text) +} + +func linkByNameFromMap(links map[string]vnl.Link) func(string) (vnl.Link, error) { + return func(name string) (vnl.Link, error) { + if link, ok := links[name]; !ok { + return nil, fmt.Errorf("link for %s not found", name) + } else { + return link, nil + } + } +} + +func TestNetlink(t *testing.T) { + t.Parallel() + + clock := clockwork.NewFakeClock() + + type testCase struct { + name string + interfaceIDs []string + linkByName func(string) (vnl.Link, error) + nodeID string + wantMetrics *telemetrypb.ExportMetricsRequest + wantErr error + } + + testCases := []testCase{ + { + name: "stats for single link", + interfaceIDs: []string{"cargo-bay-door"}, + linkByName: linkByNameFromMap(map[string]vnl.Link{ + "cargo-bay-door": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }, + }, + }, + }), + nodeID: "serenity", + wantMetrics: &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: []*telemetrypb.InterfaceMetrics{{ + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("serenity"), + InterfaceId: proto.String("cargo-bay-door"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + Time: timestamppb.New(clock.Now()), + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }}, + }}, + }, + }, + { + name: "stats for multiple links", + interfaceIDs: []string{"gemini", "apollo", "IDSS"}, + linkByName: linkByNameFromMap(map[string]vnl.Link{ + "gemini": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }, + }, + }, + "apollo": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 10, + RxPackets: 20, + TxBytes: 30, + RxBytes: 40, + TxDropped: 50, + RxDropped: 60, + TxErrors: 70, + RxErrors: 80, + }, + }, + }, + "IDSS": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 100, + RxPackets: 200, + TxBytes: 300, + RxBytes: 400, + TxDropped: 500, + RxDropped: 600, + TxErrors: 700, + RxErrors: 800, + }, + }, + }, + }), + nodeID: "ISS", + wantMetrics: &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: []*telemetrypb.InterfaceMetrics{{ + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("ISS"), + InterfaceId: proto.String("gemini"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + Time: timestamppb.New(clock.Now()), + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }}, + }, { + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("ISS"), + InterfaceId: proto.String("apollo"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{ + { + Time: timestamppb.New(clock.Now()), + TxPackets: 10, + RxPackets: 20, + TxBytes: 30, + RxBytes: 40, + TxDropped: 50, + RxDropped: 60, + TxErrors: 70, + RxErrors: 80, + }, + }, + }, { + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("ISS"), + InterfaceId: proto.String("IDSS"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{ + { + Time: timestamppb.New(clock.Now()), + TxPackets: 100, + RxPackets: 200, + TxBytes: 300, + RxBytes: 400, + TxDropped: 500, + RxDropped: 600, + TxErrors: 700, + RxErrors: 800, + }, + }, + }}, + }, + }, + { + name: "one link missing", + interfaceIDs: []string{"dry-dock", "transporter-room"}, + linkByName: linkByNameFromMap(map[string]vnl.Link{ + "dry-dock": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }, + }, + }, + }), + nodeID: "enterprise", + wantMetrics: &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: []*telemetrypb.InterfaceMetrics{{ + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("enterprise"), + InterfaceId: proto.String("dry-dock"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + Time: timestamppb.New(clock.Now()), + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }}, + }}, + }, + }, + { + name: "one link missing attrs", + interfaceIDs: []string{"dry-dock", "transporter-room"}, + linkByName: linkByNameFromMap(map[string]vnl.Link{ + "dry-dock": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }, + }, + }, + "transporter-room": &vnl.Dummy{}, + }), + nodeID: "enterprise", + wantMetrics: &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: []*telemetrypb.InterfaceMetrics{{ + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("enterprise"), + InterfaceId: proto.String("dry-dock"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + Time: timestamppb.New(clock.Now()), + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }}, + }}, + }, + }, + { + name: "one link missing stats", + interfaceIDs: []string{"dry-dock", "transporter-room"}, + linkByName: linkByNameFromMap(map[string]vnl.Link{ + "dry-dock": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{ + Statistics: &vnl.LinkStatistics{ + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }, + }, + }, + "transporter-room": &vnl.Dummy{ + LinkAttrs: vnl.LinkAttrs{}, + }, + }), + nodeID: "enterprise", + wantMetrics: &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: []*telemetrypb.InterfaceMetrics{{ + InterfaceId: textNetworkIfaceID(&commonpb.NetworkInterfaceId{ + NodeId: proto.String("enterprise"), + InterfaceId: proto.String("dry-dock"), + }), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + Time: timestamppb.New(clock.Now()), + TxPackets: 1, + RxPackets: 2, + TxBytes: 3, + RxBytes: 4, + TxDropped: 5, + RxDropped: 6, + TxErrors: 7, + RxErrors: 8, + }}, + }}, + }, + }, + { + name: "all links missing stats", + interfaceIDs: []string{"dry-dock", "transporter-room"}, + linkByName: linkByNameFromMap(map[string]vnl.Link{ + "dry-dock": &vnl.Dummy{LinkAttrs: vnl.LinkAttrs{}}, + "transporter-room": &vnl.Dummy{LinkAttrs: vnl.LinkAttrs{}}, + }), + nodeID: "enterprise", + wantErr: errNoStats, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + generator := reportGenerator{ + clock: clock, + interfaceIDs: tc.interfaceIDs, + linkByName: tc.linkByName, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + gotStats, gotErr := generator.GenerateReport(ctx, tc.nodeID) + + if gotErr != nil && !errors.Is(gotErr, tc.wantErr) { + t.Fatalf( + "test %q unexpected error: want(%s), got(%s)\n", + tc.name, + tc.wantErr, + gotErr, + ) + } + + if diff := cmp.Diff(tc.wantMetrics, gotStats, protocmp.Transform()); diff != "" { + t.Fatalf("test %q unexpected stats (-want +got):\n%s", tc.name, diff) + } + }) + } +} diff --git a/agent/telemetry/periodic_driver.go b/agent/telemetry/periodic_driver.go new file mode 100644 index 0000000..133e726 --- /dev/null +++ b/agent/telemetry/periodic_driver.go @@ -0,0 +1,86 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package telemetry + +import ( + "context" + "fmt" + "time" + + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" + "github.com/jonboulle/clockwork" + + "github.com/rs/zerolog" +) + +type ReportGenerator interface { + Stats() interface{} + GenerateReport(ctx context.Context, nodeID string) (*telemetrypb.ExportMetricsRequest, error) +} + +// PeriodicDriver is a telemetry driver that wraps a ReportGenerator and pushes reports from it at a +// set rate. +type PeriodicDriver struct { + ReportGenerator + clock clockwork.Clock + collectionPeriod time.Duration +} + +func NewPeriodicDriver(generator ReportGenerator, clock clockwork.Clock, collectionPeriod time.Duration) (*PeriodicDriver, error) { + if collectionPeriod <= 0 { + return nil, fmt.Errorf("collectionPeriod must be greater than zero") + } + + return &PeriodicDriver{ + ReportGenerator: generator, + clock: clock, + collectionPeriod: collectionPeriod, + }, nil +} + +func (pd *PeriodicDriver) Run( + ctx context.Context, + nodeID string, + reportMetrics func(*telemetrypb.ExportMetricsRequest) error, +) error { + log := zerolog.Ctx(ctx).With().Str("driver", "extproc").Logger() + + generateAndReport := func() { + report, err := pd.GenerateReport(ctx, nodeID) + if err != nil { + log.Err(err).Msg("failed to generate report") + return + } + if report == nil { + return + } + if err := reportMetrics(report); err != nil { + log.Err(err).Msg("reporting metrics") + } + } + generateAndReport() + + ticker := pd.clock.NewTicker(pd.collectionPeriod) + + for { + select { + case <-ctx.Done(): + ticker.Stop() + return ctx.Err() + case <-ticker.Chan(): + generateAndReport() + } + } +} diff --git a/agent/telemetry/periodic_driver_test.go b/agent/telemetry/periodic_driver_test.go new file mode 100644 index 0000000..696f661 --- /dev/null +++ b/agent/telemetry/periodic_driver_test.go @@ -0,0 +1,112 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package telemetry + +import ( + "context" + "testing" + "time" + + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" + "golang.org/x/sync/errgroup" + + "github.com/google/go-cmp/cmp" + "github.com/jonboulle/clockwork" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type constantGenerator struct { + clock clockwork.Clock +} + +func (cg *constantGenerator) Stats() interface{} { return nil } +func (cg *constantGenerator) GenerateReport(ctx context.Context, nodeID string) (*telemetrypb.ExportMetricsRequest, error) { + return &telemetrypb.ExportMetricsRequest{ + ModemMetrics: []*telemetrypb.ModemMetrics{{ + LinkMetricsDataPoints: []*telemetrypb.LinkMetricsDataPoint{{ + Time: timestamppb.New(cg.clock.Now()), + }}, + }}, + }, nil +} + +func TestPeriodicDriver(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + clock := clockwork.NewFakeClock() + collectionPeriod := 1 * time.Second + driver, err := NewPeriodicDriver(&constantGenerator{clock: clock}, clock, collectionPeriod) + if err != nil { + t.Fatalf("NewPeriodicDriver: %v", err) + } + + reportedStats := make(chan *telemetrypb.ExportMetricsRequest) + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return driver.Run(ctx, "node ID doesn't matter", func(gotReport *telemetrypb.ExportMetricsRequest) error { + reportedStats <- gotReport + return nil + }) + }) + + // First stats are reported immediately + firstStats := <-reportedStats + if diff := cmp.Diff(&telemetrypb.ExportMetricsRequest{ + ModemMetrics: []*telemetrypb.ModemMetrics{{ + LinkMetricsDataPoints: []*telemetrypb.LinkMetricsDataPoint{{ + Time: timestamppb.New(clock.Now()), + }}, + }}, + }, firstStats, protocmp.Transform()); diff != "" { + t.Fatalf("unexpected stats (-want +got):\n%s", diff) + } + + // Next stats aren't reported until the collection period passes + clock.BlockUntil(1) + clock.Advance(collectionPeriod - 1*time.Nanosecond) + if len(reportedStats) > 0 { + t.Fatalf("new stats reported before collection period passed") + } + + clock.Advance(1 * time.Nanosecond) + secondStats := <-reportedStats + if diff := cmp.Diff(&telemetrypb.ExportMetricsRequest{ + ModemMetrics: []*telemetrypb.ModemMetrics{{ + LinkMetricsDataPoints: []*telemetrypb.LinkMetricsDataPoint{{ + Time: timestamppb.New(clock.Now()), + }}, + }}, + }, secondStats, protocmp.Transform()); diff != "" { + t.Fatalf("unexpected stats (-want +got):\n%s", diff) + } + + // Keeps reporting indefinitely... + clock.BlockUntil(1) + clock.Advance(collectionPeriod) + <-reportedStats + + clock.BlockUntil(1) + clock.Advance(collectionPeriod) + <-reportedStats + + clock.BlockUntil(1) + clock.Advance(collectionPeriod) + <-reportedStats + + cancel() + g.Wait() +} diff --git a/agent/telemetry/prometheus/BUILD b/agent/telemetry/prometheus/BUILD new file mode 100644 index 0000000..726c98c --- /dev/null +++ b/agent/telemetry/prometheus/BUILD @@ -0,0 +1,45 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "prometheus", + srcs = ["scraper.go"], + importpath = "aalyria.com/spacetime/agent/telemetry/prometheus", + deps = [ + "//api/common:common_go_proto", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_prometheus_client_model//go", + "@com_github_prometheus_prom2json//:prom2json", + "@org_golang_google_protobuf//proto", + ], +) + +go_test( + name = "prometheus_test", + size = "small", + srcs = ["scraper_test.go"], + embed = [":prometheus"], + embedsrcs = ["node_exporter_metrics_testdata.txt"], + deps = [ + "//api/common:common_go_proto", + "@com_github_google_go_cmp//cmp", + "@com_github_jonboulle_clockwork//:clockwork", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//testing/protocmp", + ], +) diff --git a/agent/telemetry/prometheus/node_exporter_metrics_testdata.txt b/agent/telemetry/prometheus/node_exporter_metrics_testdata.txt new file mode 100644 index 0000000..7a3436f --- /dev/null +++ b/agent/telemetry/prometheus/node_exporter_metrics_testdata.txt @@ -0,0 +1,343 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# HELP node_netstat_Icmp6_InErrors Statistic Icmp6InErrors. +# TYPE node_netstat_Icmp6_InErrors untyped +node_netstat_Icmp6_InErrors 0 +# HELP node_netstat_Icmp6_InMsgs Statistic Icmp6InMsgs. +# TYPE node_netstat_Icmp6_InMsgs untyped +node_netstat_Icmp6_InMsgs 3 +# HELP node_netstat_Icmp6_OutMsgs Statistic Icmp6OutMsgs. +# TYPE node_netstat_Icmp6_OutMsgs untyped +node_netstat_Icmp6_OutMsgs 614 +# HELP node_netstat_Icmp_InErrors Statistic IcmpInErrors. +# TYPE node_netstat_Icmp_InErrors untyped +node_netstat_Icmp_InErrors 0 +# HELP node_netstat_Icmp_InMsgs Statistic IcmpInMsgs. +# TYPE node_netstat_Icmp_InMsgs untyped +node_netstat_Icmp_InMsgs 3 +# HELP node_netstat_Icmp_OutMsgs Statistic IcmpOutMsgs. +# TYPE node_netstat_Icmp_OutMsgs untyped +node_netstat_Icmp_OutMsgs 7 +# HELP node_netstat_Ip6_InOctets Statistic Ip6InOctets. +# TYPE node_netstat_Ip6_InOctets untyped +node_netstat_Ip6_InOctets 6.79806813e+08 +# HELP node_netstat_Ip6_OutOctets Statistic Ip6OutOctets. +# TYPE node_netstat_Ip6_OutOctets untyped +node_netstat_Ip6_OutOctets 6.79842197e+08 +# HELP node_netstat_IpExt_InOctets Statistic IpExtInOctets. +# TYPE node_netstat_IpExt_InOctets untyped +node_netstat_IpExt_InOctets 4.8954551592e+10 +# HELP node_netstat_IpExt_OutOctets Statistic IpExtOutOctets. +# TYPE node_netstat_IpExt_OutOctets untyped +node_netstat_IpExt_OutOctets 3.4504981937e+10 +# HELP node_netstat_Ip_Forwarding Statistic IpForwarding. +# TYPE node_netstat_Ip_Forwarding untyped +node_netstat_Ip_Forwarding 1 +# HELP node_netstat_TcpExt_ListenDrops Statistic TcpExtListenDrops. +# TYPE node_netstat_TcpExt_ListenDrops untyped +node_netstat_TcpExt_ListenDrops 0 +# HELP node_netstat_TcpExt_ListenOverflows Statistic TcpExtListenOverflows. +# TYPE node_netstat_TcpExt_ListenOverflows untyped +node_netstat_TcpExt_ListenOverflows 0 +# HELP node_netstat_TcpExt_SyncookiesFailed Statistic TcpExtSyncookiesFailed. +# TYPE node_netstat_TcpExt_SyncookiesFailed untyped +node_netstat_TcpExt_SyncookiesFailed 0 +# HELP node_netstat_TcpExt_SyncookiesRecv Statistic TcpExtSyncookiesRecv. +# TYPE node_netstat_TcpExt_SyncookiesRecv untyped +node_netstat_TcpExt_SyncookiesRecv 0 +# HELP node_netstat_TcpExt_SyncookiesSent Statistic TcpExtSyncookiesSent. +# TYPE node_netstat_TcpExt_SyncookiesSent untyped +node_netstat_TcpExt_SyncookiesSent 0 +# HELP node_netstat_TcpExt_TCPSynRetrans Statistic TcpExtTCPSynRetrans. +# TYPE node_netstat_TcpExt_TCPSynRetrans untyped +node_netstat_TcpExt_TCPSynRetrans 33 +# HELP node_netstat_TcpExt_TCPTimeouts Statistic TcpExtTCPTimeouts. +# TYPE node_netstat_TcpExt_TCPTimeouts untyped +node_netstat_TcpExt_TCPTimeouts 46 +# HELP node_netstat_Tcp_ActiveOpens Statistic TcpActiveOpens. +# TYPE node_netstat_Tcp_ActiveOpens untyped +node_netstat_Tcp_ActiveOpens 151734 +# HELP node_netstat_Tcp_CurrEstab Statistic TcpCurrEstab. +# TYPE node_netstat_Tcp_CurrEstab untyped +node_netstat_Tcp_CurrEstab 7 +# HELP node_netstat_Tcp_InErrs Statistic TcpInErrs. +# TYPE node_netstat_Tcp_InErrs untyped +node_netstat_Tcp_InErrs 0 +# HELP node_netstat_Tcp_InSegs Statistic TcpInSegs. +# TYPE node_netstat_Tcp_InSegs untyped +node_netstat_Tcp_InSegs 1.2577643e+07 +# HELP node_netstat_Tcp_OutRsts Statistic TcpOutRsts. +# TYPE node_netstat_Tcp_OutRsts untyped +node_netstat_Tcp_OutRsts 48906 +# HELP node_netstat_Tcp_OutSegs Statistic TcpOutSegs. +# TYPE node_netstat_Tcp_OutSegs untyped +node_netstat_Tcp_OutSegs 2.6822221e+07 +# HELP node_netstat_Tcp_PassiveOpens Statistic TcpPassiveOpens. +# TYPE node_netstat_Tcp_PassiveOpens untyped +node_netstat_Tcp_PassiveOpens 72763 +# HELP node_netstat_Tcp_RetransSegs Statistic TcpRetransSegs. +# TYPE node_netstat_Tcp_RetransSegs untyped +node_netstat_Tcp_RetransSegs 2798 +# HELP node_netstat_Udp6_InDatagrams Statistic Udp6InDatagrams. +# TYPE node_netstat_Udp6_InDatagrams untyped +node_netstat_Udp6_InDatagrams 0 +# HELP node_netstat_Udp6_InErrors Statistic Udp6InErrors. +# TYPE node_netstat_Udp6_InErrors untyped +node_netstat_Udp6_InErrors 0 +# HELP node_netstat_Udp6_NoPorts Statistic Udp6NoPorts. +# TYPE node_netstat_Udp6_NoPorts untyped +node_netstat_Udp6_NoPorts 3 +# HELP node_netstat_Udp6_OutDatagrams Statistic Udp6OutDatagrams. +# TYPE node_netstat_Udp6_OutDatagrams untyped +node_netstat_Udp6_OutDatagrams 3 +# HELP node_netstat_Udp6_RcvbufErrors Statistic Udp6RcvbufErrors. +# TYPE node_netstat_Udp6_RcvbufErrors untyped +node_netstat_Udp6_RcvbufErrors 0 +# HELP node_netstat_Udp6_SndbufErrors Statistic Udp6SndbufErrors. +# TYPE node_netstat_Udp6_SndbufErrors untyped +node_netstat_Udp6_SndbufErrors 0 +# HELP node_netstat_UdpLite6_InErrors Statistic UdpLite6InErrors. +# TYPE node_netstat_UdpLite6_InErrors untyped +node_netstat_UdpLite6_InErrors 0 +# HELP node_netstat_UdpLite_InErrors Statistic UdpLiteInErrors. +# TYPE node_netstat_UdpLite_InErrors untyped +node_netstat_UdpLite_InErrors 0 +# HELP node_netstat_Udp_InDatagrams Statistic UdpInDatagrams. +# TYPE node_netstat_Udp_InDatagrams untyped +node_netstat_Udp_InDatagrams 72154 +# HELP node_netstat_Udp_InErrors Statistic UdpInErrors. +# TYPE node_netstat_Udp_InErrors untyped +node_netstat_Udp_InErrors 0 +# HELP node_netstat_Udp_NoPorts Statistic UdpNoPorts. +# TYPE node_netstat_Udp_NoPorts untyped +node_netstat_Udp_NoPorts 3 +# HELP node_netstat_Udp_OutDatagrams Statistic UdpOutDatagrams. +# TYPE node_netstat_Udp_OutDatagrams untyped +node_netstat_Udp_OutDatagrams 72158 +# HELP node_netstat_Udp_RcvbufErrors Statistic UdpRcvbufErrors. +# TYPE node_netstat_Udp_RcvbufErrors untyped +node_netstat_Udp_RcvbufErrors 0 +# HELP node_netstat_Udp_SndbufErrors Statistic UdpSndbufErrors. +# TYPE node_netstat_Udp_SndbufErrors untyped +node_netstat_Udp_SndbufErrors 0 +# HELP node_network_address_assign_type Network device property: address_assign_type +# TYPE node_network_address_assign_type gauge +node_network_address_assign_type{device="docker0"} 3 +node_network_address_assign_type{device="ens4"} 0 +node_network_address_assign_type{device="lo"} 0 +# HELP node_network_carrier Network device property: carrier +# TYPE node_network_carrier gauge +node_network_carrier{device="docker0"} 0 +node_network_carrier{device="ens4"} 1 +node_network_carrier{device="lo"} 1 +# HELP node_network_carrier_changes_total Network device property: carrier_changes_total +# TYPE node_network_carrier_changes_total counter +node_network_carrier_changes_total{device="docker0"} 13 +node_network_carrier_changes_total{device="ens4"} 2 +node_network_carrier_changes_total{device="lo"} 0 +# HELP node_network_carrier_down_changes_total Network device property: carrier_down_changes_total +# TYPE node_network_carrier_down_changes_total counter +node_network_carrier_down_changes_total{device="docker0"} 7 +node_network_carrier_down_changes_total{device="ens4"} 1 +node_network_carrier_down_changes_total{device="lo"} 0 +# HELP node_network_carrier_up_changes_total Network device property: carrier_up_changes_total +# TYPE node_network_carrier_up_changes_total counter +node_network_carrier_up_changes_total{device="docker0"} 6 +node_network_carrier_up_changes_total{device="ens4"} 1 +node_network_carrier_up_changes_total{device="lo"} 0 +# HELP node_network_device_id Network device property: device_id +# TYPE node_network_device_id gauge +node_network_device_id{device="docker0"} 0 +node_network_device_id{device="ens4"} 0 +node_network_device_id{device="lo"} 0 +# HELP node_network_dormant Network device property: dormant +# TYPE node_network_dormant gauge +node_network_dormant{device="docker0"} 0 +node_network_dormant{device="ens4"} 0 +node_network_dormant{device="lo"} 0 +# HELP node_network_flags Network device property: flags +# TYPE node_network_flags gauge +node_network_flags{device="docker0"} 4099 +node_network_flags{device="ens4"} 4099 +node_network_flags{device="lo"} 9 +# HELP node_network_iface_id Network device property: iface_id +# TYPE node_network_iface_id gauge +node_network_iface_id{device="docker0"} 3 +node_network_iface_id{device="ens4"} 2 +node_network_iface_id{device="lo"} 1 +# HELP node_network_iface_link Network device property: iface_link +# TYPE node_network_iface_link gauge +node_network_iface_link{device="docker0"} 3 +node_network_iface_link{device="ens4"} 2 +node_network_iface_link{device="lo"} 1 +# HELP node_network_iface_link_mode Network device property: iface_link_mode +# TYPE node_network_iface_link_mode gauge +node_network_iface_link_mode{device="docker0"} 0 +node_network_iface_link_mode{device="ens4"} 0 +node_network_iface_link_mode{device="lo"} 0 +# HELP node_network_info Non-numeric data from /sys/class/net/, value is always 1. +# TYPE node_network_info gauge +node_network_info{address="00:00:00:00:00:00",broadcast="00:00:00:00:00:00",device="lo",duplex="",ifalias="",operstate="unknown"} 1 +node_network_info{address="02:42:fe:d5:4e:ae",broadcast="ff:ff:ff:ff:ff:ff",device="docker0",duplex="unknown",ifalias="",operstate="down"} 1 +node_network_info{address="42:01:0a:80:00:1c",broadcast="ff:ff:ff:ff:ff:ff",device="ens4",duplex="unknown",ifalias="",operstate="up"} 1 +# HELP node_network_mtu_bytes Network device property: mtu_bytes +# TYPE node_network_mtu_bytes gauge +node_network_mtu_bytes{device="docker0"} 1500 +node_network_mtu_bytes{device="ens4"} 1460 +node_network_mtu_bytes{device="lo"} 65536 +# HELP node_network_name_assign_type Network device property: name_assign_type +# TYPE node_network_name_assign_type gauge +node_network_name_assign_type{device="docker0"} 3 +node_network_name_assign_type{device="ens4"} 4 +node_network_name_assign_type{device="lo"} 2 +# HELP node_network_net_dev_group Network device property: net_dev_group +# TYPE node_network_net_dev_group gauge +node_network_net_dev_group{device="docker0"} 0 +node_network_net_dev_group{device="ens4"} 0 +node_network_net_dev_group{device="lo"} 0 +# HELP node_network_protocol_type Network device property: protocol_type +# TYPE node_network_protocol_type gauge +node_network_protocol_type{device="docker0"} 1 +node_network_protocol_type{device="ens4"} 1 +node_network_protocol_type{device="lo"} 772 +# HELP node_network_receive_bytes_total Network device statistic receive_bytes. +# TYPE node_network_receive_bytes_total counter +node_network_receive_bytes_total{device="docker0"} 4.602427e+06 +node_network_receive_bytes_total{device="ens4"} 4.9004717667e+10 +node_network_receive_bytes_total{device="lo"} 7.69496589e+08 +# HELP node_network_receive_compressed_total Network device statistic receive_compressed. +# TYPE node_network_receive_compressed_total counter +node_network_receive_compressed_total{device="docker0"} 0 +node_network_receive_compressed_total{device="ens4"} 0 +node_network_receive_compressed_total{device="lo"} 0 +# HELP node_network_receive_drop_total Network device statistic receive_drop. +# TYPE node_network_receive_drop_total counter +node_network_receive_drop_total{device="docker0"} 97 +node_network_receive_drop_total{device="ens4"} 12 +node_network_receive_drop_total{device="lo"} 0 +# HELP node_network_receive_errs_total Network device statistic receive_errs. +# TYPE node_network_receive_errs_total counter +node_network_receive_errs_total{device="docker0"} 12 +node_network_receive_errs_total{device="ens4"} 1337 +node_network_receive_errs_total{device="lo"} 0 +# HELP node_network_receive_fifo_total Network device statistic receive_fifo. +# TYPE node_network_receive_fifo_total counter +node_network_receive_fifo_total{device="docker0"} 0 +node_network_receive_fifo_total{device="ens4"} 0 +node_network_receive_fifo_total{device="lo"} 0 +# HELP node_network_receive_frame_total Network device statistic receive_frame. +# TYPE node_network_receive_frame_total counter +node_network_receive_frame_total{device="docker0"} 0 +node_network_receive_frame_total{device="ens4"} 0 +node_network_receive_frame_total{device="lo"} 0 +# HELP node_network_receive_multicast_total Network device statistic receive_multicast. +# TYPE node_network_receive_multicast_total counter +node_network_receive_multicast_total{device="docker0"} 0 +node_network_receive_multicast_total{device="ens4"} 0 +node_network_receive_multicast_total{device="lo"} 0 +# HELP node_network_receive_nohandler_total Network device statistic receive_nohandler. +# TYPE node_network_receive_nohandler_total counter +node_network_receive_nohandler_total{device="docker0"} 0 +node_network_receive_nohandler_total{device="ens4"} 0 +node_network_receive_nohandler_total{device="lo"} 0 +# HELP node_network_receive_packets_total Network device statistic receive_packets. +# TYPE node_network_receive_packets_total counter +node_network_receive_packets_total{device="docker0"} 70236 +node_network_receive_packets_total{device="ens4"} 1.0269729e+07 +node_network_receive_packets_total{device="lo"} 2.586281e+06 +# HELP node_network_speed_bytes Network device property: speed_bytes +# TYPE node_network_speed_bytes gauge +node_network_speed_bytes{device="docker0"} -125000 +node_network_speed_bytes{device="ens4"} -125000 +# HELP node_network_transmit_bytes_total Network device statistic transmit_bytes. +# TYPE node_network_transmit_bytes_total counter +node_network_transmit_bytes_total{device="docker0"} 2.895355422e+09 +node_network_transmit_bytes_total{device="ens4"} 2.8704733728e+10 +node_network_transmit_bytes_total{device="lo"} 7.69496589e+08 +# HELP node_network_transmit_carrier_total Network device statistic transmit_carrier. +# TYPE node_network_transmit_carrier_total counter +node_network_transmit_carrier_total{device="docker0"} 0 +node_network_transmit_carrier_total{device="ens4"} 0 +node_network_transmit_carrier_total{device="lo"} 0 +# HELP node_network_transmit_colls_total Network device statistic transmit_colls. +# TYPE node_network_transmit_colls_total counter +node_network_transmit_colls_total{device="docker0"} 0 +node_network_transmit_colls_total{device="ens4"} 0 +node_network_transmit_colls_total{device="lo"} 0 +# HELP node_network_transmit_compressed_total Network device statistic transmit_compressed. +# TYPE node_network_transmit_compressed_total counter +node_network_transmit_compressed_total{device="docker0"} 0 +node_network_transmit_compressed_total{device="ens4"} 0 +node_network_transmit_compressed_total{device="lo"} 0 +# HELP node_network_transmit_drop_total Network device statistic transmit_drop. +# TYPE node_network_transmit_drop_total counter +node_network_transmit_drop_total{device="docker0"} 132 +node_network_transmit_drop_total{device="ens4"} 37 +node_network_transmit_drop_total{device="lo"} 0 +# HELP node_network_transmit_errs_total Network device statistic transmit_errs. +# TYPE node_network_transmit_errs_total counter +node_network_transmit_errs_total{device="docker0"} 132 +node_network_transmit_errs_total{device="ens4"} 19 +node_network_transmit_errs_total{device="lo"} 0 +# HELP node_network_transmit_fifo_total Network device statistic transmit_fifo. +# TYPE node_network_transmit_fifo_total counter +node_network_transmit_fifo_total{device="docker0"} 0 +node_network_transmit_fifo_total{device="ens4"} 0 +node_network_transmit_fifo_total{device="lo"} 0 +# HELP node_network_transmit_packets_total Network device statistic transmit_packets. +# TYPE node_network_transmit_packets_total counter +node_network_transmit_packets_total{device="docker0"} 181958 +node_network_transmit_packets_total{device="ens4"} 5.638352e+06 +node_network_transmit_packets_total{device="lo"} 2.586281e+06 +# HELP node_network_transmit_queue_length Network device property: transmit_queue_length +# TYPE node_network_transmit_queue_length gauge +node_network_transmit_queue_length{device="docker0"} 0 +node_network_transmit_queue_length{device="ens4"} 1000 +node_network_transmit_queue_length{device="lo"} 1000 +# HELP node_network_up Value is 1 if operstate is 'up', 0 otherwise. +# TYPE node_network_up gauge +node_network_up{device="docker0"} 0 +node_network_up{device="ens4"} 1 +node_network_up{device="lo"} 0 +# HELP node_nf_conntrack_entries Number of currently allocated flow entries for connection tracking. +# TYPE node_nf_conntrack_entries gauge +node_nf_conntrack_entries 10 +# HELP node_nf_conntrack_entries_limit Maximum size of connection tracking table. +# TYPE node_nf_conntrack_entries_limit gauge +node_nf_conntrack_entries_limit 262144 +# HELP node_nf_conntrack_stat_drop Number of packets dropped due to conntrack failure. +# TYPE node_nf_conntrack_stat_drop gauge +node_nf_conntrack_stat_drop 0 +# HELP node_nf_conntrack_stat_early_drop Number of dropped conntrack entries to make room for new ones, if maximum table size was reached. +# TYPE node_nf_conntrack_stat_early_drop gauge +node_nf_conntrack_stat_early_drop 0 +# HELP node_nf_conntrack_stat_found Number of searched entries which were successful. +# TYPE node_nf_conntrack_stat_found gauge +node_nf_conntrack_stat_found 0 +# HELP node_nf_conntrack_stat_ignore Number of packets seen which are already connected to a conntrack entry. +# TYPE node_nf_conntrack_stat_ignore gauge +node_nf_conntrack_stat_ignore 0 +# HELP node_nf_conntrack_stat_insert Number of entries inserted into the list. +# TYPE node_nf_conntrack_stat_insert gauge +node_nf_conntrack_stat_insert 0 +# HELP node_nf_conntrack_stat_insert_failed Number of entries for which list insertion was attempted but failed. +# TYPE node_nf_conntrack_stat_insert_failed gauge +node_nf_conntrack_stat_insert_failed 0 +# HELP node_nf_conntrack_stat_invalid Number of packets seen which can not be tracked. +# TYPE node_nf_conntrack_stat_invalid gauge +node_nf_conntrack_stat_invalid 257 +# HELP node_nf_conntrack_stat_search_restart Number of conntrack table lookups which had to be restarted due to hashtable resizes. +# TYPE node_nf_conntrack_stat_search_restart gauge +node_nf_conntrack_stat_search_restart 190 diff --git a/agent/telemetry/prometheus/scraper.go b/agent/telemetry/prometheus/scraper.go new file mode 100644 index 0000000..8ed1794 --- /dev/null +++ b/agent/telemetry/prometheus/scraper.go @@ -0,0 +1,186 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "context" + "io" + "math/big" + "net/http" + "time" + + apipb "aalyria.com/spacetime/api/common" + + "github.com/jonboulle/clockwork" + promcli "github.com/prometheus/client_model/go" + "github.com/prometheus/prom2json" + "google.golang.org/protobuf/proto" +) + +// ScraperConfig represents the configuration for a prometheus scraper. +type ScraperConfig struct { + Clock clockwork.Clock + + // ExporterURL is the URL that hosts the metrics page for a Prometheus + // exporter. + ExporterURL string + // NodeID is the ID of the node associated with the generated + // NetworkStatsReports. + NodeID string + // ScrapeInterval is how frequently the scraper should scrape metrics. + ScrapeInterval time.Duration + // Callback gets called once a NetworkStatsReport is generated. A non-nil + // error will stop the scraper. + Callback func(*apipb.NetworkStatsReport) error +} + +// Scraper is a +type Scraper struct { + conf ScraperConfig +} + +// NewScraper returns a Scraper with the provided configuration. +func NewScraper(conf ScraperConfig) *Scraper { + return &Scraper{conf: conf} +} + +// Start kicks off the scraping process in the current goroutine. Will stop when +// the provided context is cancelled or on the first encountered error. +func (s *Scraper) Start(ctx context.Context) error { + for { + if err := s.scrape(ctx); err != nil { + return err + } + + timer := s.conf.Clock.NewTimer(s.conf.ScrapeInterval) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.Chan() + } + return ctx.Err() + + case <-timer.Chan(): + } + } +} + +func (s *Scraper) scrape(ctx context.Context) error { + stats, err := s.fetchMetrics(ctx) + if err != nil { + return err + } + + ifaceStats, err := s.extractInterfaceStats(stats) + if err != nil { + return err + } + + return s.conf.Callback(&apipb.NetworkStatsReport{ + NodeId: &s.conf.NodeID, + Timestamp: &apipb.DateTime{ + UnixTimeUsec: proto.Int64(s.conf.Clock.Now().UnixMicro()), + }, + InterfaceStatsById: ifaceStats, + }) +} + +func (s *Scraper) fetchMetrics(ctx context.Context) ([]*prom2json.Family, error) { + req, err := http.NewRequestWithContext(ctx, "GET", s.conf.ExporterURL, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return parseReader(resp.Body) +} + +// parseReader wraps the asynchronous prom2json.ParseReader function in a +// synchronous interface. +func parseReader(r io.Reader) ([]*prom2json.Family, error) { + inCh := make(chan *promcli.MetricFamily) + outCh := make(chan []*prom2json.Family) + + go func() { + stats := []*prom2json.Family{} + for s := range inCh { + stats = append(stats, prom2json.NewFamily(s)) + } + outCh <- stats + }() + + err := prom2json.ParseReader(r, inCh) + stats := <-outCh + return stats, err +} + +func (s *Scraper) extractInterfaceStats(stats []*prom2json.Family) (map[string]*apipb.InterfaceStats, error) { + result := map[string]*apipb.InterfaceStats{} + + for _, mf := range stats { + addMetricToReport := func(is *apipb.InterfaceStats, value *int64) {} + + // TODO: parameterize these key/values via the ScraperConfig + switch mf.Name { + case "node_network_transmit_bytes_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.TxBytes = value } + case "node_network_receive_bytes_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.RxBytes = value } + case "node_network_transmit_packets_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.TxPackets = value } + case "node_network_receive_packets_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.RxPackets = value } + case "node_network_transmit_drop_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.TxDropped = value } + case "node_network_receive_drop_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.RxDropped = value } + case "node_network_transmit_errs_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.TxErrors = value } + case "node_network_receive_errs_total": + addMetricToReport = func(is *apipb.InterfaceStats, value *int64) { is.RxErrors = value } + default: + continue + } + + for _, m := range mf.Metrics { + metric, ok := m.(prom2json.Metric) + if !ok { + continue + } + dev, ok := metric.Labels["device"] + if !ok { + continue + } + rep, ok := result[dev] + if !ok { + rep = &apipb.InterfaceStats{} + result[dev] = rep + } + + flt, _, err := big.ParseFloat(metric.Value, 10, 0, big.ToNearestEven) + if err != nil { + return nil, err + } + valAsInt, _ := flt.Int64() + addMetricToReport(rep, proto.Int64(valAsInt)) + } + } + + return result, nil +} diff --git a/agent/telemetry/prometheus/scraper_test.go b/agent/telemetry/prometheus/scraper_test.go new file mode 100644 index 0000000..73b1ba3 --- /dev/null +++ b/agent/telemetry/prometheus/scraper_test.go @@ -0,0 +1,186 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "context" + _ "embed" + "net" + "net/http" + "testing" + "time" + + apipb "aalyria.com/spacetime/api/common" + + "github.com/google/go-cmp/cmp" + "github.com/jonboulle/clockwork" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +type testCase struct { + description string + + metrics []byte + expected func(clockwork.Clock) *apipb.NetworkStatsReport +} + +var ( + //go:embed node_exporter_metrics_testdata.txt + nodeExporterMetrics []byte + + testCases = []testCase{ + { + description: "sample node_exporter output", + metrics: nodeExporterMetrics, + expected: func(clock clockwork.Clock) *apipb.NetworkStatsReport { + return &apipb.NetworkStatsReport{ + NodeId: proto.String("it's me, a cool node"), + Timestamp: &apipb.DateTime{ + UnixTimeUsec: proto.Int64(clock.Now().UnixMicro()), + }, + InterfaceStatsById: map[string]*apipb.InterfaceStats{ + "docker0": { + TxPackets: proto.Int64(181958), + RxPackets: proto.Int64(70236), + TxBytes: proto.Int64(2895355422), + RxBytes: proto.Int64(4602427), + TxDropped: proto.Int64(132), + RxDropped: proto.Int64(97), + TxErrors: proto.Int64(132), + RxErrors: proto.Int64(12), + }, + "ens4": { + TxPackets: proto.Int64(5638352), + RxPackets: proto.Int64(10269729), + TxBytes: proto.Int64(28704733728), + RxBytes: proto.Int64(49004717667), + TxDropped: proto.Int64(37), + RxDropped: proto.Int64(12), + TxErrors: proto.Int64(19), + RxErrors: proto.Int64(1337), + }, + "lo": { + TxPackets: proto.Int64(2586281), + RxPackets: proto.Int64(2586281), + TxBytes: proto.Int64(769496589), + RxBytes: proto.Int64(769496589), + TxDropped: proto.Int64(0), + RxDropped: proto.Int64(0), + TxErrors: proto.Int64(0), + RxErrors: proto.Int64(0), + }, + }, + } + }, + }, + } +) + +func assertProtosEqual(t *testing.T, want, got interface{}) { + t.Helper() + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("proto mismatch: (-want +got):\n%s", diff) + t.FailNow() + } +} + +func (tc testCase) createMetricServer(ctx context.Context, t *testing.T) *http.Server { + t.Helper() + + return &http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.Header().Set("Content-Type", "text/plain; charset=utf-8") + rw.WriteHeader(http.StatusOK) + _, err := rw.Write(tc.metrics) + if err != nil { + t.Errorf("error writing metrics to response: %s", err) + } + }), + BaseContext: func(_ net.Listener) context.Context { return ctx }, + } +} + +func TestScraper(t *testing.T) { + for _, tc := range testCases { + func(tc testCase) { + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + runTest(t, tc) + }) + }(tc) + } +} + +func listenOnUnusedPort(ctx context.Context, t *testing.T) net.Listener { + t.Helper() + + nl, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0") + if err != nil { + t.Fatal("error creating a TCP listener", err) + } + + return nl +} + +func runTest(t *testing.T, tc testCase) { + ctx, done := context.WithTimeout(context.Background(), 1*time.Second) + defer done() + + clock := clockwork.NewFakeClock() + expected := tc.expected(clock) + + srv := tc.createMetricServer(ctx, t) + nl := listenOnUnusedPort(ctx, t) + defer nl.Close() + srvErrCh := make(chan error) + go func() { srvErrCh <- srv.Serve(nl) }() + + reportCh := make(chan *apipb.NetworkStatsReport) + conf := ScraperConfig{ + Clock: clock, + ExporterURL: "http://" + nl.Addr().String(), + NodeID: *expected.NodeId, + ScrapeInterval: 30 * time.Second, + Callback: func(report *apipb.NetworkStatsReport) error { + reportCh <- report + return nil + }, + } + + scrapeErrCh := make(chan error) + go func() { + scrapeErrCh <- NewScraper(conf).Start(ctx) + }() + + select { + case <-ctx.Done(): + t.Errorf("context finished before NetworkStatsReport was generated: %s", ctx.Err()) + case rep := <-reportCh: + assertProtosEqual(t, expected, rep) + } + + srv.Shutdown(ctx) + if err := <-srvErrCh; err != nil && err != http.ErrServerClosed { + t.Errorf("error serving metrics: %s", err) + } + + done() + clock.Advance(conf.ScrapeInterval) + if err := <-scrapeErrCh; err != nil && err != context.Canceled { + t.Errorf("error scraping metrics: %s", err) + } +} diff --git a/agent/telemetry/telemetry.go b/agent/telemetry/telemetry.go new file mode 100644 index 0000000..f6117f5 --- /dev/null +++ b/agent/telemetry/telemetry.go @@ -0,0 +1,36 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package telemetry + +import ( + "context" + + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" +) + +// Driver is the component that gathers and reports metrics for a given +// node. +type Driver interface { + // Run will periodically report metrics by calling the provided `reportMetrics` callback. + Run( + ctx context.Context, + nodeID string, + reportMetrics func(*telemetrypb.ExportMetricsRequest) error, + ) error + // Stats returns internal statistics for the driver in an unstructured + // form. The results are exposed as a JSON endpoint via the pprof server, + // if it's configured. + Stats() any +} diff --git a/agent/telemetry_service.go b/agent/telemetry_service.go new file mode 100644 index 0000000..f8d1322 --- /dev/null +++ b/agent/telemetry_service.go @@ -0,0 +1,47 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "context" + + "aalyria.com/spacetime/agent/telemetry" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" +) + +type telemetryService struct { + nodeID string + telemetryClient telemetrypb.TelemetryClient + td telemetry.Driver +} + +func (nc *nodeController) newTelemetryService(tc telemetrypb.TelemetryClient, td telemetry.Driver) *telemetryService { + return &telemetryService{ + nodeID: nc.id, + telemetryClient: tc, + td: td, + } +} + +func (ts *telemetryService) Stats() interface{} { return ts.td.Stats() } + +func (ts *telemetryService) run(ctx context.Context) error { + reportMetrics := func(report *telemetrypb.ExportMetricsRequest) error { + _, err := ts.telemetryClient.ExportMetrics(ctx, report) + return err + } + + return ts.td.Run(ctx, ts.nodeID, reportMetrics) +} diff --git a/agent/telemetry_test.go b/agent/telemetry_test.go new file mode 100644 index 0000000..19791df --- /dev/null +++ b/agent/telemetry_test.go @@ -0,0 +1,143 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "context" + "errors" + "net" + "testing" + "time" + + apipb "aalyria.com/spacetime/api/common" + telemetrypb "aalyria.com/spacetime/telemetry/v1alpha" + + "github.com/jonboulle/clockwork" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" +) + +func textPBIfaceID(t *testing.T, nodeID, ifaceID string) string { + b, _ := prototext.Marshal(&apipb.NetworkInterfaceId{ + NodeId: proto.String(nodeID), + InterfaceId: proto.String(ifaceID), + }) + return string(b) +} + +type manualReportDriver struct { + reports chan *telemetrypb.ExportMetricsRequest +} + +func newManualReportDriver() *manualReportDriver { + return &manualReportDriver{ + reports: make(chan *telemetrypb.ExportMetricsRequest), + } +} + +func (mrd *manualReportDriver) Stats() interface{} { return nil } +func (mrd *manualReportDriver) Run(ctx context.Context, nodeID string, reportMetrics func(*telemetrypb.ExportMetricsRequest) error) error { + for { + select { + case <-ctx.Done(): + return nil + case report := <-mrd.reports: + reportMetrics(report) + } + } +} + +func TestRelaysMetricsFromDriverToController(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(baseContext(t), time.Second) + defer cancel() + + ts := NewTelemetryServer() + td := newManualReportDriver() + + srvAddr := ts.Start(ctx, t) + a := newAgent(t, + WithClock(clockwork.NewFakeClock()), + WithNode("mynode", WithTelemetryDriver(srvAddr, td, grpc.WithTransportCredentials(insecure.NewCredentials())))) + errCh := make(chan error) + go func() { errCh <- a.Run(ctx) }() + + servedReport := &telemetrypb.ExportMetricsRequest{ + InterfaceMetrics: []*telemetrypb.InterfaceMetrics{{ + InterfaceId: textPBIfaceID(t, "foobar", "lo0"), + StandardInterfaceStatisticsDataPoints: []*telemetrypb.StandardInterfaceStatisticsDataPoint{{ + TxBytes: 1, + RxBytes: 12, + }}, + }}, + } + td.reports <- servedReport + + gotReport := <-ts.reportedMetrics + assertProtosEqual(t, servedReport, gotReport) + + cancel() + checkErrIsDueToCanceledContext(t, <-errCh) +} + +type telemetryServer struct { + reportedMetrics chan *telemetrypb.ExportMetricsRequest + + telemetrypb.UnimplementedTelemetryServer +} + +func NewTelemetryServer() *telemetryServer { + return &telemetryServer{ + reportedMetrics: make(chan *telemetrypb.ExportMetricsRequest), + } +} + +func (ts *telemetryServer) Start(ctx context.Context, t *testing.T) (addr string) { + nl, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("telemetryServer: couldn't Listen: %s", err) + } + + grpcSrv := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) + telemetrypb.RegisterTelemetryServer(grpcSrv, ts) + + errCh := make(chan error) + go func() { errCh <- grpcSrv.Serve(nl) }() + go func() { + defer nl.Close() + for { + select { + case <-ctx.Done(): + grpcSrv.Stop() + case err := <-errCh: + if err != nil && !errors.Is(err, grpc.ErrServerStopped) { + t.Errorf("error serving: %s", err) + } + return + } + } + }() + + return nl.Addr().(*net.TCPAddr).String() +} + +func (ts *telemetryServer) ExportMetrics(ctx context.Context, req *telemetrypb.ExportMetricsRequest) (*emptypb.Empty, error) { + ts.reportedMetrics <- req + return nil, nil +} diff --git a/agent/timing.go b/agent/timing.go new file mode 100644 index 0000000..3f3214d --- /dev/null +++ b/agent/timing.go @@ -0,0 +1,95 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "sync" + "time" + + apipb "aalyria.com/spacetime/api/common" + + "github.com/jonboulle/clockwork" + "google.golang.org/protobuf/proto" +) + +func hzToDuration(hz float64) time.Duration { + // don't feed me a hz of 0!!!!! + return time.Duration(float64(time.Second) / hz) +} + +// reusableTicker is a wrapper around time.Ticker / clockwork.Ticker interface +// that can be created without being started and that always has a valid +// Chan(). If the ticker hasn't been started yet, the resulting channel will +// never receive any messages. +type reusableTicker struct { + mu sync.Mutex + clock clockwork.Clock + activeTicker clockwork.Ticker + inactiveCh chan time.Time + isActive bool +} + +func newReusableTicker(clock clockwork.Clock) *reusableTicker { + return &reusableTicker{ + clock: clock, + inactiveCh: make(chan time.Time), + } +} + +func (r *reusableTicker) Stop() { + wasActive := false + var oldTicker clockwork.Ticker + + r.mu.Lock() + wasActive, r.isActive = r.isActive, false + oldTicker, r.activeTicker = r.activeTicker, nil + r.mu.Unlock() + + if wasActive { + oldTicker.Stop() + } +} + +func (r *reusableTicker) Start(d time.Duration) { + newTicker := r.clock.NewTicker(d) + var oldTicker clockwork.Ticker + wasActive := false + + r.mu.Lock() + wasActive, r.isActive = r.isActive, true + oldTicker, r.activeTicker = r.activeTicker, newTicker + r.mu.Unlock() + + if wasActive { + oldTicker.Stop() + } +} + +func (r *reusableTicker) Chan() <-chan time.Time { + r.mu.Lock() + defer r.mu.Unlock() + + if r.isActive { + return r.activeTicker.Chan() + } else { + return r.inactiveCh + } +} + +func timeToProto(t time.Time) *apipb.DateTime { + return &apipb.DateTime{ + UnixTimeUsec: proto.Int64(t.UnixMicro()), + } +} diff --git a/agent/timing_test.go b/agent/timing_test.go new file mode 100644 index 0000000..16303da --- /dev/null +++ b/agent/timing_test.go @@ -0,0 +1,92 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "fmt" + "testing" + "time" + + "github.com/jonboulle/clockwork" +) + +func TestHzToDuration(t *testing.T) { + t.Parallel() + + for hz, want := range map[float64]time.Duration{ + 0.1: 10 * time.Second, + 1.0: 1 * time.Second, + 10: 100 * time.Millisecond, + 100: 10 * time.Millisecond, + 1000: 1 * time.Millisecond, + } { + hz, want := hz, want + + t.Run(fmt.Sprintf("%fhz = %s", hz, want), func(t *testing.T) { + t.Parallel() + got := hzToDuration(hz) + if got != want { + t.Errorf("converting %fhz to duration, got %s but wanted %s", hz, got, want) + } + }) + } +} + +func TestReusableTicker(t *testing.T) { + t.Parallel() + fc := clockwork.NewFakeClock() + rt := newReusableTicker(fc) + + rt.Start(30 * time.Second) + fc.Advance(30 * time.Second) + tickedAt := <-rt.Chan() + if tickedAt != fc.Now() { + t.Errorf("ticker sent wrong time on channel, got %s, want %s", tickedAt, fc.Now()) + } + + rt.Stop() + fc.Advance(3 * time.Hour) + select { + case tickedAt = <-rt.Chan(): + t.Errorf("ticker incorrectly ticked with value %s after being stopped", tickedAt) + default: + // yay + } +} + +func TestReusableTicker_CallingStartMultipleTimes(t *testing.T) { + t.Parallel() + fc := clockwork.NewFakeClock() + rt := newReusableTicker(fc) + + rt.Start(30 * time.Second) + rt.Start(30 * time.Second) + rt.Start(30 * time.Second) + + fc.Advance(30 * time.Second) + tickedAt := <-rt.Chan() + if tickedAt != fc.Now() { + t.Errorf("ticker sent wrong time on channel, got %s, want %s", tickedAt, fc.Now()) + } + + rt.Stop() + fc.Advance(3 * time.Hour) + select { + case tickedAt = <-rt.Chan(): + t.Errorf("ticker incorrectly ticked with value %s after being stopped", tickedAt) + default: + // yay + } +} diff --git a/api/BUILD b/api/BUILD new file mode 100644 index 0000000..622f5df --- /dev/null +++ b/api/BUILD @@ -0,0 +1,41 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_proto_grpc_doc//:defs.bzl", "doc_markdown_compile", "doc_template_compile") + +# bazel query 'kind("^proto_library", //api/...)' +_all_protos = [ + "//api/cdpi/v1alpha:cdpi_proto", + "//api/common:common_proto", + "//api/federation:federation_proto", + "//api/nbi/v1alpha:nbi_proto", + "//api/nbi/v1alpha/resources:resources_proto", + "//api/types:types_proto", +] + +# The default HTML template that comes with the protoc-gen-doc tool adds a +# `

` to every newline in every description. This ends up causing weird, +# short line breaks. We use a custom template that just renders the description +# as-is inside a single `

` tag. +# https://github.com/pseudomuto/protoc-gen-doc/blob/df9dd4078971bb01d06bcd88130a7b5309348be0/resources/html.tmpl +doc_template_compile( + name = "api.html", + protos = _all_protos, + template = "//api/resources:html.tmpl", +) + +doc_markdown_compile( + name = "api.md", + protos = _all_protos, +) diff --git a/api/cdpi/v1alpha/BUILD b/api/cdpi/v1alpha/BUILD new file mode 100644 index 0000000..c432588 --- /dev/null +++ b/api/cdpi/v1alpha/BUILD @@ -0,0 +1,79 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Protobufs for the AirFlow Control to Data-Plane Interface (CDPI) and its +# associated Configuration & Management Interface (AF-CONFIG). Unlike +# contemporary SDN protocols, such as OpenFlow and OF-CONFIG, AirFlow supports +# L1-L3 control of networks with wireless and mobile network platforms. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_grpc_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "cdpi_proto", + srcs = [ + "cdpi.proto", + ], + deps = [ + "//api/common:common_proto", + "@googleapis//google/rpc:status_proto", + "@protobuf//:empty_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_grpc_library( + name = "cdpi_cpp_grpc", + protos = [":cdpi_proto"], + deps = [ + "//api/common:common_cpp_proto", + "@googleapis//google/rpc:status_cc_proto", + ], +) + +go_proto_library( + name = "cdpi_go_grpc", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/cdpi/v1alpha", + proto = ":cdpi_proto", + deps = [ + "//api/common:common_go_proto", + "@org_golang_google_genproto_googleapis_rpc//status", + ], +) + +java_grpc_library( + name = "cdpi_java_grpc", + protos = [":cdpi_proto"], + deps = [ + "//api/common:common_java_proto", + "@googleapis//google/rpc:rpc_java_proto", + ], +) + +python_grpc_library( + name = "cdpi_python_grpc", + protos = [":cdpi_proto"], + deps = [ + "//api/common:common_python_proto", + ], +) diff --git a/api/cdpi/v1alpha/cdpi.proto b/api/cdpi/v1alpha/cdpi.proto new file mode 100644 index 0000000..bafe72f --- /dev/null +++ b/api/cdpi/v1alpha/cdpi.proto @@ -0,0 +1,252 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Spacetime Control-to-Data-Plane Interface (CDPI) + +syntax = "proto2"; + +package aalyria.spacetime.api.cdpi.v1alpha; + +import "api/common/control.proto"; +import "api/common/coordinates.proto"; +import "api/common/telemetry.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +option java_multiple_files = true; +option java_outer_classname = "CdpiProto"; +option java_package = "com.aalyria.spacetime.api.cdpi.v1alpha"; +option go_package = "aalyria.com/spacetime/api/cdpi/v1alpha"; + +message ControlPlanePingRequest { + // Uniquely identifies the request within the scope of the CDPI stream. Used + // by the SDN controller to match a response with its request. + optional int64 id = 1; +} + +message ControlPlanePingResponse { + // Identifies the ping request with which the response is associated. + optional int64 id = 1; + + // The response status. + optional google.rpc.Status status = 3; + + // When the ping target received the request. + optional google.protobuf.Timestamp time_of_receipt = 2; +} + +message ControlStateChangeRequest { + oneof type { + aalyria.spacetime.api.common.ScheduledControlUpdate scheduled_update = 1; + + aalyria.spacetime.api.common.ScheduledControlDeletion + scheduled_deletion = 2; + + ControlPlanePingRequest control_plane_ping_request = 3; + } +} + +message ControlStateNotification { + // Uniquely identifies the network node/element interfacing with Spacetime. + // Must be populated in the initial notification sent to Spacetime on a + // CDPI stream, and will be ignored by Spacetime in any further notifications + // sent on the stream. + optional string node_id = 1; + + // A list of statuses for updates that have been requested to be scheduled, + // unscheduled, or enacted. + repeated aalyria.spacetime.api.common.ScheduledControlUpdateStatus + statuses = 2; + + // This field may be omitted if control update statuses exist and are all + // 'OK' or 'UNAVAILABLE'. + // + // Partial ControlPlaneState updates are supported, where any combination of + // {beam_states, radio_states, forwarding_state, tunnel_states} subfields on + // the ControlPlaneState message can be set. If a subfield is unset, then + // the corresponding substate is not updated. Otherwise, the corresponding + // substate will update the SDN Controller's view of the ControlPlaneState if + // the timestamp on the substate is greater than that of the SDN Controller's + // view. + optional aalyria.spacetime.api.common.ControlPlaneState state = 3; + + // If multiple streams register the same network node by using the same + // node_id in a ControlStateNotification all registered streams may receive + // the ControlStateChangeRequest messages. + // The priority fields allows to specify the order in which streams are tried. + // Streams with the lowest priority are tried first; it is recommended to use: + // - priority = 0 for the default control-plane network + // - priority = 1 backup/secondary control paths + optional uint32 priority = 4 [default = 0]; + + // The agent's response to a control-plane ping request. + optional ControlPlanePingResponse control_plane_ping_response = 5; +} + +message TelemetryRequest { + // Uniquely identifies the network node/element. + optional string node_id = 1; + + oneof type { + // Requests a one-time network statistics report. + google.protobuf.Empty query_statistics = 2; + + // Specifies the rate at which network statistics should be streamed. + // Set statistics_publish_rate_hz = 0 to disable periodic publication. + double statistics_publish_rate_hz = 3; // in Hz + } +} + +message TelemetryUpdate { + // Deprecated; set in NetworkStatsReport or NetworkEventReport. + optional string node_id = 1 [deprecated = true]; + + oneof type { + aalyria.spacetime.api.common.NetworkStatsReport statistics = 2; + aalyria.spacetime.api.common.NetworkEventReport event = 3; + } +} + +// This service defines the Spacetime network telemetry interface for Software +// Defined Wireless Networks (SDWN). It is implemented by controllers and CDPI +// agents. +service NetworkTelemetryStreaming { + // This Network Telemetry interface follows the OpenConfig concept of having a + // "monitoring configuration" that is sent to the device, which configures + // streaming and the rate. And, like OpenConfig, it also supports 3 types of + // telemetry: bulk time series data (sent every N seconds), event/edge events, + // and operator request/response. + // + // Network elements initiate this bidirectional streaming interface by sending + // a TelemetryUpdate with their latest statistics. Telemetry events should be + // published using this interface whenever they are generated -- even if + // periodic statistics reporting is not enabled. However, further publication + // of network statistics is not required unless a TelemetryRequest is + // received, which may optionally specify a requested rate for updates. + rpc TelemetryInterface(stream TelemetryUpdate) + returns (stream TelemetryRequest) {} +} + +// Spacetime Control to Data-Plane Interface (CDPI) +// +// The Spacetime CDPI can be used by an SDN agent to receive dataplane schedule +// changes and to notify the SDN controller of state changes. +// +// Note: This service has been deprecated in favor of the scheduling interface +// defined in the aalyria.spacetime.scheduling.v1alpha package. +service Cdpi { + // Opens a stream with the SDN controller through which the controller may + // issue requests of the SDN agent, and the agent may respond. + rpc Cdpi(stream CdpiRequest) returns (stream CdpiResponse) { + option deprecated = true; + } + + rpc UpdateNodeState(CdpiNodeStateRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } + + rpc UpdateRequestStatus(CdpiRequestStatusRequest) + returns (google.protobuf.Empty) { + option deprecated = true; + } +} + +// An out-of-band, one-way update of an individual node's state that an SDN +// agent emits to the SDN controller. +message CdpiNodeStateRequest { + // Uniquely identifies the network node/element interfacing with Spacetime. + optional string node_id = 1; + + // The node's current state. + // + // Partial ControlPlaneState updates are supported, where any combination of + // {beam_states, radio_states, forwarding_state, tunnel_states} subfields on + // the ControlPlaneState message can be set. If a subfield is unset, then + // the corresponding substate is not updated. Otherwise, the corresponding + // substate will update the SDN Controller's view of the ControlPlaneState if + // the timestamp on the substate is greater than that of the SDN Controller's + // view. + optional aalyria.spacetime.api.common.ControlPlaneState state = 3; +} + +// An out-of-band, one-way update containing the results of scheduled +// enactments for an individual node that an SDN agent emits to the SDN +// controller. +message CdpiRequestStatusRequest { + // Uniquely identifies the network node/element interfacing with Spacetime. + optional string node_id = 1; + + // An update for a status that has been previously scheduled. + optional aalyria.spacetime.api.common.ScheduledControlUpdateStatus + status = 2; +} + +// The request for aalyria.spacetime.api.cdpi.v1alpha.Cdpi.Cdpi. +message CdpiRequest { + message Hello { + // Required. Identifies the network node on behalf of which the SDN agent + // is calling. This must be the ID of an existing + // aalyria.spacetime.api.common.NetworkNode entity. + optional string node_id = 1; + + // Required. When multiple sessions identify themselves as the same network + // node, the channel priority determines the order in which the SDN + // controller attempts to send a message over those sessions. Sessions with + // the lowest priority are tried first; it is recommended to use: + // * priority = 0 for the default control-plane network + // * priority = 1 for backup/secondary control paths + optional uint32 channel_priority = 2; + } + + message Response { + // Required. The ID of the request to which this response corresponds. See + // aalyria.spacetime.api.cdpi.v1alpha.CdpiResponse.request_id. + optional int64 request_id = 1; + + // The response status. + optional google.rpc.Status status = 2; + + // The response payload. + optional bytes payload = 3; + } + + // Required in the initial request of the session. Identifies the SDN agent + // and the properties of the underlying channel. + optional Hello hello = 1; + + // A response to a request received from the SDN controller. + // + // The response may originate from a proxy rather than the SDN agent itself. + // For example, if the proxy is unable to deliver the request to the SDN + // agent, the proxy may populate the response with an appropriate error. + optional Response response = 2; +} + +// The response for aalyria.spacetime.api.cdpi.v1alpha.Cdpi.Cdpi. +// +// This is a response message in that it flows from server to client, however +// it holds requests being sent to the SDN agent (the client) by the SDN +// controller (the server). +message CdpiResponse { + // An SDN-controller-generated value uniquely identifying the request within + // the scope of the CDPI session. That is, two requests received from the + // same session will always have different request IDs. The SDN agent must + // provide the ID in the response to this request (see + // aalyria.spacetime.api.cdpi.v1alpha.CdpiRequest.Response.request_id). + optional int64 request_id = 1; + + // The contents of the request. + optional bytes request_payload = 2; +} diff --git a/api/common/BUILD b/api/common/BUILD new file mode 100644 index 0000000..ee6d60f --- /dev/null +++ b/api/common/BUILD @@ -0,0 +1,95 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_proto_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_proto_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_proto_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "common_proto", + srcs = [ + "bent_pipe.proto", + "channel.proto", + "control.proto", + "control_beam.proto", + "control_flow.proto", + "control_radio.proto", + "control_tunnel.proto", + "coordinates.proto", + "network.proto", + "platform.proto", + "platform_antenna.proto", + "telemetry.proto", + "time.proto", + "tunnel.proto", + "wireless.proto", + "wireless_modcod.proto", + "wireless_receiver.proto", + "wireless_transceiver.proto", + "wireless_transmitter.proto", + ], + deps = [ + "//api/types:types_proto", + "@googleapis//google/rpc:status_proto", + "@googleapis//google/type:interval_proto", + "@protobuf//:duration_proto", + "@protobuf//:empty_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_proto_library( + name = "common_cpp_proto", + protos = [":common_proto"], + deps = [ + "//api/types:types_cpp_proto", + "@googleapis//google/rpc:status_cc_proto", + "@googleapis//google/type:interval_cc_proto", + ], +) + +go_proto_library( + name = "common_go_proto", + importpath = "aalyria.com/spacetime/api/common", + proto = ":common_proto", + deps = [ + "//api/types:types_go_proto", + "@org_golang_google_genproto//googleapis/type/interval", + "@org_golang_google_genproto_googleapis_rpc//status", + ], +) + +java_proto_library( + name = "common_java_proto", + protos = [":common_proto"], + deps = [ + "//api/types:types_java_proto", + "@googleapis//google/rpc:rpc_java_proto", + "@googleapis//google/type:type_java_proto", + ], +) + +python_proto_library( + name = "common_python_proto", + protos = [":common_proto"], + deps = [ + "//api/types:types_python_proto", + "@googleapis//google/rpc:status_py_proto", + "@googleapis//google/type:interval_py_proto", + ], +) diff --git a/api/common/bent_pipe.proto b/api/common/bent_pipe.proto new file mode 100644 index 0000000..fd65d45 --- /dev/null +++ b/api/common/bent_pipe.proto @@ -0,0 +1,115 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package aalyria.spacetime.api.common; + +import "api/common/platform_antenna.proto"; +import "api/common/wireless.proto"; +import "api/common/wireless_receiver.proto"; +import "api/common/wireless_transmitter.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// A model for a payload in a bent pipe architecture. +// +// This message can be used to represent payloads in which +// the channel bandwidths, center frequencies, polarizations, etc. +// are either fixed or configurable by the SDN (for example, for +// digital transparent processors). +// +// In order to represent a payload in which some channels are +// configurable and others are fixed, define both a `FixedPayload` and a +// `DigitalPayload`. +message BentPipePayload { + // An ID for this payload that must be unique only within the + // `aalyria.spacetime.api.common.PlatformDefinition` to which + // this payload is attached. + string id = 1; + + message AntennaAndSignalProcessors { + // An ID that must be unique only within this `BentPipePayload`. + string id = 1; + AntennaDefinition antenna = 2; + repeated TransmitSignalProcessor transmit_signal_processors = 3; + repeated ReceiveSignalProcessor receive_signal_processors = 4; + + // This field is used for a `DigitalPayload`. The SDN controller + // will create channel configurations between an input channel + // with an antenna of Direction.A and an output channel with + // an antenna of Direction.B or vice versa. For example, + // this field could be used to distinguish feeder versus access + // antennas. + enum Direction { + DIRECTION_UNSPECIFIED = 0; + A = 1; + B = 2; + } + Direction direction = 5; + } + repeated AntennaAndSignalProcessors antennas = 2; + + // A payload in which the configuration is fixed. + message FixedPayload { + // Definitions of the input and output channels. + // + // As an example, for a forward link, the input channel could + // correspond to the feeder link and the output channel + // could correspond to the access link. For a return link, + // the input channel could correspond to the access link + // and the output channel could correspond to the feeder link. + message Channel { + string id = 1; + Signal signal = 2; + // The ID of an `AntennaAndSignalProcessors` message. + string antenna_and_signal_processors_id = 3; + } + repeated Channel channels = 1; + + message ChannelConfiguration { + // An ID of a `Channel` message that represents the + // input channel. + string input_channel_id = 1; + // An ID of a `Channel` message that represents the + // output channel. + string output_channel_id = 2; + double bandwidth_hz = 3; + } + repeated ChannelConfiguration channel_configurations = 3; + } + FixedPayload fixed_payload = 3; + + // A payload which is configurable by the SDN controller, + // for example, a digital transparent processor. + message DigitalPayload { + // Spacetime will configure the channels’ center frequencies, + // bandwidths, and which input channels are connected to which + // output channels. These fields constrain the channel + // configuration. + uint64 min_input_frequency_hz = 1; + uint64 max_input_frequency_hz = 2; + uint64 min_output_frequency_hz = 3; + uint64 max_output_frequency_hz = 4; + + uint64 min_channel_bandwidth_hz = 5; + uint64 max_channel_bandwidth_hz = 6; + } + DigitalPayload digital_payload = 4; + + // Limits the number of flows through this payload. + uint32 max_processed_bandwidth_hz = 5; + uint32 max_channels = 6; +} diff --git a/api/common/channel.proto b/api/common/channel.proto new file mode 100644 index 0000000..b8c8ad6 --- /dev/null +++ b/api/common/channel.proto @@ -0,0 +1,72 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file contains Spacetime's abstractions for the modulation and coding +// (MODCOD) schemes used by network devices, and the fixed or adaptive coding +// and modulation configuration. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/wireless_modcod.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// The BandProfile is the abstraction by which Spacetime predicts the modulation +// and coding scheme (MODCOD) that would be selected by the adaptive or fixed +// coding and modulation. This abstraction allows Spacetime to determine whether +// a received signal meets the minimum strength or noise thresholds to close a +// link, and if so, to estimate the capacity of the link based on the predicted +// MODCOD. +// +// As a performance optimization, Spacetime associates channels that can be +// considered to have similar wireless propagation loss characteristics into +// a single BandProfile. Spacetime's wireless propagation analysis +// can then be executed once per BandProfile, as opposed to once per channel. +// +// To illustrate this concept, consider the 2.4GHz and 5GHz bands in WiFi. +// There are many channels in the 2.4GHz band, such as channels 1 - 11, and +// there are many channels in the 5GHz band, such as channels 36, 40, 48, etc. +// In many cases, the channels in the 2.4GHz band can be considered to have the +// same propagation loss effects, and the channels in the 5GHz band can be +// considered to have the same propagation loss effects. But, a channel in the +// 2.4GHz band *cannot* be considered to have the same propagation loss effects +// as a channel in the 5GHz band. So, these bands could be modeled as 2 +// BandProfiles, one for the 2.4GHz band and one for the 5GHz band, and the +// propagation loss would be computed once for each BandProfile. +// +// A separate BandProfile should be created for each set of channels that +// shares: +// 1) The same allocated bandwidth +// 2) The same Adaptive Coding and Modulation configuration +message BandProfile { + // Specifies the channel bandwidth or spacing configuration, in Hz. + // Required. + optional uint64 channel_width_hz = 2; + + // The symbol rate (baud rate), in symbols per second, of carriers that are + // assigned to channels of this bandwidth. + optional uint64 symbol_rate_symbols_per_second = 4; + + // A mapping between thresholds of various measurements of signal quality to + // the effective Layer 2 data rate that the link could sustain. + // This is Spacetime's abstraction for representing the MODCOD schemes that + // network devices which conform to this BandProfile might use. + // Required. + optional AdaptiveDataRateTable rate_table = 3; + + reserved 1; +} diff --git a/api/common/control.proto b/api/common/control.proto new file mode 100644 index 0000000..0e084a5 --- /dev/null +++ b/api/common/control.proto @@ -0,0 +1,145 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/control_beam.proto"; +import "api/common/control_flow.proto"; +import "api/common/control_radio.proto"; +import "api/common/control_tunnel.proto"; +import "api/common/time.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Defines sequential changes to the network control plane of an element. +message ScheduledControlUpdate { + // Uniquely identifies the network node/element. + optional string node_id = 1; + + // A unique value which identifies this update. + optional string update_id = 2; + + // Optional time at which to begin enacting the first step in the sequence. + // If this field is empty, the sequence should be enacted immediately. + // If the agent observes that this time is in the past, it should reject the + // update by setting the done status notification to DEADLINE_EXCEEDED. + optional google.protobuf.Timestamp time_to_enact = 3; + + // A new control configuration to enact. + optional ControlPlaneUpdate change = 4; +} + +// Defines deletions to the control plane schedule of an element. +message ScheduledControlDeletion { + // Uniquely identifies the network node/element. + optional string node_id = 1; + + // List of pending update IDs to be removed from the agent’s + // enactment queue. + // + // If this field contains an update ID that has already been + // enacted, the deletion request will have no effect for that + // update. + repeated string update_ids = 2; +} + +// The status of enacting one or more scheduled network control updates. +// This field is omitted if the notification is for an unscheduled change. +message ScheduledControlUpdateStatus { + // ID of the update whose enactment status is being reported. + optional string update_id = 1; + + // The timestamp of when the state was determined. + optional DateTime timestamp = 7; + + oneof state { + // The agent attempted to schedule the update for later enactment, + // according to the time_to_enact. The status contains the outcome of the + // attempt. + google.rpc.Status scheduled = 4; + + // The agent attempted to enact the update. + // The status contains the outcome of the attempt. + google.rpc.Status enactment_attempted = 6; + + // The agent attempted to unschedule the update. + // The status contains the outcome of the attempt. + google.rpc.Status unscheduled = 8; + } + + reserved 2, 3, 5; +} + +message ControlPlaneUpdate { + oneof update_type { + // WIRELESS TOPOLOGY + // Determines antenna or optical laser tasking for link establishment. + // Example: set antenna target, no target, etc. + BeamUpdate beam_update = 1; + + // COGNITIVE RADIO + // Cognitive engine interface for configuring radio-system parameters. + // Examples set transmit power, set channel, tune ARQ, etc. + RadioUpdate radio_update = 2; + + // FORWARDING + // Configures a router or switch. + // Example: update routing table. + FlowUpdate flow_update = 3; + + // TUNNELING + // Provides policies and aids in key distribution for tunnel establishment. + // Example: encap/decap, encrypt/decrypt packets based on classifier rules. + TunnelUpdate tunnel_update = 4; + } +} + +// Next ID : 7 +message ControlPlaneState { + optional BeamStates beam_states = 2; + optional RadioStates radio_states = 3; + optional FlowState forwarding_state = 5; + optional TunnelStates tunnel_states = 6; + + reserved 1, 4; +} + +// A ScheduledControlUpdate and a corresponding ScheduledControlUpdateStatus. +message ScheduledControlUpdateReq { + optional ScheduledControlUpdate compiled_update = 1; + + // Indicates that the destination agent has reported completion of an + // attempt to apply the update. + optional google.rpc.Status completed = 8; + + // Indicates that the destination agent has reported completion of + // scheduling the update to be applied at the update's time to enact. + optional bool scheduled = 11; + + // Indicates that the destination agent has reported completion of an + // attempt to unschedule the update. + optional google.rpc.Status unscheduled = 12; + + reserved 2 to 7, 9, 10; +} + +// Holds count of a CDPI stream's priority. +message TaskCdpiStreamCount { + map stream_count_per_priority = 1; +} diff --git a/api/common/control_beam.proto b/api/common/control_beam.proto new file mode 100644 index 0000000..00288d8 --- /dev/null +++ b/api/common/control_beam.proto @@ -0,0 +1,169 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/coordinates.proto"; +import "api/common/network.proto"; +import "api/common/platform.proto"; +import "api/common/time.proto"; +import "google/protobuf/duration.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// BeamUpdate messages control antenna agents. They are keyed by beam_task_id, +// and the agent consumes ADD/DELETE BeamUpdate messages. For P2MP antennas, +// multiple BeamUpdate messages may be added for the same antenna before a +// removal. For P2P antennas, adding a BeamUpdate will automatically remove +// the prior task/target (because target antenna tasks are exclusive). +message BeamUpdate { + reserved 13 to max; + + // A globally unique identifier for this task. + optional string beam_task_id = 3; + + enum Operation { + UNKNOWN = 0; + ADD = 1; // Adds the specified target for this antenna. + DELETE = 2; // Deletes a previously added beam task by beam_task_id + } + optional Operation operation = 4; + + // The registered identifier for the network interface. + // TODO: Switch use of this over to 'interface_id'. + optional string source_interface_id = 1 [deprecated = true]; + + // The registered network interface ID for the steerable antenna. + optional NetworkInterfaceId interface_id = 10; + + // Radio configurations for the source network interface. + optional RadioConfig radio_config = 11; + + // Uniquely identifies the target of the tasked network interface. + // This field must always be present for ADD operations and must always be + // omitted for DELETE operations (ensures backward compatibility). + // TODO: Switch use of this over to 'target_id'. + optional string target_interface_id = 2 [deprecated = true]; + + // Uniquely identifies the target of the tasked network interface. + // This field must always be present for ADD operations and must always be + // omitted for DELETE operations (ensures backward compatibility). + optional NetworkInterfaceId target_id = 8; + + // Optional information about target position at time-to-enact that may help + // the agent acquire a link. Note this field is set once for the time-to-enact + // and will not be updated with the target motion. + // TODO: s/TargetAcquisitionInfo/TargetPositionInfo + optional TargetAcquisitionInfo acquisition_info = 5; + + // Optional information about radio signal at target at time-to-enact that + // may help the agent acquire a link. Note this field is set once for the + // time-to-enact and will not be updated with target motion. + optional SignalAcquisitionInfo signal_info = 12; + + // Monotonically increasing sequence number per interface. This is not + // guaranteed to monontonically increase across different interfaces and is + // allowed to jump by more than 1. + optional int64 per_interface_sequence_number = 7; + + // The timeout to use for enacting this beam update. It describes the maximum + // delay to scan or steer the beam and acquire the target before an enactment + // error of DEADLINE_EXCEEDED must be returned. + optional google.protobuf.Duration establishment_timeout = 9; + + reserved 6; +} + +message RadioConfig { + message Channel { + // Defines the center of the channel in Hz. + // For RF transceivers, this is the carrier frequency. + // For optical transceivers, this may be converted to wavelength. + optional uint64 center_frequency_hz = 1; + + // Specifies the channel bandwidth or spacing configuration, in Hz. + optional uint64 channel_width_hz = 2; + } + + optional Channel tx_channel = 1; + optional Channel rx_channel = 2; + // Denotes the adaptive data modem config specified by the TS-SDN controller. + // This allows the embedded stack to configure a specific modem profile or SDR + // firmware image. Note that this field is different from band profile id. + optional string modem_config_id = 3; +} + +message SignalAcquisitionInfo { + // Modeled remote RSL when this beam is pointed at the target. This field + // serves as a hint and therefore may not be set (e.g., when the + // SignalPropagation service is down). + optional double modeled_power_at_receiver_output_dbw = 1; +} + +// This information is intended to aide in initial acquisition of the target +// and is based on propagating the target's motion forward to the time of beam +// enactment. The CDPI does not provide a stream of continuous updates to the +// target's coordinates for tracking purposes. +// TODO: s/TargetAcquisitionInfo/TargetPositionInfo +message TargetAcquisitionInfo { + reserved 1, 2, 6, 7, 8, 11, 13 to max; + + // Deprecated; clients should migrate to use 'coordinates'. + optional double longitude = 3 [deprecated = true]; + optional double latitude = 4 [deprecated = true]; + optional double height = 5 [deprecated = true]; + + // The target's motion. Any time series motion data provided for interpolation + // will start at the time of target acquisition, and the duration between the + // first and last entry in the series is guaranteed to be at least as long as + // the establishment_timeout. + optional Motion coordinates = 12; + + // Optionally specifies an ADS-B transponder for locating the target. + optional AdsbTransponder adsb_transponder = 9; + + // MAC Address for the target's wireless adapter. + // TODO: Move this to RadioUpdate. + optional bytes physical_address = 10; +} + +message BeamTask { + reserved 4 to max; + + // The registered identifier for the steerable antenna. + optional string interface_id = 1; + + // Uniquely identifies the target antenna. + optional string target_interface_id = 2 [deprecated = true]; + optional NetworkInterfaceId target_id = 3; +} + +message BeamStates { + reserved 1, 2, 5 to max; + + // Time at which the state was captured by the network element. + optional aalyria.spacetime.api.common.DateTime timestamp = 3; + + // A list of all active beam task ids. + // A beam task should only be included in this map if the beam's task is still + // installed. If a phy-layer link outage of greater than 10 seconds occurs, + // the AirFlow agent should delete the task and send a + // ControlStateNotification to the network controller (by omitting it from the + // list). Outages of 10 seconds or more cause severe network effects + // including TCP socket closures and termination of VoLTE sessions. + repeated string beam_task_ids = 4; +} diff --git a/api/common/control_flow.proto b/api/common/control_flow.proto new file mode 100644 index 0000000..325b501 --- /dev/null +++ b/api/common/control_flow.proto @@ -0,0 +1,144 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/network.proto"; +import "api/common/time.proto"; +import "api/types/ethernet.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Specifies an update to the state of a switch or router. +message FlowUpdate { + // A globally unique identifier for this rule. + optional string flow_rule_id = 1; + + // Specifies add or remove. + enum Operation { + UNKNOWN = 0; + ADD = 1; + DELETE = 2; + } + optional Operation operation = 2; + + // The rule and associated actions to which the operation is applied. + optional FlowRule rule = 3; + + // Optional monotonically increasing sequence number, which may jump by more + // than 1. + optional int64 sequence_number = 4; +} + +// Next ID: 8 +message FlowRule { + reserved 1 to 3, 8 to max; + // Classifies packets that match this rule. + optional PacketClassifier classifier = 5; + + // Specifies one or more groups of actions to take on matched packets. + // Actions are enacted sequentially within an action bucket. For example, + // if a packet needs to be copied and forwarded out multiple interfaces on + // the same network node, action buckets may be defined where each bucket + // consists of two sequential actions: (1) setting the destination MAC + // address, and (2) forwarding the packet out of a specified port. + message ActionBucket { + message Action { + message SetField { + enum Field { + FIELD_UNSPECIFIED = 0; + FIELD_ETH_DST = 2; + // Only the MPLS label, does not alter any of the other fields of + // the Label Stack Entry (RFC 3032 section-2.1). + FIELD_MPLS = 3; + FIELD_CTAG = 4; + FIELD_PBB_ITAG = 5; + } + optional Field field = 1; + optional string value_ascii = 3; + reserved 2, 4 to max; + } + + // Forward specifies details of the forwarding action to be configured + message Forward { + // out_interface_id specifies the outbound interface_id over which to + // forward traffic. This interface_id is the node-unique identifier + // specified in NetworkNode. + optional string out_interface_id = 1; + + // next_hop_ip (sometimes known as gateway IP) to direct traffic to. + // May be IPv4 or IPv6. + optional string next_hop_ip = 2; + } + + // Vis. OpenFlow Switch Specification v1.5.1 S5.8, multiple + // PushHeader/SetField actions can be enacted in a list for richer MPLS + // stack manipulation or Ethernet VLAN/PBB tagging/encapsulation + // operations. + // + // As per the OpenFlow Switch Specification, "[t]he Push tag actions + // always insert a new tag header in the outermost valid location for + // that tag, as defined by the specifications governing that tag." + message PushHeader { + enum Field { + FIELD_UNSPECIFIED = 0; + FIELD_MPLS = 1; + FIELD_CTAG = 2; + FIELD_PBB = 3; + } + optional Field field = 1; + // EtherType is important for field types that have multiple possible + // EtherTypes values, e.g. VLAN C-TAG vs S-TAG or MPLS downstream vs. + // "upstream" -assigned. + optional aalyria.spacetime.api.types.EtherType ether_type = 2; + } + + message PopHeader { + enum Field { + FIELD_UNSPECIFIED = 0; + FIELD_MPLS = 1; + FIELD_CTAG = 2; + FIELD_PBB = 3; + } + optional Field field = 1; + // As per the OpenFlow Switch Specification, "[t]he Ethertype is used + // as the Ethertype for the resulting packet", e.g. when popping the + // last MPLS label stack entry in a stack. + optional aalyria.spacetime.api.types.EtherType ether_type = 2; + } + + oneof action_type { + SetField set_field = 1; + Forward forward = 2; + PushHeader push_header = 3; + PopHeader pop_header = 4; + } + reserved 5 to max; + } + repeated Action action = 1; + } + repeated ActionBucket action_bucket = 4; +} + +message FlowState { + reserved 1, 4 to max; + // Time at which the state was captured by the network element. + optional aalyria.spacetime.api.common.DateTime timestamp = 2; + + // A list of all active flow rule ids. + repeated string flow_rule_ids = 3; +} diff --git a/api/common/control_radio.proto b/api/common/control_radio.proto new file mode 100644 index 0000000..1dd85e1 --- /dev/null +++ b/api/common/control_radio.proto @@ -0,0 +1,152 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/time.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Next ID : 7 +message TransmitterState { + // Defines the center of the channel in Hz. + // For RF transceivers, this is the carrier frequency. + // For optical transceivers, this may be converted to wavelength. + optional uint64 center_frequency_hz = 5; + + // Specifies the channel bandwidth or spacing configuration, in Hz. + optional uint64 channel_width_hz = 6; + + // The transmitter power, in Watts. + optional double transmit_power_watts = 3; + + reserved 1, 2, 4; +} + +// Next ID : 6 +message ReceiverState { + // Defines the center of the channel in Hz. + // For RF transceivers, this is the carrier frequency. + // For optical transceivers, this may be converted to wavelength. + optional uint64 center_frequency_hz = 4; + + // Specifies the channel bandwidth or spacing configuration, in Hz. + optional uint64 channel_width_hz = 5; + + reserved 1, 2, 3; +} + +// Defines a TDMA schedule for multiple access radios. +// The schedule is a list of TdmaSlots that will be repeated in order by the +// client radios. +message TdmaSchedule { + enum ScheduleType { + UNKNOWN = 0; + TX_ONLY = 1; + RX_ONLY = 2; + TX_RX = 3; + } + optional ScheduleType type = 1; + + // Defines a TDMA time slot. + message TdmaSlot { + optional Duration duration = 1; + + // Transmits to a remote receiver. + message TxSlot { + enum TxSlotType { + UNKNOWN = 0; + UNICAST = 1; + BEACON = 2; + POLLED = 3; + CONTENTION = 4; + } + optional TxSlotType type = 1; + optional string remote_receiver_id = 2; + } + + // Receives from a remote transmitter. + message RxSlot { + enum RxSlotType { + UNKNOWN = 0; + UNICAST = 1; + BROADCAST = 2; + } + + optional RxSlotType type = 1; + optional string remote_transmitter_id = 2; + } + + oneof slot_type { + TxSlot transmit = 2; + RxSlot receive = 3; + } + } + + // The sum of the individual TdmaSlot.duration fields. + optional Duration schedule_duration = 2; + repeated TdmaSlot schedule = 3; +} + +message RadioUpdate { + // A globally unique identifier for this radio config + optional string radio_config_id = 6; + + // The ID of the interface whose transmitter and receiver settings are being + // updated. + optional string interface_id = 4; + + // Configures the transmitter properties. + optional TransmitterState tx_state = 1; + + // Configures the receiver properties. + optional ReceiverState rx_state = 2; + + // Configures the TDMA schedule, if applicable. + optional TdmaSchedule tdma_schedule = 3; + + // A unique string that identifies the adaptive data modem config id selected + // by the TS-SDN controller. This field is provided to CDPI Agents to aid in + // selection of a specific modem profile, SDR firmware image, etc. Note that + // this field is typically different from the band profile id. + optional string modem_config_id = 8; + + // Optional monotonically increasing sequence number per interface. This is + // not guaranteed to monontonically increase across different interfaces and + // is allowed to jump by more than 1. + optional int64 per_interface_sequence_number = 5; + + reserved 7; +} + +message RadioStates { + // Time at which the state was captured by the network element. + optional aalyria.spacetime.api.common.DateTime timestamp = 3; + + message RadioState { + // A globally unique identifier for this radio config + optional string radio_config_id = 4; + optional TransmitterState tx_state = 1; + optional ReceiverState rx_state = 2; + optional TdmaSchedule tdma_schedule = 3; + } + + // A mapping from interface_id to radio config id. + map radio_config_id_by_interface_id = 4; + + reserved 1, 2, 5 to max; +} diff --git a/api/common/control_tunnel.proto b/api/common/control_tunnel.proto new file mode 100644 index 0000000..4f05be8 --- /dev/null +++ b/api/common/control_tunnel.proto @@ -0,0 +1,87 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/network.proto"; +import "api/common/time.proto"; +import "api/common/tunnel.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Specifies a packet tunnelling policy. +message TunnelUpdate { + // A globally unique identifier for this rule. + optional string tunnel_rule_id = 1; + + // Specifies add or remove. + enum Operation { + UNKNOWN = 0; + ADD = 1; + DELETE = 2; + } + optional Operation operation = 2; + + // The rule and associated parameters. + optional TunnelRule rule = 3; + + // Optional monotonically increasing sequence number, which may jump by more + // than 1. + optional int64 sequence_number = 4; +} + +message TunnelRule { + message EncapRule { + // Describes which packets will be encapsulated and transmitted through the + // tunnel. It describes the inner packet header. + optional PacketClassifier classifier = 1; + + // These fields describe the tunnel (outer) header. + optional string encapsulated_src_ip = 2; + optional string encapsulated_dst_ip = 3; + optional int32 encapsulated_src_port = 4; + optional int32 encapsulated_dst_port = 5; + + oneof parameters { + EspParameters esp = 6; + } + } + + message DecapRule { + // Describes which packets will be decapsulated and received through the + // tunnel. It describes the outer packet header. + optional PacketClassifier classifier = 1; + + oneof parameters { + EspParameters esp = 2; + } + } + + optional EncapRule encap_rule = 10; + optional DecapRule decap_rule = 11; + + reserved 1 to 9; +} + +message TunnelStates { + reserved 1, 4 to max; + // Time at which the state was captured by the network element. + optional aalyria.spacetime.api.common.DateTime timestamp = 2; + + // A list of all active tunnel rule ids. + repeated string tunnel_rule_ids = 3; +} diff --git a/api/common/coordinates.proto b/api/common/coordinates.proto new file mode 100644 index 0000000..2da43d7 --- /dev/null +++ b/api/common/coordinates.proto @@ -0,0 +1,582 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file defines types for expressing the time-dynamic motion of +// platforms. Note that for many types, the motion determines the orientation +// of the platform's axes. These axes must be considered, for example, when +// modeling the antenna's gain pattern or field of regard, or interpreting the +// direction vector of a link. +// TODO: Decouple the choice of a platform's motion and its axes, and +// provide options to allow users to set the platform's axes. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/time.proto"; +import "google/protobuf/timestamp.proto"; +import "google/type/interval.proto"; + +option go_package = "aalyria.com/spacetime/api/common"; +option java_package = "com.aalyria.spacetime.api.common"; + +// GeodeticWgs84, GeodeticMsl, and Geodetic are sets of curvilinear +// 3D coordinates with different vertical datum. These are natural +// choices to describe the motion of terrestrial platforms. +// +// Note that Spacetime's wireless propagation analysis considers the presence +// of terrain as a potential obstruction to wireless links. Therefore, when +// modeling terrestrial platforms, ensure that the height situates the +// platform above the terrain at that point. For example, suppose a fixed +// user terminal were located on a mountainside. If the height of +// the platform representing this user terminal were set to 0m, this user +// terminal would be considered to be "under" the mountain, and therefore, would +// not be able to form any wireless links. +// TODO: Add an option to clamp a platform to the height of the terrain +// at a given point. +// +// When a platform's motion is described using GeodeticWgs84, GeodeticMsl, or +// Geodetic, the platform's axes are oriented in the Earth's reference frame, +// such that: +// - The x-axis points in the local East direction. +// - The y-axis points in the local North direction. +// - The z-axis points in the direction of the normal vector to the WGS 84 +// ellipsoid surface which passes through the point. Conceptually, the +// z-axis is oriented "outwards" from the Earth's surface towards space. +message GeodeticWgs84 { + // Defaults to 0. + optional double longitude_deg = 1; + // Defaults to 0. + optional double latitude_deg = 2; + // The height is relative to the WGS84 ellipsoid. For reference on the + // vertical datum used, see the World Geodetic System - 1984 (WGS-84) Manual + // (Doc 9674 of the International Civil Aviation Organization). + // Defaults to 0. + optional double height_wgs84_m = 3; +} + +// See above for notes on this type. +message GeodeticMsl { + // Defaults to 0. + optional double longitude_deg = 1; + // Defaults to 0. + optional double latitude_deg = 2; + // The height is relative to mean sea level. + // Defaults to 0. + optional double height_msl_m = 3; +} + +// An alternate way to specify GeodeticWgs84 or GeodeticMsl coordinates, used by +// GeodeticTemporalInterpolation to define a platform's motion. +// See above for notes on this type. +message Geodetic { + // Defaults to 0. + optional double longitude_deg = 2; + // Defaults to 0. + optional double latitude_deg = 1; + // Defaults to 0. + optional double height_m = 3; + + // Additional values are unlikely to be added. + enum VerticalDatum { + VERTICAL_DATUM_UNSPECIFIED = 0; + WGS84_ELLIPSOID = 1; + MEAN_SEA_LEVEL = 2; + } + // Defaults to 0. + optional VerticalDatum vertical_datum = 4; +} + +// A timestamped GeodeticWgs84 coordinate. +message GeodeticWgs84Temporal { + // Required. + optional GeodeticWgs84 point = 1; + // Required. + optional google.protobuf.Timestamp time = 2; +} + +// A timestamped Geodetic coordinate. +message GeodeticTemporal { + // Required. + optional Geodetic point = 1; + // Required. + optional google.protobuf.Timestamp time = 2; +} + +// A set of rectilinear 3D coordinates described as (x, y, z) in meters. +// This type is often used to represent a 3D position vector. +message Cartesian { + // Defaults to 0. + optional double x_m = 1; + // Defaults to 0. + optional double y_m = 2; + // Defaults to 0. + optional double z_m = 3; +} + +// A 3D rotational coordinate that describes an angular rotation about an +// arbitrary axis. See +// https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation for reference. +// Quaternions are generally represented in the form: +// w + xi + yj + zk +// where x, y, z, and w are real numbers, and i, j, and k are three imaginary +// numbers. 3D rotations are also commonly represented as Euler angles, such as +// yaw, pitch, and roll. See +// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles +// for how to convert between this representation and quaternions. +// The quaternion is interpreted as the identity if none of the fields are set. +// +// A note about axes defined in terms of quaternion transformations from parent +// reference frames: Let A be a fixed point in space. Let v be A's cartesian +// coordinates in the axes reference frame and w A's cartesian coordinates in +// the parent reference frame. You can use the unit quaternion q below to +// compute +// +// w = q*v*q' or v = q'*w*q +// +// where q' is the conjugate of q and * denotes quaternion multiplication. Note +// that in the formulas above the 3-vectors v and w are treated as quaternions +// whose real part is zero and whose imaginary components correspond to the +// respective components of the 3-vectors. Note also that since q is a unit +// quaternion, q' can be computed simply by negating the three imaginary +// components of q. +// TODO: Migrate uses of this to google.type.Quaternion. +message Quaternion { + // Defaults to 0. + optional double w = 1; + // Defaults to 0. + optional double x = 2; + // Defaults to 0. + optional double y = 3; + // Defaults to 0. + optional double z = 4; +} + +// A rotational coordinate that represents a sequence of rotations about a +// reference set of axes. For an aircraft, yaw corresponds to the heading angle, +// pitch corresponds to the elevation angle, and roll corresponds to the bank +// angle. +message YawPitchRoll { + // Defaults to 0. + optional double yaw_deg = 1; + // Defaults to 0. + optional double pitch_deg = 2; + // Defaults to 0. + optional double roll_deg = 3; +} + +// A point in user-defined axes. +message PointAxes { + // Required. + optional Cartesian point = 1; + + // These axes are interpreted as an offset from the Earth-centered, + // Earth-fixed reference frame. If this field is unset, the platform's axes + // will be the Earth-centered, Earth-fixed reference frame. + // + // The quaternion q satisfies w = q*v*q' where v is a point in the axes' + // coordinate frame, and w is that same point in the parent reference frame + // (i.e. the same frame in which the "point" is specified). + // Required. + optional Quaternion axes = 2; +} + +// A timestamped point in user-defined axes. +message PointAxesTemporal { + // Required. + optional Cartesian point = 1; + + // These axes are interpreted as an offset from the Earth-centered, + // Earth-fixed reference frame. If this field is unset, the platform's axes + // will be the Earth-centered, Earth-fixed reference frame. + // + // The quaternion q satisfies w = q*v*q' where v is a point in the axes' + // coordinate frame, and w is that same point in the parent reference frame + // (i.e. the same frame in which the "point" is specified). + // Required. + optional Quaternion axes = 2; + + // TODO: Migrate to Timestamp. + // Required. + optional GpsTime gps_time = 4; + // WARNING: To specify a time with this point, the gps_time field above must + // be used, and this field will not be considered. This field is used by the + // Spacetime UI. + optional google.protobuf.Timestamp time = 5; +} + +// Interpolation methods listed in increasing order of their ability to fit +// complex functions. +enum InterpolationMethod { + UNKNOWN_METHOD = 0; + // Uses linear interpolation. See + // https://en.wikipedia.org/wiki/Linear_interpolation for reference. + LINEAR = 1; + // Uses a Lagrange interpolating polynomial. + // Conceptually, this method finds the lowest-order polynomial that intersects + // each point. See https://en.wikipedia.org/wiki/Lagrange_polynomial for + // reference. + LAGRANGE = 2; + // Uses a Hermite interpolating polynomial. + // Conceptually, this method finds the lowest-order polynomial that not only + // intersects each point but also matches the derivatives of the function. + // See https://en.wikipedia.org/wiki/Hermite_interpolation for reference. + HERMITIAN = 3; +} + +// PointAxesTemporalInterpolation, GeodeticWgs84TemporalInterpolation, and +// GeodeticTemporalInterpolation specify a list of points and orientations over +// time. Both the translational and rotational coordinates are interpolated +// to calculate the position and orientation of the platform at a given +// timestamp. This is a natural choice to express the motion of platforms with a +// known trajectory, such as an aircraft, ship, or launch vehicle. +// +// These types are commonly used in conjunction with a gRPC client that +// periodically updates the motion of a platform across the NBI. Imagine there +// is an airborne vehicle carrying a sensor that needs to constantly stream +// data to a Command Center, and an Ops Center that is tracking the +// position of this airbone vehicle. A lightweight script or binary can ingest +// the position observations from the Ops Center and then update, via the NBI, +// the corresponding Spacetime platform's position in real-time. This allows +// Spacetime to orchestrate constant connectivity between the airborne vehicle +// and the Command Center. +// +// A "location updater" job, similar to the scenario described above, could use +// one of PointAxesTemporalInterpolation, GeodeticWgs84TemporalInterpolation, or +// GeodeticTemporalInterpolation to describe the motion of a mobile platform. +// This job would simply append points to the locations_orientations_over_time +// or locations_over_time fields below in real-time. +// WARNING: If this job always set the timestamp of each observation to +// the current timestamp and the list contained no points in the future, you +// likely would not see the platform in the "live" Spacetime UI. By the time the +// request had been sent across the NBI, the timestamp would have already +// passed, so the "live" view of the Spacetime UI would not have location data +// to interpolate and render. To see the platform in the UI, either: +// 1) if available, store one or more predicted future points based on the +// platform's trajectory in the locations_over_time field +// 2) if your network can tolerate it, or for non-production purposes, shift +// the timestamp of each point into the future so that at least one +// point in the future always exists within the locations_over_time field +// TODO: Implement a configurable approach to remove the oldest points in +// the locations list to prevent this list from growing to an unbounded size. +message PointAxesTemporalInterpolation { + // If no axes are specified, the platform's axes will match the + // Earth-centered, Earth-fixed reference frame. + repeated PointAxesTemporal locations_orientations_over_time = 1; + optional InterpolationMethod interpolation_method = 2 [default = LINEAR]; + // The degree of the polynomial to use for interpolation. + optional int32 interpolation_degree = 3 [default = 1]; +} + +// See above for notes on this type. +message GeodeticWgs84TemporalInterpolation { + // The platform's axes are oriented in the Earth's reference frame, such that: + // - The x-axis points in the local East direction. + // - The y-axis points in the local North direction. + // - The z-axis points in the direction of the normal vector to the WGS 84 + // ellipsoid surface which passes through the point. Conceptually, + // the z-axis is oriented "outwards" from the Earth's surface towards + // space. + repeated GeodeticWgs84Temporal locations_over_time = 1; + optional InterpolationMethod interpolation_method = 2 [default = LINEAR]; + // The degree of the polynomial to use for interpolation. + optional int32 interpolation_degree = 3 [default = 1]; +} + +// See above for notes on this type. +message GeodeticTemporalInterpolation { + // The platform's axes are oriented in the Earth's reference frame, such that: + // - The x-axis points in the local East direction. + // - The y-axis points in the local North direction. + // - The z-axis points in the direction of the normal vector to the WGS 84 + // ellipsoid surface which passes through the point. Conceptually, + // the z-axis is oriented "outwards" from the Earth's surface towards + // space. + repeated GeodeticTemporal locations_over_time = 1; + optional InterpolationMethod interpolation_method = 2 [default = LINEAR]; + // The degree of the polynomial to use for interpolation. + optional int32 interpolation_degree = 3 [default = 1]; +} + +// A two-line element set (TLE). +// TLEs for some unclassified objects are publicly available at +// https://celestrak.org. See https://en.wikipedia.org/wiki/Two-line_element_set +// for reference. +// When a platform's motion is described using this message, the platform's axes +// are oriented in the Earth's inertial frame, such that: +// - The x-axis is aligned with the platform's velocity vector. +// - The y-axis is aligned with the negative angular momentum vector. +// - The z-axis is aligned with the negative position vector, which points +// towards the Earth's center of mass. +// For reference, see The Consultative Committee for Space Data Systems +// CCSDS 500.0-G-4 Section 4.3.7.2. +// The orbit is propagated using the NORAD SGP4/SDP4 model as defined by the +// Center for Space Standards and Innovation (CSSI). +message TwoLineElementSet { + optional string line1 = 1; + optional string line2 = 2; +} + +// Celestial bodies. +enum CentralBody { + UNKNOWN_CENTRAL_BODY = 0; + EARTH = 1; + MOON = 2; +} + +// Keplerian elements. +// See https://en.wikipedia.org/wiki/Orbital_elements#Keplerian_elements or The +// Consultative Committee for Space Data CCSDS 500.0-G-4 +// section 5.2.2.4, for reference. +// When a platform's motion is described using this message, the platform's axes +// are oriented in the Earth's inertial frame, such that: +// - The x-axis is aligned with the platform's velocity vector. +// - The y-axis is aligned with the negative angular momentum vector. +// - The z-axis is aligned with the negative position vector, which points +// towards the Earth's center of mass. +// For reference, see The Consultative Committee for Space Data Systems +// CCSDS 500.0-G-4 Section 4.3.7.2. +// The orbit is propagated using a first order J2 perturbation algorithm which +// models only the secular effects on the orbital elements. The Earth +// Gravitational Model of 1996 (EGM96) according to NASA Technical Publication +// 1998-206861 is used. +message KeplerianElements { + // Semimajor axis (distance), in meters. + // Defaults to 0. + optional double semimajor_axis_m = 1; + // Eccentricity (the shape of the orbital ellipse). + // This value must be between 0.0 (a circular orbit) and 1.0. + // Defaults to 0. + optional double eccentricity = 2; + // Inclination, in degrees. + // Defaults to 0. + optional double inclination_deg = 3; + // Argument of periapsis, in degrees. + // Defaults to 0. + optional double argument_of_periapsis_deg = 4; + // Right ascension of ascending node, in degrees. + // Defaults to 0. + optional double raan_deg = 5; + // True anomaly, in degrees. + // Defaults to 0. + optional double true_anomaly_deg = 6; + // A set of orbital elements is a snapshot, at a particular time, of the orbit + // of a satellite. This specifies the time at which the snapshot was taken. + // Required. + optional DateTime epoch = 7; + // The celestial body used to fetch gravitational parameters. + // Required. + optional CentralBody central_body = 8 [default = EARTH]; +} + +// A set of 3D velocity components. +message CartesianDot { + // Velocity along x-axis in meters per second. + // Defaults to 0. + optional double x_mps = 1; + // Velocity along y-axis in meters per second. + // Defaults to 0. + optional double y_mps = 2; + // Velocity along z-axis in meters per second. + // Defaults to 0. + optional double z_mps = 3; +} + +// Generic state vector for any satellite positions. State vectors are used in +// the CCSDS Orbit Parameter Message standard (502.0-B-2), and are typical for +// lunar and other interplanetary mission data, as well as any other generic +// trajectories where TLEs or Keplerian elements are not well suited, e.g. for +// powered flight, maneuvers, etc. +// +// When a platform's motion is described using this message, the platform's axes +// are oriented in the Moon's inertial frame, such that: +// - The x-axis is aligned with the platform's velocity vector. +// - The y-axis is aligned with the negative angular momentum vector. +// - The z-axis is aligned with the negative position vector, which points +// towards the Moon's center of mass. +// For reference, see The Consultative Committee for Space Data Systems +// CCSDS 500.0-G-4 Section 4.3.7.2. +// +// WARNING: This message is under development and not fully supported. +message StateVector { + // Time that the state vector was measured. + optional google.protobuf.Timestamp epoch = 1; + + enum CoordinateFrame { + // The frame should always be known; using this must indicate some error. + UNKNOWN_FRAME = 0; + // This indicates the frame used for most E-M Lagrange point orbit studies. + // It is a natural and convenient way to input L1/L2 orbits. + EARTH_MOON_BARYCENTER_SYNODIC_FRAME = 1; + // Internally, ECEF is used most everywhere else in our software. + ECEF_FRAME = 2; + } + // Indication of the reference frame for the state vector. + optional CoordinateFrame coordinate_frame = 2; + + // Three dimensional position and velocity relative to the reference frame. + optional Cartesian position = 3; + optional CartesianDot velocity = 4; + + enum PropagationAlgorithm { + // Generally, a particular class of algorithm should be specified instead. + UNSPECIFIED_ALGORITHM = 0; + // State vector for a vehicle in cislunar space, that should be + // propagated using a three-body algorithm considering the Earth and + // Moon's gravity contributions. + EARTH_MOON_THREE_BODY_NUMERICAL_ALGORITHM = 1; + } + // Indicate the type of propagation algorithm to be used. + optional PropagationAlgorithm propagation_algorithm = 5; + + // TODO: In the future, if we progress to more complex use cases for deep + // space, interplanetary, and other types of trajectories, we may want to + // develop a PropagationParameters message with more details about the + // relevant force models, constants, and other attributes to be used for + // accurate propagation, rather than implying the total set simply from the + // PropagationAlgorithm enum. + + /// + // Parameters relevant to modeling forces on the body in motion. + /// + + // The approximate mass of the body, in kilograms. + optional double mass_kg = 6 [default = 2000.0]; + + // The approximate coefficient of reflectivity and applicable area in + // square meters as pertains to solar radiation pressure. + // + // For the coefficient of reflectivity, only values from 0.0 (perfect + // absorption) to 1.0 (perfect reflectivity) are meaningful. + optional double reflectivity = 7 [default = 1.0]; + optional double reflective_area_sqm = 8 [default = 20.0]; +} + +// A set of curvilinear 3D coordinates relative to the Mean Lunar Radius +// according to the Report of the IAU/IAG Working Group on Cartographic +// Coordinates and Rotational Elements of the Planets and satellites: 2000. +// +// When a platform's motion is described using this message, the platform's axes +// are defined in the Moon's reference frame, such that: +// - The x-axis points in the local East direction. +// - The y-axis points in the local North direction. +// - The z-axis points in the direction of the normal vector to the reference +// Moon ellipsoid surface which passes through the point. Conceptually, the +// z-axis is oriented "outwards" from the Moon's surface towards space. +message SelenographicMlr { + // Defaults to 0. + optional double longitude_deg = 1; + // Defaults to 0. + optional double latitude_deg = 2; + // Defaults to 0. + optional double height_mlr_m = 3; +} + +// STK Ephemeris and Attitude files generated by the STK Desktop tool. +// The maximum size of this message is constrained by the maximum size +// of protocol buffers, which is 2GB. +message StkEphemAndAttitudeFile { + // Required. + optional string ephem_file = 1; + // Required. + optional string attitude_file = 2; +} + +// A type to express spatial regions over the Earth's surface. +// The S2 library defines an index over the 3D surface of the Earth. Arbitrary +// regions can be described as a collection of S2 cells, each of which can +// represent a region of a different size. See http://s2geometry.io for +// reference. +message S2Cells { + // Required. + repeated uint64 ids = 1; +} + +// An azimuth/elevation direction vector. +message PointingVector { + // Defaults to 0. + optional double azimuth_deg = 1; + // Defaults to 0. + optional double elevation_deg = 2; +} + +message Motion { + // The time interval for which this motion specification is applicable. + // + // This may be left- or right-unbounded, or unspecified altogether. When + // the start or end times (or both) are unspecified, and the motion + // specification does not contain a clear indication of valid timestamps, + // it is an error if an applicable boundary is required but cannot be + // inferred from context (e.g., if there is more than one Motion message + // in a sequence but the start/end time of one cannot be inferred from the + // end/start time of another). + optional google.type.Interval interval = 10; + + // This enum is used in various other messages, e.g. Targeting, to reference + // supported or required motion description formats. + // + // The enum values are kept deliberately in sync with the field values in the + // oneof below, but this is not stricly required in order to read the + // contents of the oneof. In this case it suffices to use the + // protobuf-constructed language-specific support for switch statements + // (which, for C++ and Java for example, generates a language-appropriate + // enum to use). + enum Type { + MOTION_UNSPECIFIED = 0; + GEODETIC_MSL = 7; + GEODETIC_WGS84 = 1; + ECEF_FIXED = 2; + ECEF_INTERPOLATION = 3; + CARTOGRAPHIC_WAYPOINTS = 4; + GEODETIC_WAYPOINTS = 12; + TLE = 5; + KEPLERIAN_ELEMENTS = 6; + // WARNING: This message is under development and not fully supported. + STATE_VECTOR = 9; + SELENOGRAPHIC_MLR = 8; + STK_EPHEM_AND_ATTITUDE_FILE = 11; + CCSDS_ORBIT_EPHEMERIS_MESSAGE = 13; + } + + oneof type { + GeodeticMsl geodetic_msl = 7; + GeodeticWgs84 geodetic_wgs84 = 1; + PointAxes ecef_fixed = 2; + PointAxesTemporalInterpolation ecef_interpolation = 3; + GeodeticWgs84TemporalInterpolation cartographic_waypoints = 4; + GeodeticTemporalInterpolation geodetic_waypoints = 12; + TwoLineElementSet tle = 5; + KeplerianElements keplerian_elements = 6; + // WARNING: This message is under development and not fully supported. + StateVector state_vector = 9; + SelenographicMlr selenographic_mlr = 8; + StkEphemAndAttitudeFile stk_ephem_and_attitude_file = 11; + CcsdsOrbitEphemerisMessage ccsds_orbit_ephemeris_message = 13; + } +} + +// A CCSDS Orbit Ephemeris Message (OEM), per the +// specification CCSDS 502.0-B-3 published April 2023. +// +// Notes: +// - Though the OEM Metadata INTERPOLATION and INTERPOLATION_DEGREE +// fields are Optional in the specification, they are required by Spacetime +// - File format can be either Key-value notation (KVN) or extensible markup +// language (XML) +// - Presently supports single-segment CCSDS OEM Files (multi-segment will +// throw error) +message CcsdsOrbitEphemerisMessage { + // CCSDS OEM File as a String + optional string file = 1; +} diff --git a/api/common/network.proto b/api/common/network.proto new file mode 100644 index 0000000..3d84f8d --- /dev/null +++ b/api/common/network.proto @@ -0,0 +1,193 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "google/protobuf/empty.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Uniquely identifies a network interface. +message NetworkInterfaceId { + // The unique ID of the node on which the interface is installed. + optional string node_id = 1; + + // The node-unique interface ID. + optional string interface_id = 2; +} + +// Defines rules for classifying a packet. +message PacketClassifier { + // If high is not specified, the range contains a single value, specified by + // low. If low and high are both specified, the range is [low, high]; the high + // value is part of the range. If low == high, it represents the single value + // of low (same as not providing a high value). + message ValueRange { + optional uint32 low = 1; + optional uint32 high = 2; // default is same value as "low" + // If high < low, the range is empty. + reserved 3 to max; // Next IDs. + } + + // Classifier for IPv4 or IPv6 packet headers. + message IpHeader { + // This field is represented as an ASCII IPRange. + optional string src_ip_range = 1; + + // This field is represented as an ASCII IPRange. + optional string dst_ip_range = 2; + + // IP protocol number. + optional uint32 protocol = 3; + + reserved 4 to max; // Next IDs. + } + optional IpHeader ip_header = 1; + + // Used to match ports for protocols TCP, UDP, SCTP, etc. + message GenericLayer4Header { + repeated ValueRange source_port = 1; + repeated ValueRange destination_port = 2; + reserved 3 to max; // Next IDs. + } + optional GenericLayer4Header l4_header = 2; + + message EthernetHeader { + // To match a multicast address set the multicast field. Alternatively, set + // the desired matching address in the address field. + oneof destination { + google.protobuf.Empty multicast = 1; + + // Ethernet address in readable colon separated format. e.g. + // "1:23:45:67:89:ab" or "01:23:45:67:89:ab" + string address = 2; + } + reserved 3 to max; // Next IDs. + } + optional EthernetHeader ethernet_header = 3; + + // [RFC 3032](https://rfc-editor.org/rfc/rfc3032) + message MplsLabelStackEntry { + optional uint32 label = 1; // restricted to unsigned 20-bit values + + // TODO: Consider https://rfc-editor.org/rfc/rfc5462 field. + // optional uint32 traffic_class = 2; unsigned 3-bit values only + + // If this MplsLabelStackEntry is last in an ordered list of entries, + // then it may be inferred that it is the bottom of the stack. Whether + // the Bottom-of-Stack bit is to be set or not may require additional + // context. + // + // TODO: Consider explicit Bottom of Stack indicator. + // optional bool s = 3; + } + optional MplsLabelStackEntry mpls_label_stack_entry = 4; + + reserved 5 to max; // Next IDs. +} + +// A subnet that can be expressed explicitly as an IP range, as a node ID, or as +// an interface ID. In the case of a node ID, the intent compiler will resolve +// the node ID to all the address ranges that the node represents. In the case +// of an interface ID, the intent compiler will resolve the interface ID to an +// address range of only the interface address. +message Subnet { + oneof subnet { + string ip_range = 1; + string node_id = 2; + // TODO: Remove when we can more cleanly specify a tunnel packet + // classifier with node ID when multiple subnets are associated with a node. + NetworkInterfaceId interface_id = 3; + } + reserved 4 to max; // Next IDs. +} + +// Classifier for IPv4 or IPv6 flows. +message IpFlowClassifier { + optional Subnet src = 1; + optional Subnet dst = 2; + + // IP protocol number. + optional uint32 protocol = 3; + reserved 4 to max; // Next IDs. +} + +// MEF EVC E-Line Flow Classifier +// +// Suitable for describing a MEF 10.4 section 8.3.1 Point-to-Point EVC, +// specifically: +// +// * "Ethernet Private Line Service (EPL) +// Point-to-Point EVC, all to one bundling" +// +// See also: MEF 6.3 Section 9.1, MEF 7.4 Section 11.1 +// +// * "Ethernet Virtual Private Line (EVPL) +// Point-to-Point EVC, bundling and/or multiplexing" +// +// See also: MEF 6.3 Section 9.2, MEF 7.4 Section 11.2 +message EvcElineFlowClassifier { + // A locally meaningful name for traffic described by this classifier. + // + // Possible examples include: + // * the MEF 10.4 section 10.1 EVC EP ID service attribute that + // corresponds to the UNI and Map service attributes below, or + // * an identifier for an SR Policy or a specific pseudowire that + // will carry this traffic. + // + // This does not appear anywhere in the data plane and the value is + // opaque to rest of the system. + optional string id = 1; + + // E.g., MEF 10.4 section 10.2 EVC EP UNI service attribute. + // Node-local interface name; node MUST be inferred from additional + // context (e.g. ServiceRequest or ScheduledControlUpdate). + optional string uni = 2; + + // MEF 10.4 section 10.4 EVC EP Map service attribute. + // + // For section 10.4.1 "EVC EP Map Service Attribute = List", add + // all applicable non-zero, non-4095 Customer Edge VLAN IDs. + // + // For section 10.4.2 "EVC EP Map service attribute = All", leave + // this list empty. + // + // For section 10.4.3 "EVC EP Map service attribute = UT/PT", use + // only a single VLAN ID, either 0 (zero) or the value of the UNI's + // Default CE-VLAN ID attribute (MEF 10.3 and earlier), whichever + // has local meaning. + repeated uint32 vlan_ids = 3; + + // For use if Ethernet service frames are classified by another + // network element and encapsulated inside an MPLS label stack + // that uniquely identifies the flow. + // + repeated PacketClassifier.MplsLabelStackEntry mpls_stack = 4; +} + +// Rules for matching a network flow. +message FlowClassifier { + optional IpFlowClassifier ip_classifier = 1; + + optional PacketClassifier.GenericLayer4Header l4_classifier = 2; + + optional PacketClassifier.EthernetHeader ethernet_classifier = 3; + + optional EvcElineFlowClassifier evc_eline_classifier = 4; + + reserved 5 to max; // Next IDs. +} diff --git a/api/common/platform.proto b/api/common/platform.proto new file mode 100644 index 0000000..86c3d7c --- /dev/null +++ b/api/common/platform.proto @@ -0,0 +1,99 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Protobuf messages to model platforms (physical things with geospatial +// coordinates and properties, such as aircraft, satellites, surface vehicles, +// ground stations) and their associated physical-layer communications assets +// (antennas, transmitters, receivers). + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/bent_pipe.proto"; +import "api/common/coordinates.proto"; +import "api/common/wireless_transceiver.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Models a physical platform with optional communications assets. +message PlatformDefinition { + reserved 1, 6 to 7, 8, 9, 10, 11, 19 to max; + + // Optional friendly, human-readable strings for UI purposes. + optional string name = 2; + optional string type = 12; + + // A freeform string, used as a key in other contexts to lookup + // context-relevant attributes (UI visual configuration, etc). + optional string category_tag = 5; + + // Root platforms have location translations relative to the center of the + // Earth and orientation rotations relative to the Earth-Centered-Earth-Fixed + // (ECEF) reference axes. + optional Motion coordinates = 14; + + // The coordinates above can be updated through different means, including + // from sources that the SDN controller can automatically fetch from. The + // motion_source signals how updates are nominally to be obtained. + enum MotionSource { + UNKNOWN_SOURCE = 0; // Used for manual updates or other means. + SPACETRACK_ORG = 1; // Indicates NORAD TLE updates are to be used. + FLIGHTRADAR_24 = 2; // FlightRadar 24 trajectory updates are used. + } + optional MotionSource motion_source = 16; + + // WARNING: This field is under development and not fully supported. + // A string identifier indicating the `MotionDefinition` ID to be used for + // the description of this platform's motion. + // + // If present, this supersedes any supplied `coordinates` message. + // + // Using a separate `MotionDefinition` is recommended whenever the motion + // is likely to be updated over time and/or have multiple different types + // of pre-planned motion (e.g. a fixed position on a launch pad, followed by + // a launch maneuver, followed by a TLE, etc). + optional string motion_ref_id = 4; + + // Communications assets on this platform. + // TransceiverModels and/or BentPipePayloads can be specified on a + // PlatformDefinition. + // + // Transceiver models associated with this platform. + // Multiple physical devices on the platform may share the same model. + repeated TransceiverModel transceiver_model = 15; + // Bent pipe payloads on this platform. + repeated BentPipePayload bent_pipe_payloads = 18; + + // Automatic dependent surveillance—broadcast (ADS-B) information. + // This field is optional and is only relevant to aircraft platforms. + optional AdsbTransponder adsb_transponder = 13; + + // If the motion_source inticates that updated TLEs will come from a + // 3rd-party, then the norad_id indicates the NORAD satellite catalog + // number that will be used for reference. + // If the platform owner provides their own higher-precision updates instead, + // then this field does not need to be set, and will have no effect if it is + // present. + optional uint32 norad_id = 17; +} + +// Models information from an ADS-B transponder. +message AdsbTransponder { + reserved 3 to max; + + optional uint32 icao_aircraft_address = 1; + optional string aircraft_identification = 2; +} diff --git a/api/common/platform_antenna.proto b/api/common/platform_antenna.proto new file mode 100644 index 0000000..11db613 --- /dev/null +++ b/api/common/platform_antenna.proto @@ -0,0 +1,212 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Protobuf messages used to model antennas and optical pointing elements. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/coordinates.proto"; +import "google/protobuf/empty.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Models an aperture (RF or optical) with a certain signal gain pattern. +// It may optionally be oriented independently of its platform to track objects. +message AntennaDefinition { + reserved 1, 4, 5, 7, 8, 19 to max; + + // An optional friendly, human-readable string. + optional string name = 2; + + // Antenna coordinates may be specified using a fixed offset from their + // parent platform. If this field is not used, the antenna will share the + // coordinates of its parent platform. + optional PointAxes fixed_coordinate_offset = 3; + + // Parameters relevant to antenna steering and target acquisition. + // This field should be omitted when modeling a non-steerable beam. + optional Targeting targeting = 18; + + // Identifies the antenna's gain pattern. + optional string antenna_pattern_id = 10; + + // Defines the polarization over time, if any. This field is optional. + optional Polarization polarization = 17; + + // Optional. Defines the unobstructed field of regard (FOR) for steerable + // antennas. If this field is set for a fixed antenna, it will instead act to + // further constrain the field of view (FOV) that is otherwise defined by the + // antenna gain pattern. + optional Projection field_of_regard = 11; + + // Specifies obstructions that subtract from the accessible field-of-regard. + repeated Projection obstructions = 12; + + // An Azimuth-Elevation Mask describes how the horizon looks to a steerable + // antenna or aperature that is fixed on the surface of a planet. It + // specifies the maximum obscured elevation angle in each sampled direction + // from the stationary object. Note that the definition of an azimuth + // elevation mask does not constrain link accessibility unless the + // minimum_azimuth_elevation_mask_separation_deg constraint is also set. + message ElevationMask { + // The azimuth angle in degrees measured from North toward East. + optional double azimuth_deg = 1; + + // The maximum obscured elevation angle, in degrees. + // Set to 90 deg if links in this azimuth direction are always inaccessible. + // Negative values are also allowed (for antennas above terrain). + optional double maximum_obscured_elevation_deg = 2; + + // When needing to describe a richer variation in obscured elevation angle + // along a given azimuth than may be permitted by the "maximum obscured + // elevation angle" alone, a sequence of ElevationRise elements may be used. + // Taken together in order of increasing distance they describe how the + // angle of obscuration changes as a function of distance along the ray + // indicated by the azimuth. + // + // Nonsensical values are silently discarded: negative distances as well as + // elevation angles lower than -90 degrees or higher than 90 degrees. + // + // Additionally, elements may be inserted at distance = 0.0m or appended to + // the end to complete the described curve from the origin point up to the + // above specified maximum_obscured_elevation_deg value. + message ElevationRise { + // Distance from the fixed antenna/aperature to the start of obscuration, + // in meters. If this field is absent the obscured_elevation_deg is used + // to create an ElevationRise at 0.0m distance. + optional double distance = 1; + + // The obscured elevation angle, in degrees, at this distance. + // Negative values are also allowed (for antennas above terrain). + optional double obscured_elevation_deg = 2; + } + repeated ElevationRise elevation_rise = 3; + } + repeated ElevationMask azimuth_elevation_mask = 9; + + // Factors that constrain the accessibility of links that use the antenna. + optional AntennaConstraints constraints = 6; +} + +message AntennaConstraints { + reserved 8 to max; + + message LinkRangeConstraint { + optional double minimum_range = 1; + optional double maximum_range = 2; + } + optional LinkRangeConstraint link_range = 1; + + // AzimuthAngleRateConstraint is relative to the plane tangent to the surface + // of the central body (Earth). This is appropriate for ground stations or + // other vehicles on the surface of the central body; but, for aerospace + // platforms, it's usually better to use the TotalAngularRateConstraint. + message AzimuthAngleRateConstraint { + optional double minimum_rate_deg_per_sec = 1; + optional double maximum_rate_deg_per_sec = 2; + } + optional AzimuthAngleRateConstraint azimuth_angle_rate = 3; + + message ElevationAngleRateConstraint { + optional double minimum_rate_deg_per_sec = 1; + optional double maximum_rate_deg_per_sec = 2; + } + optional ElevationAngleRateConstraint elevation_angle_rate = 4; + + message TotalAngularRateConstraint { + optional double minimum_rate_deg_per_sec = 1; + optional double maximum_rate_deg_per_sec = 2; + } + optional TotalAngularRateConstraint total_angular_rate = 5; + + // Link accessibility will be constrained when the angle between the sun's + // center of mass and a receiving antenna's boresight is less than this + // threshold (in degrees). + // The sun is ~32 arcminutes in diameter when viewed from the Earth's surface, + // and margin for that should be built into the configured value, if it is + // significant to the user. + optional double minimum_sun_angle_deg = 6; + + // Link accessibility will be constrained when the angle between the boresight + // and any AzimuthElevationMask associated with the antenna is less than this + // threshold (in degrees). The is measured as the angular separation in the + // positive (vertical) direction from the maximum obscured elevation in the + // azimuth direction of the link vector. + optional double minimum_azimuth_elevation_mask_separation_deg = 7; +} + +message Projection { + // Defines a conical projection. + message Conic { + // Angle between the positive Z-axis and the external boundary of the + // volume. If omitted, the volume is a sphere less any interior exclusion. + optional double outer_half_angle_deg = 1; + + // Angle between the positive Z-axis and an optional, interior conical + // volume of exclusion. Used to optionally restrict the volume. + optional double inner_half_angle_deg = 2; + } + + // Defines a rectangular projection + message Rectangular { + // Measured from the principal direction and in the direction of the X-axis. + optional double x_half_angle_deg = 1; + + // Measured from the principal direction and in the direction of the Y-axis. + optional double y_half_angle_deg = 2; + } + + // Defines a custom projection. + message Custom { + // A list of directions defining the outer perimeter of the projection. + // Azimuth is the angle in the XY plane measured from the positive X-axis + // towards the positive Y-axes. Elevation is measured from the XY-plane + // and toward the negative z-axis. + repeated PointingVector directions = 1; + } + + oneof shape_type { + Conic conic = 1; + Rectangular rectangular = 2; + Custom custom = 3; + } +} + +message Polarization { + // Constant polarization over time. + message Constant { + message Linear { + // The tilt angle, in degrees, relative to the X axis. + optional double tilt_angle_deg = 1; + } + + oneof polarization { + google.protobuf.Empty left_hand_circular = 1; + google.protobuf.Empty right_hand_circular = 2; + Linear linear = 3; + } + } + + oneof source { + Constant constant = 1; + } +} + +message Targeting { + // The format to use when providing the antenna with target acquisition info. + optional Motion.Type motion_format = 1; +} diff --git a/api/common/telemetry.proto b/api/common/telemetry.proto new file mode 100644 index 0000000..2ad3c4e --- /dev/null +++ b/api/common/telemetry.proto @@ -0,0 +1,267 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/coordinates.proto"; +import "api/common/time.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +message NetworkStatsReport { + // Uniquely identifies the network node/element. + optional string node_id = 7; + + // Time at which the report was sent. This field is optional. + optional DateTime timestamp = 1; + + // These maps are keyed on the network interface ID. + map radio_stats_by_interface_id = 4; + map interface_stats_by_id = 3; + + // This map is keyed on the antenna ID. + map beam_stats_by_antenna_id = 5; + + // Producers: use the flow_rule_id as the key in this map. + // Consumers: assume that the key is the PathIntent ID. + // The SDN controller is responsible for remapping the key before publishing. + map flow_stats_by_id = 6; + + reserved 2; +} + +message BeamStats { + // Time at which the statistics were captured by the network element. + optional DateTime timestamp = 1; + + message TargetingStats { + // The latest location that the beam is trying to target. + optional aalyria.spacetime.api.common.GeodeticWgs84 target_location = 1; + + enum ConnectionStatus { + UNKNOWN = 0; + SEEKING = 1; + LOCKED = 2; + } + + // The progress of establishing a connection with the target specified in + // current_beam_update. + optional ConnectionStatus connection_status = 2; + + // The latest beam task ID + optional string beam_task_id = 3; + + // An ID used to identify the target. For example, this can be the 48-bit + // MAC address of the remote target's network interface. + optional string target_identifier = 4; + } + // Information about the current target. If no current target exists, leave + // this blank. + optional TargetingStats targeting = 2; + + message GimbalStats { + // The current location of the gimbal. + optional aalyria.spacetime.api.common.GeodeticWgs84 location = 1; + + oneof site_orientation { + // The rotational offset relative to the ECEF frame. + aalyria.spacetime.api.common.Quaternion orientation_quaternion = 2; + + // The rotational offset from the North-East-Down (NED) frame to the + // gimbal's local reference frame. + aalyria.spacetime.api.common.YawPitchRoll orientation_ypr = 3; + } + + // The direction at which the gimbal is pointing the center of its beam. + // These coordinates are relative to the site_orientation. + optional aalyria.spacetime.api.common.PointingVector pointing_vector = 4; + // Whether this gimbal is fully initialized and should be able to perform + // tasks. + optional bool initialized = 5; + } + // Information about the gimbal. If the beam is not gimbaled, leave this + // blank. + optional GimbalStats gimbal = 3; +} + +// Measured statistics obtained from a transmitter. +message TransmitterStats { + // Time at which the statistics were captured by the network element. + optional DateTime timestamp = 4; + + // Optionally specifies the physical address of the receiver. + // This is only applicable for radios that employ a MAC protocol, like WiFi. + optional string receiver_physical_address = 5; + + // The physical-layer data rate in use by the transmitter, in bits per + // second. This may vary dynamically based on an adaptive coding and + // modulation scheme. + optional double data_rate_bps = 2; + + // The transmit packet error rate averaged over the last 5 seconds. + // This is a ratio of number of packets that did not receive positive + // acknowledgement from the receiver, to the total number of packets + // transmitted. + optional double tx_packet_error_rate = 3; + + reserved 1; +} + +// Measured statistics obtained from a receiver. +message ReceiverStats { + // Time at which the statistics were captured by the network element. + optional DateTime timestamp = 3; + + // Optionally specifies the physical address of the transmitter. + // This is only applicable for radios that employ a MAC protocol, like WiFi. + optional string transmitter_physical_address = 4; + + // The power, in dBW, of the signal at the output of the receiver, prior to + // demodulating the signal into data. This includes the effects of the + // receiving antenna gain, signal filter, and/or amplifier. + optional double power_at_receiver_output_dbw = 2; + + // Similar to power_at_receiver_output_dbw, but broken down by chain. + // The order of elements in this field should be consistent with the + // canonical ordering of the chains. + repeated double power_at_receiver_output_by_chain_dbw = 5; + + // The mean squared error, in dB, of the signal processed by the receiver. + optional double mse_db = 6; + + // The carrier to noise plus interference ratio (CINR), in dB, measured + // by the receiver. + optional double carrier_to_noise_plus_interference_db = 7; + + reserved 1; +} + +message RadioStats { + // IDs to identify the local interface for this radio + optional string interface_id = 5; + repeated TransmitterStats transmitter_stats = 3; + repeated ReceiverStats receiver_stats = 4; + + reserved 1, 2; +} + +// Summary interface statistics, with the time observed. +// +// Modeled after RFC 8343 `ietf-interfaces` module `statistics` +// entry, slightly simplified: +// +// +--ro statistics +// +--ro in-octets? yang:counter64 +// +--ro in-unicast-pkts? yang:counter64 +// +--ro in-discards? yang:counter32 +// +--ro in-errors? yang:counter32 +// +--ro out-octets? yang:counter64 +// +--ro out-unicast-pkts? yang:counter64 +// +--ro out-discards? yang:counter32 +// +--ro out-errors? yang:counter32 +// +// Field names inspired by Linux RT_NETLINK structures. +message InterfaceStats { + optional DateTime timestamp = 9; + + // Number of packets transmitted and received. + optional int64 tx_packets = 1; + optional int64 rx_packets = 2; + + // Number of bytes transmitted and received. + optional int64 tx_bytes = 3; + optional int64 rx_bytes = 4; + + // Number of packets dropped. + optional int64 tx_dropped = 5; + optional int64 rx_dropped = 6; + + // Number of packet errors (i.e., frame alignment errors, rx overruns, + // CRC errors, packet collisions, etc. + optional int64 rx_errors = 7; + optional int64 tx_errors = 8; +} + +message FlowStats { + optional DateTime timestamp = 1; + + // Number of packets transmitted and received. + optional int64 tx_packets = 2; + optional int64 rx_packets = 3; + + // Number of bytes transmitted and received. + optional int64 tx_bytes = 4; + optional int64 rx_bytes = 5; +} + +// Asynchronous network telemetry event reports +message NetworkEventReport { + // Uniquely identifies the network node/element. + optional string node_id = 5; + + // Time at which the event occurred. + optional DateTime timestamp = 1; + + oneof source_type { + RadioEvent radio_event = 2; + PortEvent port_event = 3; + InterfaceEvent interface_event = 4; + } +} + +// Asynchronous radio events +message RadioEvent { + oneof radio_id { + string transmitter_id = 1; + string receiver_id = 2; + } + enum LinkStatus { + UNKNOWN = 0; + DOWN = 1; + UP = 2; + } + oneof event { + LinkStatus link_status = 3; + } +} + +// Asynchronous (wired) port events +message PortEvent { + optional string port_id = 1; + enum PortStatus { + UNKNOWN = 0; + DOWN = 1; + UP = 2; + } + oneof event { + PortStatus port_status = 2; + } +} + +// Asynchronous logical network interface events +message InterfaceEvent { + optional string interface_id = 1; + enum InterfaceStatus { + UNKNOWN = 0; + DISABLED = 1; + ENABLED = 2; + } + oneof event { + InterfaceStatus interface_status = 2; + string ip_address = 3; + } +} diff --git a/api/common/time.proto b/api/common/time.proto new file mode 100644 index 0000000..9bae516 --- /dev/null +++ b/api/common/time.proto @@ -0,0 +1,49 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// TODO: Migrate to Timestamp. +message DateTime { + // 'Smeared' microseconds since the Unix epoch. See + // https://developers.google.com/time/smear. + optional int64 unix_time_usec = 1; + + optional GpsTime gps_time = 2; +} + +// TODO: Migrate to Timestamp. +message GpsTime { + optional int32 week_number = 1; + optional int32 second_of_week = 2; + optional int32 usec = 3; +} + +// TODO: Migrate to Duration. +message Duration { + required int64 microseconds = 1; +} + +// Time intervals are half-closed, [start_time, end_time) +// If the start and/or end time are not included, the time interval should +// be interpreted as extending to infinity in that timeline direction. +message TimeInterval { + optional DateTime start_time = 1; // closed + optional DateTime end_time = 2; // open +} diff --git a/api/common/tunnel.proto b/api/common/tunnel.proto new file mode 100644 index 0000000..07bd7e4 --- /dev/null +++ b/api/common/tunnel.proto @@ -0,0 +1,96 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "google/protobuf/empty.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +enum AuthenticationAlgorithm { + AUTH_ALGO_UNSPECIFIED = 0; + HMAC_SHA1_96 = 1; +} + +enum EncryptionAlgorithm { + ENCRYPTION_ALGO_UNSPECIFIED = 0; + AES_CBC_128 = 1; +} + +// Defines the use of an Encapsulating Security Payload (ESP). +message EspProtocol { + // Algorithm employed to ensure packet integrity. + // May be omitted to disable integrity protection. + message EspIntegrity { + optional AuthenticationAlgorithm algorithm = 1; + reserved 2; + } + optional EspIntegrity authentication = 1; + + // Encryption algorithm and its mode of operation for ensuring privacy. + // May be omitted to disable privacy protection. + message EspPrivacy { + optional EncryptionAlgorithm algorithm = 1; + reserved 2; + } + optional EspPrivacy encryption = 2; +} + +// Defines the parameters necessary to perform ESP encap/decap. +message EspParameters { + // Index uniquely identifying the parameters from all other decap parameters + // on the decap node. + optional uint32 security_parameters_index = 3; + + // Authentication algorithm and key. + // May be omitted to disable integrity protection. + message EspAuth { + optional AuthenticationAlgorithm algorithm = 1; + optional WrappedKey key = 2; // Unwrap to get the key as a binary blob + } + optional EspAuth authentication = 1; + + // Encryption algorithm and key. + // May be omitted to disable privacy protection. + message EspEncrypt { + optional EncryptionAlgorithm algorithm = 1; + optional WrappedKey key = 2; // Unwrap to get the key as a binary blob + } + optional EspEncrypt encryption = 2; +} + +// A wrapped (encrypted) key. +// The agent should use the Google Cloud Key Management Service (KMS) to +// unwrap (decrypt) the wrapped key material. +// See the Google Cloud KMS API for 'cloudkms.cryptoKeys.decrypt'. +message WrappedKey { + // The resource name of the cryptographic key used for unwrapping. + optional string unwrapper_key_name = 1; + + // Wrapped (encrypted) key ciphertext. + // Unwrap (decrypt) using Google Cloud KMS before use in packet processing. + optional bytes wrapped_key = 2; +} + +message TunnelMethod { + oneof protocol { + EspProtocol esp = 1; + // Explicit request to disable security properties on this tunnel. + google.protobuf.Empty none = 2; + } + reserved 3 to max; // Next IDs. +} diff --git a/api/common/wireless.proto b/api/common/wireless.proto new file mode 100644 index 0000000..73de4e4 --- /dev/null +++ b/api/common/wireless.proto @@ -0,0 +1,64 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/platform_antenna.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +message AmplifierDefinition { + message ConstantGainAmplifierDefinition { + optional double gain_db = 1; + optional double noise_factor = 2; + optional double reference_temperature_k = 3; + } + + message LowNoiseAmplifierDefinition { + optional double pre_lna_gain_db = 1; + optional double lna_gain_db = 2; + optional double post_lna_gain_db = 3; + optional double noise_factor = 4; + optional double reference_temperature_k = 5; + } + + oneof amplifier_type { + ConstantGainAmplifierDefinition constant_gain = 1; + LowNoiseAmplifierDefinition low_noise = 2; + } +} + +message MiscGainOrLoss { + // A name that describes the gain or loss. + optional string name = 1; + + // The gain (positive values) or loss (negative values), in dB. + optional double gain_or_loss_db = 2; +} + +// Represents a signal, with properties such as a +// center frequency, bandwidth, and polarization. +message Signal { + // The center frequency (Hz) of the signal. + optional uint64 center_frequency_hz = 1; + + // The bandwidth (Hz) of the signal. + optional uint64 bandwidth_hz = 2; + + // The polarization of the signal. + optional aalyria.spacetime.api.common.Polarization polarization = 3; +} diff --git a/api/common/wireless_modcod.proto b/api/common/wireless_modcod.proto new file mode 100644 index 0000000..f46d368 --- /dev/null +++ b/api/common/wireless_modcod.proto @@ -0,0 +1,115 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file contains Spacetime's abstraction for representing the modulation +// and coding (MODCOD) scheme that would be selected by the adaptive or fixed +// coding and modulation, based on a given received signal quality. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// A mapping between thresholds of various measurements of received signal +// quality to the effective Layer 2 data rate that the link could sustain at +// this received signal quality. +// +// As an example of how this message could be populated, consider the DVB-S2X +// standard. Table 1 of the standard relates MODCODs to their ideal Es/N0: +// +// Canonical MODCOD | Spectral efficiency | Ideal Es/N0 [dB] for (AWGN +// name | [bit/symbol] | Linear Channel) (Normative) +// ----------------------------------------------------------------------- +// QPSK 2/9 | 0.43484 | -2.85 +// QPSK 13/45 | 0.567805 | -2.03 +// QPSK 9/20 | 0.889135 | 0.22 +// QPSK 11/20 | 1.088581 | 1.45 +// ... +// For reference, see ETSI TR 102 376-2 V1.2.1 (2021-01) which is accessible +// from http://www.etsi.org/standards-search. +// +// This table can be used to define the carrier_to_noise_plus_interference_steps +// of an AdaptiveDataRateTable. +// 1) To compute the C/N values, note that: +// C/N = (Es / N0) + (symbol_rate / bandwidth) [using dB math] +// Suppose (symbol_rate / bandwidth) = 1 / 1.1 = 0.9091 = -0.4139 dB. +// 2) To compute the achievable data rate, note that: +// Data rate = (spectral_efficiency) * (symbol_rate) +// Suppose symbol_rate = 100Msps = 100_000_000 symbols/second. +// +// Using these relationships, a CarrierToNoisePlusInterferenceDataRateMapping +// can be defined. For QPSK 2/9: +// 1) To calculate C/N: +// C/N = (-2.85 dB) + (-0.4139 dB) = -3.2639 dB +// 2) To calculate the achievable data rate: +// Data rate = (0.43484 bits/symbol) * (100_000_000 symbols/second) +// = 43_484_000 bits/second = 43.484Mbps +// The following rows could be converted similarly to populate all +// carrier_to_noise_plus_interference_steps. +// This approach can be tuned based on the actual modems and their measured +// implementation loss and supported set of MODCODs, etc. +// +// Also, in many modems' Adaptive Coding and Modulation configuration, there is +// often a table that relates some measurement of received signal quality to the +// MODCOD that the system will choose. A similar approach as the preceding +// example can be used to define an AdaptiveDataRateTable from these tables. +message AdaptiveDataRateTable { + // WARNING: This ID is unused. + optional string id = 3 [deprecated = true]; + + // A mapping between thresholds of Carrier-to-(Noise + Interference) + // (C/(N+I)) to the Layer 2 data rate that the link could sustain at this + // C/(N+I). + message CarrierToNoisePlusInterferenceDataRateMapping { + // The ratio of the power of the carrier (or signal) at the demod input to + // the noise power of the signal plus the power of the interfering signals, + // in dB. + // Required. + optional double min_carrier_to_noise_plus_interference_db = 1; + + // The layer 2 data rate achievable, in bits per second, provided the + // receiver receives at least the min_carrier_to_noise_plus_interference_db. + // Required. + optional double tx_data_rate_bps = 2; + + // A human readable name (e.g. "QPSK-LDPC-2-3") describing the modulation + // and coding scheme associated with the Carrier-to-(Noise + Interference) + // to data rate mapping. + // This name is not used in any logic. This is purely for human operators to + // associate a specific C/(N+I) threshold with a MODCOD. + optional string mod_cod_scheme_name = 3; + } + // The elements should be sorted by min_carrier_to_noise_plus_interference_db + // in ascending order. + repeated CarrierToNoisePlusInterferenceDataRateMapping + carrier_to_noise_plus_interference_steps = 2; + + // A mapping between thresholds of received signal power to the Layer 2 data + // rate that the link could sustain at this received power. + message ReceivedSignalPowerDataRateMapping { + // The power of the intended signal at the receiver output, in dBW. + // Required. + optional double min_received_signal_power_dbw = 1; + + // The layer 2 data rate achievable, in bits per second, provided the + // receiver receives at least the min_received_signal_power_dbw. + // Required. + optional double tx_data_rate_bps = 2; + } + // The elements should be sorted by min_received_signal_power_dbw in ascending + // order. + repeated ReceivedSignalPowerDataRateMapping received_signal_power_steps = 1; +} diff --git a/api/common/wireless_receiver.proto b/api/common/wireless_receiver.proto new file mode 100644 index 0000000..f393452 --- /dev/null +++ b/api/common/wireless_receiver.proto @@ -0,0 +1,148 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Protobuf messages used to model digital transmitters used in communications. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/channel.proto"; +import "api/common/wireless.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Models a set of receive channels. +message RxChannels { + // The set of supported channel frequencies, in Hz, for this channel set. + // For RF transceivers, this is the carrier frequency. + // For optical transceivers, this may be converted to wavelength. + // TODO: Refactor this to be a repeated field of + // `aalyria.spacetime.api.common.Signal`s in order to + // consolidate the modeling of transmitted and received signals. + repeated int64 center_frequency_hz = 1; +} + +// Models a wireless receiver. +message ReceiverDefinition { + reserved 1, 3 to 8, 9, 12, 13 to max; + + // An optional friendly, human-readable string. + optional string name = 2; + + // Maps a band profile ID to a set of channels. + map channel_set = 10; + + // Allows the user to configure an ordered sequence that describes how the + // wireless transmission is modified or reconfigured starting from the output + // of the platform antenna that receives the signal. + repeated ReceiveSignalProcessor signal_processing_step = 11; +} + +// Defines how a wireless signal is modified, reconfigured, or received. +message ReceiveSignalProcessor { + oneof type { + Filter filter = 1; + PhotodetectorDefinition photodetector = 2; + AmplifierDefinition amplifier = 3; + MiscGainOrLoss gain_or_loss = 4; + } +} + +// Models a signal filter. +message Filter { + // Optional. Set this only if the filter's frequency is static. + // If the field is not set, it will be configured dynamically according to the + // center of frequency of the channel in use. + optional double frequency_hz = 1; + + // Optional. The offset to the lower bandwidth limit, in Hz. + // If the field is not set, it will be configured dynamically according to the + // bandwidth of the channel in use, provided through the BAND_PROFILE entity. + optional double lower_bandwidth_limit_hz = 2; + + // Optional. The offset to the upper bandwidth limit, in Hz. + // If the field is not set, it will be configured dynamically according to the + // bandwidth of the channel in use, provided through the BAND_PROFILE entity. + optional double upper_bandwidth_limit_hz = 3; + + // The thermal noise temperature of the filter, in Kelvin. + optional double noise_temperature_k = 4; + + message RectangularFilterDefinition { + } + + message LinearFilterDefinition { + // The amount of attentunation (rejection) of the output signal, in + // Decibels, per Hz of difference between the input signal frequency and the + // filter's configured / design frequency. This should be a positive value. + optional double rejection_db_per_hz = 1; + } + + oneof filter_type { + RectangularFilterDefinition rectangular = 5; + LinearFilterDefinition linear = 6; + } +} + +// Models a photodetector used in Free-Space Optical Communications (FSOC). +message PhotodetectorDefinition { + message AvalanchePhotodiodeDefinition { + optional double field_of_view_rad = 1; + optional double bandwidth_hz = 2; + optional double noise_temperature_k = 3; + optional double efficiency_percent = 4; + optional double dark_current_amp = 5; + optional double load_impedance_ohm = 6; + optional double noise_factor = 7; + optional double gain_db = 8; + optional double optical_bandpass_filter_bandwidth_hz = 9; + + // Units for radiance: W * m^-2 * sr^-1 * Hz^-1 + optional double sky_spectral_radiance = 10; + + // Units for emittance: W * m^-2 * Hz^-1 + optional double sun_spectral_radiant_emittance = 11; + + // Must be greater than or equal to 2.99792458E-9 meters and less than + // or equal to 9.993081933333333E-5 meters to model an optical wavelength. + optional double wavelength_m = 12; + } + + message PinPhotodiodeDefinition { + optional double field_of_view_rad = 1; + optional double bandwidth_hz = 2; + optional double noise_temperature_k = 3; + optional double efficiency_percent = 4; + optional double dark_current_amp = 5; + optional double load_impedance_ohm = 6; + optional double optical_bandpass_filter_bandwidth_hz = 7; + + // Units for radiance: W * m^-2 * sr^-1 * Hz^-1 + optional double sky_spectral_radiance = 8; + + // Units for emittance: W * m^-2 * Hz^-1 + optional double sun_spectral_radiant_emittance = 9; + + // Must be greater than or equal to 2.99792458E-9 meters and less than + // or equal to 9.993081933333333E-5 meters to model an optical wavelength. + optional double wavelength_m = 10; + } + + oneof photodetector_type { + AvalanchePhotodiodeDefinition avalanche_photodiode = 1; + PinPhotodiodeDefinition pin_photodiode = 2; + } +} diff --git a/api/common/wireless_transceiver.proto b/api/common/wireless_transceiver.proto new file mode 100644 index 0000000..053f98c --- /dev/null +++ b/api/common/wireless_transceiver.proto @@ -0,0 +1,141 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file contains messages used to model wireless transceivers. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/platform_antenna.proto"; +import "api/common/wireless_receiver.proto"; +import "api/common/wireless_transmitter.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// This message models a transceiver, composed of a transmitter, +// receiver, and antenna. +// +// This message is used by Spacetime to model the transmission and reception of +// wireless signals. For the transceiver to participate in the orchestrated +// network, it must be referenced by at least one wireless interface on a +// NETWORK_NODE entity. A transceiver can be referenced by multiple wireless +// interfaces if the transceiver supports the formation of multiple beams. +// Wireless interfaces are defined in +// aalyria.spacetime.api.nbi.v1alpha.resources.NetworkInterface. +// +// This model is treated as a regenerative payload +// in Spacetime's wireless propagation analysis. +message TransceiverModel { + // An identifier for the transceiver model that must be unique + // among the set of transceiver models on the + // aalyria.spacetime.api.common.PlatformDefinition that contains + // contains this model. + // Required. + optional string id = 1; + + // Defines the parameters of the wireless transmitter. + // This field is required to model a transceiver's ability to transmit + // a signal. + optional TransmitterDefinition transmitter = 2; + + // Defines the parameters of the wireless receiver. + // This field is required to model a transceiver's ability to receive + // a signal. + optional ReceiverDefinition receiver = 3; + + // Defines the parameters of the antenna. + // Required. + optional AntennaDefinition antenna = 5; + + // Defines the set of interoperable transceivers with which this + // transceiver can form a link. + // This field is required to be non-empty in order for the transceiver + // to participate in a mesh. + repeated WirelessMac macs = 6; + + message Impairment { + // An identifier that must be unique within the list of impairments + // on this transceiver. + optional string id = 1; + // The time this impairment was added to the transceiver model. + optional int64 timestamp_usec = 3; + // The reason for this impairment. + optional string reason = 4; + } + // Multiple operational impairements can be recorded by different actors + // (e.g. network operators or monitoring sub-systems in the onboard software). + // The existence of an impairment prevents this transceiver from being + // used in the network. + // Optional. + repeated Impairment operational_impairments = 7; + + reserved 4; +} + +// Uniquely identifies a transceiver model within the parent +// aalyria.spacetime.api.common.PlatformDefinition. +message TransceiverModelId { + // The globally unique ID of the PLATFORM_DEFINITION + // aalyria.spacetime.api.nbi.v1alpha.Entity that contains this + // transceiver model. + // + // NOTE: There does not necessarily need to be a 1:1 relationship + // between a PLATFORM_DEFINITION entity and a NETWORK_NODE entity. + // + // For example, it might be desirable to model geostationary satellites and + // their antennas for the purpose of asserting non-interference (i.e. in + // accordance with ITU Radio Regulations Article 22). In this example, each + // geostationary satellite might have its own PLATFORM_DEFINITION entity + // (and one or more antennas) but would not need any corresponding + // NETWORK_NODE entity as it would not need to be represented in a network + // graph. + // Required. + optional string platform_id = 1; + + // The locally scoped ID of the TransceiverModel. + // Required. + optional string transceiver_model_id = 2; +} + +// In order for two transceivers to close a link, the transceivers +// must have at least one pair of WirelessMacs that satisfy the following +// properties: +// 1) The type fields are equal. +// 2) The role fields are not equal to each other. An unset role is +// considered compatible with all other roles. +// 3) The max_connections field is non-zero. An unset max_connections +// is considered as allowing infinite connections. +// TODO: Provide an option to allow users to define custom schemes +// for determining whether a pair of transceivers is interoperable. +message WirelessMac { + // The MAC type in your network, e.g. “DVB-S2”, + // “DVB-S2X”, "802.11", "5G-NR", "Link-16", "CCSDS-AOS", etc. + // Required. + optional string type = 1; + + // The role in the MAC layer, e.g. "HUB", “REMOTE”, “AP”, + // “CLIENT”, etc. If this field is omitted, the role is + // considered compatible with all other roles. + // Optional. + optional string role = 2; + + // The maximum number of concurrent connections this transceiver can + // participate in. If this field is less than or equal to 0, + // the interface with this MAC protocol cannot form a link. If + // this field is unset, the number of links is unbounded. + // Optional. + optional int32 max_connections = 3; +} \ No newline at end of file diff --git a/api/common/wireless_transmitter.proto b/api/common/wireless_transmitter.proto new file mode 100644 index 0000000..3096588 --- /dev/null +++ b/api/common/wireless_transmitter.proto @@ -0,0 +1,68 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Protobuf messages used to model digital transmitters used in communications. + +syntax = "proto2"; + +package aalyria.spacetime.api.common; + +import "api/common/channel.proto"; +import "api/common/wireless.proto"; + +option java_package = "com.aalyria.spacetime.api.common"; +option go_package = "aalyria.com/spacetime/api/common"; + +// Models a set of transmit channels. +message TxChannels { + message TxChannelParams { + // The maximum possible transmit power on this channel. + optional double max_power_watts = 1; + } + + // Maps the channel's center frequency, in Hz, to channel parameters. + // For RF transceivers, the map key is the carrier frequency. + // For optical transceivers, the key may be converted to wavelength. + // TODO: Add an `aalyria.spacetime.api.common.Signal` field to + // `TxChannelParams`, and refactor this to be a repeated field of + // `TxChannelParams`. This helps to consolidate the modeling of + // transmitted and received signals. + map channel = 1; +} + +// Models a wireless transmitter. +message TransmitterDefinition { + reserved 1, 3 to 12, 13, 16, 18 to max; + + // An optional friendly, human-readable string. + optional string name = 2; + + // Maps a band profile ID to a set of channels. + map channel_set = 14; + + // Allows the user to configure an ordered sequence that describes how the + // wireless transmission is created or modified prior to the input of + // the platform antenna that propagates the signal. + repeated TransmitSignalProcessor signal_processing_step = 15; + + optional string coverage_heatmap_id = 17; +} + +// Defines how a wireless signal is created, modified, or propagated. +message TransmitSignalProcessor { + oneof type { + AmplifierDefinition amplifier = 1; + MiscGainOrLoss gain_or_loss = 2; + } +} diff --git a/api/federation/BUILD b/api/federation/BUILD new file mode 100644 index 0000000..5e482d8 --- /dev/null +++ b/api/federation/BUILD @@ -0,0 +1,57 @@ +# Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") + +proto_library( + name = "federation_proto", + srcs = ["federation.proto"], + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_proto", + "//api/types:types_proto", + "@googleapis//google/type:interval_proto", + "@protobuf//:duration_proto", + ], +) + +go_proto_library( + name = "federation_go_proto", + compilers = ["@rules_go//proto:go_grpc"], + importpath = "aalyria.com/spacetime/api/federation", + proto = ":federation_proto", + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_go_proto", + "//api/types:types_go_proto", + "@org_golang_google_genproto//googleapis/type/interval", + ], +) + +go_proto_library( + name = "v1alpha_go_proto", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/fed/v1alpha", + proto = ":federation_proto", + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_go_proto", + "//api/types:types_go_proto", + "@org_golang_google_genproto//googleapis/type/interval", + ], +) diff --git a/api/federation/federation.proto b/api/federation/federation.proto new file mode 100644 index 0000000..61b658b --- /dev/null +++ b/api/federation/federation.proto @@ -0,0 +1,585 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Spacetime Federation Interface + +syntax = "proto3"; + +package aalyria.spacetime.api.fed.v1alpha; + +import "api/common/bent_pipe.proto"; +import "api/common/coordinates.proto"; +import "api/common/time.proto"; +import "api/common/wireless_transceiver.proto"; +import "api/types/ietf.proto"; +import "google/protobuf/duration.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.fed.v1alpha"; +option go_package = "aalyria.com/spacetime/api/fed/v1alpha"; + +// FEDERATION OF NETWORKS +// +// Networks in the Federation operate as peers to one another, communicating via +// the Federation API. Each network exposes interconnection points, which are +// external representations of interfaces or other resources that are available +// to external networks for ingress or egress. +// +// SCHEDULING A SERVICE +// +// `ScheduleService` allows a requestor to request a service from a provider's +// network, regardless of any prior knowledge about that network. +// +// The requestor may choose to first gather more information about the service +// provider's resource locations, attributes, and SLAs before submitting a +// request for service. This additional information can give the requestor more +// fine-grained control over service selection, if desired. The RPCs that +// facilitate this are: +// +// 1) `StreamInterconnectionPoints`, which allows the requestor to see a +// long-lived stream of the service provider's interconnection points. +// 2) `ListServiceOptions`, which allows the requestor to get a list of +// available services from the service provider's network. +// +// Generally, the more prescriptive the requestor is about requesting specific +// interconnection points or service options, the less the provider needs to +// know about the requestor network. Conversely, the requestor may prefer a more +// hands-off approach to selecting and maintaining service interconnections, and +// can let the provider handle the tasking by offering more information upfront. +// +// The service ends when the last time interval has ended. +// +// SERVICE LIFECYCLE +// +// The following RPCs handle lifecycle management for Services. +// +// All are called by the Requestor to the Provider. +// +// - ScheduleService requests a Service from the Provider, returning among +// other things a unique `service_id` for lifecycle management. +// - StatusService creates a long-lived stream, through which the Provider +// generates dynamic Service status updates for one or more `service_id`. +// It is also the channel through which the Provider can notify the +// Requestor of Service termination. +// - CancelService enables Requestor cancellation of an existing +// `service_id`. +service Federation { + // Request a potentially long-lived stream of available + // `InterconnectionPoints`. The connection will stream one complete snapshot + // of InterconnectionPoints from the Provider. If the request specifies + // `snapshot_only=true`, the stream is terminated by the Provider after + // sending the snapshot. Otherwise, the Provider continues to stream any + // updates in real-time. + rpc StreamInterconnectionPoints(StreamInterconnectionPointsRequest) + returns (stream StreamInterconnectionPointsResponseChunk) {} + + // Request a finite stream of `ServiceOptions`. + rpc ListServiceOptions(ListServiceOptionsRequest) + returns (stream ListServiceOptionsResponse) {} + + // Request a Service. + rpc ScheduleService(ScheduleServiceRequest) + returns (ScheduleServiceResponse) {} + + // Request Provider status updates for one or more Services. + rpc MonitorServices(stream MonitorServicesRequest) + returns (stream MonitorServicesResponse) {} + + // Request a Service cancellation. + rpc CancelService(CancelServiceRequest) returns (CancelServiceResponse) {} +} + +// Returns a collection of ServiceOptions, message may be repeated to stream +// all ServiceOptions that resulted from the ListServiceOptions Request. +message ListServiceOptionsResponse { + // A collection of ServiceOptions that satisfy the ListServiceOptionsRequest. + repeated ServiceOption service_options = 1; +} + +// Update the requested set of Service `service_id`s for status updates. +// +// Updates are differential (specified in adds and drops) to support +// potentially large cardinality. +message MonitorServicesRequest { + // List of Services with `service_id`s to subscribe to status updates of. + repeated string add_service_ids = 1; + + // List of Services with `service_id`s to drop status updates of. + repeated string drop_service_ids = 2; +} + +// Streamed updates to subscribed `service_id`s providing updates. +message MonitorServicesResponse { + repeated ServiceStatus updated_services =1; +} + +// A StreamInterconnectionPointsResponseChunk returns a collection of Provider +// InterconnectionPoint updates and signals when an entire snapshot has +// successfully been transferred (leaving only live updates remaining). +// +// When streaming Interconnections, one or more +// StreamInterconnectionPointsResponseChunks with `mutations` may be transmitted +// in order to fully transfer the initial snapshot of InterconnectionPoints. +// +// For the longevity of a StreamInterconnectionPoints, there will be exactly one +// StreamInterconnectionPointsResponseChunk in which `snapshot_complete` is set, +// indicating that the mutations up to and including this Chunk complete the +// initial system view at the start of streaming (initial snapshot). Future +// StreamInterconnectionPointsResponseChunk (if `snapshot_only=False` or is +// unset) will contain further live updates. Otherwise the stream is closed by +// the Provider. +message StreamInterconnectionPointsResponseChunk { + // Notification that the initial state snapshot has been fully provided, up + // to and including the `mutations` in this message. + optional SnapshotComplete snapshot_complete = 1; + // A set of InterconnectionPoint advertisement changes. + repeated InterconnectionMutation mutations = 2; + + message SnapshotComplete{ + // Message empty but left for ease of backwards compatibility should + // any snapshot completion signaling need arrise (e.g. timestamp). + } +} + +// An InterconnectionMutation specifies one change to the advertised +// InterconnectionPoints of a Provider. This could be new or updated +// InterconnectionPoints (upsert), or the removal of an existing offering +// (delete). +message InterconnectionMutation { + oneof type { + // upsert: update an existing InterconnectionPoint if it already exists + // (based on UUID comparison), or insert a new InterconnectionPoint if it + // doesn't already exist. The full InterconnectionPoint object is + // required, the service does not support diffs for this field. + InterconnectionPoint upsert = 1; + // delete an existing InterconnectionPoint if it already exists (based on + // UUID comparison). + Delete delete = 2; + } + + message Delete { + // The UUID of the now-deleted InterconnectionPoint. + string uuid = 1; + } +} + +// An interconnection point is an endpoint of a link between two networks. +// +// This object is comprised of 1) an ID, and 2) fields that describe the L1 and +// L2 attributes of the interconnection point. These attributes include physical +// considerations for link budgeting such as geometric constraints and +// time-dynamic location, as well as network-level information for link +// compatibility such as interface types and modes, data rate constraints, and +// network addresses. Other information is included when relevant, such as ADSB +// transponders for any interconnection points aboard aircraft platforms. +// +// The ID is the only required field. Different fields may be populated for +// different use cases of this object. +// +// It is the network controller's responsibility to maintain mappings between +// their internal interfaces and corresponding external-facing +// `InterconnectionPoints`. Once an `InterconnectionPoint` ID has been exposed +// via the Federation API, it should never be re-mapped to a different internal +// resource, though the ID may be deprecated or deleted. +message InterconnectionPoint { + // The UUID of this interconnection point. Required. Unique across + // InterconnectionPoints advertised by this Provider. The UUID is maintained + // across mutations to this entity. + string uuid = 1; + + oneof type { + // The transceiver model used by this interconnection point. + aalyria.spacetime.api.common.TransceiverModel transceiver_model = 2; + // The bent pipe payload model used by this interconnection point. + aalyria.spacetime.api.common.BentPipePayload bent_pipe_payload = 3; + } + // The location over time of this interconnection point. + aalyria.spacetime.api.common.Motion coordinates = 4; + + // The interface's IP address represented as a CIDR notation subnet or + // single IP address string. + aalyria.spacetime.api.types.IPNetwork ip_network = 5; + + // Ethernet address in readable colon separated format. e.g. + // "1:23:45:67:89:ab" or "01:23:45:67:89:ab" + string ethernet_address = 6; + + // Mode field indicates whether or not the interface can receive packets in + // promiscuous mode, or if it can receive L2 packets only if the L2 + // destination address matches it's own physical address. + enum Mode { + MODE_PROMISCUOUS = 0; + MODE_NON_PROMISCUOUS = 1; + } + Mode rx_mode = 7; + + // Optionally specifies the local/hardware IDs within the platform or + // switch. These are not used by network controllers but may be useful for + // network elements for operational context. Example: "eth:0", "gwl:1", + // "gre:2", etc. + message LocalId { + string type = 1; + int32 index = 2; + } + repeated LocalId local_ids = 8; + + // The maximum data-rate of the interface, in layer 2 bits per second. For + // wired interconnection points only. + double max_data_rate_bps = 9; + + // Optionally used to further constrain the maximum transmit power available + // for use, in aggregate, by the node's wireless network interface. + message SignalPowerBudget { + // The time interval (set to infinite if static). + google.type.Interval interval = 1; + + // The total amount of available power, in Watts. + double max_available_signal_power_watts = 2; + } + repeated SignalPowerBudget power_budgets = 10; +} + +// A filter for interconnection points, used by the requestor to constrain the +// results they receive from the provider. Fields are optional and `AND`ed +// together. +// +// More filters will be added as the federation grows. +message InterconnectionPointFilters { + // The MAC type, e.g. "DVB-S2", "DVB-S2X", "802.11", "5G-NR", "Link-16", + // "CCSDS-AOS", etc. + string mac_type = 1; + + // The role in the MAC layer, e.g. "HUB", "REMOTE", "AP", "CLIENT", etc. + string mac_role = 2; +} + +// Requests will be treated as bidirectional by default, unless otherwise +// specified in the proto documentation. +enum Directionality { + DIRECTIONALITY_BIDIRECTIONAL = 0; + DIRECTIONALITY_X_TO_Y = 1; + DIRECTIONALITY_Y_TO_X = 2; +} + +// This request message allows filtering of requested interconnection points. +message StreamInterconnectionPointsRequest { + // If set to true, the connection will be closed by the Provider after + // streaming one snapshot's complete set of Interconnections (the stream is + // not held open for additional updates). + optional bool snapshot_only = 1; + + // Future expansion possible to support filtering characteristics for + // the Stream. +} + +// This object is used to frame `ServiceRequests` and `ServiceOptionRequests` +// sent from the requestor to the provider, and defines two sets of potential +// service endpoints: +// A) one or more interconnection points in the requestor's network +// B) an IP address that is reachable via the provider's network +// +// Any endpoint in (A) may be connected by a service to the IP address in (B). +// Directionality may be specified; otherwise defaults to bidirectional. +// +// Because the provider will be tasking the interconnection resources if the +// requestor uses this object in a request, interconnection points must contain +// full link budgeting and compatilibity information. +message RequestorEdgeToIpNetwork { + // An unordered set of `InterconnectionPoints` from the requestor's network. + // Each point must contain enough information to enable full link budgeting + // and compatibility analysis. + repeated InterconnectionPoint x_interconnection_points = 1; + // The IP address the requestor wishes to reach, represented as a CIDR + // notation subnet or single IP address string. + aalyria.spacetime.api.types.IPNetwork y_ip_network = 2; + // Defines the directionality of this pair of endpoint sets. Assumed to be + // bidirectional by default. + Directionality directionality = 3; +} + +// This object is used to frame `ServiceRequests` and `ServiceOptionRequests` +// sent from the requestor to the provider, and defines two sets of potential +// service endpoints: +// A) one or more interconnection points in the requestor's network +// B) one or more interconnection points in the requestor's network +// +// Any endpoint in (A) may be connected by a service to any endpoint in (B). +// Directionality may be specified; otherwise defaults to bidirectional. +// +// Because the provider will be tasking the interconnection resources if the +// requestor uses this object in a request, interconnection points must contain +// full link budgeting and compatilibity information. +message RequestorEdgeToRequestorEdge { + // An unordered set of `InterconnectionPoints` from the requestor's network. + // Each point must contain enough information to enable full link budgeting + // and compatibility analysis. + repeated InterconnectionPoint x_interconnection_points = 1; + // An unordered set of `InterconnectionPoints` from the requestor's network. + // Each point must contain enough information to enable full link budgeting + // and compatibility analysis. + repeated InterconnectionPoint y_interconnection_points = 2; + // Defines the directionality of this pair of endpoint sets. Assumed to be + // bidirectional by default. + Directionality directionality = 3; +} + +// A set of filters that define an unordered set of interconnection points from +// the provider's network. This object is used in certain requests from the +// requestor to the provider. All fields are optional, though filling in some +// values is strongly encouraged. +message InterconnectionPointSet { + // A filter that explicitly specifies one or more interconnection points, each + // requiring only the ID. + repeated InterconnectionPoint interconnection_points = 1; + + // Broad filters that specify attributes of interconnection points. + InterconnectionPointFilters interconnection_point_filter = 2; +} + +// This object is used to frame `ServiceRequests` and `ServiceOptionRequests` +// sent from the requestor to the provider, and defines two sets of potential +// service endpoints: +// A) one or more interconnection points in the provider's network +// B) an IP address that is reachable via the provider's network. +// +// Any endpoint in (A) may be connected by a service to the IP address in (B). +// Directionality may be specified; otherwise defaults to bidirectional. +message ProviderEdgeToIpNetwork { + // One or more interconnection points in the provider's network. + InterconnectionPointSet x_interconnection_points = 1; + + // The IP address the requestor wishes to reach, represented as a CIDR + // notation subnet or single IP address string. + aalyria.spacetime.api.types.IPNetwork y_ip_network = 3; + + // Defines the directionality of this pair of endpoint sets. Assumed to be + // bidirectional by default. + Directionality directionality = 4; +} + +// This object is used to frame `ServiceRequests` and `ServiceOptionRequests` +// sent from the requestor to the provider, and defines two sets of potential +// service endpoints: +// A) one or more interconnection points in the provider's network, +// B) one or more interconnection points in the provider's network, +// +// Any endpoint in (A) may be connected by a service to any endpoint in (B). +// Directionality may be specified; otherwise defaults to bidirectional. +message ProviderEdgeToProviderEdge { + // One or more interconnection points in the provider's network. + InterconnectionPointSet x_interconnection_points = 1; + + // One or more interconnection points in the provider's network. + InterconnectionPointSet y_interconnection_points = 3; + + // Defines the directionality of this pair of endpoint sets. Assumed to be + // bidirectional by default. + Directionality directionality = 5; +} + +// A set of filters for network interconnectivity information, which describes +// how data should get through the network, with what characteristics, and in +// which direction. This includes filters for attributes such as traffic types +// and encapsulations, guaranteed MTU, and latency and/or bandwidth. +// +// This set contains three filters: bidirectional, x_to_y, and y_to_x. Any +// fields populated in the x_to_y and y_to_x service attribute filters will +// override the corresponding fields in the bidirectional filter. +// +// For example: +// bidirectional_filter: { max_a = 5, max_b = 10, max_c = [EMPTY] } +// x_to_y_filter: { max_b = 7 } +// y_to_x_filter: { max_c = 2 } +// Resulting x_to_y direction filter: { max_a = 5, max_b = 7 } +// Resulting y_to_x direction filter: { max_a = 5, max_b = 10, max_c = 2 } +message ServiceAttributesFilterSet { + // A filter for network interconnectivity information, going one way. + // In other words, fields such as latency describe one-way latency, not RTT. + // + // More types of filters will be added, such as for traffice types, as the + // federation grows. + message ServiceAttributesFilter { + // The minimum bandwidth, in layer 2 bits per second. Defaults to 100 bps. + double bandwidth_bps_minimum = 3; + // The maximum allowed end-to-end latency for the flow. + google.protobuf.Duration one_way_latency_maximum = 4; + } + + // Service-level one-way filter used to prune `Services` and `ServiceOptions`. + // A given `Service` or `ServiceOption` passes this filter if all of its + // attributes in either the `x_to_y` or `y_to_x` direction pass. + // + // The `Directionality` field itself in the `Service` or `ServiceOption` is + // ignored. + ServiceAttributesFilter bidirectional_service_attributes_filter = 4; + + // Service-level one-way filter used to prune `Services` and `ServiceOptions` + // by their attributes in the x_to_y direction. + // + // The `Directionality` field itself in the `Service` or `ServiceOption` is + // ignored. + ServiceAttributesFilter x_to_y_service_attributes_filter = 5; + + // Service-level one-way filter used to prune `Services` and `ServiceOptions` + // by their attributes in the y_to_x direction. + // + // The `Directionality` field itself in the `Service` or `ServiceOption` is + // ignored. + ServiceAttributesFilter y_to_x_service_attributes_filter = 6; +} + +// Network interconnectivity information, which describes how data will get +// through the network, and with what characteristics. This includes traffic +// types and encapsulations, guaranteed MTU, and latency and/or bandwidth. +// +// More attributes will be added as the federation grows. +message TemporalServiceAttributes { + // The time interval during which these attributes are applicable. + google.type.Interval time_interval = 6; +} + +// A request for service options. All fields are requirements that are ANDed +// together to constrain the resulting set of service options. +message ListServiceOptionsRequest { + // Requestable endpoint set pairs. Optional. + oneof type { + // All `InterconnectionPoints` must contain link compatibility and link + // evaluation fields. + RequestorEdgeToRequestorEdge requestor_edge_to_requestor_edge = 1; + // Only basic ID information on `InterconnectionPoints` is required. + ProviderEdgeToProviderEdge provider_edge_to_provider_edge = 2; + // Only basic ID information on `InterconnectionPoints` is required. + ProviderEdgeToIpNetwork provider_edge_to_ip_network = 3; + } + + // Service-level filters used to prune `ServiceOptions`. + ServiceAttributesFilterSet service_attributes_filters = 4; +} + +// A service option. +message ServiceOption { + // The ID of the service option. + string id = 1; + + // One endpoint of the service option. + oneof endpoint_x { + // An interconnection point in the requestor's network. Only ID required. + InterconnectionPoint x_requestor_interconnection = 2; + // An interconnection point in the provider's network. Only ID required. + InterconnectionPoint x_provider_interconnection = 3; + } + + // The other endpoint of the service option. + oneof endpoint_y { + // An interconnection point in the requestor's network. Only ID required. + InterconnectionPoint y_requestor_interconnection = 4; + // An interconnection point in the provider's network. Only ID required. + InterconnectionPoint y_provider_interconnection = 5; + // An IP address, represented as a CIDR notation subnet or single IP address + // string. + aalyria.spacetime.api.types.IPNetwork ip_network = 6; + } + + // Defines the directionality of this pair of endpoint sets. Assumed to be + // bidirectional by default. + Directionality directionality = 7; + + // The service attributes of this service option, aggregated over the given + // time interval. + TemporalServiceAttributes service_attributes_x_to_y = 8; + + TemporalServiceAttributes service_attributes_y_to_x = 9; +} + +// A request for service. All fields are requirements that are ANDed together. +// If the number of potential services is more than one, after all requirements +// have been applied, then the provider selects one for the requestor. +message ScheduleServiceRequest { + // Requestable endpoint set pairs. + oneof type { + // The service option to request. + string service_option_id = 2; + // All `InterconnectionPoints` must contain link compatibility and link + // evaluation fields. + RequestorEdgeToRequestorEdge requestor_edge_to_requestor_edge = 3; + // All `InterconnectionPoints` must contain link compatibility and link + // evaluation fields. + RequestorEdgeToIpNetwork requestor_edge_to_ip_network = 4; + } + + // Service-level filters. Optional. + ServiceAttributesFilterSet service_attributes_filters = 5; + + // A request will be treated as having a higher priority if the value of this + // field is greater than that of another service request from the same + // requestor. + uint32 priority = 6; +} + + +// The immediate result of a ScheduleService call, providing a unique +// `service_id` for lifecycle managment of the resulting Service. +message ScheduleServiceResponse { + // A unique service_id generated by the provider. + string service_id = 1; + + // Any additional immutable Service fields can be added here. +} + +// A ServiceUpdate to notify the Requestor of Service characteristics which may +// change over time. +message ServiceStatus { + // The ID of the service. + string id = 1; + + oneof type { + // A Provider initiated notification of Service cancellation. + ServiceCancelled provider_cancelled = 2; + // A Provider notification of Service status changes. + ServiceUpdate service_update = 3; + } + + message ServiceCancelled { + // To aid proto forward compatibility, leaving a message for future + // cancellation characteristics. + } + + message ServiceUpdate { + // One endpoint of the service. + InterconnectionPoint x_service_endpoint = 1; + + // The other endpoint of the service. + InterconnectionPoint y_service_endpoint = 2; + + // Planned service attributes, confirmed by Requestor. + TemporalServiceAttributes planned_service_attributes_x_to_y = 3; + TemporalServiceAttributes planned_service_attributes_y_to_x = 4; + + // Actual service attributes, reported by Requestor, which may differ + // from planned. + TemporalServiceAttributes reported_service_attributes_x_to_y = 5; + TemporalServiceAttributes reported_service_attributes_y_to_x = 6; + + // True if the service is currently active and provisioned by the network. + bool is_active = 7; + } +} + +message CancelServiceRequest { + string service_id = 1; +} + +message CancelServiceResponse { + bool cancelled = 1; +} diff --git a/api/model/v1alpha/BUILD b/api/model/v1alpha/BUILD new file mode 100644 index 0000000..b3209c7 --- /dev/null +++ b/api/model/v1alpha/BUILD @@ -0,0 +1,60 @@ +# Copyright (c) Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_grpc_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "model_proto", + srcs = ["model.proto"], + deps = [ + "@org_outernetcouncil_nmts//proto:nmts_proto", + "@protobuf//:empty_proto", + ], +) + +cpp_grpc_library( + name = "model_cpp_grpc", + generate_mocks = True, + protos = [":model_proto"], + deps = ["@org_outernetcouncil_nmts//proto:nmts_cpp_proto"], +) + +java_grpc_library( + name = "model_java_grpc", + protos = [":model_proto"], + deps = ["@org_outernetcouncil_nmts//proto:nmts_java_proto"], +) + +python_grpc_library( + name = "model_python_grpc", + protos = [":model_proto"], + deps = ["@org_outernetcouncil_nmts//proto:nmts_python_proto"], +) + +go_proto_library( + name = "v1alpha_go_proto", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/model/v1alpha", + proto = ":model_proto", + deps = ["@org_outernetcouncil_nmts//proto:nmts_go_proto"], +) diff --git a/api/model/v1alpha/model.proto b/api/model/v1alpha/model.proto new file mode 100644 index 0000000..b479388 --- /dev/null +++ b/api/model/v1alpha/model.proto @@ -0,0 +1,115 @@ +// Copyright (c) Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package aalyria.spacetime.api.model.v1alpha; + +import "google/protobuf/empty.proto"; +import "nmts/proto/nmts.proto"; + +option java_package = "com.aalyria.spacetime.api.model.v1alpha"; +option go_package = "aalyria.com/spacetime/api/model/v1alpha"; + +service Model { + // Insert or overwrite an nmts.Entity. + // + // Returns a gRPC error whenver: + // - the supplied nmts.Entity's Id is ill-formed + // - the supplied nmts.Entity's Kind is missing + // - the supplied nmts.Entity would change the Kind of an existing + // entity (with the same Id), because doing so could circumvent + // restrictions on permitted Relationships + rpc UpsertEntity(UpsertEntityRequest) returns (UpsertEntityResponse) {} + + // Change a portion of an nmts.Entity. + rpc UpdateEntity(UpdateEntityRequest) returns (UpdateEntityResponse) {} + + // Delete an nmts.Entity. + // + // Also deletes any nmts.Relationships in which the nmts.Entity + // participates; if so, these are listed in the DeleteEntityResponse. + // + // Returns a gRPC NotFound error if an Entity with the supplied Id + // is not in the model. + rpc DeleteEntity(DeleteEntityRequest) returns (DeleteEntityResponse) {} + + // Insert an nmts.Relationship. + // + // Returns a gRPC error if any of the preconditions for a valid + // Relationship are not met (see NMTS's //lib/validation). + rpc InsertRelationship(InsertRelationshipRequest) + returns (InsertRelationshipResponse) {} + + // Delete an nmts.Relationship. + // + // Returns a gRPC NotFound error if the Relationship is not in the model. + rpc DeleteRelationship(DeleteRelationshipRequest) + returns (google.protobuf.Empty) {} + + // Retrieve an nmts.Entity and any associated ntms.Relationships. + // + // Returns a gRPC NotFound error if an Entity with the supplied Id + // is not in the model. + rpc GetEntity(GetEntityRequest) returns (GetEntityResponse) {} + + // Return all nmts.Entity and nmts.Relatioship instances in the model. + rpc ListElements(ListElementsRequest) returns (ListElementsResponse) {} +} + +message UpsertEntityRequest { + nmts.Entity entity = 1; +} +message UpsertEntityResponse { + // presently empty +} + +message UpdateEntityRequest { + nmts.PartialEntity patch = 1; +} +message UpdateEntityResponse { + // presently empty +} + +message DeleteEntityRequest { + string entity_id = 1; +} +message DeleteEntityResponse { + repeated nmts.Relationship deleted_relationships = 1; +} + +message InsertRelationshipRequest { + nmts.Relationship relationship = 1; +} +message InsertRelationshipResponse { + // presently empty +} + +message DeleteRelationshipRequest { + nmts.Relationship relationship = 1; +} + +message GetEntityRequest { + string entity_id = 1; +} +message GetEntityResponse { + nmts.EntityEdges entity_edges = 1; +} + +message ListElementsRequest { + // TODO: Support expressing some filtering/querying. +} +message ListElementsResponse { + nmts.Fragment elements = 1; +} diff --git a/api/nbi/v1alpha/BUILD b/api/nbi/v1alpha/BUILD new file mode 100644 index 0000000..03d2250 --- /dev/null +++ b/api/nbi/v1alpha/BUILD @@ -0,0 +1,85 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_grpc_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "nbi_proto", + srcs = [ + "nbi.proto", + "signal_propagation.proto", + "txtpb_entities.proto", + ], + deps = [ + "//api/common:common_proto", + "//api/nbi/v1alpha/resources:resources_proto", + "@googleapis//google/type:interval_proto", + "@protobuf//:duration_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_grpc_library( + name = "nbi_cpp_grpc", + generate_mocks = True, + protos = [":nbi_proto"], + deps = [ + "//api/common:common_cpp_proto", + "//api/nbi/v1alpha/resources:nbi_resources_cpp_grpc", + "@googleapis//google/rpc:code_cc_proto", + "@googleapis//google/type:interval_cc_proto", + ], +) + +java_grpc_library( + name = "nbi_java_grpc", + protos = [":nbi_proto"], + deps = [ + "//api/common:common_java_proto", + "//api/nbi/v1alpha/resources:nbi_resources_java_grpc", + "@googleapis//google/api:api_java_proto", + "@googleapis//google/rpc:rpc_java_proto", + "@googleapis//google/type:type_java_proto", + ], +) + +python_grpc_library( + name = "nbi_python_grpc", + protos = [":nbi_proto"], + deps = [ + "//api/common:common_python_proto", + "//api/nbi/v1alpha/resources:nbi_resources_python_grpc", + ], +) + +go_proto_library( + name = "v1alpha_go_proto", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/nbi/v1alpha", + proto = ":nbi_proto", + deps = [ + "//api/common:common_go_proto", + "//api/nbi/v1alpha/resources:nbi_resources_go_grpc", + "@org_golang_google_genproto//googleapis/type/interval", + ], +) diff --git a/api/nbi/v1alpha/nbi.proto b/api/nbi/v1alpha/nbi.proto new file mode 100644 index 0000000..695b32e --- /dev/null +++ b/api/nbi/v1alpha/nbi.proto @@ -0,0 +1,273 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha; + +import "api/common/channel.proto"; +import "api/common/control.proto"; +import "api/common/coordinates.proto"; +import "api/common/platform.proto"; +import "api/common/telemetry.proto"; +import "api/common/time.proto"; +import "api/nbi/v1alpha/resources/antenna_pattern.proto"; +import "api/nbi/v1alpha/resources/coverage.proto"; +import "api/nbi/v1alpha/resources/devices_in_region.proto"; +import "api/nbi/v1alpha/resources/intent.proto"; +import "api/nbi/v1alpha/resources/motion_evaluation.proto"; +import "api/nbi/v1alpha/resources/network_element.proto"; +import "api/nbi/v1alpha/resources/network_link.proto"; +import "api/nbi/v1alpha/resources/service_request.proto"; +import "api/nbi/v1alpha/resources/wireless_interference.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha"; + +enum EntityType { + ENTITY_TYPE_UNSPECIFIED = 0; + ANTENNA_PATTERN = 12; + BAND_PROFILE = 16; + DEVICES_IN_REGION = 24; + STATION_SET = 21; + SURFACE_REGION = 22; + INTENT = 6; + INTERFACE_LINK_REPORT = 5; + INTERFERENCE_CONSTRAINT = 23; + // WARNING: This type is under development and not fully supported. + NETWORK_NODE = 3; + NETWORK_STATS_REPORT = 8; + PLATFORM_DEFINITION = 1; + SERVICE_REQUEST = 2; + TRANSCEIVER_LINK_REPORT = 19; + // WARNING: This type is under development and not fully supported. + COMPUTED_MOTION = 7; + + reserved 4, 14, 15, 17, 20, 25 to max; // (-- Next ID --) +} + +message EntityGroup { + // Type of the Entity, part of the Entity key. + optional EntityType type = 1; + // Name of the SDN component that created and manages this entity. + // Not part of Entity key. + optional string app_id = 3; +} + +// Provides endpoint from which this resource was retrieved, from the provider +// that spawned this resource. +message ResourceOrigin { + optional string provider_endpoint = 1; +} + +message Entity { + // The {group.EntityType, id} pair is globally unique. + optional EntityGroup group = 1; + + // The {EntityType, id} pair is globally unique. + optional string id = 2; + + // The origin of this Entity. Used to specify 3rd party provenance. + // If this is empty, then this Entity belongs to the current instance + // by default. + optional ResourceOrigin resource_origin = 34; + + // Microseconds since epoch. Updated by the data layer. + optional int64 commit_timestamp = 3; + + // Microseconds since epoch. Only set when querying entity history with an + // interval set and the entity is updated or deleted again within the + // interval. Clients that set an entity filter can use this timestamp to + // know when the entity stopped matching a filter or was deleted. + optional int64 next_commit_timestamp = 33; + + // The user who last modified the entity. Used for debugging only. Control + // systems should not rely on this field. + optional string last_modified_by = 22; + + oneof value { + aalyria.spacetime.api.nbi.v1alpha.resources.AntennaPattern antenna_pattern = + 15; + aalyria.spacetime.api.common.BandProfile band_profile = 20; + // WARNING: This message is under development and not fully supported. + aalyria.spacetime.api.nbi.v1alpha.resources.ComputedMotion computed_motion = + 10; + aalyria.spacetime.api.nbi.v1alpha.resources.DevicesInRegion + devices_in_region = 30; + aalyria.spacetime.api.nbi.v1alpha.resources.StationSet station_set = 27; + aalyria.spacetime.api.nbi.v1alpha.resources.SurfaceRegion surface_region = + 28; + aalyria.spacetime.api.nbi.v1alpha.resources.Intent intent = 9; + aalyria.spacetime.api.nbi.v1alpha.resources.InterfaceLinkReport + interface_link_report = 8; + aalyria.spacetime.api.nbi.v1alpha.resources.InterferenceConstraint + interference_constraint = 29; + // WARNING: This message is under development and not fully supported. + aalyria.spacetime.api.nbi.v1alpha.resources.NetworkNode network_node = 6; + aalyria.spacetime.api.common.NetworkStatsReport network_stats_report = 11; + aalyria.spacetime.api.common.PlatformDefinition platform = 4; + aalyria.spacetime.api.nbi.v1alpha.resources.ServiceRequest service_request = + 5; + aalyria.spacetime.api.nbi.v1alpha.resources.TransceiverLinkReport + transceiver_link_report = 25; + } + + reserved 7, 18, 23, 26, 35 to max; +} + +service NetOps { + rpc GetEntity(GetEntityRequest) returns (Entity) {} + + rpc CreateEntity(CreateEntityRequest) returns (Entity) {} + + rpc UpdateEntity(UpdateEntityRequest) returns (Entity) {} + + rpc ListEntities(ListEntitiesRequest) returns (ListEntitiesResponse) {} + + rpc ListEntitiesOverTime(ListEntitiesOverTimeRequest) + returns (ListEntitiesOverTimeResponse) {} + + rpc DeleteEntity(DeleteEntityRequest) returns (DeleteEntityResponse) {} + + rpc VersionInfo(VersionInfoRequest) returns (VersionInfoResponse) {} +} + +message GetEntityRequest { + optional EntityType type = 1; + optional string id = 2; +} + +// The id can be omitted, in which case a unique ID will be generated +// by the backend. +message CreateEntityRequest { + optional Entity entity = 1; +} + +// The posted Entity will replace the existing one, or created if missing. +message UpdateEntityRequest { + // Unless `ignore_consistency_check` is set to true, the `commit_timestamp` + // of the provided entity must match the one of the entity being updated. + // This ensures that users do not override changes that they are not aware of. + optional Entity entity = 1; + + // When set to true, the request is executed without verifying + // the correctness of the `last_commit_timestamp`. + optional bool ignore_consistency_check = 2; +} + +message ListEntitiesRequest { + reserved 2, 7 to max; // NEXT_ID: 7 + optional EntityType type = 1; // required. + // The time interval of the history being requested. If both start and end + // time are empty, return only the latest entities of the above type. + // Do not use yet; results may be incomplete. + optional google.type.Interval interval = 3; + + // If set, only return entities that match the filter. + optional EntityFilter filter = 5; + + optional bool compute_cartesian_coordinates = 6; +} + +message ListEntitiesResponse { + repeated Entity entities = 1; +} + +message ListEntitiesOverTimeRequest { + optional EntityType type = 1; // required. + + // The time range of the history being requested. + optional aalyria.spacetime.api.common.TimeInterval interval = 2; // required. + + optional bool compute_cartesian_coordinates = 3; + + // If set, only return entities that match the filter. Empty entities that + // represent deletes never match filters and will not be returned. + optional EntityFilter filter = 4; + + // If set, only return entities with an ID in the list. + repeated string ids = 5; + + // If set, only return entities that changed between the interval's start + // and end times. End may be before start to diff backwards in time. + // + // If multiple versions of an entity exist during the interval, the version + // that existed at the end time is returned (latest commit timestamp that's + // before or equal to the end time). If the entity doesn't exist or doesn't + // match the filter at the end time, but did exist or match at the start + // time, an entity with no value is returned. + optional bool diff = 6; +} + +message ListEntitiesOverTimeResponse { + repeated Entity entities = 1; +} + +message EntityFilter { + // If one or more node IDs are given, only return entities that reference one + // or more of the node IDs. + repeated string references_node = 1; + + // If one or more service request IDs are given, only return entities that + // reference one or more of the service request IDs. + repeated string references_service_request = 2; + + // If one or more intent states are given, only return intent entities whose + // states are included in the given list. + repeated aalyria.spacetime.api.nbi.v1alpha.resources.IntentState + include_intent_states = 3; + + // If one or more field paths are given, only return entities with at least + // one of the fields set and remove all other fields from the message except + // "id", "group.type", "commit_timestamp", and "next_commit_timestamp". + // + // To include all entities, even ones that don't match any fields in the field + // mask, explicitly add "id" which exists in all entities. + // + // Each path is a list of protobuf field names separated by periods. Every + // field name except the last must be a message field or a repeated message + // field. The last field name may be any message or scalar field, repeated or + // not. The first field in each path is a field in the `Entity` message. + // + // Map items and oneof names are not supported. Instead, include the entire + // map or use the name of specific fields in the oneof. + repeated string field_masks = 4; +} + +message DeleteEntityRequest { + optional EntityType type = 1; + optional string id = 2; + + // Required unless `ignore_consistency_check` is set to true. + // The delete request will fail if it's different from the current + // `commit_timestamp` of the entity being deleted. + optional int64 last_commit_timestamp = 3; // Units are microseconds. + + // When set to true, the request is executed without verifying + // the correctness of the `last_commit_timestamp`. + optional bool ignore_consistency_check = 4; +} + +message DeleteEntityResponse { +} + +message VersionInfoRequest { +} + +message VersionInfoResponse { + // Version of the Spacetime build. + // Uses Semantic Versioning 2.0.0 format. See: https://semver.org/ + optional string build_version = 1; +} diff --git a/api/nbi/v1alpha/resources/BUILD b/api/nbi/v1alpha/resources/BUILD new file mode 100644 index 0000000..0e95c83 --- /dev/null +++ b/api/nbi/v1alpha/resources/BUILD @@ -0,0 +1,96 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_grpc_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "resources_proto", + srcs = [ + "antenna_pattern.proto", + "coverage.proto", + "devices_in_region.proto", + "intent.proto", + "motion_evaluation.proto", + "network_element.proto", + "network_link.proto", + "service_request.proto", + "wireless_evaluation.proto", + "wireless_interference.proto", + ], + deps = [ + "//api/common:common_proto", + "//api/types:types_proto", + "@googleapis//google/rpc:code_proto", + "@googleapis//google/type:interval_proto", + "@googleapis//google/type:money_proto", + "@protobuf//:any_proto", + "@protobuf//:duration_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_grpc_library( + name = "nbi_resources_cpp_grpc", + generate_mocks = True, + protos = [":resources_proto"], + deps = [ + "//api/common:common_cpp_proto", + "//api/types:types_cpp_proto", + "@googleapis//google/api:annotations_cc_proto", + "@googleapis//google/rpc:code_cc_proto", + "@googleapis//google/type:interval_cc_proto", + "@googleapis//google/type:money_cc_proto", + ], +) + +go_proto_library( + name = "nbi_resources_go_grpc", + importpath = "aalyria.com/spacetime/api/nbi/v1alpha/resources", + proto = ":resources_proto", + deps = [ + "//api/common:common_go_proto", + "//api/types:types_go_proto", + "@org_golang_google_genproto//googleapis/type/interval", + "@org_golang_google_genproto//googleapis/type/money", + "@org_golang_google_genproto_googleapis_rpc//code", + ], +) + +java_grpc_library( + name = "nbi_resources_java_grpc", + protos = [":resources_proto"], + deps = [ + "//api/common:common_java_proto", + "//api/types:types_java_proto", + "@googleapis//google/api:api_java_proto", + "@googleapis//google/rpc:rpc_java_proto", + "@googleapis//google/type:type_java_proto", + ], +) + +python_grpc_library( + name = "nbi_resources_python_grpc", + protos = [":resources_proto"], + deps = [ + "//api/common:common_python_proto", + "@googleapis//google/rpc:code_py_proto", + "@googleapis//google/type:money_py_proto", + ], +) diff --git a/api/nbi/v1alpha/resources/antenna_pattern.proto b/api/nbi/v1alpha/resources/antenna_pattern.proto new file mode 100644 index 0000000..8aded3e --- /dev/null +++ b/api/nbi/v1alpha/resources/antenna_pattern.proto @@ -0,0 +1,384 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file contains messages used to model antenna radiation patterns. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// Models an antenna pattern. Multiple platform definitions can +// reference the same antenna pattern. +message AntennaPattern { + reserved 8, 14 to max; + + // A custom antenna pattern defined through spherical coordinates + // (IEEE 149-1979). + // This message can be used to define any 3D antenna pattern. + // + // For further reference on the spherical coordinate system used, + // see the "physics convention" at + // https://en.wikipedia.org/wiki/Spherical_coordinate_system. + message CustomPhiThetaAntennaPattern { + message SphericalGainValue { + // Required. + optional double gain_db = 1; + + // The phi and theta angles are evaluated within the + // aalyria.spacetime.api.common.AntennaDefinition's axes. + // Note that if an offset is not explicitly defined on the antenna + // through the fixed_coordinate_offset field, the antenna inherits + // the axes of its parent platform. + + // Phi is defined as the angle, in radians, from the positive x-axis + // to the orthogonal projection of the vector in the x-y plane. Positive + // phi is measured from the positive x-axis towards the positive y-axis. + // Range: [0, 2π). + // Required. + optional double phi_rad = 2; + + // Theta is the zenith angle, in radians, from the positive + // z-axis to the vector. Positive theta is measured from the + // positive z-axis towards the negative z-axis. + // Range: [0, π]. + // Required. + optional double theta_rad = 3; + + // For example, + // (phi: 0, theta: 0): aligns the vector with the positive z-axis. + // (phi: 0, theta: π/2): aligns the vector with the positive x-axis. + // (phi: π/2, theta: π/2): aligns the vector with the positive y-axis. + } + // From this message, a 2D array is created, where rows are phi cuts + // through the antenna pattern, columns are theta cuts through the antenna + // pattern, and the values represent the gain in decibels. + // + // Requirements: + // 1) No missing values can exist in this array. For example, suppose an + // antenna pattern is defined for phi = {0, π/6, π/4} and + // theta = {0, π/9, 2π/9, 2π/5, 3π/8}. A gain value corresponding to each + // entry in this 2D array must be provided, such as in this notional + // example: + // + // Theta (θ) + // Phi (φ) | 0 | π/9 | 2π/9 | 2π/5 | 3π/8 | + // --------------------------------------------------------------------- + // 0 | 20 | 18 | 16 | 15 | 14 | + // π/6 | 18 | 16 | 14 | 12 | 11 | + // π/4 | 16 | 14 | 12 | 10 | 10 | + // + // In this example, the gain_value field must contain 15 elements that + // correspond to each element of this array. + // + // 2) There must be strictly more than 1 phi point and strictly more than 1 + // theta point. The smallest array allowed is 2x2. + // + // Notes: + // 1) The gain in directions that fall between the defined data will + // be interpolated using bilinear interpolation in decibels + // (https://en.wikipedia.org/wiki/Bilinear_interpolation). The gain in + // directions that fall outside the defined data will yield 0 (-infinity dB) + // gain. + // + // 2) The elements are not required to be sorted in any particular order. + // + // 3) The interval between the phi angles does not need to be consistent. + // The interval between the theta angles does not need to be consistent. + // + // Required. + repeated SphericalGainValue gain_value = 1; + } + + // An antenna pattern defined through azimuth and elevation in a rectangular + // coordinate system. + // + // Because of the coordinate system used, this message is a natural choice to + // model the gain patterns of fixed LEO or GEO beams. When a platform's motion + // is defined as a TLE, the axes of the platform orients the positive z-axis + // along the negative position vector, the positive x-axis along the + // velocity vector, and the positive y-axis along the negative angular + // momentum vector. Consider a GEO satellite in an equatorial orbit, orbiting + // from west to east. This axes orients the z-axis towards the Earth, the + // positive x-axis towards the east, and the positive y-axis towards the + // south. Based on the definitions below, azimuth is measured from nadir + // towards the east, and elevation is measured from the Equator towards the + // north. + message CustomAzElAntennaPattern { + message AzElGainValue { + // A gain value (in decibels) for the angle. + // Required. + optional double gain_db = 1; + + // The azimuth and elevation angles are evaluated within the + // aalyria.spacetime.api.common.AntennaDefinition's axes. + // Note that if an offset is not explicitly defined on the antenna + // through the fixed_coordinate_offset field, the antenna inherits + // the axes of its parent platform. + + // Azimuth is defined as the angle from the z-axis to the projection + // of the direction vector onto the x-z plane. Positive azimuth is + // measured from the positive z-axis towards the positive x-axis. + // Range: [-180, 180). + // Required. + optional double az_deg = 2; + + // *** WARNING: This definition of elevation is non-standard. *** + // In other coordinate systems, elevation is often measured as + // the angle above a platform's local horizon, or more generally, + // the angle above the 0-elevation plane. + // HOWEVER, in this message, elevation is defined as the angle from the + // z-axis to the *projection of the direction vector onto the y-z plane*. + // In other words, this measures the angle of the *projection of the + // direction vector onto the y-z plane* above the 0-elevation plane. + // Positive elevation is measured towards the *negative* y-axis. + // Range [-180, 180). + // Required. + optional double el_deg = 3; + + // For example, for a GEO satellite, + // (azimuth: 0, elevation: 0): aligns the vector with the Equator. + // (azimuth: 0, elevation: 6.28°): aligns the vector with ~40°N latitude. + } + // From this message, a 2D array is created, where rows are azimuth cuts + // through the antenna pattern, columns are elevation cuts through the + // antenna patterns, and the values represent the gain in decibels. + // + // To compute the gain for a given link vector, the vector is decomposed + // into its x, y, and z components. The arctan(x/z) yields the azimuth angle + // for the link, and the arctan(-y/z) yields the elevation angle. The gain + // corresponding to this azimuth and elevation is then looked up in the 2D + // array. + // + // Requirements: + // 1) No missing values can exist in this array. For example, suppose an + // antenna pattern is defined for phi = {0°, 30°, 60°} and + // theta = {0°, 2°, 4°, 6°}. A gain value corresponding to each entry in + // this 2D array must be provided, such as in this notional example: + // + // Theta (θ) + // Phi (φ) | 0° | 2° | 4° | 6° | + // --------------------------------------------------------- + // 0° | 20 | 18 | 16 | 15 | + // 30° | 18 | 16 | 14 | 12 | + // 60° | 16 | 14 | 12 | 10 | + // + // In this example, the gain_value field must contain 12 elements that + // correspond to each element of this array. + // + // 2) The azimuth angles must have an equal interval between them. The + // elevation angles must have an equal interval between them. The interval + // between the azimuth angles does not have to be equal to the interval + // between the elevation angles. + // + // Notes: + // 1) To compute the gain in directions that fall between the defined data, + // the azimuth and elevation is rounded to the nearest angle for which gain + // values exist, and the corresponding gain is returned. The gain in + // directions that fall outside the defined range is clamped to the minimum + // or maximum angle for which a gain value is defined. + // TODO: Implement an interpolation approach. + // + // 2) The elements are not required to be sorted in any particular order. + // Required. + repeated AzElGainValue gain_values = 1; + } + + // An antenna pattern that represents a unique 3D gain pattern at various + // scan angles. + // + // This message is a natural choice to model the antenna patterns of phased + // array or electronically steered antennas. The same coordinate system as + // CustomPhiThetaAntennaPattern is used here. + message CustomAntennaPatternPerAngle { + message ScanAngleAndCustomPattern { + // The phi and theta angles are evaluated within the + // aalyria.spacetime.api.common.AntennaDefinition's axes. + // Note that if an offset is not explicitly defined on the antenna + // through the fixed_coordinate_offset field, the antenna inherits + // the axes of its parent platform. + + // The phi component of the scan angle. + // Phi is defined as the angle, in radians, from the positive x-axis + // to the orthogonal projection of the vector in the x-y plane. Positive + // phi is measured from the positive x-axis towards the positive y-axis. + // Range: [-π, π). + // Required. + optional float phi_rad = 1; + + // The theta component of the scan angle. + // Theta is the zenith angle, in radians, from the positive + // z-axis to the vector. Positive theta is measured from the + // positive z-axis towards the negative z-axis. + // Range: [0, π]. + // Required. + optional float theta_rad = 2; + + // The 3D antenna pattern for this scan angle. This pattern assigns + // a gain value to each look angle. + // The phi and theta in this pattern must also conform to the ranges + // above. + // Required. + optional CustomPhiThetaAntennaPattern custom_pattern = 3; + } + // From this message, a 4D array is created, where for each scan angle, a + // 2D array is stored to represent the 3D antenna pattern at this scan + // angle. + // + // Requirements: + // 1) No missing values can exist in this array. For example, suppose an + // antenna pattern is defined for scan angles of phi = {0, π/3} and + // theta = {0, π/4}. For each scan angle, suppose we have the gain defined + // at 4 look angles, at phi = {0, π/8} and theta = {0, π/8}. + // Conceptually, the array would resemble: + // + // Scan Angle Theta (θ) + // Scan Angle Phi (φ) | 0 | π/4 | + // -------------------------------------------------------------------- + // | 20 | 18 | 16 | 15 | + // 0 |_ _ _ _ _ _|_ _ _ _ _ _| _ _ _ _ _ |_ _ _ _ _ _| + // | 19 | 17 | 17 | 13 | + // ____________________|___________|___________|___________|___________| + // | 17 | 17 | 14 | 15 | + // π/3 |_ _ _ _ _ _|_ _ _ _ _ _| _ _ _ _ _ |_ _ _ _ _ _| + // | 16 | 15 | 12 | 11 | + // ____________________|___________|___________|___________|___________| + // + // where each "inner" 2D array corresponds to the gain values at the look + // angles. + // + // In this example, the scan_angle_and_custom_patterns field must contain 4 + // elements, and each CustomPhiThetaAntennaPattern must contain 4 elements + // in the gain_value field. + // + // 2) The phi angles must have an equal interval between them. The theta + // angles must have an equal interval between them. The interval for the phi + // angles does not have to be the same as the interval for the theta angles. + // The intervals for the scan angles do not have to be the same as the + // intervals for the look angles. Within each CustomPhiThetaAntennaPattern, + // unlike a standalone CustomPhiThetaAntennaPattern, the phi and theta + // angles must each have an equal interval between them, and must have the + // same phi and theta range across elements. + // + // Notes: + // 1) To compute the gain in directions that fall between the defined data, + // the phi and theta is rounded to the nearest angle for which gain values + // exist, and the corresponding gain is returned. The gain in directions + // that fall outside the defined range is clamped to the minimum or maximum + // angle for which a gain value is defined. + // TODO: Implement an interpolation approach. + // + // 2) The elements are not required to be sorted in any particular order. + // + // Required. + repeated ScanAngleAndCustomPattern scan_angle_and_custom_patterns = 1; + } + + message GaussianAntennaPattern { + optional double diameter_m = 1; + optional double efficiency_percent = 2; + optional double backlobe_gain_db = 3; + } + + message HelicalAntennaPattern { + optional double diameter_m = 1; + optional double efficiency_percent = 2; + optional double backlobe_gain_db = 3; + optional double number_of_turns = 4; + optional double turn_spacing_m = 5; + } + + message IsotropicAntennaPattern { + } + + message ParabolicAntennaPattern { + optional double diameter_m = 1; + optional double efficiency_percent = 2; + optional double backlobe_gain_db = 3; + } + + message SquareHornAntennaPattern { + optional double diameter_m = 1; + optional double efficiency_percent = 2; + optional double backlobe_gain_db = 3; + } + + message GaussianOpticalAntennaPattern { + optional double diameter_m = 1; + optional double efficiency_percent = 2; + optional double divergence_angle_rad = 3; + optional double pointing_error_rad = 4; + } + + message PhasedArrayAntennaPattern { + optional double design_frequency_hz = 1; + optional double backlobe_suppression_db = 2; + + message Element { + // These fields determine the position of each element in the phased + // array. The center of the array is considered to be at (0, 0). Since + // this models a planar array, the z-coordinate is 0.0. + optional double x = 1; + optional double y = 2; + } + repeated Element elements = 3; + + message MinimumVarianceDistortionlessResponseBeamformer { + optional double beam_variance = 1; + } + oneof beamformer { + MinimumVarianceDistortionlessResponseBeamformer + minimum_variance_distortionless_response_beamformer = 4; + } + + message CosineExponentElementFactor { + optional double cosine_exponent = 1; + // The element area in meters squared. + optional double element_area_m2 = 2; + } + optional CosineExponentElementFactor cosine_exponent_element_factor = 5; + } + + // Applies a separate near-field antenna radiation pattern when the other + // link end is within the near field range (in meters). + message NearAndFarFieldAntennaPattern { + optional AntennaPattern near_field_pattern = 1; + optional AntennaPattern far_field_pattern = 2; + optional double near_field_range_m = 3; + } + + // Applies a different antenna radiation pattern for the transmitter + // and receiver. + message TransmitterAndReceiverAntennaPattern { + optional AntennaPattern transmitter_pattern = 1; + optional AntennaPattern receiver_pattern = 2; + } + + oneof pattern_type { + CustomPhiThetaAntennaPattern custom_phi_theta_pattern = 1; + GaussianAntennaPattern gaussian_pattern = 2; + HelicalAntennaPattern helical_pattern = 3; + IsotropicAntennaPattern isotropic_pattern = 4; + ParabolicAntennaPattern parabolic_pattern = 5; + SquareHornAntennaPattern square_horn_pattern = 6; + GaussianOpticalAntennaPattern gaussian_optical_pattern = 7; + PhasedArrayAntennaPattern phased_array_pattern = 10; + NearAndFarFieldAntennaPattern near_and_far_field_pattern = 9; + TransmitterAndReceiverAntennaPattern transmitter_and_receiver_pattern = 11; + CustomAntennaPatternPerAngle custom_antenna_pattern_per_angle = 12; + CustomAzElAntennaPattern custom_az_el_pattern = 13; + } +} diff --git a/api/nbi/v1alpha/resources/coverage.proto b/api/nbi/v1alpha/resources/coverage.proto new file mode 100644 index 0000000..f27711e --- /dev/null +++ b/api/nbi/v1alpha/resources/coverage.proto @@ -0,0 +1,179 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/coordinates.proto"; +import "api/common/wireless_transceiver.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// Defines a grouping of transceiver models. +message TransceiverSet { + repeated aalyria.spacetime.api.common.TransceiverModelId transceiver_ids = 1; +} + +// Defines a grouping of platforms. +message PlatformSet { + repeated string platform_ids = 1; +} + +// The term "station" is used in the sense of the ITU definition: +// "One or more transmitters or receivers or a combination of transmitters +// and receivers, including the accessory equipment, necessary at one +// location for carrying on a radiocommunication service, or the radio +// astronomy service." +message StationSet { + oneof station_set_type { + // Use transceivers when the radio equipment model information is + // relevant to the description of the stations. + TransceiverSet transceivers = 1; + // Use platforms, when only the coordinates and motion are relevant to the + // description of the station. + PlatformSet platforms = 2; + } +} + +message StationSubset { + // References a StationSet entity ID. + optional string station_set_id = 1; + + // May be optionally used to filter the station set to only those that exist + // above a surface region, which is specified by referencing a region entity + // ID. All stations in the set are selected if this field is omitted. + optional string region_id = 2; +} + +// References a set of coordinates derived from an array of transceivers, +// platforms, or spatial surface region. +message CoordinateArray { + oneof derivation { + S2CoverageGrid surface = 1; + GeostationaryArc geo_arc = 2; + StationSubset stations = 3; + } +} + +// The signal power over a coordinate grid. +message SignalPowerCoverage { + oneof type { + SurfacePfdRegions surface = 1; + GeostationaryArcPfd geo_arc = 2; + PfdOverConstellation constellation = 3; + } +} + +// Defines a GeoJSON formatted geographic region. +message GeoJsonRegion { + // This provides a human-readable name for the region. + optional string name = 1; + // Geobuf encoded geoJSON region (see https://github.com/mapbox/geobuf). + repeated bytes encoded_geo_buffer = 2; +} + +// Defines a set of S2 cells (that can be at different levels) that togeher +// cover some region. This can be used to represent the boundaries of a nation, +// as one example. +message SurfaceRegion { + repeated int64 s2_cell_ids = 1; + // This provides a human-readable name for the region. + optional string name = 2; +} + +// Defines a set of s2 cells and an analysis level over a region. Calculations +// use WGS84 ellipsoid, altitude is assumed to be zero and terrain is not +// factored into the coverage calculation. See http://s2geometry.io/ for +// background information. +message S2CoverageGrid { + // A region on Earth's surface. Defined by an outer covering of S2 cells at + // various levels. See http://s2geometry.io/devguide/examples/coverings. + oneof region_description { + // An explicit list of cell IDs. + aalyria.spacetime.api.common.S2Cells region = 1; + // The entity key for a SurfaceRegion object in the store. + string region_id = 3; + } + + // The desired s2 cell resolution for coverage analysis over this region. + // See levels at http://s2geometry.io/resources/s2cell_statistics. If an + // s2_level is provided that does not match the level of a provided cell id, + // the parents or children of the provided cell will be found that match the + // desired s2 _level. + optional uint32 s2_level = 2 [default = 6]; // range: [0..30] +} + +// Power flux density over a region. Terrain is not utilized for these results. +// WGS84 ellipsoid is used and altitude is assumed to be zero. +message PfdOverS2Region { + // A region on Earth's surface. Defined by an outer covering of S2 cells at + // various levels. See http://s2geometry.io/devguide/examples/coverings. + optional aalyria.spacetime.api.common.S2Cells region = 1; + + // The received power flux density over the region, in dB(W/m2). + optional double received_power_flux_density_db_w_per_m2 = 2; +} + +// The power flux density over one or more regions. +message SurfacePfdRegions { + repeated PfdOverS2Region pfd_region = 1; +} + +// A grid on the circular arc of geostationary orbit, approximately 35,786 km +// above Earth's surface. +message GeostationaryArc { + // The absolute value of the minimum and maximum latitude, in degrees, above + // and below the geostationary arc. If this field is omitted, grid points are + // assigned directly to the geostationary arc at the specified resolution. + optional double latitude_bound_deg = 1; + + // The maximum angular spacing between grid points, in degrees. + optional double resolution_deg = 2; +} + +// The power flux density over some grid cell in geostationary orbit. Altitude +// is assumed to be 35,786 km. +message PfdOverGeoArcCell { + // The longitude on the geostationary arc, in degrees. + optional double longitude_deg = 1; + + // The latitude on the geostationary arc, in degrees. + optional double latitude_deg = 2; + + // The received power flux density over this cell, in dB(W/m2). + optional double received_power_flux_density_db_w_per_m2 = 3; +} + +// The power flux density over a set of cells in the geostationary arc. +message GeostationaryArcPfd { + repeated PfdOverGeoArcCell cell = 1; +} + +// The power flux density at an antenna. +message PfdAtAntenna { + reserved 1; + + optional aalyria.spacetime.api.common.TransceiverModelId + transceiver_model_id = 3; + + // The received power flux density at the antenna's location, in dB(W/m2). + optional double received_power_flux_density_db_w_per_m2 = 2; +} + +// The power flux density observed by a constellation of antennas. +message PfdOverConstellation { + repeated PfdAtAntenna victim = 1; +} diff --git a/api/nbi/v1alpha/resources/devices_in_region.proto b/api/nbi/v1alpha/resources/devices_in_region.proto new file mode 100644 index 0000000..45556c6 --- /dev/null +++ b/api/nbi/v1alpha/resources/devices_in_region.proto @@ -0,0 +1,47 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/wireless_transceiver.proto"; +import "api/nbi/v1alpha/resources/coverage.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// DevicesInRegion defines a geographic region within which devices of a +// given transceiver model and density reside in. +message DevicesInRegion { + // Uniquely identifies a distribution of devices in a specific region. + optional string device_in_region_id = 1; + + // A model of the user equipment, user terminal, handset, or other + // customer premises equipment as the basis for coverage request. + // These constrain the eligible bands/channels and establish the + // receiver filters, amplifiers, and receive antenna gain to expect. + // Consider specifying the antenna gain and channel capabilities for + // the least capable device in the population of equipment. + + // The coordinate system of the provided transceiver model is + // assumed to be East-North-Up. + optional aalyria.spacetime.api.common.TransceiverModel reference_device = 2; + + // The outline of the geographic region. + optional GeoJsonRegion region_on_earth = 3; + + // The average density of devices per kilometer in this request. + optional double devices_per_km2 = 4; +} diff --git a/api/nbi/v1alpha/resources/intent.proto b/api/nbi/v1alpha/resources/intent.proto new file mode 100644 index 0000000..ea46473 --- /dev/null +++ b/api/nbi/v1alpha/resources/intent.proto @@ -0,0 +1,324 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// An Intent describes an application's request to the Spacetime core to alter +// the network's behavior. It specifies policy rather than mechanism. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/control.proto"; +import "api/common/network.proto"; +import "api/common/time.proto"; +import "api/common/tunnel.proto"; +import "api/nbi/v1alpha/resources/network_link.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/code.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// Intent states are based on the ONOS intent framework, +// see https://goo.gl/aO4ivn, but with more granularity. +enum IntentState { + UNKNOWN = 0; + INSTALL_REQ = 1; + SCHEDULED = 10; + SCHEDULING = 12; + INSTALLING = 3; // In process of being deprecated. + INSTALLED = 4; + WITHDRAW_REQ = 5; + + // TODO: Subdivide WITHDRAWING state as was done for the + // INSTALLING state. + WITHDRAWING = 6; + FAILED = 8; + + reserved 2, 7, 9, 11; +} + +// Specifies a policy that reserves the use of a network resource. +// Once the controller has started compiling an intent, Network Applications +// must treat all fields except 'state' as immutable. To modify the resource +// reserved by an intent, submit a new intent and withdraw the existing intent +// if necessary. +message Intent { + // TODO: Replace the use of this field with app_id in EntityGroup + // and remove it. + optional string app_id = 1 [deprecated = true]; + + // Determines what time the install request should be enacted. + // If left blank, the intent compiler will request that the changes be made at + // the earliest time that it believes it can deliver the messages to the + // affected agents (that is, the maximum control plane latency to all affected + // agents. + optional google.protobuf.Timestamp time_to_enact = 3; + oneof value { + LinkIntent link = 4; + RadioIntent radio = 5; + PathIntent route = 6; + TunnelIntent tunnel = 10; + ModemIntent modem = 18; + } + optional IntentState state = 7 [default = INSTALL_REQ]; + + // This field is only set after the intent enters the INSTALLING or + // WITHDRAWING phases. It holds a timestamp indicating when the request was + // made. + optional int64 request_timestamp_us = 11; + + // This field is only set after a successful compile phase. + // The field contains the updates necessary for intent installation. + repeated aalyria.spacetime.api.common.ScheduledControlUpdate + compiled_updates = 8; + + // This field is set and defined if state = FAILED. + optional IntentFailure failure = 9; + + // This field is set in the WITHDRAW_REQ state, and only defined in + // WITHDRAW_REQ, WITHDRAWING, and WITHDRAWN states. + optional IntentWithdrawal withdrawal = 14; + + // Determines what time the intent should be withdrawn. + // If left blank, the intent will remain in the INSTALLED state until it is + // updated to WITHDRAW_REQ or the intent fails. + // + // TODO: replace with google.protobuf.Timestamp. + optional aalyria.spacetime.api.common.DateTime time_to_withdraw = 15; + + message ServiceRequestAndIntervals { + optional string service_request_id = 1; + repeated google.type.Interval interval = 2; + } + + // An annotation indicating the service requests that the intent supports over + // time. + repeated ServiceRequestAndIntervals dependent_service_requests = 16; + + // This field is only set after a successful compile phase. + // The field contains the updates necessary for intent withdrawal. + repeated aalyria.spacetime.api.common.ScheduledControlUpdate + compiled_withdrawal_updates = 17; + + reserved 2, 12, 19 to max; // Next IDs. +} + +// Models an intent to configure steerable beams and/or radio resources. +message LinkIntent { + oneof link_type { + // Configures a single wireless interface. + DirectionalLink directional_link = 3; + + // Configures a point-to-point link between a pair of wireless interfaces. + BidirectionalLink bidirectional_link = 2; + } + + reserved 1; + reserved 4 to max; // Next IDs. +} + +// Models an intent to change the radio parameters of one or more radios. +// In practice, apps are advised to only use repeated entries if the changes +// are related (for example, to set the transmiter and receiver participating +// in the same wireless link to use the same radio channel). +message RadioIntent { + repeated RadioConfiguration configurations = 2; + + reserved 1; + reserved 3 to max; // Next IDs. +} + +// Models a route/path through the network. +message PathIntent { + reserved 4 to 8; + // The network node/element that is the source of the network path. + optional string src_element_id = 1 [deprecated = true]; + + // The network node/element that is the destination of the network path. + optional string dst_element_id = 2 [deprecated = true]; + + // Defines rules for matching flows that should be forwarded along the path.. + optional aalyria.spacetime.api.common.FlowClassifier classifier = 10; + + // The path segments that comprise the route. + repeated NetworkLink path_segments = 3; + + reserved 11 to max; // Next IDs. +} + +message TunnelIntent { + message TunnelEndpoint { + optional string node_id = 1; + + // Rules for classifying flows transitting the node that should be subject + // to encapsulation. + optional aalyria.spacetime.api.common.FlowClassifier classifier = 5; + + // A node-unique identifier for the network interface. Must match a known + // interface_id of this node (see network_element.proto). Used to derive the + // tunnel (outer) endpoint address. + optional string encapsulated_src_interface_id = 6; + + // The port used to populate the source and destination port fields of + // the tunnel (outer) L4 header. + optional int32 encapsulated_src_port = 3; + reserved 2, 4; + reserved 7 to max; // Next IDs. + } + optional TunnelEndpoint a = 10; + optional TunnelEndpoint b = 11; + + optional aalyria.spacetime.api.common.TunnelMethod method = 5; + + reserved 1 to 4, 6 to 9; + reserved 12 to max; // Next IDs. +} + +message ModemIntent { + optional aalyria.spacetime.api.common.NetworkInterfaceId interface_id = 1; + + // The collection of beams to be illuminated. The beams are to be illuminated + // one-at-a-time in a repeating sequence determined by the + // beam_hopping_time_slots. + repeated Beam beams = 2; + + // A collection of beam-hopping time slots representing a beam-hopping frame. + // Each slot consists of a zero-based index into the collection of beams, + // representing the beam to be illuminated during that time slot. A value of + // -1 represents that no beam should be illuminated during that time slot. + repeated int32 beam_hopping_time_slots = 3; +} + +message Beam { + optional BeamTarget target = 1; + + optional RxConfiguration rx = 2; + optional TxConfiguration tx = 3; + + // The collection of endpoints served by the beam. + // For example, a collection of user terminals (UTs) served. + // + // Each endpoint is the ID of a network node in Spacetime's network model. + repeated Endpoint endpoints = 4; +} + +message RxConfiguration { + optional uint64 center_frequency_hz = 1; + optional uint64 channel_bandwidth_hz = 2; + optional Polarization polarization = 3; + + // Symbol rate in Megasymbols per second. + optional double symbol_rate_msps = 5; + + optional ModemMode mode = 4; +} + +message TxConfiguration { + optional uint64 center_frequency_hz = 1; + optional uint64 channel_bandwidth_hz = 2; + optional Polarization polarization = 3; + + // Transmit power in Watts. + optional double power_w = 5; + + // Symbol rate in Megasymbols per second. + optional double symbol_rate_msps = 6; + + optional ModemMode mode = 4; + + // The lowest MODCOD common among all endpoints. + optional string lowest_common_modcod = 7; +} + +enum Polarization { + POLARIZATION_UNSPECIFIED = 0; + POLARIZATION_LHCP = 1; // Left-handed circular polarization + POLARIZATION_RHCP = 2; // Right-handed circular polarization +} + +enum ModemMode { + MODEM_MODE_UNSPECIFIED = 0; + MODEM_MODE_DVB_RCS2 = 1; + MODEM_MODE_DVB_S2X = 2; +} + +message Endpoint { + optional string id = 1; + optional string lowest_supported_rx_modcod = 2; + optional double rx_reference_throughput_bps = 3; + optional double tx_reference_throughput_bps = 4; +} + +// IntentFailure provides context on the failure of an intent. It's purpose is +// to allow apps to make better decision when faced with failures. +message IntentFailure { + enum IntentFailureType { + UNKNOWN = 0; + COMPILATION_FAILURE = 1; + AGENT_INSTALLATION_FAILURE = 2; + UNREACHABLE_AGENT = 3; + // An intent in the INSTALLING or INSTALLED state failed because of a CDPI + // report with unexpected state (e.g. due to loss of antenna tracking, bug + // in the agent, etc). An intent in the INSTALLING state can also report + // this failure if another intent concurrently modified control plane state + // after compiled updates have been acked by agents, but before the compiler + // had a chance to observe the desired control plane state. + UNEXPECTED_CDPI_STATE_CHANGE = 4; + PRECONDITION_FAILED = 5; + DEADLINE_EXCEEDED = 6; + // The precondition is in WITHDRAW_REQ, WITHDRAWING or WITHDRAWN state. + PRECONDITION_EXPIRED = 7; + + SDN_INTERNAL_ERROR = 9; + + reserved 8, 10 to max; // Next IDs. + } + + // Failure type is set if the state = FAILED. + optional IntentFailureType type = 1; + // This field is set if type = PRECONDITION_FAILED. + optional IntentFailure precondition_failure = 2; + // Human readable description of the failure. + optional string description = 3; + // This field is set if failure_type = AGENT_INSTALLATION_FAILURE, + // UNREACHABLE_AGENT, UNEXPECTED_CDPI_STATE_CHANGE, or SDN_INTERNAL_ERROR. + repeated string agent_ids = 4; + // This field is set if failure_type = AGENT_INSTALLATION_FAILURE. + optional google.rpc.Code agent_failure_code = 5; + + reserved 6 to max; // Next IDs. +} + +// IntentWithdrawal can expose more information about why an app chose to +// withdraw an intent. It is designed to surface decision making information to +// administrators to explain the SDN behavior. +message IntentWithdrawal { + enum WithdrawType { + UNKNOWN = 0; + INACCESSIBLE = 1; + NOT_REQUIRED = 2; + reserved 3 to max; // Next IDs. + } + + optional WithdrawType type = 1; + + // Human readable details of the withdraw reason. + optional string description = 2; + + reserved 3 to max; // Next IDs. +} + + diff --git a/api/nbi/v1alpha/resources/motion_evaluation.proto b/api/nbi/v1alpha/resources/motion_evaluation.proto new file mode 100644 index 0000000..e418905 --- /dev/null +++ b/api/nbi/v1alpha/resources/motion_evaluation.proto @@ -0,0 +1,47 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/coordinates.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +message ComputedMotion { + // The metadata that identifies the `MotionDefinition` entity that is + // used as the basis for the computed motion data in this message. + message ReferenceMotion { + // The string ID of the source `MotionDefinition` entity. + optional string motion_ref_id = 1; + // Commit timestamp of the source `MotionDefinition` entity. + // Microseconds since epoch. + optional int64 commit_timestamp = 2; + } + optional ReferenceMotion reference = 1; + + // Holds `Motion` messages that may be more useful to internal consumers. + // These include trajectory descriptions that have been converted from + // another description type, e.g.: + // + // * `PointAxesTemporalInterpolation` waypoints resulting from propogation + // of a `StateVector` under an assumed set of forces, or + // + // * an extrapolation of waypoints for platforms moving on fixed or + // slowly varying headings (e.g. ships, planes, balloons, etc with + // motion reported via relay of AIS or ADS-B information). + repeated aalyria.spacetime.api.common.Motion motions = 2; +} diff --git a/api/nbi/v1alpha/resources/network_element.proto b/api/nbi/v1alpha/resources/network_element.proto new file mode 100644 index 0000000..78487a4 --- /dev/null +++ b/api/nbi/v1alpha/resources/network_element.proto @@ -0,0 +1,249 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/time.proto"; +import "api/common/wireless_transceiver.proto"; +import "api/types/ietf.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/duration.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// Models an abstract network node (a device, or an entire network). +message NetworkNode { + reserved 11 to max; + + // A globally unique identifier for the network node. + optional string node_id = 1; + + // Optional friendly, human-readable strings for UI purposes. + optional string name = 2; + optional string type = 7; + + // A freeform string, used as a key in other contexts to lookup + // context-relevant attributes (UI visual configuration, etc). + optional string category_tag = 6; + + message RoutingConfiguration { + // The router ID of this NetworkNode. + optional types.RouterId router_id = 1; + + // The Segment Routing (RFC 8402) Node SID for this NetworkNode. + optional types.SegmentId node_sid = 2; + } + // Note: Only a single IP VRF at this time. + optional RoutingConfiguration routing_config = 3; + + // A node may optionally act a gateway for a set of specified IP subnets. + repeated string subnet = 8; + + // A network node may have one or more network interfaces/adapters. + repeated NetworkInterface node_interface = 4; + + // Included if the network node is an SDN-enabled network element (optional). + optional SdnAgent agent = 5; + + // Optionally used to further constrain the maximum transmit power available + // for use, in aggregate, by the node's wireless network interface. + message SignalPowerBudget { + // The time interval (set to infinite if static). + optional aalyria.spacetime.api.common.TimeInterval interval = 1; + + // The total amount of available power, in Watts. + optional double available_signal_power_watts = 2; + } + repeated SignalPowerBudget power_budget = 9; + + // Model a node’s ability to store data in order to tolerate disruption to + // end-to-end connectivity (i.e. "store and forward" of Bundle Protocol + // data units [BPDUs]). + // + // Any non-negative value for available_bytes indicates a node's support for + // store-and-forward operation of at least one of: + // + // * BPv6 (RFC 5050), or + // * BPv7 (RFC 9171) + // + // data units (BPDUs), even if that value is 1 byte. + // + // Support for originating BPDUs is also implied, as generation of a + // "status report" BPDU may be required under certain circumstances. + message Storage { + // The amount of storage, in bytes, that is available for use by + // store and forward operations. + // + // This includes the amount of storage, in bytes, that is available for + // storage of Bundle Protocol data units in transit. + // + // This can include space used by an application for flows that originate + // from this node as the source. This is usually from an application-layer + // queue, buffer, or cache. + // + // Nodes that separate storage for originated BPDUs from that use for + // bundles in transit may wish to set this value to the bytes allocated + // only for transit storage. + optional int64 available_bytes = 1; + + // TODO: decide whether indication of specific Bundle Protocol + // version(s) is needed. + // + // For now it seems reasonable to require that a given Spacetime-modelled + // fleet support Bundle Protocol versions uniformly across the fleet, i.e. + // all store-and-forward capable nodes support BPv6 only, BPv7 only, or all + // are "dualstack" (to borrow a term from IPv4/IPv6 coexistence). + } + optional Storage storage = 10; +} + +// Models a Software-Defined Networking (SDN) agent that handles the SDN +// control-to-data-plane interface (CDPI). +message SdnAgent { + enum CdpiProtocol { + UNKNOWN = 0; + AIRFLOW = 1; + } + optional CdpiProtocol type = 1; + + // The email address associated with the SDN agent's google account. + optional string google_user_id = 2; + + // When sending updates to nodes, the SDN must send control updates far enough + // ahead of their enactment time to account for the maximum control plane + // latency of the control stream. This field maps the CDPI stream priority to + // the maximum estimated latency of that stream. + map maximum_control_plane_latency = 3; +} + +// Models a network interface (aka network device, port, cable, or adapter). +// Next ID: 18 +message NetworkInterface { + reserved 2, 5, 9 to 13, 18 to max; + + // A node-unique identifier for the network interface. + optional string interface_id = 1; + + // Optional friendly, human-readable string. Named after the analagous + // description field in the YANG model for interfaces. See: + // https://datatracker.ietf.org/doc/html/rfc8343#section-5 + optional string description = 17; + + // The interface's IP address represented as a CIDR notation subnet or single + // IP address string. + optional string ip_address = 14; + + // Ethernet address in readable colon separated format. e.g. + // "1:23:45:67:89:ab" or "01:23:45:67:89:ab" + optional string ethernet_address = 15; + + // Mode field indicates whether or not the interface can receive packets in + // promiscuous mode, or if it can receive L2 packets only if the L2 + // destination address matches it's own physical address. + enum Mode { + PROMISCUOUS = 0; + NON_PROMISCUOUS = 1; + } + optional Mode rx_mode = 8; + + // Optionally specifies the local/hardware IDs within the platform or switch. + // These are not used by network controllers but may be useful for network + // elements for operational context. Example: "eth:0", "gwl:1", "gre:2", etc. + message LocalId { + optional string type = 1; + optional int32 index = 2; + } + repeated LocalId local_id = 3; + + // Provides additional attributes based on the adapter/port it traverses. + oneof interface_medium { + WiredDevice wired = 4; + WirelessDevice wireless = 6; + } + + message RoutingConfiguration { + message AdjacencySidEntry { + // Required. + optional types.SegmentId sid = 1; + // Only required if the link is not P2P (e.g., a LAN or multiple + // beams/channels associated with a single interface). + // + // This will typically be an IPv4 or IPv6 address, but if multiple + // beams or channels are associated with a single interface this + // might also be an identifier for beam/subchannel/subcarrier. + optional string next_hop = 2; + } + repeated AdjacencySidEntry adjacency_sids = 1; + } + optional RoutingConfiguration routing_config = 7; + + message Impairment { + enum Type { + // The interface is known to be unusable. + DEFAULT_UNUSABLE = 0; + + // The reliability or performance of the interface is impaired in a + // non-deterministic manner or in a manner that is not otherwise modeled. + // The network should avoid using the interface; but, it might work. + UNRELIABLE = 1; + } + optional Type type = 1; + // AppId adding this impairment. + optional string app_id = 2; + // Time this impairment was added to the interface model. + optional int64 timestamp_usec = 3; + // The reason for this impairment. + optional string reason = 4; + + // Customers can use these to add extra fields (for example classifications + // of the impairment reasons). + optional google.protobuf.Any details = 5; + } + + // TODO: Add ability to submit drain intents. + // Reason (some internal transient state (power, etc.)) that affects its use. + repeated Impairment operational_impairment = 16; +} + +// Models a wired interface to a static (wired or wireless) network link. The +// Link Evaluator will not generate link metrics for these interfaces; instead, +// an SDN topology manager must explictly define link accessibility and metrics +// by adding an InterfaceLinkReport for each direction (see network_link.proto). +message WiredDevice { + // The maximimum data-rate of the interface, in layer 2 bits per second. + optional double max_data_rate_bps = 1; + + // Optionally specifies a physical platform associated with the wired device. + // This may be used for geospatial visualization of statically defined links. + optional string platform_id = 2; +} + +// Models a wireless device that uses modeled transmitters & receivers. +// Spacetime's Link Evaluator services will write InterfaceLinkReports for +// all possible wireless links based on models if real metrics are unavailable. +// Next ID : 7 +message WirelessDevice { + reserved 1, 2, 4, 7 to max; + // Identifies a physical transceiver model from the SDN Store. + optional aalyria.spacetime.api.common.TransceiverModelId + transceiver_model_id = 5; + + // Explicit timeout for enacting this wireless device. If present, this + // should be used in BeamUpdate to time out the process of beam steering and + // target acquisition. + optional google.protobuf.Duration link_establishment_timeout = 6; +} diff --git a/api/nbi/v1alpha/resources/network_link.proto b/api/nbi/v1alpha/resources/network_link.proto new file mode 100644 index 0000000..c50b91b --- /dev/null +++ b/api/nbi/v1alpha/resources/network_link.proto @@ -0,0 +1,334 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/control_radio.proto"; +import "api/common/coordinates.proto"; +import "api/common/network.proto"; +import "api/common/platform_antenna.proto"; +import "api/common/time.proto"; +import "api/common/wireless_transceiver.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// Defines a directional link between two logical network interfaces. +message NetworkLink { + optional aalyria.spacetime.api.common.NetworkInterfaceId src = 3; + optional aalyria.spacetime.api.common.NetworkInterfaceId dst = 4; + + reserved 1, 2; +} + +message RadioConfiguration { + optional aalyria.spacetime.api.common.NetworkInterfaceId interface_id = 1; + + // Configures the transmitter properties. + optional aalyria.spacetime.api.common.TransmitterState tx_state = 2; + + // Configures the receiver properties. + optional aalyria.spacetime.api.common.ReceiverState rx_state = 3; + + // Identifies the adaptive data rate table id used to model the link between + // the transmitter and receiver. This is a required field. + optional string rate_table_id = 5; + + // Configures the TDMA schedule, if applicable. + optional aalyria.spacetime.api.common.TdmaSchedule tdma_schedule = 4; + reserved 6 to max; // Next IDs. +} + +message Radio { + // Defines the center of the channel in Hz. + // For RF transceivers, this is the carrier frequency. + // For optical transceivers, this may be converted to wavelength. + optional uint64 center_frequency_hz = 1; + optional string band_profile_id = 2; + optional aalyria.spacetime.api.common.Polarization polarization = 3; + + message TxRadioConfiguration { + + message BeamPower { + enum PowerType { + POWER_TYPE_UNSPECIFIED = 0; + POWER_TYPE_NADIR_EQUIVALENT_BEAM_PEAK_EIRP_PSD_DBW_PER_MHZ = 1; + POWER_TYPE_BEAM_PEAK_EIRP_PSD_DBW_PER_MHZ = 2; + POWER_TYPE_WATTS = 3; + } + optional PowerType power_type = 1; + optional double power_value = 2; + } + optional BeamPower transmit_power = 1; + + optional string modulator_id = 2; + + enum ModulatorMode { + MODULATOR_MODE_UNSPECIFIED = 0; + MODULATOR_MODE_DVB_S2X = 1; + MODULATOR_MODE_DIGITAL_TRANSPARENT = 2; + } + optional ModulatorMode modulator_mode = 3; + } + + optional TxRadioConfiguration tx_radio = 4; + + optional RxRadioConfiguration rx_radio = 5; +} + +message RxRadioConfiguration { + optional string demodulator_id = 1; + + enum DemodulatorMode { + DEMODULATOR_MODE_UNSPECIFIED = 0; + DEMODULATOR_MODE_DVB_S2X = 1; + DEMODULATOR_MODE_DVB_RCS2_TDMA = 2; + DEMODULATOR_MODE_DIGITAL_TRANSPARENT = 3; + } + optional DemodulatorMode demodulator_mode = 2; +} + +message LinkEnd { + optional aalyria.spacetime.api.common.NetworkInterfaceId id = 3; + reserved 1, 2, 4; +} + +message BeamTarget { + oneof type { + aalyria.spacetime.api.common.TransceiverModelId transceiver_id = 1; + string platform_id = 3; + aalyria.spacetime.api.common.Motion coordinates = 2; + } +} + +// Defines a bidirectional network link. If necessary, asymmetric roles for the +// endpoints may be specified. +message BidirectionalLink { + optional LinkEnd a = 1; + optional LinkEnd b = 2; + + // These fields are optional and may be omitted for wireless interfaces that + // do not require centralized orchestration of radio-related parameters. + // TODO: migrate RadioIntent usage to these fields instead. + optional Radio a_to_b_radio = 3; + optional Radio b_to_a_radio = 4; +} + +// Configures a directional link. The link may be a point-to-multipoint link +// between a transmitter and a list of receiving platforms, or a +// point-to-point link between two transceivers. +message DirectionalLink { + // Identifies the wireless network interface. + optional aalyria.spacetime.api.common.NetworkInterfaceId id = 1; + + // This field is optional and may be omitted for wireless interfaces that + // do not require centralized orchestration of radio-related parameters. + optional Radio radio_configuration = 2; + + // This field is optional and may be omitted for wireless interfaces whose + // beams are fixed to the platform. It is required for steerable beams. + optional BeamTarget target = 3; + + // Identifies the platforms that are on the receiving end of the + // point-to-multipoint link. + repeated RxPlatform rx_platforms = 4; +} + +message RxPlatform { + optional aalyria.spacetime.api.common.NetworkInterfaceId id = 1; + + // Defines the radio configuration to apply to the receiving platform. + optional RxRadioConfiguration rx_radio = 2; +} + +// Defines whether or not a link is usable or possible. +enum Accessibility { + ACCESS_UNKNOWN = 0; + ACCESS_EXISTS = 1; + ACCESS_MARGINAL = 3; + NO_ACCESS = 2; +} + +// A modeled, physical-layer wireless link budget. +message WirelessLinkBudget { + reserved 10 to 14, 17 to max; + + // The transmitter antenna's gain in the link direction, in dB. + optional double transmitter_antenna_gain_in_link_direction_db = 1; + + // The effective isotropic radiated power from the transmitter, in dBW. + optional double effective_isotropic_radiated_power_dbw = 2; + + // Maps the name of each propagation loss model to the amount of propagation + // loss, in dB, that is attributable to that particular model. + map component_propagation_loss_db = 15; + + // The total amount of signal propagation loss, in dB. + optional double propagation_loss_db = 3; + + // The power at the receiver antenna before receive antenna gain. + optional double received_isotropic_power_dbw = 4; + + // The received power flux density, in dB(W/m2). + optional double received_power_flux_density_db_w_per_m2 = 5; + + // The receiver antenna's gain in the link direction, in dB. + optional double receiver_antenna_gain_in_link_direction_db = 6; + + // This is power at the receiver antenna facing into the rest of the receive + // chain (the output of the antenna, after Rx antenna gain). + optional double power_at_receiver_output_dbw = 7; + + // The carrier to noise ratio. + optional double carrier_to_noise_db = 8; + + // The carrier to noise-plus-interference ratio (C / (N + I)), + // in dB. + optional double carrier_to_noise_plus_interference_db = 16; + + // The carrier to noise density, in dB/Hz. + optional double carrier_to_noise_density_db_per_hz = 9; +} + +// Describes expected changes to the accessibility of a link to a destination +// on a particular channel. +message WirelessLinkReport { + // The ID of the destination transceiver model described by this report. + optional aalyria.spacetime.api.common.TransceiverModelId dst = 1; // required + + optional string band_profile_id = 2; // required + repeated uint64 center_frequencies_hz = 3; + + // A series of non-overlapping time intervals that describe the predicted + // accessibility of the link -- and link metrics within accessible intervals. + message WirelessAccessInterval { + // The applicable time interval (may be open ended). + optional aalyria.spacetime.api.common.TimeInterval interval = 1; + + // Whether or not the link is predicted/known to be accessible (usable). + optional Accessibility accessibility = 2; + repeated string no_access_reason = 3; + + // Modeled link metric predictions. + message WirelessLinkMetrics { + // A timestamp. + optional aalyria.spacetime.api.common.DateTime timestamp = 1; + + // The electromagnetic propagation delay. + optional aalyria.spacetime.api.common.Duration propagation_delay = 2; + + // The link direction from transmitter towards the receiver. + // This field is only applicable to wireless links. + optional aalyria.spacetime.api.common.PointingVector pointing_vector = 3; + + // The link distance, in meters. + // This field is only applicable to wireless links. + optional double range_m = 4; + + // The modeled data rate capacity, in layer 2 bits per second. + optional double data_rate_bps = 6; + + // The transmitter antenna's gain in the link direction, in dB. + optional double transmitter_antenna_gain_in_link_direction_db = 7; + + // The receiver antenna's gain in the link direction, in dB. + optional double receiver_antenna_gain_in_link_direction_db = 8; + + // A human readable name (e.g. "QPSK-LDPC-2-3") describing the modulation + // and coding scheme associated with the modeled data rate capacity. + optional string mod_cod_scheme_name = 9; + } + repeated WirelessLinkMetrics sampled_metrics = 4; + } + repeated WirelessAccessInterval access_intervals = 4; +} + +// Provides interface link report between `src` and `dst` interfaces, +// at different `access_intervals`. +// It is the responsibility of some SDN application to insert link +// reports corresponding to the physical availability of connections from each +// source interface they manage to any other interfaces which should be +// considered reachable. +message InterfaceLinkReport { + reserved 1, 2, 3; + // The source interface. + optional aalyria.spacetime.api.common.NetworkInterfaceId src = 4; + + // The destination interface. + optional aalyria.spacetime.api.common.NetworkInterfaceId dst = 5; + + // A series of non-overlapping time intervals that describe the predicted + // accessibility of the link -- and link metrics within accessible intervals. + message AccessInterval { + // The applicable time interval (may be open ended). + optional aalyria.spacetime.api.common.TimeInterval interval = 1; + + // Whether or not the link is predicted/known to be accessible (usable). + optional Accessibility accessibility = 2; + + // Delay incurred by a data-frame when communicated over this link. + optional aalyria.spacetime.api.common.Duration frame_delay = 3; + + // The modeled data rate capacity, in layer 2 bits per second. + optional double data_rate_bps = 4; + } + repeated AccessInterval access_intervals = 6; +} + +// Provides transceiver link reports for all possible wireless links from a +// source transceiver. It is the responsibility of some SDN application to +// insert link reports corresponding to the physical availability of connections +// from each source transceiver they manage to any other transceivers which +// should be considered reachable. For example, wireless transmitters may have +// these reports updated for each reachable receiver as a result of the link +// evaluation model. +message TransceiverLinkReport { + // The source transceiver for all of the reported possible wireless links. + optional aalyria.spacetime.api.common.TransceiverModelId src = 1; + // A set of possible wireless links that this source transceiver could + // participate in. + repeated WirelessLinkReport links = 2; +} + +// BeamCandidate defines the modeled link evaluation of a potential +// point-to-multipoint link. The candidate can model either the downlink +// direction, from the beam to a set of receiving platforms, +// or the uplink, from a set of platforms to the beam. + +// Note: BeamCandidate is still under development. +message BeamCandidate { + optional aalyria.spacetime.api.common.TransceiverModelId transceiver_id = 1; + + // The coordinates at which the beam is pointed. + optional aalyria.spacetime.api.common.Motion target = 2; + + // AccessibilityInterval defines a link evaluation interval and the + // corresponding groups of platforms and their modeled data. + message AccessibilityInterval { + + // GainGroup defines the modeled gain between the beam + // and a set of transceivers and/or Earth Fixed H3 cells. + message GainGroup { + optional double gain_db = 1; + repeated string platform_ids = 2; + repeated fixed64 h3_cells = 3; + } + repeated GainGroup gain_groups = 1; + optional google.type.Interval interval = 2; + } + repeated AccessibilityInterval accessibility_intervals = 3; +} diff --git a/api/nbi/v1alpha/resources/service_request.proto b/api/nbi/v1alpha/resources/service_request.proto new file mode 100644 index 0000000..a843679 --- /dev/null +++ b/api/nbi/v1alpha/resources/service_request.proto @@ -0,0 +1,137 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/network.proto"; +import "api/common/time.proto"; +import "api/common/tunnel.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "google/type/interval.proto"; +import "google/type/money.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +message TunnelConfiguration { + optional string src_interface_id = 1; + optional string dst_interface_id = 2; + optional aalyria.spacetime.api.common.TunnelMethod method = 3; + + // Toporouter will produce a tunnel classifier based on L3 addresses of the + // provided interface IDs. Optionally, additional classification criteria can + // be provided here. These values are used as the bottom layer of a template, + // and then the L3 source/destination spec is overlaid. + optional aalyria.spacetime.api.common.FlowClassifier classifier = 4; + reserved 5 to max; +} + +// Describes a request to provision a network flow. +message ServiceRequest { + // Labels the network flow type in the NetOps UI. + optional string type = 13; // [(google.api.field_behavior) = OPTIONAL]; + + oneof src_type { + string src_node_id = 2; + string src_devices_in_region_id = 8; + } + + oneof dst_type { + string dst_node_id = 16; + string dst_devices_in_region_id = 18; + } + + + // By default, packets are classified as belonging to this provisioned flow if + // their IP source & destination address fields are within the subnet address + // ranges of the source and destination nodes chosen by toporouter from the + // source and destination elements specified above. This field allows a client + // to optionally specify a custom classifier. Custom classifiers defined in + // terms of nodes will have the node overwritten based on the solution route. + optional aalyria.spacetime.api.common.FlowClassifier classifier = 9; + + // Requests that the network flow traffic be tunnelled. + // TODO: Reconsider how to specify these. + repeated TunnelConfiguration tunnels = 10; + + // The priority field allows the requester to specify the order in which + // service requests are satisfied relative to other requests. + // A request will be treated as having a higher priority if the value of this + // field is greater than that of another service request. + // Users may want to directly map it to some utility metric of relevance to + // their network, such as estimated revenue, number of customers served, etc. + optional double priority = 6; + + message FlowRequirements { + reserved 4, 7 to max; // NEXT_ID: 7 + // The time interval over which these requirements are applicable. + optional aalyria.spacetime.api.common.TimeInterval time_interval = 1; + + // The minimum bandwidth to provision for the flow (optional), in layer 2 + // bits per second. If the minimum bandwidth cannot be achieved, the flow + // will not be provisioned at all. If the minimum bandwidth is not + // specified or is too low, the routing service will assume 100 bps. + optional double bandwidth_bps_minimum = 2; + + // Requested bandwidth to provision for the flow, in layer 2 bits/sec. + optional double bandwidth_bps_requested = 3; + + // The maximum allowed end-to-end latency for the flow (optional). + optional google.protobuf.Duration latency_maximum = 5; + + // Set to true if the network flow being requested may be stored and + // forwarded later — either at the source node or in transit along the + // transmission path using store-and-forward protocols. + // + // The flow is considered provisioned so long as data accumulating at + // ‘bandwidth_bps_minimum’ rate across the entirety of the requested time + // interval or planning horizon may be satisfied by on-path NetworkNodes' + // Storage.available_bytes. + optional bool is_disruption_tolerant = 6; + } + repeated FlowRequirements requirements = 4; + + // True while the Topology & Routing app observes routes installed that + // satisfy the bandwidth_bps_minimum capacity of the service request. + // Apps may watch this field to know when the provision request is satisfied. + optional bool is_provisioned_now = 5; + + // Chronological intervals during which the service request is considered to + // be provisioned. When the interval's start timestamp is in the future, the + // service request is considered to be scheduled such that the route + // provision will be satisfied beginning at that timestamp. + repeated google.type.Interval provisioned_intervals = 15; + + message IntentAndIntervals { + optional string intent_id = 1; + repeated google.type.Interval interval = 2; + } + + // An annotation indicating the intents that support this service request over + // time. + repeated IntentAndIntervals intent_dependencies = 14; + + // This should be set to true if the requestor wants to allow service + // fulfillment using partner resources. Defaults to false. + optional bool allow_partner_resources = 19; + + // The maximum budget allowed for this service request. + optional google.type.Money cost_per_minute_maximum = 20; + + reserved 1, 3, 7, 11 to 12, 17; + reserved 21 to max; // Next IDs. +} diff --git a/api/nbi/v1alpha/resources/wireless_evaluation.proto b/api/nbi/v1alpha/resources/wireless_evaluation.proto new file mode 100644 index 0000000..addca3f --- /dev/null +++ b/api/nbi/v1alpha/resources/wireless_evaluation.proto @@ -0,0 +1,138 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/common/coordinates.proto"; +import "api/common/wireless_transceiver.proto"; +import "api/nbi/v1alpha/resources/coverage.proto"; +import "api/nbi/v1alpha/resources/network_link.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha.resources"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +/* Messages that relate to how a wireless signal is propagated. */ + +// Models a transceiver with a specified motion. +// This can be used for one-off analyses to compute link budgets +// with the +// aalyria.spacetime.nbi.v1alpha.SignalPropagation service. +message TransceiverWithMotion { + optional aalyria.spacetime.api.common.TransceiverModel model = 1; + optional aalyria.spacetime.api.common.Motion motion = 2; +} + +// Models either a transceiver on an existing PlatformDefinition, +// or a transceiver with a specified motion for one-off +// analysis. This is used by the +// aalyria.spacetime.nbi.v1alpha.SignalPropagation service. +message TransceiverProvider { + oneof source { + aalyria.spacetime.api.common.TransceiverModelId id_in_store = 1; + TransceiverWithMotion definition = 2; + } +} + +// Explains the reason that a wireless link is inaccessible. +message NoAccessReason { + enum Constraint { + UNKNOWN_CONSTRAINT = 1; + + // Required separation from an azimuth/elevation mask. + // See `minimum_azimuth_elevation_mask_separation_deg` in + // aalyria.spacetime.api.common.AntennaConstraints. + SEPARATION_FROM_AZ_EL_MASK = 2; + + // Minimum and/or maximum link slant range. + // See `link_range` in + // aalyria.spacetime.api.common.AntennaConstraints. + LINK_RANGE = 3; + + // Field of view or regard. + // See `field_of_regard` in + // aalyria.spacetime.api.common.AntennaDefinition. + FIELD_OF_VIEW_OR_REGARD = 4; + + // Required angle between the sun's center of mass and the link vector. + // See `minimum_sun_angle_deg` in + // aalyria.spacetime.api.common.AntennaConstraints. + SUN_EXCLUSION = 5; + + // Something on the local platform is obstructing the link. + // See `obstructions` in + // aalyria.spacetime.api.common.AntennaDefinition. + PLATFORM_OBSTRUCTION = 6; + + // Terrain is obstructing the link vector. + TERRAIN_OBSTRUCTION = 7; + + // The modeled carrier-to-noise ratio at the receiver is too low. + RECEIVER_CNR_TOO_LOW = 8; + + // The Earth (or other planetary body) is obstructing the link. + NO_LINE_OF_SIGHT = 9; + } + optional Constraint constraint = 1; + + // Identifies the constrained link end. + optional aalyria.spacetime.api.common.TransceiverModelId + transceiver_link_end = 2; +} + +// Defines the predicted accessibility of the target. Modeled signal +// propagation information is included within accessible intervals. +message AccessInterval { + // The applicable time interval (may be open ended). + optional google.type.Interval interval = 1; + + // The predicted accessibility of the link target over the entire interval. + // A new AccessInterval is created for each change in modeled accessibility. + // Changes in the reason for inaccessibility also result in new intervals. + optional Accessibility accessibility = 2; + + // The set of reasons that the link is inaccessible over this interval. + repeated NoAccessReason no_access_reason = 3; + + // Modeled signal propagation, to coordinates of interest, over time. + // Timestamped entries are sorted in ascending order into the future. + // This field is omitted during inaccessible intervals. + repeated SpatialPropagation propagation_over_time = 4; +} + +// The modeled signal propagation across space at an instance in time. +message SpatialPropagation { + // Time as observed by the transmitter. + optional google.protobuf.Timestamp timestamp = 1; + + // Modeled signal propagation to the target. + optional WirelessLinkBudget reception = 2; + + // The link direction from transmitter towards the target. + optional aalyria.spacetime.api.common.PointingVector pointing_vector = 3; + + // The range to the target's coordinates, in meters. + optional double range_m = 4; + + // The electromagnetic propagation delay to target's coordinates. + optional google.protobuf.Duration propagation_delay = 5; + + // The spatial propagation to specified points of interest. + optional SignalPowerCoverage coverage = 6; +} + diff --git a/api/nbi/v1alpha/resources/wireless_interference.proto b/api/nbi/v1alpha/resources/wireless_interference.proto new file mode 100644 index 0000000..d383a04 --- /dev/null +++ b/api/nbi/v1alpha/resources/wireless_interference.proto @@ -0,0 +1,123 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha.resources; + +import "api/nbi/v1alpha/resources/coverage.proto"; + +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha/resources"; + +// The Loon SDN controller needs to understand several different types of +// interference avoidance criteria. The InterferenceConstraint message is used +// to describe these. Use cases include: +// - Loon's E-band interference avoidance. +// - Avoiding ground stations tracking HAPS or NGSOs from interfering with +// GSO victims; policy may require the whole GSO arc to be masked. +// - Avoiding ground stations tracking HAPS or NGSOs from interfering with other +// HAPS or NGSO victims. +// - Geofencing to limit spectrum occupancy within specific areas. +// +// Some users have policies that are based on simply avoiding pointing within +// certain angles of potential victims. Others have needs that require actual +// evaluation of the PFD versus a spectral mask at victim receivers. Advanced +// cases of dynamic spectrum access can require evaluation of effective power +// spectral density levels across a range of frequencies. +message InterferenceConstraint { + // Human-readable description of the purpose for this InterferenceConstraint. + optional string description = 1; + + // A set of coordinates for the possible victims of interference. This may be + // derived by referencing an array of transceivers, platforms, or a spatial + // surface region. + repeated CoordinateArray victims = 2; + + // The set of transceivers that must satisfy the interference constraint; + // this set may be filtered based on predicates, like transceiver location. + repeated StationSubset interferers = 3; + + // PfdConstraints can be checked against interference predictions using the + // structures in coverage.proto (e.g. PfdOverGeoArcCell, PfdOverS2Region, + // etc.), and PointingConstraints against TargetAcquisitionInfo in a potential + // network configuration plan phase. + oneof constraints { + PfdConstraints pfd_constraints = 4; + // Effective PFD should be evaluated by including the receiver model. This + // allows users to put in the reference receivers and EPFD levels from ITU + // regulations. + PfdConstraints epfd_constraints = 5; + // Aggregate PFD is evaluated against the sum of PFDs. + PfdConstraints apfd_constraints = 6; + // Describes underlay mask for power spectral density. + PsdConstraint psd_constraint = 7; + // Describes a simple pointing mask angle. + PointingConstraint pointing_constraint = 8; + } + + // Constraints are generally evaluated without regard to the relative + // direction of the receiver, since this is not generally known or able to be + // used for satellite cases. However for system self-interference, or other + // advanced dynamic spectrum access coordination, it may be included. + // If not included, false is assumed. + optional bool use_receiver_orientation = 9; +} + +message PfdConstraint { + // The band definition matches IEEE 1900.5.2. + message Band { + required double start_frequency_mhz = 1; // in MHz. + required double end_frequency_mhz = 2; // in MHz. + } + optional Band band = 1; + // Power Flux Density (PFD) computed in dBW / m^2 + optional double received_pfd_dbw_per_sqm = 2; + optional double resolution_bw_mhz = 3; // in MHz. + + optional double time_fraction = 4; // Should be 0..1. +} + +message PfdConstraints { + repeated PfdConstraint pfd_constraints = 1; +} + +// The PsdConstraint relates to the basic usage of a reference power and +// underlay mask in an IEEE 1900.5.2 RxModel. It does not reflect several +// unneeded parts of IEEE 1900.5.2, like hopping model or "confidence" levels. +message PsdConstraint { + // The ScmMask definition is from IEEE 1900.5.2. + // SCM = Spectrum Consumption Message, used elsewhere in IEEE 1900.5.2. + message ScmMask { + // The reference frequency is typically the center frequency of the mask. + required double ref_frequency_mhz = 1; + // Control points should be ordered in non-decreasing frequency order. + repeated ControlPoint control_points = 2; // At least 2 should be present. + } + message ControlPoint { + // Frequency of control points is in MHz delta from the reference frequency. + required double frequency_mhz = 1; + // relative_power is in dB from the reference power. + required double relative_power_db = 2; + } + // Reference power is in dBW per IEEE 1900.5.2. + optional double reference_power_dbw = 1; + optional ScmMask psd_mask = 2; + // Resolution bandwidth is in MHz per IEEE 1900.5.2. + optional double resolution_bw_mhz = 3; +} + +message PointingConstraint { + // Minimum angle in degrees allowed between a beam center and victim. + optional double min_angle_deg = 1; +} diff --git a/api/nbi/v1alpha/signal_propagation.proto b/api/nbi/v1alpha/signal_propagation.proto new file mode 100644 index 0000000..00165f9 --- /dev/null +++ b/api/nbi/v1alpha/signal_propagation.proto @@ -0,0 +1,103 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha; + +import "api/common/wireless_transceiver.proto"; +import "api/nbi/v1alpha/resources/coverage.proto"; +import "api/nbi/v1alpha/resources/wireless_evaluation.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha"; + +// A service that evaluates the time-dynamic geometric accessibility and +// coverage of a wireless signal propagated from a transmitter towards +// some target location. +service SignalPropagation { + // Returns the link budget during the intervals in which this link + // is accessible. + rpc Evaluate(SignalPropagationRequest) returns (SignalPropagationResponse) {} +} + +message SignalPropagationRequest { + // Identifies the transmitting transceiver model. + optional aalyria.spacetime.api.nbi.v1alpha.resources.TransceiverProvider + transmitter_model = 1; + + // This string should correspond to the `id` field of an + // Entity message that contains a aalyria.spacetime.api.common.BandProfile + // message. + optional string band_profile_id = 2; + + // Identifies the time-dynamic target of a steerable beam. + // This field may only be omitted if the beam is fixed / non-steerable. + // If this field is omitted, the `accessibility` field in each + // `access_interval` within the SignalPropagationResponse will be + // omitted, and the access interval will match the request interval. + // Only coverage calculations will be completed. The link budget from + // the transmitter to target will not be computed. + optional aalyria.spacetime.api.nbi.v1alpha.resources.TransceiverProvider + target = 3; + + // This field is useful for interference analysis. + // Omitting this field causes 'coverage' to be omitted from the response. + optional aalyria.spacetime.api.nbi.v1alpha.resources.CoordinateArray + coverage = 4; + + oneof analysis_time { + // The interval of time to evaluate signal propagation. This must be a + // closed interval with both a start and end time. + google.type.Interval analysis_interval = 6; + + // The instant in time to evaluate signal propagation. + google.protobuf.Timestamp analysis_instant = 10; + } + + // Sets the analysis step size and the temporal resolution of the response. + // This step size determines the rate at which constraints of link + // accessibility (such as the link being obstructed by terrain) are + // sampled with respect to time. + optional google.protobuf.Duration step_size = 7; + + // Sets the analysis step size for spatial propagation metrics. + // This step size determines the rate at which propagation loss + // metrics are sampled in order to compute the access intervals in + // the response. If the step is set to 0, then spatial propagation + // will not be calculated. + optional google.protobuf.Duration spatial_propagation_step_size = 9; + + // If explain_inaccessibility is true, the server will spend additional + // computational time determining the specific set of access constraints + // that were not satisfied and including these reasons in the response. + // Set this to false to optimize performance. + optional bool explain_inaccessibility = 8; + + // If past/historic reference data should be used (e.g. platform motion, + // weather, etc) rather than current / most-recent data, this + // field indicates the time for retrieving that reference data. + optional google.protobuf.Timestamp reference_data_time = 11; +} + +message SignalPropagationResponse { + // A timeline of target accessibility (subject to geometric constraints). + // Accessible intervals contain the modeled temporospatial signal propagation. + // Access intervals are non-overlapping. + repeated aalyria.spacetime.api.nbi.v1alpha.resources.AccessInterval + access_interval = 1; +} diff --git a/api/nbi/v1alpha/txtpb_entities.proto b/api/nbi/v1alpha/txtpb_entities.proto new file mode 100644 index 0000000..4c015f5 --- /dev/null +++ b/api/nbi/v1alpha/txtpb_entities.proto @@ -0,0 +1,31 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.nbi.v1alpha; + +import "api/nbi/v1alpha/nbi.proto"; + +option java_package = "com.aalyria.spacetime.api.nbi.v1alpha"; +option go_package = "aalyria.com/spacetime/api/nbi/v1alpha"; + +// Protobuf message for storing a collection of Entities in text-format (.txtpb) +// This format should be preferred by end-user tools that deal with text-format, +// like the nbictl CLI. +message TxtpbEntities { + // Use singular name `entity` to improve text-format UX, + // despite the protobuf convention of pluralizing repeated field-names. + repeated Entity entity = 1; +} \ No newline at end of file diff --git a/api/resources/BUILD b/api/resources/BUILD new file mode 100644 index 0000000..a5ff796 --- /dev/null +++ b/api/resources/BUILD @@ -0,0 +1,15 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exports_files(["html.tmpl"]) diff --git a/api/resources/html.tmpl b/api/resources/html.tmpl new file mode 100644 index 0000000..0238534 --- /dev/null +++ b/api/resources/html.tmpl @@ -0,0 +1,440 @@ + + + + + + + Protocol Documentation + + + + + + + + + + +

Protocol Documentation

+ +

Table of Contents

+ +
+ +
+ + {{range .Files}} + {{$file_name := .Name}} +
+

{{.Name}}

Top +
+

{{.Description}}

+ + {{range .Messages}} +

{{.LongName}}

+

{{.Description}}

+ + {{if .HasFields}} + + + + + + {{range .Fields}} + + + + + + + {{end}} + +
FieldTypeLabelDescription
{{.Name}}{{.LongType}}{{.Label}}

{{if (index .Options "deprecated"|default false)}}Deprecated. {{end}}{{.Description}} {{if .DefaultValue}}Default: {{.DefaultValue}}{{end}}

+ + {{$message := .}} + {{- range .FieldOptions}} + {{$option := .}} + {{if eq . "validator.field" "validate.rules" }} +

Validated Fields

+ + + + + + + + + {{range $message.FieldsWithOption .}} + + + + + {{end}} + +
FieldValidations
{{.Name}} +
    + {{range (.Option $option).Rules}} +
  • {{.Name}}: {{.Value}}
  • + {{end}} +
+
+ {{else}} +

Fields with {{.}} option

+ + + + + + + + + {{range $message.FieldsWithOption .}} + + + + + {{end}} + +
NameOption
{{.Name}}

{{ printf "%+v" (.Option $option)}}

+ {{end}} + {{end -}} + {{end}} + + {{if .HasExtensions}} +
+ + + + + + {{range .Extensions}} + + + + + + + + {{end}} + +
ExtensionTypeBaseNumberDescription
{{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

{{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

+ {{end}} + {{end}} + + {{range .Enums}} +

{{.LongName}}

+

{{.Description}}

+ + + + + + {{range .Values}} + + + + + + {{end}} + +
NameNumberDescription
{{.Name}}{{.Number}}

{{.Description}}

+ {{end}} + + {{if .HasExtensions}} +

File-level Extensions

+ + + + + + {{range .Extensions}} + + + + + + + + {{end}} + +
ExtensionTypeBaseNumberDescription
{{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

{{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

+ {{end}} + + {{range .Services}} +

{{.Name}}

+

{{.Description}}

+ + + + + + {{range .Methods}} + + + + + + + {{end}} + +
Method NameRequest TypeResponse TypeDescription
{{.Name}}{{.RequestLongType}}{{if .RequestStreaming}} stream{{end}}{{.ResponseLongType}}{{if .ResponseStreaming}} stream{{end}}

{{.Description}}

+ + {{$service := .}} + {{- range .MethodOptions}} + {{$option := .}} + {{if eq . "google.api.http"}} +

Methods with HTTP bindings

+ + + + + + + + + + + {{range $service.MethodsWithOption .}} + {{$name := .Name}} + {{range (.Option $option).Rules}} + + + + + + + {{end}} + {{end}} + +
Method NameMethodPatternBody
{{$name}}{{.Method}}{{.Pattern}}{{.Body}}
+ {{else}} +

Methods with {{.}} option

+ + + + + + + + + {{range $service.MethodsWithOption .}} + + + + + {{end}} + +
Method NameOption
{{.Name}}

{{ printf "%+v" (.Option $option)}}

+ {{end}} + {{end -}} + {{end}} + {{end}} + +

Scalar Value Types

+ + + + + + {{range .Scalars}} + + + + + + + + + + + + {{end}} + +
.proto TypeNotesC++JavaPythonGoC#PHPRuby
{{.ProtoType}}{{.Notes}}{{.CppType}}{{.JavaType}}{{.PythonType}}{{.GoType}}{{.CSharp}}{{.PhpType}}{{.RubyType}}
+ + diff --git a/api/scheduling/v1alpha/BUILD b/api/scheduling/v1alpha/BUILD new file mode 100644 index 0000000..d098348 --- /dev/null +++ b/api/scheduling/v1alpha/BUILD @@ -0,0 +1,68 @@ +# Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_grpc_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "scheduling_proto", + srcs = [ + "scheduling.proto", + ], + deps = [ + "@googleapis//google/rpc:status_proto", + "@protobuf//:empty_proto", + "@protobuf//:field_mask_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_grpc_library( + name = "scheduling_cpp_grpc", + protos = [":scheduling_proto"], + deps = [ + "@googleapis//google/rpc:status_cc_proto", + ], +) + +go_proto_library( + name = "scheduling_go_grpc", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/scheduling/v1alpha", + proto = ":scheduling_proto", + deps = [ + "@org_golang_google_genproto_googleapis_rpc//status", + ], +) + +java_grpc_library( + name = "scheduling_java_grpc", + protos = [":scheduling_proto"], + deps = [ + "@googleapis//google/rpc:rpc_java_proto", + ], +) + +python_grpc_library( + name = "scheduling_python_grpc", + protos = [":scheduling_proto"], +) diff --git a/api/scheduling/v1alpha/scheduling.proto b/api/scheduling/v1alpha/scheduling.proto new file mode 100644 index 0000000..175ffb1 --- /dev/null +++ b/api/scheduling/v1alpha/scheduling.proto @@ -0,0 +1,411 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package aalyria.spacetime.scheduling.v1alpha; + +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +option go_package = "aalyria.com/spacetime/api/scheduling/v1alpha"; +option java_package = "com.aalyria.spacetime.scheduling.v1alpha"; + +service Scheduling { + // Establishes a bidirectional stream through which the SDN controller may + // send scheduling requests to the SDN agent, and the agent may respond. + // + // The use of the stream allows the SDN controller to send requests to the + // agent even though the SDN controller may not be able to initiate a + // connection to the agent (if the agent is behind a firewall, for example, + // or a NAT gateway). + // + // The SDN controller will leave the stream open indefinitely. In the event + // that the stream closes, the agent should re-establish the stream to + // continue receiving requests. + rpc ReceiveRequests(stream ReceiveRequestsMessageToController) + returns (stream ReceiveRequestsMessageFromController) {} + + + // Notifies the SDN controller that an agent's schedule has been reset. The + // agent must call this upon startup and after any event that has reset the + // schedule. + rpc Reset(ResetRequest) returns (google.protobuf.Empty) {} +} + +// The message type for Scheduling.ReceiveRequests in the direction toward the +// controller. +message ReceiveRequestsMessageToController { + message Hello { + // Required. Identifies the SDN agent whose schedule is to be managed by + // this scheduling session. + string agent_id = 1; + } + + message Response { + // Required. The ID of the request to which this response corresponds. See + // ReceiveRequestsMessageFromController.request_id. + int64 request_id = 1; + + // The response status. + google.rpc.Status status = 2; + } + + // Required in the initial request of the session. Identifies the SDN agent + // and the properties of the underlying channel. + Hello hello = 1; + + // A response to a request received from the SDN controller. + // + // The response may originate from a proxy rather than the SDN agent itself. + // For example, if the proxy is unable to deliver the request to the SDN + // agent, the proxy may populate the response with an appropriate error. + Response response = 2; +} + +// The message type for Scheduling.ReceiveRequests in the direction from the +// controller. +// +// This is a response message in that it flows from server to client, however +// it holds requests being sent to the SDN agent (the client) by the SDN +// controller (the server). +message ReceiveRequestsMessageFromController { + // An SDN-controller-generated value uniquely identifying the request within + // the scope of the scheduling session. That is, two requests received from + // the same session will always have different request IDs. The SDN agent + // must provide the ID in the response to this request (see + // ReceiveRequestsMessageToController.Response.request_id). + int64 request_id = 1; + + // The contents of the request. + oneof request { + // Creates an entry in the agent's schedule. Note that entries are + // immutable: once created, an entry may not changed, though it may be + // deleted. + CreateEntryRequest create_entry = 2; + + // Deletes an entry from the agent's schedule. + DeleteEntryRequest delete_entry = 3; + + // Finalizes all schedule entries earlier than a specified time. + // + // Notifies the agent that all aspects of its schedule pertaining to the + // interval prior to a given instant will no longer be modified . This frees + // the agent to garbage-collect all entries scheduled for before that + // instant. + FinalizeRequest finalize = 4; + } +} + +message CreateEntryRequest { + // Required. A token that must match the agent's token for the request to be + // accepted (see ResetRequest.schedule_manipulation_token). This ensures that + // the agent does not execute an operation intended for an old edition of the + // schedule on a newer version. + string schedule_manipulation_token = 1; + + // Required. The request's sequence number among all requests in the + // Scheduling service with the same schedule manipulation token. The sequence + // number resets with each new schedule manipulation token. + uint64 seqno = 2; + + // Required. A unique identifier of the schedule entry being created. + string id = 3; + + // Required. The time at which the entry is scheduled to be executed. + google.protobuf.Timestamp time = 4; + + oneof configuration_change { + UpdateBeam update_beam = 15; + DeleteBeam delete_beam = 12; + SetRoute set_route = 5; + DeleteRoute delete_route = 6; + SetSrPolicy set_sr_policy = 9; + DeleteSrPolicy delete_sr_policy = 10; + } + + reserved 7, 8, 11, 13, 14; +} + +message SetRoute { + // The source prefix of the route. + // + // An IP or IPv6 address optionally followed by a slash and the prefix + // length. + string from = 1; + + // The destination prefix of the route. + // + // An IP or IPv6 address optionally followed by a slash and the prefix + // length. + string to = 2; + + // The output device name. + string dev = 3; + + // The address of the nexthop router. + // + // An IP or IPv6 address. + string via = 4; +} + +message DeleteRoute { + // The source prefix of the route. + // + // An IP or IPv6 address optionally followed by a slash and the prefix + // length. + string from = 1; + + // The destination prefix of the route. + // + // An IP or IPv6 address optionally followed by a slash and the prefix + // length. + string to = 2; +} + +message Beam { + reserved 7; + + string id = 1; + + // Identifies the pointing target for the beam to be aimed at & track. + BeamTarget target = 2; + + // Identifies the antenna that is used to transmit and/or receive the beam. + string antenna_id = 9; + + // Parameters specifying the configuration of the beam in the receive + // direction, keyed by arbitrary IDs. + // Empty if the beam is to be used for transmit only. + map rxs = 3; + + // Parameters specifying the configuration of the beam in the transmit + // direction, keyed by arbitrary IDs. + // Empty if the beam is to be used for receiving only. + map txs = 4; + + // The collection of endpoints served by the beam. + // For example, a collection of user terminals (UTs) served. + // + // Each key is the ID of the endpoint's network node in Spacetime's network + // model. + map endpoints = 5; + + // ID of the beam shape. + string shape_id = 6; + + // Only present if beam-hopping is to be used. + BeamHoppingPlan beam_hopping_plan = 8; +} + +message BeamHoppingPlan { + message Section { + // The length of the section, in number of time slots. + int32 length = 1; + + // An entry in the beam-hopping plan section representing a time slot during + // which the associated beam is to be illuminated. + message Entry { + // The entry's time slot within the section. + int32 time_slot = 1; + + string modem_id = 2; + } + + // The entries in the beam-hopping plan section. The beam should not be + // illuminated during any time slot not represented in the collection of + // entries. + repeated Entry entries = 2; + } + + repeated Section sections = 1; +} + +message UpdateBeam { + Beam beam = 1; + + google.protobuf.FieldMask update_mask = 2; +} + +message DeleteBeam { + string id = 1; +} + +message BeamTarget { + oneof target { + Ecef ecef = 1; + string ccsds_oem_file_content = 2; + AzEl az_el = 3; + } +} + +message Ecef { + int64 x_m = 1; + int64 y_m = 2; + int64 z_m = 3; +} + +// An azimuth and elevation angle pair. +// +// The azimuth angle of a vector is the angle between the x-axis and the +// orthogonal projection of the vector onto the xy plane. The angle is positive +// in going from the x axis toward the y axis. The elevation angle is the angle +// between the vector and its orthogonal projection onto the xy-plane. +message AzEl { + // Azimuth, in degrees. + double az_deg = 1; + + // Elevation, in degrees. + double el_deg = 2; +} + +message RxConfiguration { + uint64 center_frequency_hz = 1; + uint64 channel_bandwidth_hz = 2; + Polarization polarization = 3; + + // Symbol rate in Megasymbols per second. + double symbol_rate_msps = 5; + + // Identifies the receive modem associated with the carrier on this beam. + // Must be present if beam-hopping is not used, otherwise it should be + // empty and the modem IDs are found via the beam hopping plan. + string modem_id = 6; + + ModemMode mode = 4; +} + +message TxConfiguration { + uint64 center_frequency_hz = 1; + uint64 channel_bandwidth_hz = 2; + Polarization polarization = 3; + + // Transmit power in Watts. + double power_w = 5; + + // Symbol rate in Megasymbols per second. + double symbol_rate_msps = 6; + + // Identifies the transmit modem associated with the carrier on this beam. + // Must be present if beam-hopping is not used, otherwise it should be + // empty and the modem IDs are found via the beam hopping plan. + string modem_id = 8; + + ModemMode mode = 4; + + string initial_modcod = 7; +} + +enum Polarization { + POLARIZATION_UNSPECIFIED = 0; + POLARIZATION_LHCP = 1; // Left-handed circular polarization + POLARIZATION_RHCP = 2; // Right-handed circular polarization +} + +enum ModemMode { + MODEM_MODE_UNSPECIFIED = 0; + MODEM_MODE_DVB_RCS2 = 1; + MODEM_MODE_DVB_S2X = 2; +} + +message Endpoint { + string lowest_supported_rx_modcod = 2; + double rx_reference_throughput_bps = 3; + double tx_reference_throughput_bps = 4; + + reserved 1; +} + +// A Segment Routing Policy Architecture policy description. +// Based in part upon elements from: +// * RFC 9256 +// * draft-ietf-idr-sr-policy-safi +// * draft-ietf-idr-bgp-sr-segtypes-ext +// * draft-ietf-spring-sr-policy-yang +message SetSrPolicy { + // The name of this SR policy, unique among all SR policies, and + // matching the name given in the `ServiceRequest` that caused + // this policy to be instantiated. + string name = 1; + + repeated ExplicitPath paths = 2; + + message ExplicitPath { + // Assigning meaning to color can be used to indicate alternate + // paths (backup paths of various types). + uint32 color = 1; + + // Within explicit paths of the same color, the weight to be + // given to this specific segment list (for multipathing). + uint32 weight = 2; + + // An ordered list of segments from ingress to egress. The first + // segment may be used to indicate the first hop and therefore + // need not be encoded in any dataplane encapsulation. + repeated SrSegment segments = 3; + } +} + +message SrSegment { + oneof type { + uint32 mpls = 1; // "Type A"; RFC 8294 mpls-label + } +} + +message DeleteSrPolicy { + string name = 1; +} + +message DeleteEntryRequest { + // Required. A token that must match the agent's token for the request to be + // accepted (see ResetRequest.schedule_manipulation_token). This ensures that + // the agent does not execute an operation intended for an old edition of the + // schedule on a newer version. + string schedule_manipulation_token = 1; + + // Required. The request's sequence number among all requests in the + // Scheduling service with the same schedule manipulation token. The sequence + // number resets with each new schedule manipulation token. + uint64 seqno = 2; + + // Required. The schedule entry to delete. + string id = 3; +} + +message FinalizeRequest { + // Required. A token that must match the agent's token for the request to be + // accepted (see ResetRequest.schedule_manipulation_token). This ensures that + // the agent does not execute an operation intended for an old edition of the + // schedule on a newer version. + string schedule_manipulation_token = 1; + + // Required. The request's sequence number among all requests in the + // Scheduling service with the same schedule manipulation token. The sequence + // number resets with each new schedule manipulation token. + uint64 seqno = 2; + + // Required. The time before which the schedule will no longer be modified. + google.protobuf.Timestamp up_to = 3; +} + +message ResetRequest { + // Required. Identifies the agent whose schedule has been reset. + string agent_id = 1; + + // Required. The new schedule's manipulation token. Only requests annotated + // with a matching token should be accepted. + string schedule_manipulation_token = 2; +} diff --git a/api/solver/v1alpha/BUILD b/api/solver/v1alpha/BUILD new file mode 100644 index 0000000..facf2d5 --- /dev/null +++ b/api/solver/v1alpha/BUILD @@ -0,0 +1,92 @@ +# Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library", "cpp_proto_library") + +proto_library( + name = "solver_proto", + srcs = ["solver.proto"], + visibility = ["//visibility:public"], + deps = [ + "//api/nbi/v1alpha/resources:resources_proto", + "@googleapis//google/type:interval_proto", + ], +) + +go_proto_library( + name = "solver_go_proto", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/solver/v1alpha", + proto = ":solver_proto", + visibility = ["//visibility:public"], + deps = [ + "//api/nbi/v1alpha/resources:nbi_resources_go_grpc", + "@org_golang_google_genproto//googleapis/type/interval", + ], +) + +proto_library( + name = "beam_hopping_proto", + srcs = ["beam_hopping.proto"], + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_proto", + "//api/scheduling/v1alpha:scheduling_proto", + "//api/telemetry:telemetry_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_grpc_library( + name = "beam_hopping_cpp_grpc", + protos = [":beam_hopping_proto"], + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_cpp_proto", + "//api/scheduling/v1alpha:scheduling_cpp_grpc", + "//api/telemetry:telemetry_cpp_grpc", + "@googleapis//google/rpc:status_cc_proto", + ], +) + +cpp_proto_library( + name = "beam_hopping_cpp_proto", + protos = ["beam_hopping_proto"], + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_cpp_proto", + "//api/solver/v1alpha:beam_hopping_cpp_grpc", + ], +) + +go_proto_library( + name = "beam_hopping_go_grpc", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/api/beam_hopping/v1alpha", + proto = ":beam_hopping_proto", + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_go_proto", + "//api/scheduling/v1alpha:scheduling_go_grpc", + "//api/telemetry:telemetry_go_grpc", + ], +) diff --git a/api/solver/v1alpha/beam_hopping.proto b/api/solver/v1alpha/beam_hopping.proto new file mode 100644 index 0000000..ddbd990 --- /dev/null +++ b/api/solver/v1alpha/beam_hopping.proto @@ -0,0 +1,153 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file defines the BeamHoppingSolver service, which allows clients to +// query for real-time, dynamically computed, beam hopping schedules for a +// satellite payload. + +// The BeamHoppingSolver is a stateless service. Clients are expected to +// include all network state necessary for the computation of a satellite's +// beam hopping schedule. This includes user link signal quality telemetry, +// user demand telemetry, user CIR and EIR, modem bank configuration, etc. + +// WARNING: This proto is under development and may be subject to change +// in the future. +syntax = "proto3"; + +package aalyria.spacetime.api.beam_hopping.v1alpha; + +import "api/common/network.proto"; +import "api/scheduling/v1alpha/scheduling.proto"; +import "api/telemetry/telemetry.proto"; +import "google/protobuf/timestamp.proto"; + +option java_package = "com.aalyria.spacetime.api.beam_hopping.v1alpha"; +option go_package = "aalyria.com/spacetime/api/beam_hopping/v1alpha"; + +service BeamHoppingSolver { + // Issues a request to compute an optimal schedule of satellite beam + // dwell times, taking into account real time link quality and user + // demand. + rpc GetBeamHoppingPlans(GetBeamHoppingPlansRequest) + returns (GetBeamHoppingPlansResponse) {} +} + +// The request for aalyria.spacetime.api.beam_hopping.v1alpha.BeamHoppingSolver +// .GetBeamHoppingPlans +message GetBeamHoppingPlansRequest { + // Required. Identifies the satellite node whose beam hopping plans are + // to be computed. + string node_id = 1; + + enum Direction { + DIRECTION_UNSPECIFIED = 0; + DIRECTION_DOWNLINK = 1; + DIRECTION_UPLINK = 2; + } + + // Required. The connectivity direction of the beam hopping plan. + Direction direction = 2; + + // Models a satellite user link beam and its associated real-time state. + message UserLinkBeam { + // Required. Uniquely identifies a specific beam on the satellite payload. + string beam_id = 1; + + // Required. Identifies the beam's assigned modem bank. Note that if the + // modem bank contains multiple modems, the beam may utilize more than one + // modem over the duration of its beam hopping plan. + string modem_bank_id = 2; + + // Required. A list of recent modem metrics between the beam and its users. + // This field is used to derive the beam's total capacity. The client must + // include at least one data point for each beam to user link. + repeated aalyria.spacetime.telemetry.v1alpha.ModemMetrics modem_metrics = 3; + + // Models a collection of user demand metrics for a user connected to + // the beam. + message UserDemandMetrics { + // Required. Identifies the modem of the user. + string modem_id = 1; + + message UserDemandDataPoint { + // Required. When the value was captured. + google.protobuf.Timestamp time = 1; + + // Required. The demanded transmit data rate for the user, in bits per + // second. + double tx_data_rate_bps = 2; + + // Required. The demanded receive data rate for the user, in bits per + // second. + double rx_data_rate_bps = 3; + } + // Required. Datapoints describing the time varying values of the user's + // demand data rates. + repeated UserDemandDataPoint user_demand_data_points = 2; + } + + // Required. A list of recent demand metrics for the users being served + // by the beam. The field is used to derive the beam's total demand. The + // client must include at least one data point for each user. + repeated UserDemandMetrics user_demand_metrics = 4; + + // Models a user being served by the beam. + message Endpoint { + // Required. Identifies the user's modem that is connected to the beam. + string modem_id = 1; + + // Required. The committed information rate for the user. + // The beam hopping solver will attempt to allocate sufficient capacity + // to meet the minimum of the user's real-time demand and their CIR. + double cir_data_rate_bps = 2; + + // Required. The excess information rate for the user. + // The beam hopping solver will, when possible, attempt to allocate + // sufficient capacity to meet the minimum of the user's real-time + // demand and their EIR. + double eir_data_rate_bps = 3; + } + repeated Endpoint endpoints = 5; + } + repeated UserLinkBeam user_link_beams = 3; + + // Models a collection of modems on a satellite that operate in tandem. + message ModemBank{ + // Required. A unique identifier for the modem bank. + string id = 1; + + // Required. The list of modem IDs included in the bank. Note that if a + // modem bank consists of multiple modems, the user link beams assigned to + // it may be redistributed across modems within the bank, over the duration + // of the computed plan. If this behavior is undesired, clients should + // limit this field to a single modem. + repeated string modem_ids = 2; + } + repeated ModemBank modem_banks = 4; + +} + +message BeamIdAndHoppingPlan { + string beam_id = 1; + aalyria.spacetime.scheduling.v1alpha.BeamHoppingPlan beam_hopping_plan = 2; +} + +// The response for aalyria.spacetime.api.beam_hopping.v1alpha +// .BeamHoppingSolver.GetBeamHoppingPlans +message GetBeamHoppingPlansResponse { + // A list of the computed beam hopping plans. + repeated BeamIdAndHoppingPlan beam_hopping_plans = 2; + + reserved 1; +} \ No newline at end of file diff --git a/api/solver/v1alpha/solver.proto b/api/solver/v1alpha/solver.proto new file mode 100644 index 0000000..4e33009 --- /dev/null +++ b/api/solver/v1alpha/solver.proto @@ -0,0 +1,139 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file defines the SolutionIngester service, which allows clients to solve +// the allocation of system and radio resources within the network instead of +// Spacetime's native solving engine. +// +// Clients should use the Northbound interface's +// aalyria.spacetime.api.nbi.v1alpha.NetOps RPCs to +// fetch the state of the network and maintain an up-to-date view of the +// network's resources and the requests for connectivity across the network. +// TODO: Add a tailored RPC to this service to allow clients to stream +// the state of the network. +// +// Client are expected to analyze the wireless signal propagation from the +// devices in the network and accordingly, compute an allocation of system +// resources (e.g. assigning satellites to ground stations, user terminals to +// satellites, ISL resources, transmit power levels) and radio resources (e.g. +// assigning carriers to links, beam hopping plans). The RPCs in the +// SolutionIngester service allow clients to send these allocations to +// Spacetime, and in turn, Spacetime will distribute messages to each network +// element across the Southbound interface in order to configure the resources +// in the network. +// +// Specifically, clients should query the following entity types to fetch the +// state of the network: PLATFORM_DEFINITION, NETWORK_NODE, BAND_PROFILE, +// ANTENNA_PATTERN, INTERFACE_LINK_REPORT, SERVICE_REQUEST. +// (NETWORK_STATS_REPORT entities can also optionally be fetched when resource +// allocation calculations consider network telemetry. INTENT entities can also +// optionally be fetched when calculations consider the current allocation of +// resources in the network.) +// Spacetime uses INTENT entities to describe intended changes to the network, +// so each resource allocation is defined by an INTENT entity. + +syntax = "proto3"; + +package aalyria.spacetime.api.solver.v1alpha; + +import "api/nbi/v1alpha/resources/intent.proto"; +import "google/type/interval.proto"; + +option java_package = "com.aalyria.spacetime.api.solver.v1alpha"; +option go_package = "aalyria.com/spacetime/api/solver/v1alpha"; + +// Used to represent the requests for connectivity across the network that each +// resource allocation serves to fulfill over time. The requests for +// connectivity are represented as SERVICE_REQUEST entities in Spacetime's data +// model. +message ServiceRequestAndIntervals { + // A SERVICE_REQUEST entity's ID. + // Required. + string service_request_id = 1; + + // Required. + repeated google.type.Interval intervals = 2; +} + +// SystemAndRadioResource and Path messages should be treated as being +// immutable. To modify a previously submitted resource, clients should submit +// the updated resources, each with a unique ID, in a new request with the same +// ApplySystemAndRadioResourceAllocationRequest.interval. This will delete the +// previous allocation (e.g. the corresponding INTENT entities in Spacetime) and +// submit the updated version. + +// Models a list of system and radio resource allocations. +// When submitted to Spacetime, each SystemAndRadioResource is used to +// initialize an INTENT entity, and its ID is set to a UUID. +message SystemAndRadioResource { + // The interval over which this resource allocation exists. + // Required. + google.type.Interval interval = 1; + + // Defines the resource allocation. + // Required. + aalyria.spacetime.api.nbi.v1alpha.resources.ModemIntent resource = 2; + + // The requests for service that this resource serves to fulfill over time. + // Required. + repeated ServiceRequestAndIntervals dependent_service_requests = 3; + + // TODO: Consider refactoring this message into separate + // SystemResource and RadioResource messages to more clearly delineate the + // differences between the two resource types. +} + +// Models a list of paths that represent routing solutions to the network flows +// specified by SERVICE_REQUEST entities. +// When submitted to Spacetime, each Path is used to initialize an INTENT +// entity, and its ID is set to a UUID. +message Path { + // The interval over which this path exists. + // Required. + google.type.Interval interval = 1; + + // Defines the path. + // Required. + aalyria.spacetime.api.nbi.v1alpha.resources.PathIntent path = 2; + + // The requests for service that this path serves to fulfill over time. + // Required. + repeated ServiceRequestAndIntervals dependent_service_requests = 3; +} + +message ApplySystemAndRadioResourceAllocationRequest { + // The total interval covered by the resource allocation plan in the request. + // Any previous resource allocations for this interval will be deleted. + // Required. + google.type.Interval interval = 1; + + // The allocation of system and radio resources. + repeated SystemAndRadioResource system_and_radio_resources = 2; + + // The network paths that correspond to the resource allocations. + repeated Path paths = 3; +} + +message ApplySystemAndRadioResourceAllocationResponse { + // The RPC will succeed when all resource allocations were successfully + // written to Spacetime. Otherwise, the RPC will fail with an error. +} + +service SolutionIngester { + // Issues a request to submit the allocation of system and radio resources + // over an interval. + rpc ApplySystemAndRadioResourceAllocation( + ApplySystemAndRadioResourceAllocationRequest) returns + (ApplySystemAndRadioResourceAllocationResponse) {} +} \ No newline at end of file diff --git a/api/telemetry/BUILD b/api/telemetry/BUILD new file mode 100644 index 0000000..a3d7153 --- /dev/null +++ b/api/telemetry/BUILD @@ -0,0 +1,60 @@ +# Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_grpc_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_grpc_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_grpc_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "telemetry_proto", + srcs = [ + "telemetry.proto", + ], + deps = [ + "@protobuf//:empty_proto", + "@protobuf//:timestamp_proto", + ], +) + +cpp_grpc_library( + name = "telemetry_cpp_grpc", + protos = [":telemetry_proto"], +) + +go_proto_library( + name = "telemetry_go_grpc", + compilers = [ + "@rules_go//proto:go_proto", + "@rules_go//proto:go_grpc_v2", + ], + importpath = "aalyria.com/spacetime/telemetry/v1alpha", + proto = ":telemetry_proto", +) + +java_grpc_library( + name = "telemetry_java_grpc", + protos = [":telemetry_proto"], + deps = [ + "@googleapis//google/rpc:rpc_java_proto", + ], +) + +python_grpc_library( + name = "telemetry_python_grpc", + protos = [":telemetry_proto"], +) diff --git a/api/telemetry/telemetry.proto b/api/telemetry/telemetry.proto new file mode 100644 index 0000000..8f972f3 --- /dev/null +++ b/api/telemetry/telemetry.proto @@ -0,0 +1,298 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package aalyria.spacetime.telemetry.v1alpha; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "aalyria.com/spacetime/telemetry/v1alpha"; +option java_package = "com.aalyria.spacetime.telemetry.v1alpha"; + +service Telemetry { + // Pushes metrics to the SDN controller. + rpc ExportMetrics(ExportMetricsRequest) returns (google.protobuf.Empty) {} +} + +// The request for aalyria.spacetime.telemetry.v1alpha.Telemetry.ExportMetrics. +message ExportMetricsRequest { + repeated InterfaceMetrics interface_metrics = 1; + repeated ModemMetrics modem_metrics = 2; +} + +// A collection of metrics from a network interface. +message InterfaceMetrics { + // Required. The interface producing telemetry. + string interface_id = 1; + + // Data points describing the time-varying value of the interface's + // operational state. + repeated IfOperStatusDataPoint operational_state_data_points = 2; + + // Data points describing the time-varying values of the interface's standard + // statistics. + repeated StandardInterfaceStatisticsDataPoint + standard_interface_statistics_data_points = 3; +} + +// A data point in a timeseries that describes a network interface's time- +// varying operational state. +message IfOperStatusDataPoint { + // Required. When the value was captured. + google.protobuf.Timestamp time = 1; + + IfOperStatus value = 2; +} + +// Network interface operational state. +// +// See [RFC 2863](https://www.rfc-editor.org/rfc/rfc2863.html). +enum IfOperStatus { + IF_OPER_STATUS_UNSPECIFIED = 0; + + // Ready to pass packets. + IF_OPER_STATUS_UP = 1; + + IF_OPER_STATUS_DOWN = 2; + + // In some test mode. + IF_OPER_STATUS_TESTING = 3; + + // Status can not be determined for some reason. + IF_OPER_STATUS_UNKNOWN = 4; + + IF_OPER_STATUS_DORMANT = 5; + + // Some component is missing. + IF_OPER_STATUS_NOT_PRESENT = 6; + + // Down due to state of lower-layer interface(s). + IF_OPER_STATUS_LOWER_LAYER_DOWN = 7; +} + +// A data point in a timeseries that describes the time-varying values of +// a standard collection of network interface statistics. +// +// Specifically, the statistics are defined to mirror Linux's standard interface +// the fields and their documentation are derived. +message StandardInterfaceStatisticsDataPoint { + // The start time of the sums. + google.protobuf.Timestamp start_time = 1; + + // Required. When the statistics were captured. + google.protobuf.Timestamp time = 2; + + // Number of good packets received by the interface. For hardware interfaces + // counts all good packets received from the device by the host, including + // packets which host had to drop at various stages of processing (even in + // the driver). + int64 rx_packets = 3; + + // Number of packets successfully transmitted. For hardware interfaces counts + // packets which host was able to successfully hand over to the device, which + // does not necessarily mean that packets had been successfully transmitted + // out of the device, only that device acknowledged it copied them out of + // host memory. + int64 tx_packets = 4; + + // Number of good received bytes, corresponding to rx_packets. + // + // For IEEE 802.3 devices should count the length of Ethernet Frames + // excluding the FCS. + int64 rx_bytes = 5; + + // Number of good transmitted bytes, corresponding to tx_packets. + // + // For IEEE 802.3 devices should count the length of Ethernet Frames + // excluding the FCS. + int64 tx_bytes = 6; + + // Total number of bad packets received on this network device. This counter + // must include events counted by rx_length_errors, rx_crc_errors, + // rx_frame_errors and other errors not otherwise counted. + int64 rx_errors = 7; + + // Total number of transmit problems. This counter must include events + // counter by tx_aborted_errors, tx_carrier_errors, tx_fifo_errors, + // tx_heartbeat_errors, tx_window_errors and other errors not otherwise + // counted. + int64 tx_errors = 8; + + // Number of packets received but not processed, e.g. due to lack of + // resources or unsupported protocol. For hardware interfaces this counter + // may include packets discarded due to L2 address filtering but should not + // include packets dropped by the device due to buffer exhaustion which are + // counted separately in rx_missed_errors (since procfs folds those two + // counters together). + int64 rx_dropped = 9; + + // Number of packets dropped on their way to transmission, e.g. due to lack + // of resources. + int64 tx_dropped = 10; + + // Multicast packets received. For hardware interfaces this statistic is + // commonly calculated at the device level (unlike rx_packets) and therefore + // may include packets which did not reach the host. + // + // For IEEE 802.3 devices this counter may be equivalent to: + // * 30.3.1.1.21 aMulticastFramesReceivedOK + int64 multicast = 11; + + // Number of collisions during packet transmissions. + int64 collisions = 12; + + // Number of packets dropped due to invalid length. + // + // For IEEE 802.3 devices this counter should be equivalent to a sum of the + // following attributes: + // * 30.3.1.1.23 aInRangeLengthErrors + // * 30.3.1.1.24 aOutOfRangeLengthField + // * 30.3.1.1.25 aFrameTooLongErrors + int64 rx_length_errors = 13; + + // Receiver FIFO overflow event counter. + // + // Historically the count of overflow events. Such events may be reported in + // the receive descriptors or via interrupts, and may not correspond one-to- + // one with dropped packets. + // + // The recommended interpretation for high speed interfaces is - number of + // packets dropped because they did not fit into buffers provided by the host, + // e.g. packets larger than MTU or next buffer in the ring was not available + // for a scatter transfer. + // + // This statistics was historically used interchangeably with rx_fifo_errors. + // + // This statistic corresponds to hardware events and is not commonly used on + // software devices. + int64 rx_over_errors = 14; + + // Number of packets received with a CRC error. + // + // For IEEE 802.3 devices this counter must be equivalent to: + // * 30.3.1.1.6 aFrameCheckSequenceErrors + int64 rx_crc_errors = 15; + + // Receiver frame alignment errors. + // + // For IEEE 802.3 devices this counter should be equivalent to: + // * 30.3.1.1.7 aAlignmentErrors + int64 rx_frame_errors = 16; + + // Receiver FIFO error counter. + // + // Historically the count of overflow events. Those events may be reported in + // the receive descriptors or via interrupts, and may not correspond one-to- + // one with dropped packets. + // + // This statistics was used interchangeably with rx_over_errors. Not + // recommended for use in drivers for high speed interfaces. + // + // This statistic is used on software devices, e.g. to count software packet + // queue overflow (can) or sequencing errors (GRE). + int64 rx_fifo_errors = 17; + + // Count of packets missed by the host. + // + // Counts number of packets dropped by the device due to lack of buffer space. + // This usually indicates that the host interface is slower than the network + // interface, or host is not keeping up with the receive packet rate. + // + // This statistic corresponds to hardware events and is not used on software + // devices. + int64 rx_missed_errors = 18; + + + // For IEEE 802.3 devices capable of half-duplex operation this counter must + // be equivalent to: + // * 30.3.1.1.11 aFramesAbortedDueToXSColls + // + // High speed interfaces may use this counter as a general device discard + // counter. + int64 tx_aborted_errors = 19; + + // Number of frame transmission errors due to loss of carrier during + // transmission. + // + // For IEEE 802.3 devices this counter must be equivalent to: + // * 30.3.1.1.13 aCarrierSenseErrors + int64 tx_carrier_errors = 20; + + // Number of frame transmission errors due to device FIFO underrun / + // underflow. This condition occurs when the device begins transmission of a + // frame but is unable to deliver the entire frame to the transmitter in time + // for transmission. + int64 tx_fifo_errors = 21; + + // Number of Heartbeat / SQE Test errors for old half-duplex Ethernet. + // + // For IEEE 802.3 devices possibly equivalent to: + // * 30.3.2.1.4 aSQETestErrors + int64 tx_heartbeat_errors = 22; + + // Number of frame transmission errors due to late collisions (for Ethernet - + // after the first 64B of transmission). + // + // For IEEE 802.3 devices this counter must be equivalent to: + // * 30.3.1.1.10 aLateCollisions + int64 tx_window_errors = 23; + + // Number of correctly received compressed packets. This counters is only + // meaningful for interfaces which support packet compression (e.g. CSLIP, + // PPP). + int64 rx_compressed = 24; + + // Number of transmitted compressed packets. This counters is only meaningful + // for interfaces which support packet compression (e.g. CSLIP, PPP). + int64 tx_compressed = 25; + + // Number of packets received on the interface but dropped by the networking + // stack because the device is not designated to receive packets (e.g. backup + // link in a bond). + int64 rx_nohandler = 26; + + // Number of packets dropped due to mismatch in destination MAC address. + int64 rx_otherhost_dropped = 27; +} + +// A collection of metrics from a modem. +message ModemMetrics { + // Required. The modem producing telemetry. + string modem_id = 1; + + // Data points describing the time-varying properties of the modem's links. + repeated LinkMetricsDataPoint link_metrics_data_points = 2; +} + +// A data point in a timeseries that describes the time-varying values of +// a link's metrics. +message LinkMetricsDataPoint { + // Required. When the values were captured. + google.protobuf.Timestamp time = 1; + + // The transmitting modem. + string tx_modem_id = 2; + + // The data rate in bits per second. + double data_rate_bps = 3; + + // The energy per symbol to noise power spectral density, expressed in + // decibels. + double esn0_db = 4; + + // The signal-to-interference-plus-noise ratio, expressed in decibels. + double sinr_db = 5; +} diff --git a/api/types/BUILD b/api/types/BUILD new file mode 100644 index 0000000..270322c --- /dev/null +++ b/api/types/BUILD @@ -0,0 +1,50 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") +load("@rules_proto_grpc_cpp//:defs.bzl", "cpp_proto_library") +load("@rules_proto_grpc_java//:defs.bzl", "java_proto_library") +load("@rules_proto_grpc_python//:defs.bzl", "python_proto_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "types_proto", + srcs = [ + "ethernet.proto", + "ietf.proto", + ], +) + +cpp_proto_library( + name = "types_cpp_proto", + protos = [":types_proto"], +) + +go_proto_library( + name = "types_go_proto", + importpath = "aalyria.com/spacetime/api/types", + proto = ":types_proto", +) + +java_proto_library( + name = "types_java_proto", + protos = [":types_proto"], +) + +python_proto_library( + name = "types_python_proto", + protos = [":types_proto"], +) diff --git a/api/types/ethernet.proto b/api/types/ethernet.proto new file mode 100644 index 0000000..5bc0ae4 --- /dev/null +++ b/api/types/ethernet.proto @@ -0,0 +1,45 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package aalyria.spacetime.api.types; + +option java_package = "com.aalyria.spacetime.api.types"; +option go_package = "aalyria.com/spacetime/api/types"; + +// Identifies an "EtherType" value. +// +// See also the IANA registry for IEEE 802 Numbers: +// https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml +message EtherType { + // A collection of well-known EtherTypes. + enum Eth { + ETH_UNSPECIFIED = 0; + ETH_IPV4 = 2048; // 0x0800 + ETH_ARP = 2054; // 0x0806 + ETH_VLAN_CTAG = 33024; // 0x8100 (IEEE 802.1Q) + ETH_IPV6 = 34525; // 0x86dd + ETH_MPLS = 34887; // 0x8847 + // Formerly MPLS multicast; see RFC 5332. + ETH_MPLS_UPSTREAM = 34888; // 0x8848; upstream-assigned label + ETH_VLAN_STAG = 34984; // 0x88A8 (IEEE 802.1ad, "QinQ") + ETH_PBB = 35047; // 0x88E7 (IEEE 802.1Q, Backbone Service Instance) + } + + oneof value { + Eth well_known = 1; + uint32 explicit = 2; + } +} diff --git a/api/types/ietf.proto b/api/types/ietf.proto new file mode 100644 index 0000000..7d789c4 --- /dev/null +++ b/api/types/ietf.proto @@ -0,0 +1,99 @@ +// Copyright 2024 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package aalyria.spacetime.api.types; + +option java_package = "com.aalyria.spacetime.api.types"; +option go_package = "aalyria.com/spacetime/api/types"; + +// A type representing an IP prefix that may optionally need an +// additional qualifier to be properly unique. This is required, for +// example, when multiple non-public IP prefixes may be referenced +// within an NMTS model (e.g., overlapping RFC 1918 networks, or +// incorrectly-allocated ULAs). +// +// The `ip_prefix` field is the approximate equivalent of +// "inet:ip-prefix" from the "ietf-inet-types" YANG module; see also +// [RFC 6991](https://rfc-editor.org/rfc/rfc6991). It contains the +// the string representation of an IPv4 or IPv6 prefix, with a CIDR +// suffix and without any zone/scope ID qualifier. Text formatting +// should follow [RFC 5952](https://rfc-editor.org/rfc/rfc5952), +// especially section 4.3. +// +// The `realm` field is an optional qualifier containing a string name +// for a "realm" that is significant within the model and sufficient +// to differentiate one instance of a non-public prefix from another. +// The name "realm" is modeled after RFC 3102/3103 Realm-specific IP, +// though this in no way implies support for this protocol. +message IPNetwork { + string ip_prefix = 1; + string realm = 2; +} + +// The format of the `dotted_quad` string field is given by RFC 6991 +// [Section 3](https://rfc-editor.org/rfc/rfc6991#section-3): +// +// "An unsigned 32-bit number expressed in the dotted-quad +// notation, i.e., four octets written as decimal numbers +// and separated with the '.' (full stop) character." +// +// See also: +// * [OSPFv2](https://rfc-editor.org/rfc/rfc2328#section-1.2) +// * [OSPFv3](https://rfc-editor.org/rfc/rfc5340#section-2.11) +// +// IS-IS identifiers may be much larger, but 4-octet conventions and +// uses are common practice, e.g.: +// * [IS-IS for IP](https://rfc-editor.org/rfc/rfc1195#section-3.3) +// * [IS-IS TE](https://rfc-editor.org/rfc/rfc5305#section-4.3) +// +// Note: "0.0.0.0" is commonly considered RESERVED in several router +// identification and configuration contexts and SHOULD NOT be used. +message RouterId { + oneof type { + string dotted_quad = 1; + uint32 u32 = 2; + } +} + +// A Segment Routing Architecture Segment Identifier (SID). +// +// One representation of a SID is as an index relative to a block of +// other identifiers. That representation is presently NOT RECOMMENDED +// here; all SIDs should be resolved to concrete data plane values +// prior to input to the model. +// +// It is not expected that networks would operate both SR-MPLS and +// SRv6 at the same time. Nevertheless, a router could conceivably +// "bridge" an SR-MPLS domain and an SRv6 domain (especially since +// an SRv6 domain can more easily span multiple administrative +// domains, whether advisable or not). +// +// Values for `mpls` fields are 20-bit unsigned integers. Zero (0), +// being reserved for the "IPv4 Explicit NULL Label", is not a valid +// SR-MPLS label value. See also: +// https://rfc-editor.org/rfc/rfc3032#section-2.1 +// https://iana.org/assignments/mpls-label-values +// +// Values for `ipv6` fields may be any forwardable unicast IPv6 +// address. Use of addresses from the IANA-reserved 5f00::/16 prefix +// is RECOMMENDED. Empty `IPv6Address` strings and the zero-value "::" +// are both invalid SRv6 SIDs. See also: +// https://datatracker.ietf.org/doc/draft-ietf-6man-sids/ +// https://iana.org/assignments/iana-ipv6-special-registry +message SegmentId { + optional uint32 mpls = 1; // restricted to unsigned 20-bit values + optional string ipv6 = 2; // An IPv6 (SRv6) address. +} \ No newline at end of file diff --git a/auth/BUILD b/auth/BUILD new file mode 100644 index 0000000..3dab0fb --- /dev/null +++ b/auth/BUILD @@ -0,0 +1,41 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "auth", + srcs = [ + "auth.go", + "doc.go", + ], + importpath = "aalyria.com/spacetime/auth", + visibility = ["//visibility:public"], + deps = [ + "@com_github_golang_jwt_jwt_v5//:jwt", + "@com_github_jonboulle_clockwork//:clockwork", + "@org_golang_google_grpc//credentials", + ], +) + +go_test( + name = "auth_test", + srcs = ["auth_test.go"], + embed = [":auth"], + tags = ["block-network"], + deps = [ + "//auth/authtest", + "@com_github_jonboulle_clockwork//:clockwork", + ], +) diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..ae4cafc --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,298 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth // import "aalyria.com/spacetime/auth" + +import ( + "cmp" + "context" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "maps" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/jonboulle/clockwork" + "google.golang.org/grpc/credentials" +) + +const ( + authHeader = "authorization" + proxyAuthHeader = "proxy-authorization" + tokenLifetime = 1 * time.Hour + tokenExpirationWindow = 5 * time.Minute + proxyAudience = "60292403139-me68tjgajl5dcdbpnlm2ek830lvsnslq.apps.googleusercontent.com" + GoogleOIDCURL = "https://www.googleapis.com/oauth2/v4/token" +) + +var ( + // Google OIDC only supports RS256: https://accounts.google.com/.well-known/openid-configuration + proxySigningMethod = jwt.SigningMethodRS256 + // Spacetime expects RS384 + spacetimeSigningMethod = jwt.SigningMethodRS384 +) + +// authCredentials is an implementation of [credentials.PerRPCCredentials]. +type authCredentials struct { + spacetimeTokenSrc, proxyTokenSrc func(context.Context) (string, error) +} + +func (ac authCredentials) RequireTransportSecurity() bool { return true } + +func (ac authCredentials) fetch(ctx context.Context) (stToken, proxyToken string, _ error) { + wg := sync.WaitGroup{} + wg.Add(2) + + var stErr, proxyErr error + go func() { + defer wg.Done() + stToken, stErr = ac.spacetimeTokenSrc(ctx) + }() + go func() { + defer wg.Done() + proxyToken, proxyErr = ac.proxyTokenSrc(ctx) + }() + + wg.Wait() + + return stToken, proxyToken, errors.Join(stErr, proxyErr) +} + +// This was copied from the grpc/credentials/oauth.TokenSource implementation. +// The gRPC version doesn't allow changing the header, whereas we need to +// support both "authorization" and "proxy-authorization". +func (ac authCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + stToken, proxyToken, err := ac.fetch(ctx) + if err != nil { + return nil, err + } + + ri, _ := credentials.RequestInfoFromContext(ctx) + if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil { + return nil, fmt.Errorf("unable to transfer TokenSource PerRPCCredentials: %w", err) + } + + return map[string]string{ + authHeader: "Bearer " + stToken, + proxyAuthHeader: "Bearer " + proxyToken, + }, nil +} + +type Config struct { + Client *http.Client + Clock clockwork.Clock + PrivateKey io.Reader + PrivateKeyID string + Email string + Host string +} + +// NewCredentials creates a [credentials.PerRPCCredentials] implementation that +// can be used to authenticate outgoing gRPC requests with Spacetime services. +func NewCredentials(ctx context.Context, c Config) (credentials.PerRPCCredentials, error) { + errs := []error{} + switch { + case c.Clock == nil: + errs = append(errs, errors.New("missing required field 'Clock'")) + case c.Email == "": + errs = append(errs, errors.New("missing required field 'Email'")) + case c.PrivateKeyID == "": + errs = append(errs, errors.New("missing required field 'PrivateKeyID'")) + case c.PrivateKey == nil: + errs = append(errs, errors.New("missing required field 'PrivateKey'")) + case c.Host == "": + errs = append(errs, errors.New("missing required field 'Host'")) + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + pkeyBytes, err := io.ReadAll(c.PrivateKey) + if err != nil { + return nil, fmt.Errorf("getting private key bytes: %w", err) + } else if len(pkeyBytes) == 0 { + return nil, errors.New("empty private key") + } + + pkeyBlock, _ := pem.Decode(pkeyBytes) + if pkeyBlock == nil { + return nil, errors.New("PrivateKey not PEM-encoded") + } + pkey, err := parsePrivateKey(pkeyBlock.Bytes) + if err != nil { + return nil, err + } + + var ( + stSrc, proxySrc func(context.Context) (string, error) + stErr, proxyErr error + wg sync.WaitGroup + ) + wg.Add(2) + go func() { + defer wg.Done() + stSrc, stErr = newSpacetimeTokenSource(ctx, c, pkey) + }() + go func() { + defer wg.Done() + proxySrc, proxyErr = newProxyTokenSource(ctx, c, pkey) + }() + wg.Wait() + + return authCredentials{spacetimeTokenSrc: stSrc, proxyTokenSrc: proxySrc}, errors.Join(stErr, proxyErr) +} + +func newSpacetimeTokenSource(ctx context.Context, c Config, pkey any) (func(context.Context) (string, error), error) { + return reuseToken(ctx, c, pkey, func(context.Context) (*expiringToken, error) { + return generateNewJWT(c, pkey, spacetimeSigningMethod, nil) + }) +} + +func newProxyTokenSource(ctx context.Context, c Config, pkey any) (func(context.Context) (string, error), error) { + return reuseToken(ctx, c, pkey, func(ctx context.Context) (*expiringToken, error) { + toExchange, err := generateNewJWT(c, pkey, proxySigningMethod, map[string]any{ + "aud": GoogleOIDCURL, + "target_audience": proxyAudience, + }) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", GoogleOIDCURL, strings.NewReader((url.Values{ + "grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"}, + "assertion": {toExchange.tok}, + }).Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", `application/x-www-form-urlencoded`) + + resp, err := cmp.Or(c.Client, http.DefaultClient).Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + type oidcResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` + IDToken string `json:"id_token"` + } + r := oidcResponse{} + if err := json.Unmarshal(data, &r); err != nil { + return nil, err + } + if r.Error != "" { + return nil, fmt.Errorf("exchanging OIDC token: %s: %s", r.Error, r.ErrorDescription) + } + return &expiringToken{tok: r.IDToken, expiresAt: toExchange.expiresAt}, nil + }) +} + +func reuseToken(ctx context.Context, c Config, pkey any, genToken func(context.Context) (*expiringToken, error)) (func(context.Context) (string, error), error) { + freshToken, err := genToken(ctx) + if err != nil { + return nil, err + } + + mu := &sync.Mutex{} + return func(_ context.Context) (string, error) { + mu.Lock() + defer mu.Unlock() + + if freshToken.isStale(c.Clock) { + ft, err := genToken(ctx) + if err != nil { + return "", err + } + freshToken = ft + } + return freshToken.tok, nil + }, nil +} + +func parsePrivateKey(data []byte) (any, error) { + var pkey any + ok := false + parseErrs := []error{} + for algName, parse := range map[string]func([]byte) (any, error){ + "pkcs1": func(d []byte) (any, error) { + k, err := x509.ParsePKCS1PrivateKey(d) + return any(k), err + }, + "pkcs8": x509.ParsePKCS8PrivateKey, + } { + k, err := parse(data) + if err != nil { + parseErrs = append(parseErrs, fmt.Errorf("%s: %w", algName, err)) + continue + } + + pkey = k + ok = true + } + + if !ok { + return nil, errors.Join(parseErrs...) + } + return pkey, nil +} + +type expiringToken struct { + expiresAt time.Time + tok string +} + +func generateNewJWT(c Config, pkey any, signingMethod jwt.SigningMethod, extraClaims map[string]any) (*expiringToken, error) { + now := c.Clock.Now() + expiresAt := now.Add(tokenLifetime) + + claims := jwt.MapClaims{ + // AUDience + "aud": c.Host, + // Key ID + "kid": c.PrivateKeyID, + // ISSuer + "iss": c.Email, + // SUBject + "sub": c.Email, + // EXPires at + "exp": jwt.NewNumericDate(expiresAt), + // Issued AT + "iat": jwt.NewNumericDate(now), + } + maps.Insert(claims, maps.All(extraClaims)) + + token, err := jwt.NewWithClaims(signingMethod, claims).SignedString(pkey) + if err != nil { + return nil, fmt.Errorf("signing auth jwt: %w", err) + } + + return &expiringToken{tok: token, expiresAt: expiresAt}, nil +} + +func (et *expiringToken) isStale(clock clockwork.Clock) bool { + return clock.Now().After(et.expiresAt.Add(tokenExpirationWindow)) +} diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 0000000..0d98637 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,200 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "bytes" + "cmp" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "testing" + "time" + + "aalyria.com/spacetime/auth/authtest" + "github.com/jonboulle/clockwork" +) + +const validToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjcyMTk0YjI2MzU0YzIzYzBiYTU5YTZkNzUxZGZmYWEyNTg2NTkwNGUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjQvdG9rZW4iLCJleHAiOjE2ODE3OTIzMTksImlhdCI6MTY4MTc4ODcxOSwiaXNzIjoiY2RwaS1hZ2VudEBhNWEtc3BhY2V0aW1lLWdrZS1iYWNrLWRldi5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6ImNkcGktYWdlbnRAYTVhLXNwYWNldGltZS1na2UtYmFjay1kZXYuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJ0YXJnZXRfYXVkaWVuY2UiOiI2MDI5MjQwMzEzOS1tZTY4dGpnYWpsNWRjZGJwbmxtMmVrODMwbHZzbnNscS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSJ9.QyOi7vkFCwdmjT4ChT3_yVY4ZObUJkZkYC0q7alF_thiotdJKRiSo1ZHp_XnS0nM4WSWcQYLGHUDdAMPS0R22brFGzCl8ndgNjqI38yp_LDL8QVTqnLBGUj-m3xB5wH17Q_Dt8riBB4IE-mSS8FB-R6sqSwn-seMfMDydScC0FrtOF3-2BCYpIAlf1AQKN083QdtKgNEVDi72npPr2MmsWV3tct6ydXHWNbxG423kfSD6vCZSUTvWXAuVjuOwnbc2LHZS04U-jiLpvHxu06OwHOQ5LoGVPyd69o8Ny_Bapd2m0YCX2xJr8_HH2nw1jH7EplFf-owbBYz9ZtQoQ2YTA` + +var testKey = generateRSAPrivateKey() + +type badReader struct{ err error } + +func (b badReader) Read(_ []byte) (int, error) { return 0, b.err } + +func TestNewCredentials_validation(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + want string + c Config + }{ + { + name: "missing email", + want: "missing required field 'Email'", + c: Config{ + Email: "", + PrivateKey: bytes.NewBuffer(testKey.privatePEM), + PrivateKeyID: "1", + Clock: clockwork.NewRealClock(), + Host: "example.com", + }, + }, + { + name: "missing private key ID", + want: "missing required field 'PrivateKeyID'", + c: Config{ + Email: "some@example.com", + PrivateKey: bytes.NewBuffer(testKey.privatePEM), + PrivateKeyID: "", + Clock: clockwork.NewRealClock(), + Host: "example.com", + }, + }, + { + name: "missing clock", + want: "missing required field 'Clock'", + c: Config{ + Email: "some@example.com", + PrivateKey: bytes.NewBuffer(testKey.privatePEM), + PrivateKeyID: "1", + Clock: nil, + Host: "example.com", + }, + }, + { + name: "bad reader for private key", + want: "getting private key bytes: read went wrong!", + c: Config{ + Email: "some@example.com", + PrivateKey: badReader{errors.New("read went wrong!")}, + PrivateKeyID: "1", + Clock: clockwork.NewRealClock(), + Host: "example.com", + }, + }, + { + name: "empty private key", + want: "empty private key", + c: Config{ + Email: "some@example.com", + PrivateKey: bytes.NewBuffer([]byte{}), + PrivateKeyID: "1", + Clock: clockwork.NewRealClock(), + Host: "example.com", + }, + }, + { + name: "empty host", + want: `missing required field 'Host'`, + c: Config{ + Email: "some@example.com", + PrivateKey: bytes.NewBuffer(testKey.privatePEM), + PrivateKeyID: "1", + Clock: clockwork.NewRealClock(), + Host: "", + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + _, err := NewCredentials(ctx, tc.c) + if got := cmp.Or(err, errors.New("")).Error(); got != tc.want { + t.Errorf("unexpected validation error: got %q, but expected %q", got, tc.want) + } + }) + } +} + +func TestNewCredentials(t *testing.T) { + t.Parallel() + + srv := authtest.NewOIDCServer(validToken) + defer srv.Close() + + conf := Config{ + Client: srv.Client(), + Email: "some@example.com", + PrivateKey: bytes.NewBuffer(testKey.privatePEM), + PrivateKeyID: "1", + Clock: clockwork.NewFakeClockAt(time.Date(2011, time.February, 16, 0, 0, 0, 0, time.UTC)), + Host: "example.com", + } + + creds, err := NewCredentials(context.Background(), conf) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !creds.RequireTransportSecurity() { + t.Errorf("credentials should enforce transport security") + } + if nc := srv.NumberOfCalls(); nc != 1 { + t.Errorf("expected test server to be called once, but got %d calls", nc) + } + + // TODO: setup a secure local gRPC server and add a test that + // checks that the credentials are adding the metadata appropriately. This + // is more difficult than it sounds because the gRPC RequestInfo struct is + // required and only gets added by the grpc/internal/credentials package, + // which can't be used outside the grpc packages. +} + +type rsaKeyForTesting struct { + privateKey *rsa.PrivateKey + privatePEM []byte + publicPEM []byte +} + +func generateRSAPrivateKey() rsaKeyForTesting { + bitSize := 2048 + // Generate RSA key. + key, err := rsa.GenerateKey(rand.Reader, bitSize) + if err != nil { + panic(err) + } + + // Extract public component. + pub := key.Public() + + // Encode private key to PKCS#1 ASN.1 PEM. + keyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }, + ) + + // Encode public key to PKCS#1 ASN.1 PEM. + pubPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(pub.(*rsa.PublicKey)), + }, + ) + + return rsaKeyForTesting{ + privateKey: key, + privatePEM: keyPEM, + publicPEM: pubPEM, + } +} diff --git a/auth/authtest/BUILD b/auth/authtest/BUILD new file mode 100644 index 0000000..8af2180 --- /dev/null +++ b/auth/authtest/BUILD @@ -0,0 +1,23 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "authtest", + testonly = 1, + srcs = ["authtest.go"], + importpath = "aalyria.com/spacetime/auth/authtest", + visibility = ["//visibility:public"], +) diff --git a/auth/authtest/authtest.go b/auth/authtest/authtest.go new file mode 100644 index 0000000..dff8f6d --- /dev/null +++ b/auth/authtest/authtest.go @@ -0,0 +1,71 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package authtest provides helpers for testing functionality that uses the +// auth package. +package authtest // import "aalyria.com/spacetime/auth/authtest" + +import ( + "context" + "encoding/json" + "net" + "net/http" + "net/http/httptest" + "sync/atomic" + "time" +) + +type OIDCServer struct { + *httptest.Server + + numCalls *atomic.Int64 +} + +func NewOIDCServer(idToken string) *OIDCServer { + numCalls := &atomic.Int64{} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + numCalls.Add(1) + json.NewEncoder(w).Encode(map[string]interface{}{"id_token": idToken}) + })) + + return &OIDCServer{ + Server: ts, + numCalls: numCalls, + } +} + +func (s *OIDCServer) NumberOfCalls() int64 { return s.numCalls.Load() } + +func (s *OIDCServer) Client() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if addr == "www.googleapis.com:443" { + addr = s.Server.Listener.Addr().String() + } + return (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext(ctx, network, addr) + }, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 30 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: time.Second, + }, + } +} diff --git a/auth/doc.go b/auth/doc.go new file mode 100644 index 0000000..58b6729 --- /dev/null +++ b/auth/doc.go @@ -0,0 +1,23 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package auth provides credential helpers for connecting to Spacetime APIs. +// +// Users will create a [Config] and call [NewCredentials] to create a +// [google.golang.org/grpc/credentials.PerRPCCredentials] instance that can be +// used with the [google.golang.org/grpc.WithPerRPCCredentials] dial option to +// authenticate RPCs. See the [auth documentation] for more information. +// +// [auth documentation]: https://docs.spacetime.aalyria.com/authentication +package auth diff --git a/bazel/bazel_downloader.cfg b/bazel/bazel_downloader.cfg new file mode 100644 index 0000000..0becf6f --- /dev/null +++ b/bazel/bazel_downloader.cfg @@ -0,0 +1,17 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# block hostName - Will block access to the given host and subdomains +# block cn domains +block cn diff --git a/bazel/java_format_rules/BUILD b/bazel/java_format_rules/BUILD new file mode 100644 index 0000000..69e1381 --- /dev/null +++ b/bazel/java_format_rules/BUILD @@ -0,0 +1,18 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exports_files([ + "def.bzl", + "runner.bash.template", +]) diff --git a/bazel/java_format_rules/def.bzl b/bazel/java_format_rules/def.bzl new file mode 100644 index 0000000..876d9bd --- /dev/null +++ b/bazel/java_format_rules/def.bzl @@ -0,0 +1,74 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Bazel rules for testing compliance with google-java-format.""" + +load("@bazel_skylib//lib:shell.bzl", "shell") + +def _java_format_test_impl(ctx): + out_file = ctx.actions.declare_file(ctx.label.name + ".bash") + runfiles = [ctx.executable.google_java_format] + substitutions = { + "@@GOOGLE_JAVA_FORMAT@@": shell.quote(ctx.executable.google_java_format.short_path), + "@@SRCS@@": "", + } + if ctx.file.workspace != None: + runfiles.append(ctx.file.workspace) + substitutions["@@WORKSPACE@@"] = ctx.file.workspace.path + else: + for f in ctx.files.srcs: + runfiles.append(f) + substitutions["@@SRCS@@"] = " ".join([shell.quote(f.short_path) for f in ctx.files.srcs]) + + ctx.actions.expand_template( + template = ctx.file._runner, + output = out_file, + substitutions = substitutions, + is_executable = True, + ) + + shell_runfiles = ctx.runfiles(files = runfiles) + merged_runfiles = shell_runfiles.merge(ctx.attr.google_java_format[DefaultInfo].default_runfiles) + + return DefaultInfo( + files = depset([out_file]), + runfiles = merged_runfiles, + executable = out_file, + ) + +_java_format_test = rule( + implementation = _java_format_test_impl, + test = True, + attrs = { + "srcs": attr.label_list( + allow_files = [".java"], + doc = "A list of Java files to check for formatting", + ), + "google_java_format": attr.label( + default = "//third_party/java/google_java_format", + cfg = "exec", + executable = True, + ), + "workspace": attr.label( + allow_single_file = True, + doc = "Label of the WORKSPACE file", + ), + "_runner": attr.label( + default = ":runner.bash.template", + allow_single_file = True, + ), + }, +) + +def java_format_test(size = "small", **kwargs): + _java_format_test(size = size, **kwargs) diff --git a/bazel/java_format_rules/runner.bash.template b/bazel/java_format_rules/runner.bash.template new file mode 100644 index 0000000..4c469be --- /dev/null +++ b/bazel/java_format_rules/runner.bash.template @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +WORKSPACE="@@WORKSPACE@@" +SRCS=(@@SRCS@@) +GOOGLE_JAVA_FORMAT=@@GOOGLE_JAVA_FORMAT@@ +GOOGLE_JAVA_FORMAT="$(readlink "$GOOGLE_JAVA_FORMAT")" + +main() { + if [[ ${#SRCS[@]} -gt 0 ]] + then + check_srcs + else + check_repo + fi +} + +check_srcs() { + # the bulk of the time spent checking formatting is in the jvm startup time, + # but the full batch output is difficult to read and generate diffs off. To + # get a mix of speed and helpful error messages, we run the tool once against + # all the files with --dry-run set (which prints the list of files that are + # incorrectly formatted) then we loop over those if necessary to generate the + # human-friendly diffs + local unformatted_files=() + mapfile -t unformatted_files < <("$GOOGLE_JAVA_FORMAT" --dry-run "${SRCS[@]}") + + for src in "${unformatted_files[@]}"; + do + "$GOOGLE_JAVA_FORMAT" "$src" | diff -u3 "$src" - || true + done + + [[ "${#unformatted_files[@]}" -eq 0 ]] +} + +check_repo() { + # Use TEST_WORKSPACE to determine if the script is being ran under a test + if [[ ! -z "${TEST_WORKSPACE+x}" && -z "${BUILD_WORKSPACE_DIRECTORY+x}" ]]; then + FIND_FILE_TYPE="l" + # If WORKSPACE was provided, then the script is being run under a test in no_sandbox mode + # cd to the directory containing the WORKSPACE file + if [[ ! -z "${WORKSPACE+x}" ]]; then + FIND_FILE_TYPE="f" + WORKSPACE_PATH="$(dirname "$(realpath ${WORKSPACE})")" + if ! cd "$WORKSPACE_PATH" ; then + echo "Unable to change to workspace (WORKSPACE_PATH: ${WORKSPACE_PATH})" + fi + fi + else + # Change into the workspace directory if this is _not_ a test + if ! cd "$BUILD_WORKSPACE_DIRECTORY"; then + echo "Unable to change to workspace (BUILD_WORKSPACE_DIRECTORY: ${BUILD_WORKSPACE_DIRECTORY})" + exit 1 + fi + fi + + find . \ + -type "${FIND_FILE_TYPE:-f}" \ + \( -name '*.java' \ + \) -print0 | xargs -0 "$GOOGLE_JAVA_FORMAT" --set-exit-if-changed --dry-run + +} + +main "$@" diff --git a/bazel/java_rules/BUILD b/bazel/java_rules/BUILD new file mode 100644 index 0000000..3c45eee --- /dev/null +++ b/bazel/java_rules/BUILD @@ -0,0 +1,15 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exports_files(["def.bzl"]) diff --git a/bazel/java_rules/def.bzl b/bazel/java_rules/def.bzl new file mode 100644 index 0000000..65635dc --- /dev/null +++ b/bazel/java_rules/def.bzl @@ -0,0 +1,105 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of small wrappers around the native Java rules that enable more +cohesive uberjar builds (see _java_deployable_jar) and code intelligence (see +_actual_javacopts).""" + +load("@rules_java//java:defs.bzl", native_java_binary = "java_binary", native_java_library = "java_library", native_java_test = "java_test") + +def _java_deployable_jar_impl(ctx): + jars = [x for x in ctx.attr.binary[JavaInfo].compilation_info.runtime_classpath.to_list()] + + patched_jar = ctx.actions.declare_file("%s.jar" % ctx.label.name) + args_json = ctx.actions.declare_file("%s_args.json" % ctx.label.name) + ctx.actions.write( + output = args_json, + content = json.encode({ + "classpath_jars": [j.path for j in jars], + "input_jar": ctx.file.deploy_jar.path, + "output_jar": patched_jar.path, + }), + ) + + ctx.actions.run( + outputs = [patched_jar], + inputs = [args_json, ctx.file.deploy_jar] + jars, + arguments = [args_json.path], + executable = ctx.executable._jardoctor, + ) + + return [ + DefaultInfo(files = depset([patched_jar])), + ctx.attr.binary[JavaInfo], + ] + +_java_deployable_jar = rule( + implementation = _java_deployable_jar_impl, + attrs = { + "binary": attr.label( + mandatory = True, + doc = "java_binary to produce a deployable jar for", + providers = [JavaInfo], + ), + "deploy_jar": attr.label( + mandatory = True, + doc = "_deploy.jar artifact for given binary", + allow_single_file = ["_deploy.jar"], + ), + "_jardoctor": attr.label( + default = "//bazel/java_rules/jardoctor", + executable = True, + cfg = "exec", + ), + }, +) + +def java_library(javacopts = [], plugins = [], **kwargs): + native_java_library( + javacopts = _actual_javacopts(javacopts), + plugins = _actual_plugins(plugins), + **kwargs + ) + +def java_binary(name, javacopts = [], plugins = [], **kwargs): + native_java_binary( + name = name, + javacopts = _actual_javacopts(javacopts), + plugins = _actual_plugins(plugins), + **kwargs + ) + _java_deployable_jar( + name = "%s_deployable" % name, + binary = ":%s" % name, + deploy_jar = ":%s_deploy.jar" % name, + ) + +def java_test(javacopts = [], plugins = [], **kwargs): + native_java_test( + javacopts = _actual_javacopts(javacopts), + plugins = _actual_plugins(plugins), + **kwargs + ) + +def _actual_javacopts(javacopts): + return select({ + "@scip_java//semanticdb-javac:is_enabled": ["'-Xplugin:semanticdb -build-tool:bazel'"] + javacopts, + "//conditions:default": javacopts, + }) + +def _actual_plugins(plugins): + return select({ + "@scip_java//semanticdb-javac:is_enabled": ["@scip_java//semanticdb-javac:plugin"] + plugins, + "//conditions:default": plugins, + }) diff --git a/bazel/java_rules/jardoctor/BUILD b/bazel/java_rules/jardoctor/BUILD new file mode 100644 index 0000000..209ba7e --- /dev/null +++ b/bazel/java_rules/jardoctor/BUILD @@ -0,0 +1,27 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "jardoctor_lib", + srcs = ["main.go"], + importpath = "aalyria.com/spacetime/bazel/java_rules/jardoctor", +) + +go_binary( + name = "jardoctor", + embed = [":jardoctor_lib"], + visibility = ["//visibility:public"], +) diff --git a/bazel/java_rules/jardoctor/main.go b/bazel/java_rules/jardoctor/main.go new file mode 100644 index 0000000..8455f92 --- /dev/null +++ b/bazel/java_rules/jardoctor/main.go @@ -0,0 +1,351 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main provides a tool that attempts to fix shortcomings of the bazel +// uberjar build process. It handles combining flags.xml declarations which are +// created by the Google flags library we're using, as well as combinig +// Log4j2Plugins.dat files which are required to configure the log4j handlers. +// The default Bazel build process does not merge these correctly, instead it +// chooses a winner and only includes that version. +package main + +import ( + "archive/zip" + "bytes" + "context" + "encoding/binary" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "slices" + "strings" + "sync" +) + +const ( + log4jDataPath = "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat" + flagDataPath = "flags.xml" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "usage: %s ARGS.JSON\n", os.Args[0]) + os.Exit(1) + } + + if err := run(context.Background(), os.Args[1]); err != nil { + fmt.Fprintf(os.Stderr, "fatal error: %v\n", err) + os.Exit(1) + } +} + +type Args struct { + InputJar string `json:"input_jar"` + OutputJar string `json:"output_jar"` + ClasspathJars []string `json:"classpath_jars"` +} + +func run(_ context.Context, argFile string) error { + args, err := readArgs(argFile) + if err != nil { + return err + } + + logConf, err := mergeLogConfigs(gatherLogConfigs(args.ClasspathJars)) + if err != nil { + return err + } + flagDoc := mergeDocs(gatherFlagDocuments(args.ClasspathJars)) + + r, err := zip.OpenReader(args.InputJar) + if err != nil { + return err + } + defer r.Close() + + out, err := os.Create(args.OutputJar) + if err != nil { + return err + } + defer out.Close() + + return patchZip(logConf, flagDoc, out, r) +} + +func readArgs(argFile string) (Args, error) { + f, err := os.Open(argFile) + if err != nil { + return Args{}, err + } + defer f.Close() + + dec := json.NewDecoder(f) + a := Args{} + if err := dec.Decode(&a); err != nil { + return Args{}, err + } + return a, nil +} + +func patchZip(logConf *LogConfig, doc *FlagDocument, out io.Writer, r *zip.ReadCloser) error { + w := zip.NewWriter(out) + for _, f := range r.File { + if f.Name == flagDataPath || f.Name == log4jDataPath || strings.HasSuffix(f.Name, "/") { + continue + } + + if err := w.Copy(f); err != nil { + return fmt.Errorf("copying %s to new jar: %w", f.Name, err) + } + } + + // copy flag data + fw, err := w.Create(flagDataPath) + if err != nil { + return err + } + enc := xml.NewEncoder(fw) + if err := enc.Encode(doc); err != nil { + return err + } + if err := enc.Close(); err != nil { + return err + } + + // copy log4j data + lw, err := w.Create(log4jDataPath) + if err != nil { + return err + } + logData, err := marshalLogConfig(logConf) + if err != nil { + return err + } + if _, err := lw.Write(logData); err != nil { + return err + } + + if err := w.Flush(); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + return nil +} + +// FlagDocument represents the contents of a flags.xml file. +// +// Example document: +// +// +// +// com.google.googlex.minkowski.link.weather.WeatherClient.defaultUrl +// weather_server_url +// Base URL of the weather server +// PUBLIC +// java.lang.String +// +// +type FlagDocument struct { + XMLName xml.Name `xml:"flags"` + Flags []struct { + Body []byte `xml:",innerxml"` + } `xml:"flag"` +} + +func mergeDocs(docs []*FlagDocument) *FlagDocument { + doc := &FlagDocument{} + for _, d := range docs { + if d != nil { + doc.Flags = append(doc.Flags, d.Flags...) + } + } + return doc +} + +// Log4j data files are written using the PluginCache.writeCache function +// https://github.com/apache/logging-log4j2/blob/d7febdda98dd409a4e2f3c6c538ea7535e66e2c9/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java#L66. +// It uses a DataOutputStream to serialize information about a plugin to a +// binary file. The format looks roughly like this: +// +// size (int32) +// [ +// category (string) +// num_plugins (int32) +// [ +// key (string) +// className (string) +// name (string) +// printable (bool) +// defer (bool) +// ] +// ] +// +// Instead of decoding the inner portion, this code only handles rewriting the +// root `size` field and then merely concatenates the individual entries +// together (stored in the [LogConfig.Body] field. +type LogConfig struct { + Src string + Data []byte + Size int32 +} + +func mergeLogConfigs(confs []*LogConfig) (*LogConfig, error) { + confs = slices.DeleteFunc(confs, func(lc *LogConfig) bool { + return lc == nil + }) + + n := int32(0) + for _, lc := range confs { + n += lc.Size + } + + buf := &bytes.Buffer{} + for _, lc := range confs { + if _, err := buf.Write(lc.Data); err != nil { + return nil, fmt.Errorf("writing log config data from %s: %w", lc.Src, err) + } + } + return &LogConfig{Src: "combined", Size: n, Data: buf.Bytes()}, nil +} + +func marshalLogConfig(lc *LogConfig) ([]byte, error) { + buf := &bytes.Buffer{} + if err := binary.Write(buf, binary.BigEndian, &lc.Size); err != nil { + return nil, fmt.Errorf("writing merged size (%d): %w", lc.Size, err) + } + if _, err := buf.Write(lc.Data); err != nil { + return nil, fmt.Errorf("writing merged data: %w", err) + } + return buf.Bytes(), nil +} + +func unmarshalLogConfig(src string, data []byte) (*LogConfig, error) { + buf := bytes.NewBuffer(data) + var size int32 + if err := binary.Read(buf, binary.BigEndian, &size); err != nil { + return nil, fmt.Errorf("reading size: %w", err) + } + + return &LogConfig{Src: src, Size: size, Data: buf.Bytes()}, nil +} + +func gatherLogConfigs(jars []string) []*LogConfig { + wg := &sync.WaitGroup{} + + results := make([]*LogConfig, len(jars)) + for i, jar := range jars { + i, jar := i, jar + wg.Add(1) + + go func() { + defer wg.Done() + + doc, err := extractLogConfig(jar) + if err != nil { + fmt.Fprintf(os.Stderr, "error fetching Log4j2Plugins.dat: %v\n", err) + return + } + results[i] = doc + }() + } + wg.Wait() + + return results +} + +func gatherFlagDocuments(jars []string) []*FlagDocument { + wg := &sync.WaitGroup{} + + results := make([]*FlagDocument, len(jars)) + for i, jar := range jars { + i, jar := i, jar + wg.Add(1) + + go func() { + defer wg.Done() + + doc, err := extractFlagDocument(jar) + if err != nil { + fmt.Fprintf(os.Stderr, "error fetching %s: %v\n", flagDataPath, err) + return + } + results[i] = doc + }() + } + wg.Wait() + + return results +} + +func extractFlagDocument(jar string) (fd *FlagDocument, err error) { + if jar, err = filepath.EvalSymlinks(jar); err != nil { + return fd, err + } + r, err := zip.OpenReader(jar) + if err != nil { + return fd, fmt.Errorf("reading jar %s: %w", jar, err) + } + defer r.Close() + + f, err := r.Open(flagDataPath) + if err != nil && errors.Is(err, os.ErrNotExist) { + return nil, nil + } else if err != nil { + return fd, fmt.Errorf("opening %s in %s: %w", flagDataPath, jar, err) + } + defer f.Close() + + xmlData, err := io.ReadAll(f) + if err != nil { + return fd, fmt.Errorf("reading %s in %s: %v", flagDataPath, jar, err) + } + fd = &FlagDocument{} + if err := xml.Unmarshal(xmlData, fd); err != nil { + return fd, fmt.Errorf("unmarshalling %s in %s: %v", flagDataPath, jar, err) + } + + return fd, nil +} + +func extractLogConfig(jar string) (lc *LogConfig, err error) { + if jar, err = filepath.EvalSymlinks(jar); err != nil { + return nil, err + } + r, err := zip.OpenReader(jar) + if err != nil { + return nil, fmt.Errorf("reading jar %s: %w", jar, err) + } + defer r.Close() + + f, err := r.Open(log4jDataPath) + if err != nil && errors.Is(err, os.ErrNotExist) { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("opening %s in %s: %w", log4jDataPath, jar, err) + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", log4jDataPath, err) + } + + return unmarshalLogConfig(jar, data) +} diff --git a/common.bazelrc b/common.bazelrc new file mode 100644 index 0000000..b4070c2 --- /dev/null +++ b/common.bazelrc @@ -0,0 +1,78 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file defines bazelrc settings shared across workspaces. + +## +# Make Bazel use an environment with a static value for PATH to facilitate +# cross-user caching. +# +# Note that, at one point, this was the Bazel default, but that was reverted. See +# https://github.com/bazelbuild/bazel/issues/7026. +## +build --incompatible_strict_action_env + +# Go 1.20 introduced some changes that can cause unnecessary rebuilds without +# this flag: See https://github.com/bazelbuild/rules_go/issues/3430. +build --experimental_output_directory_naming_scheme=diff_against_dynamic_baseline + +## +# For information on C/C++ compiler warnings see: +# https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html +# +# Set C++20 as the standard version to use +build --cxxopt='-std=c++20' +build --cxxopt='-Wall' +build --cxxopt='-Werror' + +# TODO: remove these flags when grpc builds without them. +build --cxxopt='-Wno-comment' +build --cxxopt='-Wno-class-memaccess' +build --cxxopt='-Wno-unused-variable' +build --cxxopt='-Wno-unused-function' +# TODO: remove this flag when protobuf builds without it. +# See: https://github.com/protocolbuffers/protobuf/issues/12432 +build --cxxopt='-Wno-sign-compare' +# TODO: remove -Wno-deprecated-declarations and live a healthier, happier life. +build --cxxopt='-Wno-deprecated-declarations' + +## +# Disable strict warnings in header files from external repositories. +# - feature request: https://github.com/bazelbuild/bazel/issues/12009 +# - commit: https://github.com/bazelbuild/bazel/commit/08936aecb96f2937c61bdedfebcf1c5a41a0786d +## +build --features=external_include_paths + +## +# Enable hermetic testing and compilation of Java using a JVM downloaded from a +# remote repository. +## +build --tool_java_runtime_version=remotejdk_21 --java_runtime_version=remotejdk_21 + +## +# Allow Java constructs compatible with the Java 21 specification. +## +build --tool_java_language_version=21 --java_language_version=21 + +## +# Don't automatically create __init__.py files in the runfiles of +# Python targets. +# See: https://github.com/bazelbuild/bazel/issues/10076 +# +# This is motivated by our python proto library targets which produce +# overlapping import paths. +## +build --incompatible_default_to_explicit_init_py + +common --experimental_downloader_config=./bazel/bazel_downloader.cfg diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 0000000..341e2aa --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,6 @@ +# Contributions + +This is an open-source directory of real hardware that has been modeled in Spacetime and used in real networks. Contributions to this directory are welcome and are governed by the same policies as stated in the repository's [governance document](../GOVERNANCE.md). + +For example: +- [loon/antenna_pattern.textproto](loon/antenna_pattern.textproto): An antenna pattern specified using phi, theta, and gain, with separate patterns for near- and far- field. This pattern models the millimeter-wave antennas used by Loon's balloons for gateway and inter-platform links. diff --git a/contrib/loon/antenna_pattern.textproto b/contrib/loon/antenna_pattern.textproto new file mode 100644 index 0000000..eddd626 --- /dev/null +++ b/contrib/loon/antenna_pattern.textproto @@ -0,0 +1,36191 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +group { + type: ANTENNA_PATTERN +} +id: "gain/payload/mmwave/1" +antenna_pattern { + near_and_far_field_pattern: { + near_field_pattern: { + custom_phi_theta_pattern: { + gain_value { + gain_db: 13.566 + phi_rad: 0.0 + theta_rad: 0.0 + } + gain_value { + gain_db: 13.566 + phi_rad: 0.0 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: 3.5661 + phi_rad: 0.0 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -6.4339 + phi_rad: 0.0 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -6.4339 + phi_rad: 0.0 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -26.434 + phi_rad: 0.0 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -26.434 + phi_rad: 0.0 + theta_rad: 3.141592653589793 + } + gain_value { + gain_db: 13.566 + phi_rad: 1.5707963267948966 + theta_rad: 0.0 + } + gain_value { + gain_db: 13.566 + phi_rad: 1.5707963267948966 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: 3.5661 + phi_rad: 1.5707963267948966 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -6.4339 + phi_rad: 1.5707963267948966 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -6.4339 + phi_rad: 1.5707963267948966 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -26.434 + phi_rad: 1.5707963267948966 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -26.434 + phi_rad: 1.5707963267948966 + theta_rad: 3.141592653589793 + } + gain_value { + gain_db: 13.566 + phi_rad: 3.141592653589793 + theta_rad: 0.0 + } + gain_value { + gain_db: 13.566 + phi_rad: 3.141592653589793 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: 3.5661 + phi_rad: 3.141592653589793 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -6.4339 + phi_rad: 3.141592653589793 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -6.4339 + phi_rad: 3.141592653589793 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -26.434 + phi_rad: 3.141592653589793 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -26.434 + phi_rad: 3.141592653589793 + theta_rad: 3.141592653589793 + } + gain_value { + gain_db: 13.566 + phi_rad: 4.71238898038469 + theta_rad: 0.0 + } + gain_value { + gain_db: 13.566 + phi_rad: 4.71238898038469 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: 3.5661 + phi_rad: 4.71238898038469 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -6.4339 + phi_rad: 4.71238898038469 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -6.4339 + phi_rad: 4.71238898038469 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -26.434 + phi_rad: 4.71238898038469 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -26.434 + phi_rad: 4.71238898038469 + theta_rad: 3.141592653589793 + } + } + } + far_field_pattern: { + custom_phi_theta_pattern: { + gain_value { + gain_db: 44.0 + phi_rad: 0.0 + theta_rad: 0.0 + } + gain_value { + gain_db: 43.979 + phi_rad: 0.0 + theta_rad: 8.726646259971648E-4 + } + gain_value { + gain_db: 43.911 + phi_rad: 0.0 + theta_rad: 0.0017453292519943296 + } + gain_value { + gain_db: 43.79 + phi_rad: 0.0 + theta_rad: 0.002617993877991494 + } + gain_value { + gain_db: 43.615 + phi_rad: 0.0 + theta_rad: 0.003490658503988659 + } + gain_value { + gain_db: 43.346 + phi_rad: 0.0 + theta_rad: 0.004363323129985824 + } + gain_value { + gain_db: 43.008 + phi_rad: 0.0 + theta_rad: 0.005235987755982988 + } + gain_value { + gain_db: 42.58 + phi_rad: 0.0 + theta_rad: 0.006108652381980153 + } + gain_value { + gain_db: 42.013 + phi_rad: 0.0 + theta_rad: 0.006981317007977318 + } + gain_value { + gain_db: 41.349 + phi_rad: 0.0 + theta_rad: 0.007853981633974483 + } + gain_value { + gain_db: 40.443 + phi_rad: 0.0 + theta_rad: 0.008726646259971648 + } + gain_value { + gain_db: 39.462 + phi_rad: 0.0 + theta_rad: 0.009599310885968814 + } + gain_value { + gain_db: 38.399 + phi_rad: 0.0 + theta_rad: 0.010471975511965976 + } + gain_value { + gain_db: 37.02 + phi_rad: 0.0 + theta_rad: 0.011344640137963142 + } + gain_value { + gain_db: 35.589 + phi_rad: 0.0 + theta_rad: 0.012217304763960306 + } + gain_value { + gain_db: 34.148 + phi_rad: 0.0 + theta_rad: 0.013089969389957472 + } + gain_value { + gain_db: 31.986 + phi_rad: 0.0 + theta_rad: 0.013962634015954637 + } + gain_value { + gain_db: 30.013 + phi_rad: 0.0 + theta_rad: 0.014835298641951801 + } + gain_value { + gain_db: 27.669 + phi_rad: 0.0 + theta_rad: 0.015707963267948967 + } + gain_value { + gain_db: 25.667 + phi_rad: 0.0 + theta_rad: 0.01658062789394613 + } + gain_value { + gain_db: 24.917 + phi_rad: 0.0 + theta_rad: 0.017453292519943295 + } + gain_value { + gain_db: 24.913 + phi_rad: 0.0 + theta_rad: 0.01832595714594046 + } + gain_value { + gain_db: 25.101 + phi_rad: 0.0 + theta_rad: 0.019198621771937627 + } + gain_value { + gain_db: 25.371 + phi_rad: 0.0 + theta_rad: 0.02007128639793479 + } + gain_value { + gain_db: 25.637 + phi_rad: 0.0 + theta_rad: 0.020943951023931952 + } + gain_value { + gain_db: 25.797 + phi_rad: 0.0 + theta_rad: 0.02181661564992912 + } + gain_value { + gain_db: 25.843 + phi_rad: 0.0 + theta_rad: 0.022689280275926284 + } + gain_value { + gain_db: 25.745 + phi_rad: 0.0 + theta_rad: 0.02356194490192345 + } + gain_value { + gain_db: 25.507 + phi_rad: 0.0 + theta_rad: 0.024434609527920613 + } + gain_value { + gain_db: 25.173 + phi_rad: 0.0 + theta_rad: 0.02530727415391778 + } + gain_value { + gain_db: 24.729 + phi_rad: 0.0 + theta_rad: 0.026179938779914945 + } + gain_value { + gain_db: 24.162 + phi_rad: 0.0 + theta_rad: 0.027052603405912107 + } + gain_value { + gain_db: 23.645 + phi_rad: 0.0 + theta_rad: 0.027925268031909273 + } + gain_value { + gain_db: 23.122 + phi_rad: 0.0 + theta_rad: 0.028797932657906436 + } + gain_value { + gain_db: 22.54 + phi_rad: 0.0 + theta_rad: 0.029670597283903602 + } + gain_value { + gain_db: 22.064 + phi_rad: 0.0 + theta_rad: 0.030543261909900768 + } + gain_value { + gain_db: 21.569 + phi_rad: 0.0 + theta_rad: 0.031415926535897934 + } + gain_value { + gain_db: 21.025 + phi_rad: 0.0 + theta_rad: 0.0322885911618951 + } + gain_value { + gain_db: 20.469 + phi_rad: 0.0 + theta_rad: 0.03316125578789226 + } + gain_value { + gain_db: 19.843 + phi_rad: 0.0 + theta_rad: 0.034033920413889425 + } + gain_value { + gain_db: 19.168 + phi_rad: 0.0 + theta_rad: 0.03490658503988659 + } + gain_value { + gain_db: 18.467 + phi_rad: 0.0 + theta_rad: 0.03577924966588375 + } + gain_value { + gain_db: 17.734 + phi_rad: 0.0 + theta_rad: 0.03665191429188092 + } + gain_value { + gain_db: 17.158 + phi_rad: 0.0 + theta_rad: 0.03752457891787808 + } + gain_value { + gain_db: 16.671 + phi_rad: 0.0 + theta_rad: 0.038397243543875255 + } + gain_value { + gain_db: 16.425 + phi_rad: 0.0 + theta_rad: 0.039269908169872414 + } + gain_value { + gain_db: 16.306 + phi_rad: 0.0 + theta_rad: 0.04014257279586958 + } + gain_value { + gain_db: 16.384 + phi_rad: 0.0 + theta_rad: 0.041015237421866746 + } + gain_value { + gain_db: 16.604 + phi_rad: 0.0 + theta_rad: 0.041887902047863905 + } + gain_value { + gain_db: 16.893 + phi_rad: 0.0 + theta_rad: 0.04276056667386108 + } + gain_value { + gain_db: 17.233 + phi_rad: 0.0 + theta_rad: 0.04363323129985824 + } + gain_value { + gain_db: 17.541 + phi_rad: 0.0 + theta_rad: 0.0445058959258554 + } + gain_value { + gain_db: 17.826 + phi_rad: 0.0 + theta_rad: 0.04537856055185257 + } + gain_value { + gain_db: 18.017 + phi_rad: 0.0 + theta_rad: 0.046251225177849735 + } + gain_value { + gain_db: 18.108 + phi_rad: 0.0 + theta_rad: 0.0471238898038469 + } + gain_value { + gain_db: 18.074 + phi_rad: 0.0 + theta_rad: 0.04799655442984406 + } + gain_value { + gain_db: 17.94 + phi_rad: 0.0 + theta_rad: 0.048869219055841226 + } + gain_value { + gain_db: 17.677 + phi_rad: 0.0 + theta_rad: 0.04974188368183839 + } + gain_value { + gain_db: 17.303 + phi_rad: 0.0 + theta_rad: 0.05061454830783556 + } + gain_value { + gain_db: 16.808 + phi_rad: 0.0 + theta_rad: 0.051487212933832724 + } + gain_value { + gain_db: 16.28 + phi_rad: 0.0 + theta_rad: 0.05235987755982989 + } + gain_value { + gain_db: 15.628 + phi_rad: 0.0 + theta_rad: 0.05323254218582705 + } + gain_value { + gain_db: 14.87 + phi_rad: 0.0 + theta_rad: 0.054105206811824215 + } + gain_value { + gain_db: 14.022 + phi_rad: 0.0 + theta_rad: 0.05497787143782138 + } + gain_value { + gain_db: 13.141 + phi_rad: 0.0 + theta_rad: 0.05585053606381855 + } + gain_value { + gain_db: 12.155 + phi_rad: 0.0 + theta_rad: 0.05672320068981571 + } + gain_value { + gain_db: 11.126 + phi_rad: 0.0 + theta_rad: 0.05759586531581287 + } + gain_value { + gain_db: 9.9971 + phi_rad: 0.0 + theta_rad: 0.05846852994181004 + } + gain_value { + gain_db: 9.0396 + phi_rad: 0.0 + theta_rad: 0.059341194567807204 + } + gain_value { + gain_db: 8.1985 + phi_rad: 0.0 + theta_rad: 0.06021385919380437 + } + gain_value { + gain_db: 7.4569 + phi_rad: 0.0 + theta_rad: 0.061086523819801536 + } + gain_value { + gain_db: 6.9546 + phi_rad: 0.0 + theta_rad: 0.061959188445798695 + } + gain_value { + gain_db: 6.5396 + phi_rad: 0.0 + theta_rad: 0.06283185307179587 + } + gain_value { + gain_db: 6.2722 + phi_rad: 0.0 + theta_rad: 0.06370451769779303 + } + gain_value { + gain_db: 6.0362 + phi_rad: 0.0 + theta_rad: 0.0645771823237902 + } + gain_value { + gain_db: 5.7558 + phi_rad: 0.0 + theta_rad: 0.06544984694978735 + } + gain_value { + gain_db: 5.4846 + phi_rad: 0.0 + theta_rad: 0.06632251157578452 + } + gain_value { + gain_db: 5.1954 + phi_rad: 0.0 + theta_rad: 0.06719517620178168 + } + gain_value { + gain_db: 4.9511 + phi_rad: 0.0 + theta_rad: 0.06806784082777885 + } + gain_value { + gain_db: 4.7011 + phi_rad: 0.0 + theta_rad: 0.06894050545377602 + } + gain_value { + gain_db: 4.3894 + phi_rad: 0.0 + theta_rad: 0.06981317007977318 + } + gain_value { + gain_db: 3.9065 + phi_rad: 0.0 + theta_rad: 0.07068583470577035 + } + gain_value { + gain_db: 3.2553 + phi_rad: 0.0 + theta_rad: 0.0715584993317675 + } + gain_value { + gain_db: 2.5091 + phi_rad: 0.0 + theta_rad: 0.07243116395776468 + } + gain_value { + gain_db: 1.8796 + phi_rad: 0.0 + theta_rad: 0.07330382858376185 + } + gain_value { + gain_db: 1.5555 + phi_rad: 0.0 + theta_rad: 0.07417649320975901 + } + gain_value { + gain_db: 1.7576 + phi_rad: 0.0 + theta_rad: 0.07504915783575616 + } + gain_value { + gain_db: 2.3499 + phi_rad: 0.0 + theta_rad: 0.07592182246175333 + } + gain_value { + gain_db: 3.1327 + phi_rad: 0.0 + theta_rad: 0.07679448708775051 + } + gain_value { + gain_db: 4.0237 + phi_rad: 0.0 + theta_rad: 0.07766715171374766 + } + gain_value { + gain_db: 4.7911 + phi_rad: 0.0 + theta_rad: 0.07853981633974483 + } + gain_value { + gain_db: 5.3392 + phi_rad: 0.0 + theta_rad: 0.079412480965742 + } + gain_value { + gain_db: 5.7724 + phi_rad: 0.0 + theta_rad: 0.08028514559173916 + } + gain_value { + gain_db: 6.1025 + phi_rad: 0.0 + theta_rad: 0.08115781021773633 + } + gain_value { + gain_db: 6.3008 + phi_rad: 0.0 + theta_rad: 0.08203047484373349 + } + gain_value { + gain_db: 6.5185 + phi_rad: 0.0 + theta_rad: 0.08290313946973066 + } + gain_value { + gain_db: 6.7415 + phi_rad: 0.0 + theta_rad: 0.08377580409572781 + } + gain_value { + gain_db: 6.9815 + phi_rad: 0.0 + theta_rad: 0.08464846872172498 + } + gain_value { + gain_db: 7.1954 + phi_rad: 0.0 + theta_rad: 0.08552113334772216 + } + gain_value { + gain_db: 7.3608 + phi_rad: 0.0 + theta_rad: 0.08639379797371932 + } + gain_value { + gain_db: 7.4943 + phi_rad: 0.0 + theta_rad: 0.08726646259971647 + } + gain_value { + gain_db: 7.5462 + phi_rad: 0.0 + theta_rad: 0.08813912722571364 + } + gain_value { + gain_db: 7.5384 + phi_rad: 0.0 + theta_rad: 0.0890117918517108 + } + gain_value { + gain_db: 7.4187 + phi_rad: 0.0 + theta_rad: 0.08988445647770797 + } + gain_value { + gain_db: 7.1689 + phi_rad: 0.0 + theta_rad: 0.09075712110370514 + } + gain_value { + gain_db: 6.7692 + phi_rad: 0.0 + theta_rad: 0.0916297857297023 + } + gain_value { + gain_db: 6.1978 + phi_rad: 0.0 + theta_rad: 0.09250245035569947 + } + gain_value { + gain_db: 5.5395 + phi_rad: 0.0 + theta_rad: 0.09337511498169662 + } + gain_value { + gain_db: 4.7931 + phi_rad: 0.0 + theta_rad: 0.0942477796076938 + } + gain_value { + gain_db: 3.9767 + phi_rad: 0.0 + theta_rad: 0.09512044423369097 + } + gain_value { + gain_db: 3.0345 + phi_rad: 0.0 + theta_rad: 0.09599310885968812 + } + gain_value { + gain_db: 2.1521 + phi_rad: 0.0 + theta_rad: 0.09686577348568529 + } + gain_value { + gain_db: 1.4166 + phi_rad: 0.0 + theta_rad: 0.09773843811168245 + } + gain_value { + gain_db: 0.60343 + phi_rad: 0.0 + theta_rad: 0.09861110273767963 + } + gain_value { + gain_db: -0.6205 + phi_rad: 0.0 + theta_rad: 0.09948376736367678 + } + gain_value { + gain_db: -3.0498 + phi_rad: 0.0 + theta_rad: 0.10035643198967395 + } + gain_value { + gain_db: -6.839 + phi_rad: 0.0 + theta_rad: 0.10122909661567112 + } + gain_value { + gain_db: -2.0537 + phi_rad: 0.0 + theta_rad: 0.10210176124166827 + } + gain_value { + gain_db: -0.24373 + phi_rad: 0.0 + theta_rad: 0.10297442586766545 + } + gain_value { + gain_db: 1.0363 + phi_rad: 0.0 + theta_rad: 0.10384709049366261 + } + gain_value { + gain_db: 1.9795 + phi_rad: 0.0 + theta_rad: 0.10471975511965978 + } + gain_value { + gain_db: 2.6171 + phi_rad: 0.0 + theta_rad: 0.10559241974565693 + } + gain_value { + gain_db: 3.1454 + phi_rad: 0.0 + theta_rad: 0.1064650843716541 + } + gain_value { + gain_db: 3.4567 + phi_rad: 0.0 + theta_rad: 0.10733774899765128 + } + gain_value { + gain_db: 3.5734 + phi_rad: 0.0 + theta_rad: 0.10821041362364843 + } + gain_value { + gain_db: 3.4816 + phi_rad: 0.0 + theta_rad: 0.1090830782496456 + } + gain_value { + gain_db: 3.3016 + phi_rad: 0.0 + theta_rad: 0.10995574287564276 + } + gain_value { + gain_db: 3.0337 + phi_rad: 0.0 + theta_rad: 0.11082840750163991 + } + gain_value { + gain_db: 2.7435 + phi_rad: 0.0 + theta_rad: 0.1117010721276371 + } + gain_value { + gain_db: 2.4233 + phi_rad: 0.0 + theta_rad: 0.11257373675363426 + } + gain_value { + gain_db: 2.174 + phi_rad: 0.0 + theta_rad: 0.11344640137963143 + } + gain_value { + gain_db: 1.9858 + phi_rad: 0.0 + theta_rad: 0.11431906600562858 + } + gain_value { + gain_db: 1.8648 + phi_rad: 0.0 + theta_rad: 0.11519173063162574 + } + gain_value { + gain_db: 1.7966 + phi_rad: 0.0 + theta_rad: 0.11606439525762292 + } + gain_value { + gain_db: 1.7313 + phi_rad: 0.0 + theta_rad: 0.11693705988362008 + } + gain_value { + gain_db: 1.7096 + phi_rad: 0.0 + theta_rad: 0.11780972450961724 + } + gain_value { + gain_db: 1.7516 + phi_rad: 0.0 + theta_rad: 0.11868238913561441 + } + gain_value { + gain_db: 1.9552 + phi_rad: 0.0 + theta_rad: 0.11955505376161157 + } + gain_value { + gain_db: 2.2305 + phi_rad: 0.0 + theta_rad: 0.12042771838760874 + } + gain_value { + gain_db: 2.5432 + phi_rad: 0.0 + theta_rad: 0.1213003830136059 + } + gain_value { + gain_db: 2.8284 + phi_rad: 0.0 + theta_rad: 0.12217304763960307 + } + gain_value { + gain_db: 2.9952 + phi_rad: 0.0 + theta_rad: 0.12304571226560022 + } + gain_value { + gain_db: 3.1014 + phi_rad: 0.0 + theta_rad: 0.12391837689159739 + } + gain_value { + gain_db: 3.0757 + phi_rad: 0.0 + theta_rad: 0.12479104151759457 + } + gain_value { + gain_db: 2.9772 + phi_rad: 0.0 + theta_rad: 0.12566370614359174 + } + gain_value { + gain_db: 2.7619 + phi_rad: 0.0 + theta_rad: 0.1265363707695889 + } + gain_value { + gain_db: 2.4494 + phi_rad: 0.0 + theta_rad: 0.12740903539558607 + } + gain_value { + gain_db: 2.2061 + phi_rad: 0.0 + theta_rad: 0.1282817000215832 + } + gain_value { + gain_db: 2.14 + phi_rad: 0.0 + theta_rad: 0.1291543646475804 + } + gain_value { + gain_db: 2.1576 + phi_rad: 0.0 + theta_rad: 0.13002702927357757 + } + gain_value { + gain_db: 2.0528 + phi_rad: 0.0 + theta_rad: 0.1308996938995747 + } + gain_value { + gain_db: 1.81 + phi_rad: 0.0 + theta_rad: 0.13177235852557187 + } + gain_value { + gain_db: 1.5509 + phi_rad: 0.0 + theta_rad: 0.13264502315156904 + } + gain_value { + gain_db: 1.2754 + phi_rad: 0.0 + theta_rad: 0.13351768777756623 + } + gain_value { + gain_db: 1.0511 + phi_rad: 0.0 + theta_rad: 0.13439035240356337 + } + gain_value { + gain_db: 0.64953 + phi_rad: 0.0 + theta_rad: 0.13526301702956053 + } + gain_value { + gain_db: -0.0085667 + phi_rad: 0.0 + theta_rad: 0.1361356816555577 + } + gain_value { + gain_db: -0.76823 + phi_rad: 0.0 + theta_rad: 0.13700834628155487 + } + gain_value { + gain_db: -0.7292 + phi_rad: 0.0 + theta_rad: 0.13788101090755203 + } + gain_value { + gain_db: -0.12507 + phi_rad: 0.0 + theta_rad: 0.1387536755335492 + } + gain_value { + gain_db: 0.29567 + phi_rad: 0.0 + theta_rad: 0.13962634015954636 + } + gain_value { + gain_db: 0.55973 + phi_rad: 0.0 + theta_rad: 0.14049900478554353 + } + gain_value { + gain_db: 0.6792 + phi_rad: 0.0 + theta_rad: 0.1413716694115407 + } + gain_value { + gain_db: 0.78753 + phi_rad: 0.0 + theta_rad: 0.14224433403753786 + } + gain_value { + gain_db: 0.95873 + phi_rad: 0.0 + theta_rad: 0.143116998663535 + } + gain_value { + gain_db: 1.0691 + phi_rad: 0.0 + theta_rad: 0.1439896632895322 + } + gain_value { + gain_db: 1.0887 + phi_rad: 0.0 + theta_rad: 0.14486232791552936 + } + gain_value { + gain_db: 0.9098 + phi_rad: 0.0 + theta_rad: 0.1457349925415265 + } + gain_value { + gain_db: 0.6043 + phi_rad: 0.0 + theta_rad: 0.1466076571675237 + } + gain_value { + gain_db: 0.2134 + phi_rad: 0.0 + theta_rad: 0.14748032179352083 + } + gain_value { + gain_db: -0.0796 + phi_rad: 0.0 + theta_rad: 0.14835298641951802 + } + gain_value { + gain_db: -0.30623 + phi_rad: 0.0 + theta_rad: 0.1492256510455152 + } + gain_value { + gain_db: -0.3984 + phi_rad: 0.0 + theta_rad: 0.15009831567151233 + } + gain_value { + gain_db: -0.44927 + phi_rad: 0.0 + theta_rad: 0.15097098029750952 + } + gain_value { + gain_db: -0.55987 + phi_rad: 0.0 + theta_rad: 0.15184364492350666 + } + gain_value { + gain_db: -0.63787 + phi_rad: 0.0 + theta_rad: 0.15271630954950383 + } + gain_value { + gain_db: -0.66467 + phi_rad: 0.0 + theta_rad: 0.15358897417550102 + } + gain_value { + gain_db: -0.5252 + phi_rad: 0.0 + theta_rad: 0.15446163880149816 + } + gain_value { + gain_db: -0.24163 + phi_rad: 0.0 + theta_rad: 0.15533430342749532 + } + gain_value { + gain_db: 0.069433 + phi_rad: 0.0 + theta_rad: 0.1562069680534925 + } + gain_value { + gain_db: 0.3574 + phi_rad: 0.0 + theta_rad: 0.15707963267948966 + } + gain_value { + gain_db: 0.67767 + phi_rad: 0.0 + theta_rad: 0.15795229730548685 + } + gain_value { + gain_db: 0.99067 + phi_rad: 0.0 + theta_rad: 0.158824961931484 + } + gain_value { + gain_db: 1.3097 + phi_rad: 0.0 + theta_rad: 0.15969762655748115 + } + gain_value { + gain_db: 1.4861 + phi_rad: 0.0 + theta_rad: 0.16057029118347832 + } + gain_value { + gain_db: 1.5526 + phi_rad: 0.0 + theta_rad: 0.16144295580947549 + } + gain_value { + gain_db: 1.4171 + phi_rad: 0.0 + theta_rad: 0.16231562043547265 + } + gain_value { + gain_db: 1.1098 + phi_rad: 0.0 + theta_rad: 0.16318828506146982 + } + gain_value { + gain_db: 0.54613 + phi_rad: 0.0 + theta_rad: 0.16406094968746698 + } + gain_value { + gain_db: -0.20823 + phi_rad: 0.0 + theta_rad: 0.16493361431346412 + } + gain_value { + gain_db: -0.96263 + phi_rad: 0.0 + theta_rad: 0.16580627893946132 + } + gain_value { + gain_db: -1.5319 + phi_rad: 0.0 + theta_rad: 0.16667894356545848 + } + gain_value { + gain_db: -1.5054 + phi_rad: 0.0 + theta_rad: 0.16755160819145562 + } + gain_value { + gain_db: -1.0769 + phi_rad: 0.0 + theta_rad: 0.1684242728174528 + } + gain_value { + gain_db: -0.57807 + phi_rad: 0.0 + theta_rad: 0.16929693744344995 + } + gain_value { + gain_db: -0.093 + phi_rad: 0.0 + theta_rad: 0.17016960206944712 + } + gain_value { + gain_db: 0.2626 + phi_rad: 0.0 + theta_rad: 0.1710422666954443 + } + gain_value { + gain_db: 0.46757 + phi_rad: 0.0 + theta_rad: 0.17191493132144145 + } + gain_value { + gain_db: 0.54543 + phi_rad: 0.0 + theta_rad: 0.17278759594743864 + } + gain_value { + gain_db: 0.45573 + phi_rad: 0.0 + theta_rad: 0.17366026057343578 + } + gain_value { + gain_db: 0.36907 + phi_rad: 0.0 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: 0.292 + phi_rad: 0.0 + theta_rad: 0.17540558982543014 + } + gain_value { + gain_db: 0.2438 + phi_rad: 0.0 + theta_rad: 0.17627825445142728 + } + gain_value { + gain_db: 0.17173 + phi_rad: 0.0 + theta_rad: 0.17715091907742445 + } + gain_value { + gain_db: 0.123 + phi_rad: 0.0 + theta_rad: 0.1780235837034216 + } + gain_value { + gain_db: -0.0124 + phi_rad: 0.0 + theta_rad: 0.17889624832941878 + } + gain_value { + gain_db: -0.1253 + phi_rad: 0.0 + theta_rad: 0.17976891295541594 + } + gain_value { + gain_db: -0.2783 + phi_rad: 0.0 + theta_rad: 0.1806415775814131 + } + gain_value { + gain_db: -0.39803 + phi_rad: 0.0 + theta_rad: 0.18151424220741028 + } + gain_value { + gain_db: -0.42103 + phi_rad: 0.0 + theta_rad: 0.18238690683340741 + } + gain_value { + gain_db: -0.35783 + phi_rad: 0.0 + theta_rad: 0.1832595714594046 + } + gain_value { + gain_db: -0.28873 + phi_rad: 0.0 + theta_rad: 0.18413223608540177 + } + gain_value { + gain_db: -0.1256 + phi_rad: 0.0 + theta_rad: 0.18500490071139894 + } + gain_value { + gain_db: -0.1454 + phi_rad: 0.0 + theta_rad: 0.1858775653373961 + } + gain_value { + gain_db: -0.20607 + phi_rad: 0.0 + theta_rad: 0.18675022996339324 + } + gain_value { + gain_db: -0.34933 + phi_rad: 0.0 + theta_rad: 0.18762289458939044 + } + gain_value { + gain_db: -0.64057 + phi_rad: 0.0 + theta_rad: 0.1884955592153876 + } + gain_value { + gain_db: -0.96487 + phi_rad: 0.0 + theta_rad: 0.18936822384138474 + } + gain_value { + gain_db: -1.4379 + phi_rad: 0.0 + theta_rad: 0.19024088846738194 + } + gain_value { + gain_db: -1.9031 + phi_rad: 0.0 + theta_rad: 0.19111355309337907 + } + gain_value { + gain_db: -2.425 + phi_rad: 0.0 + theta_rad: 0.19198621771937624 + } + gain_value { + gain_db: -2.8768 + phi_rad: 0.0 + theta_rad: 0.19285888234537343 + } + gain_value { + gain_db: -3.1352 + phi_rad: 0.0 + theta_rad: 0.19373154697137057 + } + gain_value { + gain_db: -3.2984 + phi_rad: 0.0 + theta_rad: 0.19460421159736774 + } + gain_value { + gain_db: -3.3752 + phi_rad: 0.0 + theta_rad: 0.1954768762233649 + } + gain_value { + gain_db: -3.3647 + phi_rad: 0.0 + theta_rad: 0.19634954084936207 + } + gain_value { + gain_db: -3.3042 + phi_rad: 0.0 + theta_rad: 0.19722220547535926 + } + gain_value { + gain_db: -3.3016 + phi_rad: 0.0 + theta_rad: 0.1980948701013564 + } + gain_value { + gain_db: -3.5014 + phi_rad: 0.0 + theta_rad: 0.19896753472735357 + } + gain_value { + gain_db: -4.0221 + phi_rad: 0.0 + theta_rad: 0.19984019935335073 + } + gain_value { + gain_db: -4.7647 + phi_rad: 0.0 + theta_rad: 0.2007128639793479 + } + gain_value { + gain_db: -5.642 + phi_rad: 0.0 + theta_rad: 0.20158552860534507 + } + gain_value { + gain_db: -6.8786 + phi_rad: 0.0 + theta_rad: 0.20245819323134223 + } + gain_value { + gain_db: -7.7774 + phi_rad: 0.0 + theta_rad: 0.2033308578573394 + } + gain_value { + gain_db: -9.1486 + phi_rad: 0.0 + theta_rad: 0.20420352248333654 + } + gain_value { + gain_db: -8.4477 + phi_rad: 0.0 + theta_rad: 0.20507618710933373 + } + gain_value { + gain_db: -5.8471 + phi_rad: 0.0 + theta_rad: 0.2059488517353309 + } + gain_value { + gain_db: -3.7449 + phi_rad: 0.0 + theta_rad: 0.20682151636132803 + } + gain_value { + gain_db: -2.3714 + phi_rad: 0.0 + theta_rad: 0.20769418098732523 + } + gain_value { + gain_db: -1.3161 + phi_rad: 0.0 + theta_rad: 0.20856684561332237 + } + gain_value { + gain_db: -0.582 + phi_rad: 0.0 + theta_rad: 0.20943951023931956 + } + gain_value { + gain_db: -0.0847 + phi_rad: 0.0 + theta_rad: 0.21031217486531673 + } + gain_value { + gain_db: 0.21943 + phi_rad: 0.0 + theta_rad: 0.21118483949131386 + } + gain_value { + gain_db: 0.26 + phi_rad: 0.0 + theta_rad: 0.21205750411731106 + } + gain_value { + gain_db: 0.083167 + phi_rad: 0.0 + theta_rad: 0.2129301687433082 + } + gain_value { + gain_db: -0.3563 + phi_rad: 0.0 + theta_rad: 0.21380283336930536 + } + gain_value { + gain_db: -1.015 + phi_rad: 0.0 + theta_rad: 0.21467549799530256 + } + gain_value { + gain_db: -1.8558 + phi_rad: 0.0 + theta_rad: 0.2155481626212997 + } + gain_value { + gain_db: -2.788 + phi_rad: 0.0 + theta_rad: 0.21642082724729686 + } + gain_value { + gain_db: -3.6602 + phi_rad: 0.0 + theta_rad: 0.21729349187329403 + } + gain_value { + gain_db: -4.003 + phi_rad: 0.0 + theta_rad: 0.2181661564992912 + } + gain_value { + gain_db: -3.7387 + phi_rad: 0.0 + theta_rad: 0.21903882112528836 + } + gain_value { + gain_db: -3.5873 + phi_rad: 0.0 + theta_rad: 0.21991148575128552 + } + gain_value { + gain_db: -3.491 + phi_rad: 0.0 + theta_rad: 0.2207841503772827 + } + gain_value { + gain_db: -3.6275 + phi_rad: 0.0 + theta_rad: 0.22165681500327983 + } + gain_value { + gain_db: -3.9324 + phi_rad: 0.0 + theta_rad: 0.22252947962927702 + } + gain_value { + gain_db: -4.3522 + phi_rad: 0.0 + theta_rad: 0.2234021442552742 + } + gain_value { + gain_db: -4.9736 + phi_rad: 0.0 + theta_rad: 0.22427480888127135 + } + gain_value { + gain_db: -5.9446 + phi_rad: 0.0 + theta_rad: 0.22514747350726852 + } + gain_value { + gain_db: -7.249 + phi_rad: 0.0 + theta_rad: 0.22602013813326566 + } + gain_value { + gain_db: -9.3301 + phi_rad: 0.0 + theta_rad: 0.22689280275926285 + } + gain_value { + gain_db: -10.873 + phi_rad: 0.0 + theta_rad: 0.22776546738526002 + } + gain_value { + gain_db: -11.415 + phi_rad: 0.0 + theta_rad: 0.22863813201125716 + } + gain_value { + gain_db: -12.31 + phi_rad: 0.0 + theta_rad: 0.22951079663725435 + } + gain_value { + gain_db: -8.2884 + phi_rad: 0.0 + theta_rad: 0.2303834612632515 + } + gain_value { + gain_db: -6.0015 + phi_rad: 0.0 + theta_rad: 0.23125612588924865 + } + gain_value { + gain_db: -4.4736 + phi_rad: 0.0 + theta_rad: 0.23212879051524585 + } + gain_value { + gain_db: -3.1716 + phi_rad: 0.0 + theta_rad: 0.23300145514124299 + } + gain_value { + gain_db: -2.1041 + phi_rad: 0.0 + theta_rad: 0.23387411976724015 + } + gain_value { + gain_db: -1.1943 + phi_rad: 0.0 + theta_rad: 0.23474678439323732 + } + gain_value { + gain_db: -0.45033 + phi_rad: 0.0 + theta_rad: 0.23561944901923448 + } + gain_value { + gain_db: 0.2221 + phi_rad: 0.0 + theta_rad: 0.23649211364523168 + } + gain_value { + gain_db: 0.90237 + phi_rad: 0.0 + theta_rad: 0.23736477827122882 + } + gain_value { + gain_db: 1.5842 + phi_rad: 0.0 + theta_rad: 0.23823744289722598 + } + gain_value { + gain_db: 2.2865 + phi_rad: 0.0 + theta_rad: 0.23911010752322315 + } + gain_value { + gain_db: 2.8305 + phi_rad: 0.0 + theta_rad: 0.2399827721492203 + } + gain_value { + gain_db: 3.3538 + phi_rad: 0.0 + theta_rad: 0.24085543677521748 + } + gain_value { + gain_db: 3.7906 + phi_rad: 0.0 + theta_rad: 0.24172810140121465 + } + gain_value { + gain_db: 4.1315 + phi_rad: 0.0 + theta_rad: 0.2426007660272118 + } + gain_value { + gain_db: 4.3915 + phi_rad: 0.0 + theta_rad: 0.24347343065320895 + } + gain_value { + gain_db: 4.614 + phi_rad: 0.0 + theta_rad: 0.24434609527920614 + } + gain_value { + gain_db: 4.7577 + phi_rad: 0.0 + theta_rad: 0.2452187599052033 + } + gain_value { + gain_db: 4.7962 + phi_rad: 0.0 + theta_rad: 0.24609142453120045 + } + gain_value { + gain_db: 4.7899 + phi_rad: 0.0 + theta_rad: 0.24696408915719764 + } + gain_value { + gain_db: 4.7553 + phi_rad: 0.0 + theta_rad: 0.24783675378319478 + } + gain_value { + gain_db: 4.7008 + phi_rad: 0.0 + theta_rad: 0.24870941840919197 + } + gain_value { + gain_db: 4.6389 + phi_rad: 0.0 + theta_rad: 0.24958208303518914 + } + gain_value { + gain_db: 4.5142 + phi_rad: 0.0 + theta_rad: 0.2504547476611863 + } + gain_value { + gain_db: 4.3317 + phi_rad: 0.0 + theta_rad: 0.25132741228718347 + } + gain_value { + gain_db: 4.1135 + phi_rad: 0.0 + theta_rad: 0.2522000769131806 + } + gain_value { + gain_db: 3.8731 + phi_rad: 0.0 + theta_rad: 0.2530727415391778 + } + gain_value { + gain_db: 3.5708 + phi_rad: 0.0 + theta_rad: 0.25394540616517497 + } + gain_value { + gain_db: 3.0882 + phi_rad: 0.0 + theta_rad: 0.25481807079117214 + } + gain_value { + gain_db: 2.6806 + phi_rad: 0.0 + theta_rad: 0.2556907354171693 + } + gain_value { + gain_db: 2.0696 + phi_rad: 0.0 + theta_rad: 0.2565634000431664 + } + gain_value { + gain_db: 1.4726 + phi_rad: 0.0 + theta_rad: 0.25743606466916363 + } + gain_value { + gain_db: 0.74527 + phi_rad: 0.0 + theta_rad: 0.2583087292951608 + } + gain_value { + gain_db: -0.036867 + phi_rad: 0.0 + theta_rad: 0.2591813939211579 + } + gain_value { + gain_db: -0.7915 + phi_rad: 0.0 + theta_rad: 0.26005405854715513 + } + gain_value { + gain_db: -1.2887 + phi_rad: 0.0 + theta_rad: 0.26092672317315224 + } + gain_value { + gain_db: -1.7929 + phi_rad: 0.0 + theta_rad: 0.2617993877991494 + } + gain_value { + gain_db: -2.4195 + phi_rad: 0.0 + theta_rad: 0.26267205242514663 + } + gain_value { + gain_db: -2.8739 + phi_rad: 0.0 + theta_rad: 0.26354471705114374 + } + gain_value { + gain_db: -3.3119 + phi_rad: 0.0 + theta_rad: 0.2644173816771409 + } + gain_value { + gain_db: -3.9277 + phi_rad: 0.0 + theta_rad: 0.26529004630313807 + } + gain_value { + gain_db: -4.5006 + phi_rad: 0.0 + theta_rad: 0.26616271092913524 + } + gain_value { + gain_db: -5.0634 + phi_rad: 0.0 + theta_rad: 0.26703537555513246 + } + gain_value { + gain_db: -5.6284 + phi_rad: 0.0 + theta_rad: 0.26790804018112957 + } + gain_value { + gain_db: -6.0667 + phi_rad: 0.0 + theta_rad: 0.26878070480712674 + } + gain_value { + gain_db: -6.3413 + phi_rad: 0.0 + theta_rad: 0.2696533694331239 + } + gain_value { + gain_db: -6.3188 + phi_rad: 0.0 + theta_rad: 0.27052603405912107 + } + gain_value { + gain_db: -5.9476 + phi_rad: 0.0 + theta_rad: 0.27139869868511823 + } + gain_value { + gain_db: -5.5722 + phi_rad: 0.0 + theta_rad: 0.2722713633111154 + } + gain_value { + gain_db: -5.362 + phi_rad: 0.0 + theta_rad: 0.27314402793711257 + } + gain_value { + gain_db: -5.4811 + phi_rad: 0.0 + theta_rad: 0.27401669256310973 + } + gain_value { + gain_db: -5.7996 + phi_rad: 0.0 + theta_rad: 0.2748893571891069 + } + gain_value { + gain_db: -6.2875 + phi_rad: 0.0 + theta_rad: 0.27576202181510406 + } + gain_value { + gain_db: -6.8893 + phi_rad: 0.0 + theta_rad: 0.27663468644110123 + } + gain_value { + gain_db: -7.4064 + phi_rad: 0.0 + theta_rad: 0.2775073510670984 + } + gain_value { + gain_db: -7.722 + phi_rad: 0.0 + theta_rad: 0.27838001569309556 + } + gain_value { + gain_db: -7.6265 + phi_rad: 0.0 + theta_rad: 0.2792526803190927 + } + gain_value { + gain_db: -7.2582 + phi_rad: 0.0 + theta_rad: 0.2801253449450899 + } + gain_value { + gain_db: -6.7748 + phi_rad: 0.0 + theta_rad: 0.28099800957108706 + } + gain_value { + gain_db: -6.2941 + phi_rad: 0.0 + theta_rad: 0.28187067419708417 + } + gain_value { + gain_db: -5.9742 + phi_rad: 0.0 + theta_rad: 0.2827433388230814 + } + gain_value { + gain_db: -5.8225 + phi_rad: 0.0 + theta_rad: 0.28361600344907856 + } + gain_value { + gain_db: -5.8958 + phi_rad: 0.0 + theta_rad: 0.2844886680750757 + } + gain_value { + gain_db: -6.0715 + phi_rad: 0.0 + theta_rad: 0.2853613327010729 + } + gain_value { + gain_db: -6.2101 + phi_rad: 0.0 + theta_rad: 0.28623399732707 + } + gain_value { + gain_db: -6.2597 + phi_rad: 0.0 + theta_rad: 0.2871066619530672 + } + gain_value { + gain_db: -6.127 + phi_rad: 0.0 + theta_rad: 0.2879793265790644 + } + gain_value { + gain_db: -5.8948 + phi_rad: 0.0 + theta_rad: 0.28885199120506155 + } + gain_value { + gain_db: -5.6163 + phi_rad: 0.0 + theta_rad: 0.2897246558310587 + } + gain_value { + gain_db: -5.1213 + phi_rad: 0.0 + theta_rad: 0.29059732045705583 + } + gain_value { + gain_db: -4.5489 + phi_rad: 0.0 + theta_rad: 0.291469985083053 + } + gain_value { + gain_db: -4.0029 + phi_rad: 0.0 + theta_rad: 0.2923426497090502 + } + gain_value { + gain_db: -3.5924 + phi_rad: 0.0 + theta_rad: 0.2932153143350474 + } + gain_value { + gain_db: -3.3808 + phi_rad: 0.0 + theta_rad: 0.29408797896104455 + } + gain_value { + gain_db: -3.3452 + phi_rad: 0.0 + theta_rad: 0.29496064358704166 + } + gain_value { + gain_db: -3.4813 + phi_rad: 0.0 + theta_rad: 0.2958333082130388 + } + gain_value { + gain_db: -3.6291 + phi_rad: 0.0 + theta_rad: 0.29670597283903605 + } + gain_value { + gain_db: -3.7273 + phi_rad: 0.0 + theta_rad: 0.2975786374650332 + } + gain_value { + gain_db: -3.7502 + phi_rad: 0.0 + theta_rad: 0.2984513020910304 + } + gain_value { + gain_db: -3.742 + phi_rad: 0.0 + theta_rad: 0.2993239667170275 + } + gain_value { + gain_db: -3.6237 + phi_rad: 0.0 + theta_rad: 0.30019663134302466 + } + gain_value { + gain_db: -3.4923 + phi_rad: 0.0 + theta_rad: 0.3010692959690218 + } + gain_value { + gain_db: -3.4046 + phi_rad: 0.0 + theta_rad: 0.30194196059501904 + } + gain_value { + gain_db: -3.4866 + phi_rad: 0.0 + theta_rad: 0.3028146252210162 + } + gain_value { + gain_db: -3.6707 + phi_rad: 0.0 + theta_rad: 0.3036872898470133 + } + gain_value { + gain_db: -4.0023 + phi_rad: 0.0 + theta_rad: 0.3045599544730105 + } + gain_value { + gain_db: -4.4476 + phi_rad: 0.0 + theta_rad: 0.30543261909900765 + } + gain_value { + gain_db: -4.9739 + phi_rad: 0.0 + theta_rad: 0.3063052837250049 + } + gain_value { + gain_db: -5.3832 + phi_rad: 0.0 + theta_rad: 0.30717794835100204 + } + gain_value { + gain_db: -5.6084 + phi_rad: 0.0 + theta_rad: 0.30805061297699915 + } + gain_value { + gain_db: -5.8264 + phi_rad: 0.0 + theta_rad: 0.3089232776029963 + } + gain_value { + gain_db: -6.019 + phi_rad: 0.0 + theta_rad: 0.3097959422289935 + } + gain_value { + gain_db: -6.2376 + phi_rad: 0.0 + theta_rad: 0.31066860685499065 + } + gain_value { + gain_db: -6.4914 + phi_rad: 0.0 + theta_rad: 0.31154127148098787 + } + gain_value { + gain_db: -6.3655 + phi_rad: 0.0 + theta_rad: 0.312413936106985 + } + gain_value { + gain_db: -6.1709 + phi_rad: 0.0 + theta_rad: 0.31328660073298215 + } + gain_value { + gain_db: -5.8735 + phi_rad: 0.0 + theta_rad: 0.3141592653589793 + } + gain_value { + gain_db: -5.7056 + phi_rad: 0.0 + theta_rad: 0.3150319299849765 + } + gain_value { + gain_db: -5.8329 + phi_rad: 0.0 + theta_rad: 0.3159045946109737 + } + gain_value { + gain_db: -6.3346 + phi_rad: 0.0 + theta_rad: 0.3167772592369708 + } + gain_value { + gain_db: -7.0303 + phi_rad: 0.0 + theta_rad: 0.317649923862968 + } + gain_value { + gain_db: -7.943 + phi_rad: 0.0 + theta_rad: 0.31852258848896514 + } + gain_value { + gain_db: -8.1354 + phi_rad: 0.0 + theta_rad: 0.3193952531149623 + } + gain_value { + gain_db: -7.6023 + phi_rad: 0.0 + theta_rad: 0.3202679177409595 + } + gain_value { + gain_db: -6.8685 + phi_rad: 0.0 + theta_rad: 0.32114058236695664 + } + gain_value { + gain_db: -6.4098 + phi_rad: 0.0 + theta_rad: 0.3220132469929538 + } + gain_value { + gain_db: -6.1356 + phi_rad: 0.0 + theta_rad: 0.32288591161895097 + } + gain_value { + gain_db: -5.952 + phi_rad: 0.0 + theta_rad: 0.32375857624494814 + } + gain_value { + gain_db: -5.9391 + phi_rad: 0.0 + theta_rad: 0.3246312408709453 + } + gain_value { + gain_db: -6.1241 + phi_rad: 0.0 + theta_rad: 0.3255039054969424 + } + gain_value { + gain_db: -6.5057 + phi_rad: 0.0 + theta_rad: 0.32637657012293964 + } + gain_value { + gain_db: -7.0586 + phi_rad: 0.0 + theta_rad: 0.3272492347489368 + } + gain_value { + gain_db: -7.2468 + phi_rad: 0.0 + theta_rad: 0.32812189937493397 + } + gain_value { + gain_db: -6.8253 + phi_rad: 0.0 + theta_rad: 0.32899456400093113 + } + gain_value { + gain_db: -6.165 + phi_rad: 0.0 + theta_rad: 0.32986722862692824 + } + gain_value { + gain_db: -5.539 + phi_rad: 0.0 + theta_rad: 0.3307398932529254 + } + gain_value { + gain_db: -5.0923 + phi_rad: 0.0 + theta_rad: 0.33161255787892263 + } + gain_value { + gain_db: -4.8451 + phi_rad: 0.0 + theta_rad: 0.3324852225049198 + } + gain_value { + gain_db: -4.6639 + phi_rad: 0.0 + theta_rad: 0.33335788713091696 + } + gain_value { + gain_db: -4.659 + phi_rad: 0.0 + theta_rad: 0.3342305517569141 + } + gain_value { + gain_db: -4.5701 + phi_rad: 0.0 + theta_rad: 0.33510321638291124 + } + gain_value { + gain_db: -4.4437 + phi_rad: 0.0 + theta_rad: 0.33597588100890846 + } + gain_value { + gain_db: -4.2593 + phi_rad: 0.0 + theta_rad: 0.3368485456349056 + } + gain_value { + gain_db: -3.9198 + phi_rad: 0.0 + theta_rad: 0.3377212102609028 + } + gain_value { + gain_db: -3.4462 + phi_rad: 0.0 + theta_rad: 0.3385938748868999 + } + gain_value { + gain_db: -2.9978 + phi_rad: 0.0 + theta_rad: 0.33946653951289707 + } + gain_value { + gain_db: -2.6137 + phi_rad: 0.0 + theta_rad: 0.34033920413889424 + } + gain_value { + gain_db: -2.2799 + phi_rad: 0.0 + theta_rad: 0.34121186876489146 + } + gain_value { + gain_db: -2.0323 + phi_rad: 0.0 + theta_rad: 0.3420845333908886 + } + gain_value { + gain_db: -1.9275 + phi_rad: 0.0 + theta_rad: 0.34295719801688573 + } + gain_value { + gain_db: -1.9443 + phi_rad: 0.0 + theta_rad: 0.3438298626428829 + } + gain_value { + gain_db: -2.0457 + phi_rad: 0.0 + theta_rad: 0.34470252726888007 + } + gain_value { + gain_db: -2.2021 + phi_rad: 0.0 + theta_rad: 0.3455751918948773 + } + gain_value { + gain_db: -2.4093 + phi_rad: 0.0 + theta_rad: 0.34644785652087445 + } + gain_value { + gain_db: -2.5835 + phi_rad: 0.0 + theta_rad: 0.34732052114687156 + } + gain_value { + gain_db: -2.6896 + phi_rad: 0.0 + theta_rad: 0.34819318577286873 + } + gain_value { + gain_db: -2.6782 + phi_rad: 0.0 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -2.6707 + phi_rad: 0.0 + theta_rad: 0.34993851502486306 + } + gain_value { + gain_db: -2.5845 + phi_rad: 0.0 + theta_rad: 0.3508111796508603 + } + gain_value { + gain_db: -2.4337 + phi_rad: 0.0 + theta_rad: 0.3516838442768574 + } + gain_value { + gain_db: -2.3088 + phi_rad: 0.0 + theta_rad: 0.35255650890285456 + } + gain_value { + gain_db: -2.1257 + phi_rad: 0.0 + theta_rad: 0.3534291735288517 + } + gain_value { + gain_db: -1.9782 + phi_rad: 0.0 + theta_rad: 0.3543018381548489 + } + gain_value { + gain_db: -1.841 + phi_rad: 0.0 + theta_rad: 0.3551745027808461 + } + gain_value { + gain_db: -1.6608 + phi_rad: 0.0 + theta_rad: 0.3560471674068432 + } + gain_value { + gain_db: -1.5597 + phi_rad: 0.0 + theta_rad: 0.3569198320328404 + } + gain_value { + gain_db: -1.5121 + phi_rad: 0.0 + theta_rad: 0.35779249665883756 + } + gain_value { + gain_db: -1.5209 + phi_rad: 0.0 + theta_rad: 0.3586651612848347 + } + gain_value { + gain_db: -1.5801 + phi_rad: 0.0 + theta_rad: 0.3595378259108319 + } + gain_value { + gain_db: -1.6926 + phi_rad: 0.0 + theta_rad: 0.36041049053682905 + } + gain_value { + gain_db: -1.8426 + phi_rad: 0.0 + theta_rad: 0.3612831551628262 + } + gain_value { + gain_db: -2.0385 + phi_rad: 0.0 + theta_rad: 0.3621558197888234 + } + gain_value { + gain_db: -2.2091 + phi_rad: 0.0 + theta_rad: 0.36302848441482055 + } + gain_value { + gain_db: -2.2907 + phi_rad: 0.0 + theta_rad: 0.3639011490408177 + } + gain_value { + gain_db: -2.2946 + phi_rad: 0.0 + theta_rad: 0.36477381366681483 + } + gain_value { + gain_db: -2.1677 + phi_rad: 0.0 + theta_rad: 0.36564647829281205 + } + gain_value { + gain_db: -2.0302 + phi_rad: 0.0 + theta_rad: 0.3665191429188092 + } + gain_value { + gain_db: -1.8255 + phi_rad: 0.0 + theta_rad: 0.3673918075448064 + } + gain_value { + gain_db: -1.687 + phi_rad: 0.0 + theta_rad: 0.36826447217080355 + } + gain_value { + gain_db: -1.6232 + phi_rad: 0.0 + theta_rad: 0.36913713679680066 + } + gain_value { + gain_db: -1.6439 + phi_rad: 0.0 + theta_rad: 0.3700098014227979 + } + gain_value { + gain_db: -1.7729 + phi_rad: 0.0 + theta_rad: 0.37088246604879505 + } + gain_value { + gain_db: -1.9407 + phi_rad: 0.0 + theta_rad: 0.3717551306747922 + } + gain_value { + gain_db: -2.2965 + phi_rad: 0.0 + theta_rad: 0.3726277953007894 + } + gain_value { + gain_db: -2.555 + phi_rad: 0.0 + theta_rad: 0.3735004599267865 + } + gain_value { + gain_db: -2.7727 + phi_rad: 0.0 + theta_rad: 0.37437312455278365 + } + gain_value { + gain_db: -2.8903 + phi_rad: 0.0 + theta_rad: 0.3752457891787809 + } + gain_value { + gain_db: -2.9116 + phi_rad: 0.0 + theta_rad: 0.37611845380477804 + } + gain_value { + gain_db: -2.8219 + phi_rad: 0.0 + theta_rad: 0.3769911184307752 + } + gain_value { + gain_db: -2.677 + phi_rad: 0.0 + theta_rad: 0.3778637830567723 + } + gain_value { + gain_db: -2.5088 + phi_rad: 0.0 + theta_rad: 0.3787364476827695 + } + gain_value { + gain_db: -2.3914 + phi_rad: 0.0 + theta_rad: 0.37960911230876665 + } + gain_value { + gain_db: -2.3064 + phi_rad: 0.0 + theta_rad: 0.38048177693476387 + } + gain_value { + gain_db: -2.2419 + phi_rad: 0.0 + theta_rad: 0.38135444156076104 + } + gain_value { + gain_db: -2.2323 + phi_rad: 0.0 + theta_rad: 0.38222710618675815 + } + gain_value { + gain_db: -2.3014 + phi_rad: 0.0 + theta_rad: 0.3830997708127553 + } + gain_value { + gain_db: -2.4917 + phi_rad: 0.0 + theta_rad: 0.3839724354387525 + } + gain_value { + gain_db: -2.7584 + phi_rad: 0.0 + theta_rad: 0.3848451000647497 + } + gain_value { + gain_db: -3.074 + phi_rad: 0.0 + theta_rad: 0.38571776469074687 + } + gain_value { + gain_db: -3.2884 + phi_rad: 0.0 + theta_rad: 0.386590429316744 + } + gain_value { + gain_db: -3.3779 + phi_rad: 0.0 + theta_rad: 0.38746309394274114 + } + gain_value { + gain_db: -3.4822 + phi_rad: 0.0 + theta_rad: 0.3883357585687383 + } + gain_value { + gain_db: -3.5368 + phi_rad: 0.0 + theta_rad: 0.3892084231947355 + } + gain_value { + gain_db: -3.6267 + phi_rad: 0.0 + theta_rad: 0.3900810878207327 + } + gain_value { + gain_db: -3.7331 + phi_rad: 0.0 + theta_rad: 0.3909537524467298 + } + gain_value { + gain_db: -3.9481 + phi_rad: 0.0 + theta_rad: 0.391826417072727 + } + gain_value { + gain_db: -4.1737 + phi_rad: 0.0 + theta_rad: 0.39269908169872414 + } + gain_value { + gain_db: -4.4191 + phi_rad: 0.0 + theta_rad: 0.3935717463247213 + } + gain_value { + gain_db: -4.7481 + phi_rad: 0.0 + theta_rad: 0.3944444109507185 + } + gain_value { + gain_db: -5.0084 + phi_rad: 0.0 + theta_rad: 0.39531707557671564 + } + gain_value { + gain_db: -5.2343 + phi_rad: 0.0 + theta_rad: 0.3961897402027128 + } + gain_value { + gain_db: -5.2443 + phi_rad: 0.0 + theta_rad: 0.39706240482870997 + } + gain_value { + gain_db: -5.2153 + phi_rad: 0.0 + theta_rad: 0.39793506945470714 + } + gain_value { + gain_db: -5.2502 + phi_rad: 0.0 + theta_rad: 0.3988077340807043 + } + gain_value { + gain_db: -5.3465 + phi_rad: 0.0 + theta_rad: 0.39968039870670147 + } + gain_value { + gain_db: -5.5841 + phi_rad: 0.0 + theta_rad: 0.40055306333269863 + } + gain_value { + gain_db: -5.8251 + phi_rad: 0.0 + theta_rad: 0.4014257279586958 + } + gain_value { + gain_db: -5.8583 + phi_rad: 0.0 + theta_rad: 0.40229839258469297 + } + gain_value { + gain_db: -5.678 + phi_rad: 0.0 + theta_rad: 0.40317105721069013 + } + gain_value { + gain_db: -5.3615 + phi_rad: 0.0 + theta_rad: 0.40404372183668724 + } + gain_value { + gain_db: -5.0829 + phi_rad: 0.0 + theta_rad: 0.40491638646268446 + } + gain_value { + gain_db: -4.8463 + phi_rad: 0.0 + theta_rad: 0.40578905108868163 + } + gain_value { + gain_db: -4.7327 + phi_rad: 0.0 + theta_rad: 0.4066617157146788 + } + gain_value { + gain_db: -4.7363 + phi_rad: 0.0 + theta_rad: 0.40753438034067596 + } + gain_value { + gain_db: -4.977 + phi_rad: 0.0 + theta_rad: 0.40840704496667307 + } + gain_value { + gain_db: -5.4262 + phi_rad: 0.0 + theta_rad: 0.4092797095926703 + } + gain_value { + gain_db: -6.0034 + phi_rad: 0.0 + theta_rad: 0.41015237421866746 + } + gain_value { + gain_db: -6.5748 + phi_rad: 0.0 + theta_rad: 0.4110250388446646 + } + gain_value { + gain_db: -7.0284 + phi_rad: 0.0 + theta_rad: 0.4118977034706618 + } + gain_value { + gain_db: -7.1783 + phi_rad: 0.0 + theta_rad: 0.4127703680966589 + } + gain_value { + gain_db: -7.1985 + phi_rad: 0.0 + theta_rad: 0.41364303272265607 + } + gain_value { + gain_db: -7.0432 + phi_rad: 0.0 + theta_rad: 0.4145156973486533 + } + gain_value { + gain_db: -6.9296 + phi_rad: 0.0 + theta_rad: 0.41538836197465046 + } + gain_value { + gain_db: -6.8458 + phi_rad: 0.0 + theta_rad: 0.4162610266006476 + } + gain_value { + gain_db: -6.8586 + phi_rad: 0.0 + theta_rad: 0.41713369122664473 + } + gain_value { + gain_db: -7.12 + phi_rad: 0.0 + theta_rad: 0.4180063558526419 + } + gain_value { + gain_db: -7.617 + phi_rad: 0.0 + theta_rad: 0.4188790204786391 + } + gain_value { + gain_db: -8.3809 + phi_rad: 0.0 + theta_rad: 0.4197516851046363 + } + gain_value { + gain_db: -9.1491 + phi_rad: 0.0 + theta_rad: 0.42062434973063345 + } + gain_value { + gain_db: -9.8006 + phi_rad: 0.0 + theta_rad: 0.42149701435663056 + } + gain_value { + gain_db: -10.332 + phi_rad: 0.0 + theta_rad: 0.4223696789826277 + } + gain_value { + gain_db: -10.999 + phi_rad: 0.0 + theta_rad: 0.4232423436086249 + } + gain_value { + gain_db: -11.454 + phi_rad: 0.0 + theta_rad: 0.4241150082346221 + } + gain_value { + gain_db: -11.286 + phi_rad: 0.0 + theta_rad: 0.4249876728606193 + } + gain_value { + gain_db: -10.897 + phi_rad: 0.0 + theta_rad: 0.4258603374866164 + } + gain_value { + gain_db: -10.166 + phi_rad: 0.0 + theta_rad: 0.42673300211261356 + } + gain_value { + gain_db: -9.6755 + phi_rad: 0.0 + theta_rad: 0.4276056667386107 + } + gain_value { + gain_db: -9.3521 + phi_rad: 0.0 + theta_rad: 0.4284783313646079 + } + gain_value { + gain_db: -9.4566 + phi_rad: 0.0 + theta_rad: 0.4293509959906051 + } + gain_value { + gain_db: -9.6727 + phi_rad: 0.0 + theta_rad: 0.4302236606166022 + } + gain_value { + gain_db: -9.9775 + phi_rad: 0.0 + theta_rad: 0.4310963252425994 + } + gain_value { + gain_db: -10.338 + phi_rad: 0.0 + theta_rad: 0.43196898986859655 + } + gain_value { + gain_db: -10.799 + phi_rad: 0.0 + theta_rad: 0.4328416544945937 + } + gain_value { + gain_db: -11.156 + phi_rad: 0.0 + theta_rad: 0.43371431912059094 + } + gain_value { + gain_db: -11.172 + phi_rad: 0.0 + theta_rad: 0.43458698374658805 + } + gain_value { + gain_db: -11.306 + phi_rad: 0.0 + theta_rad: 0.4354596483725852 + } + gain_value { + gain_db: -11.643 + phi_rad: 0.0 + theta_rad: 0.4363323129985824 + } + gain_value { + gain_db: -12.203 + phi_rad: 0.0 + theta_rad: 0.43720497762457955 + } + gain_value { + gain_db: -13.194 + phi_rad: 0.0 + theta_rad: 0.4380776422505767 + } + gain_value { + gain_db: -14.256 + phi_rad: 0.0 + theta_rad: 0.4389503068765739 + } + gain_value { + gain_db: -14.408 + phi_rad: 0.0 + theta_rad: 0.43982297150257105 + } + gain_value { + gain_db: -13.639 + phi_rad: 0.0 + theta_rad: 0.4406956361285682 + } + gain_value { + gain_db: -13.761 + phi_rad: 0.0 + theta_rad: 0.4415683007545654 + } + gain_value { + gain_db: -13.856 + phi_rad: 0.0 + theta_rad: 0.44244096538056255 + } + gain_value { + gain_db: -14.567 + phi_rad: 0.0 + theta_rad: 0.44331363000655966 + } + gain_value { + gain_db: -15.452 + phi_rad: 0.0 + theta_rad: 0.4441862946325569 + } + gain_value { + gain_db: -16.393 + phi_rad: 0.0 + theta_rad: 0.44505895925855404 + } + gain_value { + gain_db: -17.526 + phi_rad: 0.0 + theta_rad: 0.4459316238845512 + } + gain_value { + gain_db: -20.772 + phi_rad: 0.0 + theta_rad: 0.4468042885105484 + } + gain_value { + gain_db: -18.034 + phi_rad: 0.0 + theta_rad: 0.4476769531365455 + } + gain_value { + gain_db: -14.947 + phi_rad: 0.0 + theta_rad: 0.4485496177625427 + } + gain_value { + gain_db: -13.181 + phi_rad: 0.0 + theta_rad: 0.4494222823885399 + } + gain_value { + gain_db: -11.766 + phi_rad: 0.0 + theta_rad: 0.45029494701453704 + } + gain_value { + gain_db: -10.904 + phi_rad: 0.0 + theta_rad: 0.4511676116405342 + } + gain_value { + gain_db: -10.348 + phi_rad: 0.0 + theta_rad: 0.4520402762665313 + } + gain_value { + gain_db: -10.073 + phi_rad: 0.0 + theta_rad: 0.4529129408925285 + } + gain_value { + gain_db: -9.9589 + phi_rad: 0.0 + theta_rad: 0.4537856055185257 + } + gain_value { + gain_db: -10.236 + phi_rad: 0.0 + theta_rad: 0.45465827014452287 + } + gain_value { + gain_db: -10.78 + phi_rad: 0.0 + theta_rad: 0.45553093477052004 + } + gain_value { + gain_db: -11.216 + phi_rad: 0.0 + theta_rad: 0.45640359939651715 + } + gain_value { + gain_db: -10.682 + phi_rad: 0.0 + theta_rad: 0.4572762640225143 + } + gain_value { + gain_db: -9.4509 + phi_rad: 0.0 + theta_rad: 0.45814892864851153 + } + gain_value { + gain_db: -8.5066 + phi_rad: 0.0 + theta_rad: 0.4590215932745087 + } + gain_value { + gain_db: -7.6673 + phi_rad: 0.0 + theta_rad: 0.45989425790050587 + } + gain_value { + gain_db: -7.0984 + phi_rad: 0.0 + theta_rad: 0.460766922526503 + } + gain_value { + gain_db: -6.4852 + phi_rad: 0.0 + theta_rad: 0.46163958715250014 + } + gain_value { + gain_db: -5.9808 + phi_rad: 0.0 + theta_rad: 0.4625122517784973 + } + gain_value { + gain_db: -5.5683 + phi_rad: 0.0 + theta_rad: 0.46338491640449453 + } + gain_value { + gain_db: -5.3474 + phi_rad: 0.0 + theta_rad: 0.4642575810304917 + } + gain_value { + gain_db: -5.0737 + phi_rad: 0.0 + theta_rad: 0.4651302456564888 + } + gain_value { + gain_db: -4.8668 + phi_rad: 0.0 + theta_rad: 0.46600291028248597 + } + gain_value { + gain_db: -4.5943 + phi_rad: 0.0 + theta_rad: 0.46687557490848314 + } + gain_value { + gain_db: -4.2878 + phi_rad: 0.0 + theta_rad: 0.4677482395344803 + } + gain_value { + gain_db: -3.9991 + phi_rad: 0.0 + theta_rad: 0.4686209041604775 + } + gain_value { + gain_db: -3.8219 + phi_rad: 0.0 + theta_rad: 0.46949356878647464 + } + gain_value { + gain_db: -3.5989 + phi_rad: 0.0 + theta_rad: 0.4703662334124718 + } + gain_value { + gain_db: -3.3863 + phi_rad: 0.0 + theta_rad: 0.47123889803846897 + } + gain_value { + gain_db: -2.999 + phi_rad: 0.0 + theta_rad: 0.47211156266446613 + } + gain_value { + gain_db: -2.663 + phi_rad: 0.0 + theta_rad: 0.47298422729046335 + } + gain_value { + gain_db: -2.3551 + phi_rad: 0.0 + theta_rad: 0.47385689191646047 + } + gain_value { + gain_db: -2.1367 + phi_rad: 0.0 + theta_rad: 0.47472955654245763 + } + gain_value { + gain_db: -1.9966 + phi_rad: 0.0 + theta_rad: 0.4756022211684548 + } + gain_value { + gain_db: -1.9043 + phi_rad: 0.0 + theta_rad: 0.47647488579445196 + } + gain_value { + gain_db: -1.8687 + phi_rad: 0.0 + theta_rad: 0.47734755042044913 + } + gain_value { + gain_db: -1.798 + phi_rad: 0.0 + theta_rad: 0.4782202150464463 + } + gain_value { + gain_db: -1.6696 + phi_rad: 0.0 + theta_rad: 0.47909287967244346 + } + gain_value { + gain_db: -1.516 + phi_rad: 0.0 + theta_rad: 0.4799655442984406 + } + gain_value { + gain_db: -1.3012 + phi_rad: 0.0 + theta_rad: 0.4808382089244378 + } + gain_value { + gain_db: -1.0337 + phi_rad: 0.0 + theta_rad: 0.48171087355043496 + } + gain_value { + gain_db: -0.7724 + phi_rad: 0.0 + theta_rad: 0.48258353817643207 + } + gain_value { + gain_db: -0.5412 + phi_rad: 0.0 + theta_rad: 0.4834562028024293 + } + gain_value { + gain_db: -0.3711 + phi_rad: 0.0 + theta_rad: 0.48432886742842646 + } + gain_value { + gain_db: -0.2728 + phi_rad: 0.0 + theta_rad: 0.4852015320544236 + } + gain_value { + gain_db: -0.236 + phi_rad: 0.0 + theta_rad: 0.4860741966804208 + } + gain_value { + gain_db: -0.2816 + phi_rad: 0.0 + theta_rad: 0.4869468613064179 + } + gain_value { + gain_db: -0.33327 + phi_rad: 0.0 + theta_rad: 0.4878195259324151 + } + gain_value { + gain_db: -0.38393 + phi_rad: 0.0 + theta_rad: 0.4886921905584123 + } + gain_value { + gain_db: -0.4219 + phi_rad: 0.0 + theta_rad: 0.48956485518440945 + } + gain_value { + gain_db: -0.4185 + phi_rad: 0.0 + theta_rad: 0.4904375198104066 + } + gain_value { + gain_db: -0.4171 + phi_rad: 0.0 + theta_rad: 0.49131018443640373 + } + gain_value { + gain_db: -0.40307 + phi_rad: 0.0 + theta_rad: 0.4921828490624009 + } + gain_value { + gain_db: -0.3787 + phi_rad: 0.0 + theta_rad: 0.4930555136883981 + } + gain_value { + gain_db: -0.32497 + phi_rad: 0.0 + theta_rad: 0.4939281783143953 + } + gain_value { + gain_db: -0.28973 + phi_rad: 0.0 + theta_rad: 0.49480084294039245 + } + gain_value { + gain_db: -0.2143 + phi_rad: 0.0 + theta_rad: 0.49567350756638956 + } + gain_value { + gain_db: -0.1892 + phi_rad: 0.0 + theta_rad: 0.4965461721923867 + } + gain_value { + gain_db: -0.19983 + phi_rad: 0.0 + theta_rad: 0.49741883681838395 + } + gain_value { + gain_db: -0.2804 + phi_rad: 0.0 + theta_rad: 0.4982915014443811 + } + gain_value { + gain_db: -0.40087 + phi_rad: 0.0 + theta_rad: 0.4991641660703783 + } + gain_value { + gain_db: -0.59917 + phi_rad: 0.0 + theta_rad: 0.5000368306963754 + } + gain_value { + gain_db: -0.93633 + phi_rad: 0.0 + theta_rad: 0.5009094953223726 + } + gain_value { + gain_db: -1.3809 + phi_rad: 0.0 + theta_rad: 0.5017821599483697 + } + gain_value { + gain_db: -1.8504 + phi_rad: 0.0 + theta_rad: 0.5026548245743669 + } + gain_value { + gain_db: -2.2752 + phi_rad: 0.0 + theta_rad: 0.503527489200364 + } + gain_value { + gain_db: -2.4922 + phi_rad: 0.0 + theta_rad: 0.5044001538263612 + } + gain_value { + gain_db: -2.5482 + phi_rad: 0.0 + theta_rad: 0.5052728184523584 + } + gain_value { + gain_db: -2.5129 + phi_rad: 0.0 + theta_rad: 0.5061454830783556 + } + gain_value { + gain_db: -2.4416 + phi_rad: 0.0 + theta_rad: 0.5070181477043527 + } + gain_value { + gain_db: -2.3634 + phi_rad: 0.0 + theta_rad: 0.5078908123303499 + } + gain_value { + gain_db: -2.4208 + phi_rad: 0.0 + theta_rad: 0.508763476956347 + } + gain_value { + gain_db: -2.6427 + phi_rad: 0.0 + theta_rad: 0.5096361415823443 + } + gain_value { + gain_db: -2.9262 + phi_rad: 0.0 + theta_rad: 0.5105088062083414 + } + gain_value { + gain_db: -3.1324 + phi_rad: 0.0 + theta_rad: 0.5113814708343386 + } + gain_value { + gain_db: -3.2696 + phi_rad: 0.0 + theta_rad: 0.5122541354603357 + } + gain_value { + gain_db: -3.2739 + phi_rad: 0.0 + theta_rad: 0.5131268000863328 + } + gain_value { + gain_db: -3.2548 + phi_rad: 0.0 + theta_rad: 0.51399946471233 + } + gain_value { + gain_db: -3.3173 + phi_rad: 0.0 + theta_rad: 0.5148721293383273 + } + gain_value { + gain_db: -3.327 + phi_rad: 0.0 + theta_rad: 0.5157447939643244 + } + gain_value { + gain_db: -3.3939 + phi_rad: 0.0 + theta_rad: 0.5166174585903216 + } + gain_value { + gain_db: -3.4505 + phi_rad: 0.0 + theta_rad: 0.5174901232163187 + } + gain_value { + gain_db: -3.6183 + phi_rad: 0.0 + theta_rad: 0.5183627878423158 + } + gain_value { + gain_db: -3.8198 + phi_rad: 0.0 + theta_rad: 0.519235452468313 + } + gain_value { + gain_db: -4.0565 + phi_rad: 0.0 + theta_rad: 0.5201081170943103 + } + gain_value { + gain_db: -4.2765 + phi_rad: 0.0 + theta_rad: 0.5209807817203074 + } + gain_value { + gain_db: -4.4201 + phi_rad: 0.0 + theta_rad: 0.5218534463463045 + } + gain_value { + gain_db: -4.4047 + phi_rad: 0.0 + theta_rad: 0.5227261109723017 + } + gain_value { + gain_db: -4.3034 + phi_rad: 0.0 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -4.1014 + phi_rad: 0.0 + theta_rad: 0.524471440224296 + } + gain_value { + gain_db: -3.9832 + phi_rad: 0.0 + theta_rad: 0.5253441048502933 + } + gain_value { + gain_db: -4.0227 + phi_rad: 0.0 + theta_rad: 0.5262167694762904 + } + gain_value { + gain_db: -4.1497 + phi_rad: 0.0 + theta_rad: 0.5270894341022875 + } + gain_value { + gain_db: -4.3843 + phi_rad: 0.0 + theta_rad: 0.5279620987282847 + } + gain_value { + gain_db: -4.6726 + phi_rad: 0.0 + theta_rad: 0.5288347633542818 + } + gain_value { + gain_db: -4.8708 + phi_rad: 0.0 + theta_rad: 0.529707427980279 + } + gain_value { + gain_db: -5.1097 + phi_rad: 0.0 + theta_rad: 0.5305800926062761 + } + gain_value { + gain_db: -5.2179 + phi_rad: 0.0 + theta_rad: 0.5314527572322734 + } + gain_value { + gain_db: -5.3231 + phi_rad: 0.0 + theta_rad: 0.5323254218582705 + } + gain_value { + gain_db: -5.405 + phi_rad: 0.0 + theta_rad: 0.5331980864842677 + } + gain_value { + gain_db: -5.4314 + phi_rad: 0.0 + theta_rad: 0.5340707511102649 + } + gain_value { + gain_db: -5.4721 + phi_rad: 0.0 + theta_rad: 0.534943415736262 + } + gain_value { + gain_db: -5.4753 + phi_rad: 0.0 + theta_rad: 0.5358160803622591 + } + gain_value { + gain_db: -5.5272 + phi_rad: 0.0 + theta_rad: 0.5366887449882564 + } + gain_value { + gain_db: -5.6449 + phi_rad: 0.0 + theta_rad: 0.5375614096142535 + } + gain_value { + gain_db: -5.8674 + phi_rad: 0.0 + theta_rad: 0.5384340742402507 + } + gain_value { + gain_db: -6.0967 + phi_rad: 0.0 + theta_rad: 0.5393067388662478 + } + gain_value { + gain_db: -6.1605 + phi_rad: 0.0 + theta_rad: 0.540179403492245 + } + gain_value { + gain_db: -6.0253 + phi_rad: 0.0 + theta_rad: 0.5410520681182421 + } + gain_value { + gain_db: -5.7545 + phi_rad: 0.0 + theta_rad: 0.5419247327442394 + } + gain_value { + gain_db: -5.5171 + phi_rad: 0.0 + theta_rad: 0.5427973973702365 + } + gain_value { + gain_db: -5.254 + phi_rad: 0.0 + theta_rad: 0.5436700619962336 + } + gain_value { + gain_db: -5.0942 + phi_rad: 0.0 + theta_rad: 0.5445427266222308 + } + gain_value { + gain_db: -4.9604 + phi_rad: 0.0 + theta_rad: 0.545415391248228 + } + gain_value { + gain_db: -4.9736 + phi_rad: 0.0 + theta_rad: 0.5462880558742251 + } + gain_value { + gain_db: -5.1155 + phi_rad: 0.0 + theta_rad: 0.5471607205002224 + } + gain_value { + gain_db: -5.2109 + phi_rad: 0.0 + theta_rad: 0.5480333851262195 + } + gain_value { + gain_db: -5.4538 + phi_rad: 0.0 + theta_rad: 0.5489060497522167 + } + gain_value { + gain_db: -5.7435 + phi_rad: 0.0 + theta_rad: 0.5497787143782138 + } + gain_value { + gain_db: -5.888 + phi_rad: 0.0 + theta_rad: 0.550651379004211 + } + gain_value { + gain_db: -5.9736 + phi_rad: 0.0 + theta_rad: 0.5515240436302081 + } + gain_value { + gain_db: -5.8381 + phi_rad: 0.0 + theta_rad: 0.5523967082562052 + } + gain_value { + gain_db: -5.5769 + phi_rad: 0.0 + theta_rad: 0.5532693728822025 + } + gain_value { + gain_db: -5.4013 + phi_rad: 0.0 + theta_rad: 0.5541420375081997 + } + gain_value { + gain_db: -5.2555 + phi_rad: 0.0 + theta_rad: 0.5550147021341968 + } + gain_value { + gain_db: -5.1027 + phi_rad: 0.0 + theta_rad: 0.555887366760194 + } + gain_value { + gain_db: -5.1495 + phi_rad: 0.0 + theta_rad: 0.5567600313861911 + } + gain_value { + gain_db: -5.2722 + phi_rad: 0.0 + theta_rad: 0.5576326960121882 + } + gain_value { + gain_db: -5.4192 + phi_rad: 0.0 + theta_rad: 0.5585053606381855 + } + gain_value { + gain_db: -5.6472 + phi_rad: 0.0 + theta_rad: 0.5593780252641826 + } + gain_value { + gain_db: -5.8014 + phi_rad: 0.0 + theta_rad: 0.5602506898901798 + } + gain_value { + gain_db: -5.9812 + phi_rad: 0.0 + theta_rad: 0.5611233545161769 + } + gain_value { + gain_db: -6.2519 + phi_rad: 0.0 + theta_rad: 0.5619960191421741 + } + gain_value { + gain_db: -6.5289 + phi_rad: 0.0 + theta_rad: 0.5628686837681712 + } + gain_value { + gain_db: -6.5451 + phi_rad: 0.0 + theta_rad: 0.5637413483941683 + } + gain_value { + gain_db: -6.4548 + phi_rad: 0.0 + theta_rad: 0.5646140130201657 + } + gain_value { + gain_db: -6.0903 + phi_rad: 0.0 + theta_rad: 0.5654866776461628 + } + gain_value { + gain_db: -5.8343 + phi_rad: 0.0 + theta_rad: 0.56635934227216 + } + gain_value { + gain_db: -5.4166 + phi_rad: 0.0 + theta_rad: 0.5672320068981571 + } + gain_value { + gain_db: -5.0548 + phi_rad: 0.0 + theta_rad: 0.5681046715241542 + } + gain_value { + gain_db: -4.8126 + phi_rad: 0.0 + theta_rad: 0.5689773361501514 + } + gain_value { + gain_db: -4.6349 + phi_rad: 0.0 + theta_rad: 0.5698500007761486 + } + gain_value { + gain_db: -4.5864 + phi_rad: 0.0 + theta_rad: 0.5707226654021458 + } + gain_value { + gain_db: -4.5867 + phi_rad: 0.0 + theta_rad: 0.5715953300281429 + } + gain_value { + gain_db: -4.7029 + phi_rad: 0.0 + theta_rad: 0.57246799465414 + } + gain_value { + gain_db: -4.9067 + phi_rad: 0.0 + theta_rad: 0.5733406592801373 + } + gain_value { + gain_db: -5.1722 + phi_rad: 0.0 + theta_rad: 0.5742133239061344 + } + gain_value { + gain_db: -5.4071 + phi_rad: 0.0 + theta_rad: 0.5750859885321317 + } + gain_value { + gain_db: -5.4533 + phi_rad: 0.0 + theta_rad: 0.5759586531581288 + } + gain_value { + gain_db: -5.3006 + phi_rad: 0.0 + theta_rad: 0.5768313177841259 + } + gain_value { + gain_db: -4.9043 + phi_rad: 0.0 + theta_rad: 0.5777039824101231 + } + gain_value { + gain_db: -4.4204 + phi_rad: 0.0 + theta_rad: 0.5785766470361202 + } + gain_value { + gain_db: -3.9898 + phi_rad: 0.0 + theta_rad: 0.5794493116621174 + } + gain_value { + gain_db: -3.6713 + phi_rad: 0.0 + theta_rad: 0.5803219762881145 + } + gain_value { + gain_db: -3.5158 + phi_rad: 0.0 + theta_rad: 0.5811946409141117 + } + gain_value { + gain_db: -3.49 + phi_rad: 0.0 + theta_rad: 0.5820673055401089 + } + gain_value { + gain_db: -3.5743 + phi_rad: 0.0 + theta_rad: 0.582939970166106 + } + gain_value { + gain_db: -3.7078 + phi_rad: 0.0 + theta_rad: 0.5838126347921033 + } + gain_value { + gain_db: -3.7971 + phi_rad: 0.0 + theta_rad: 0.5846852994181004 + } + gain_value { + gain_db: -3.8167 + phi_rad: 0.0 + theta_rad: 0.5855579640440975 + } + gain_value { + gain_db: -3.7861 + phi_rad: 0.0 + theta_rad: 0.5864306286700948 + } + gain_value { + gain_db: -3.5964 + phi_rad: 0.0 + theta_rad: 0.5873032932960919 + } + gain_value { + gain_db: -3.4178 + phi_rad: 0.0 + theta_rad: 0.5881759579220891 + } + gain_value { + gain_db: -3.2542 + phi_rad: 0.0 + theta_rad: 0.5890486225480862 + } + gain_value { + gain_db: -3.1149 + phi_rad: 0.0 + theta_rad: 0.5899212871740833 + } + gain_value { + gain_db: -3.0153 + phi_rad: 0.0 + theta_rad: 0.5907939518000805 + } + gain_value { + gain_db: -2.9161 + phi_rad: 0.0 + theta_rad: 0.5916666164260777 + } + gain_value { + gain_db: -2.8008 + phi_rad: 0.0 + theta_rad: 0.592539281052075 + } + gain_value { + gain_db: -2.598 + phi_rad: 0.0 + theta_rad: 0.5934119456780721 + } + gain_value { + gain_db: -2.4202 + phi_rad: 0.0 + theta_rad: 0.5942846103040692 + } + gain_value { + gain_db: -2.2838 + phi_rad: 0.0 + theta_rad: 0.5951572749300664 + } + gain_value { + gain_db: -2.2034 + phi_rad: 0.0 + theta_rad: 0.5960299395560635 + } + gain_value { + gain_db: -2.2272 + phi_rad: 0.0 + theta_rad: 0.5969026041820608 + } + gain_value { + gain_db: -2.3069 + phi_rad: 0.0 + theta_rad: 0.5977752688080579 + } + gain_value { + gain_db: -2.5307 + phi_rad: 0.0 + theta_rad: 0.598647933434055 + } + gain_value { + gain_db: -2.8518 + phi_rad: 0.0 + theta_rad: 0.5995205980600522 + } + gain_value { + gain_db: -3.2368 + phi_rad: 0.0 + theta_rad: 0.6003932626860493 + } + gain_value { + gain_db: -3.5955 + phi_rad: 0.0 + theta_rad: 0.6012659273120465 + } + gain_value { + gain_db: -3.827 + phi_rad: 0.0 + theta_rad: 0.6021385919380436 + } + gain_value { + gain_db: -3.8043 + phi_rad: 0.0 + theta_rad: 0.6030112565640408 + } + gain_value { + gain_db: -3.4687 + phi_rad: 0.0 + theta_rad: 0.6038839211900381 + } + gain_value { + gain_db: -3.0793 + phi_rad: 0.0 + theta_rad: 0.6047565858160352 + } + gain_value { + gain_db: -2.7769 + phi_rad: 0.0 + theta_rad: 0.6056292504420324 + } + gain_value { + gain_db: -2.483 + phi_rad: 0.0 + theta_rad: 0.6065019150680295 + } + gain_value { + gain_db: -2.444 + phi_rad: 0.0 + theta_rad: 0.6073745796940266 + } + gain_value { + gain_db: -2.5972 + phi_rad: 0.0 + theta_rad: 0.6082472443200239 + } + gain_value { + gain_db: -2.8202 + phi_rad: 0.0 + theta_rad: 0.609119908946021 + } + gain_value { + gain_db: -3.173 + phi_rad: 0.0 + theta_rad: 0.6099925735720182 + } + gain_value { + gain_db: -3.6269 + phi_rad: 0.0 + theta_rad: 0.6108652381980153 + } + gain_value { + gain_db: -3.9448 + phi_rad: 0.0 + theta_rad: 0.6117379028240124 + } + gain_value { + gain_db: -4.1078 + phi_rad: 0.0 + theta_rad: 0.6126105674500097 + } + gain_value { + gain_db: -4.1651 + phi_rad: 0.0 + theta_rad: 0.6134832320760069 + } + gain_value { + gain_db: -4.1496 + phi_rad: 0.0 + theta_rad: 0.6143558967020041 + } + gain_value { + gain_db: -4.0149 + phi_rad: 0.0 + theta_rad: 0.6152285613280012 + } + gain_value { + gain_db: -3.7987 + phi_rad: 0.0 + theta_rad: 0.6161012259539983 + } + gain_value { + gain_db: -3.499 + phi_rad: 0.0 + theta_rad: 0.6169738905799955 + } + gain_value { + gain_db: -3.4041 + phi_rad: 0.0 + theta_rad: 0.6178465552059926 + } + gain_value { + gain_db: -3.4517 + phi_rad: 0.0 + theta_rad: 0.6187192198319899 + } + gain_value { + gain_db: -3.7103 + phi_rad: 0.0 + theta_rad: 0.619591884457987 + } + gain_value { + gain_db: -4.0356 + phi_rad: 0.0 + theta_rad: 0.6204645490839841 + } + gain_value { + gain_db: -4.3558 + phi_rad: 0.0 + theta_rad: 0.6213372137099813 + } + gain_value { + gain_db: -4.5528 + phi_rad: 0.0 + theta_rad: 0.6222098783359784 + } + gain_value { + gain_db: -4.6101 + phi_rad: 0.0 + theta_rad: 0.6230825429619757 + } + gain_value { + gain_db: -4.5316 + phi_rad: 0.0 + theta_rad: 0.6239552075879728 + } + gain_value { + gain_db: -4.3874 + phi_rad: 0.0 + theta_rad: 0.62482787221397 + } + gain_value { + gain_db: -4.3029 + phi_rad: 0.0 + theta_rad: 0.6257005368399672 + } + gain_value { + gain_db: -4.4759 + phi_rad: 0.0 + theta_rad: 0.6265732014659643 + } + gain_value { + gain_db: -4.7846 + phi_rad: 0.0 + theta_rad: 0.6274458660919615 + } + gain_value { + gain_db: -5.1888 + phi_rad: 0.0 + theta_rad: 0.6283185307179586 + } + gain_value { + gain_db: -5.5387 + phi_rad: 0.0 + theta_rad: 0.6291911953439557 + } + gain_value { + gain_db: -5.6891 + phi_rad: 0.0 + theta_rad: 0.630063859969953 + } + gain_value { + gain_db: -5.6627 + phi_rad: 0.0 + theta_rad: 0.6309365245959501 + } + gain_value { + gain_db: -5.5144 + phi_rad: 0.0 + theta_rad: 0.6318091892219474 + } + gain_value { + gain_db: -5.2885 + phi_rad: 0.0 + theta_rad: 0.6326818538479445 + } + gain_value { + gain_db: -5.0724 + phi_rad: 0.0 + theta_rad: 0.6335545184739416 + } + gain_value { + gain_db: -4.9982 + phi_rad: 0.0 + theta_rad: 0.6344271830999388 + } + gain_value { + gain_db: -5.0073 + phi_rad: 0.0 + theta_rad: 0.635299847725936 + } + gain_value { + gain_db: -5.1304 + phi_rad: 0.0 + theta_rad: 0.6361725123519332 + } + gain_value { + gain_db: -5.3426 + phi_rad: 0.0 + theta_rad: 0.6370451769779303 + } + gain_value { + gain_db: -5.6357 + phi_rad: 0.0 + theta_rad: 0.6379178416039274 + } + gain_value { + gain_db: -5.8021 + phi_rad: 0.0 + theta_rad: 0.6387905062299246 + } + gain_value { + gain_db: -5.9387 + phi_rad: 0.0 + theta_rad: 0.6396631708559217 + } + gain_value { + gain_db: -5.8606 + phi_rad: 0.0 + theta_rad: 0.640535835481919 + } + gain_value { + gain_db: -5.7213 + phi_rad: 0.0 + theta_rad: 0.6414085001079161 + } + gain_value { + gain_db: -5.513 + phi_rad: 0.0 + theta_rad: 0.6422811647339133 + } + gain_value { + gain_db: -5.2701 + phi_rad: 0.0 + theta_rad: 0.6431538293599105 + } + gain_value { + gain_db: -5.1295 + phi_rad: 0.0 + theta_rad: 0.6440264939859076 + } + gain_value { + gain_db: -4.8704 + phi_rad: 0.0 + theta_rad: 0.6448991586119048 + } + gain_value { + gain_db: -4.5689 + phi_rad: 0.0 + theta_rad: 0.6457718232379019 + } + gain_value { + gain_db: -4.2651 + phi_rad: 0.0 + theta_rad: 0.646644487863899 + } + gain_value { + gain_db: -4.1743 + phi_rad: 0.0 + theta_rad: 0.6475171524898963 + } + gain_value { + gain_db: -4.1443 + phi_rad: 0.0 + theta_rad: 0.6483898171158934 + } + gain_value { + gain_db: -4.2328 + phi_rad: 0.0 + theta_rad: 0.6492624817418906 + } + gain_value { + gain_db: -4.2327 + phi_rad: 0.0 + theta_rad: 0.6501351463678877 + } + gain_value { + gain_db: -4.1366 + phi_rad: 0.0 + theta_rad: 0.6510078109938848 + } + gain_value { + gain_db: -4.0729 + phi_rad: 0.0 + theta_rad: 0.6518804756198822 + } + gain_value { + gain_db: -3.8679 + phi_rad: 0.0 + theta_rad: 0.6527531402458793 + } + gain_value { + gain_db: -3.7283 + phi_rad: 0.0 + theta_rad: 0.6536258048718765 + } + gain_value { + gain_db: -3.5072 + phi_rad: 0.0 + theta_rad: 0.6544984694978736 + } + gain_value { + gain_db: -3.3302 + phi_rad: 0.0 + theta_rad: 0.6553711341238707 + } + gain_value { + gain_db: -3.1434 + phi_rad: 0.0 + theta_rad: 0.6562437987498679 + } + gain_value { + gain_db: -3.0974 + phi_rad: 0.0 + theta_rad: 0.657116463375865 + } + gain_value { + gain_db: -2.9517 + phi_rad: 0.0 + theta_rad: 0.6579891280018623 + } + gain_value { + gain_db: -2.7999 + phi_rad: 0.0 + theta_rad: 0.6588617926278594 + } + gain_value { + gain_db: -2.6894 + phi_rad: 0.0 + theta_rad: 0.6597344572538565 + } + gain_value { + gain_db: -2.6456 + phi_rad: 0.0 + theta_rad: 0.6606071218798537 + } + gain_value { + gain_db: -2.6219 + phi_rad: 0.0 + theta_rad: 0.6614797865058508 + } + gain_value { + gain_db: -2.6164 + phi_rad: 0.0 + theta_rad: 0.6623524511318482 + } + gain_value { + gain_db: -2.6494 + phi_rad: 0.0 + theta_rad: 0.6632251157578453 + } + gain_value { + gain_db: -2.7093 + phi_rad: 0.0 + theta_rad: 0.6640977803838424 + } + gain_value { + gain_db: -2.7847 + phi_rad: 0.0 + theta_rad: 0.6649704450098396 + } + gain_value { + gain_db: -2.8757 + phi_rad: 0.0 + theta_rad: 0.6658431096358367 + } + gain_value { + gain_db: -2.9706 + phi_rad: 0.0 + theta_rad: 0.6667157742618339 + } + gain_value { + gain_db: -3.0748 + phi_rad: 0.0 + theta_rad: 0.667588438887831 + } + gain_value { + gain_db: -3.2256 + phi_rad: 0.0 + theta_rad: 0.6684611035138281 + } + gain_value { + gain_db: -3.2805 + phi_rad: 0.0 + theta_rad: 0.6693337681398254 + } + gain_value { + gain_db: -3.2502 + phi_rad: 0.0 + theta_rad: 0.6702064327658225 + } + gain_value { + gain_db: -3.0389 + phi_rad: 0.0 + theta_rad: 0.6710790973918198 + } + gain_value { + gain_db: -2.6723 + phi_rad: 0.0 + theta_rad: 0.6719517620178169 + } + gain_value { + gain_db: -2.3233 + phi_rad: 0.0 + theta_rad: 0.672824426643814 + } + gain_value { + gain_db: -2.0795 + phi_rad: 0.0 + theta_rad: 0.6736970912698113 + } + gain_value { + gain_db: -1.9983 + phi_rad: 0.0 + theta_rad: 0.6745697558958084 + } + gain_value { + gain_db: -2.0746 + phi_rad: 0.0 + theta_rad: 0.6754424205218056 + } + gain_value { + gain_db: -2.252 + phi_rad: 0.0 + theta_rad: 0.6763150851478027 + } + gain_value { + gain_db: -2.4618 + phi_rad: 0.0 + theta_rad: 0.6771877497737998 + } + gain_value { + gain_db: -2.6858 + phi_rad: 0.0 + theta_rad: 0.678060414399797 + } + gain_value { + gain_db: -2.8477 + phi_rad: 0.0 + theta_rad: 0.6789330790257941 + } + gain_value { + gain_db: -2.9152 + phi_rad: 0.0 + theta_rad: 0.6798057436517914 + } + gain_value { + gain_db: -2.8834 + phi_rad: 0.0 + theta_rad: 0.6806784082777885 + } + gain_value { + gain_db: -2.7761 + phi_rad: 0.0 + theta_rad: 0.6815510729037857 + } + gain_value { + gain_db: -2.5783 + phi_rad: 0.0 + theta_rad: 0.6824237375297829 + } + gain_value { + gain_db: -2.3311 + phi_rad: 0.0 + theta_rad: 0.68329640215578 + } + gain_value { + gain_db: -2.0131 + phi_rad: 0.0 + theta_rad: 0.6841690667817772 + } + gain_value { + gain_db: -1.8207 + phi_rad: 0.0 + theta_rad: 0.6850417314077744 + } + gain_value { + gain_db: -1.7421 + phi_rad: 0.0 + theta_rad: 0.6859143960337715 + } + gain_value { + gain_db: -1.7757 + phi_rad: 0.0 + theta_rad: 0.6867870606597687 + } + gain_value { + gain_db: -1.9379 + phi_rad: 0.0 + theta_rad: 0.6876597252857658 + } + gain_value { + gain_db: -2.0881 + phi_rad: 0.0 + theta_rad: 0.688532389911763 + } + gain_value { + gain_db: -2.2232 + phi_rad: 0.0 + theta_rad: 0.6894050545377601 + } + gain_value { + gain_db: -2.2843 + phi_rad: 0.0 + theta_rad: 0.6902777191637572 + } + gain_value { + gain_db: -2.2654 + phi_rad: 0.0 + theta_rad: 0.6911503837897546 + } + gain_value { + gain_db: -2.198 + phi_rad: 0.0 + theta_rad: 0.6920230484157517 + } + gain_value { + gain_db: -2.078 + phi_rad: 0.0 + theta_rad: 0.6928957130417489 + } + gain_value { + gain_db: -1.9351 + phi_rad: 0.0 + theta_rad: 0.693768377667746 + } + gain_value { + gain_db: -1.9402 + phi_rad: 0.0 + theta_rad: 0.6946410422937431 + } + gain_value { + gain_db: -2.0062 + phi_rad: 0.0 + theta_rad: 0.6955137069197403 + } + gain_value { + gain_db: -2.1463 + phi_rad: 0.0 + theta_rad: 0.6963863715457375 + } + gain_value { + gain_db: -2.3209 + phi_rad: 0.0 + theta_rad: 0.6972590361717347 + } + gain_value { + gain_db: -2.5571 + phi_rad: 0.0 + theta_rad: 0.6981317007977318 + } + gain_value { + gain_db: -2.8054 + phi_rad: 0.0 + theta_rad: 0.6990043654237289 + } + gain_value { + gain_db: -2.8911 + phi_rad: 0.0 + theta_rad: 0.6998770300497261 + } + gain_value { + gain_db: -2.91 + phi_rad: 0.0 + theta_rad: 0.7007496946757232 + } + gain_value { + gain_db: -2.8617 + phi_rad: 0.0 + theta_rad: 0.7016223593017206 + } + gain_value { + gain_db: -2.8704 + phi_rad: 0.0 + theta_rad: 0.7024950239277177 + } + gain_value { + gain_db: -2.9564 + phi_rad: 0.0 + theta_rad: 0.7033676885537148 + } + gain_value { + gain_db: -3.0796 + phi_rad: 0.0 + theta_rad: 0.704240353179712 + } + gain_value { + gain_db: -3.1892 + phi_rad: 0.0 + theta_rad: 0.7051130178057091 + } + gain_value { + gain_db: -3.376 + phi_rad: 0.0 + theta_rad: 0.7059856824317063 + } + gain_value { + gain_db: -3.6475 + phi_rad: 0.0 + theta_rad: 0.7068583470577035 + } + gain_value { + gain_db: -3.8874 + phi_rad: 0.0 + theta_rad: 0.7077310116837006 + } + gain_value { + gain_db: -4.1658 + phi_rad: 0.0 + theta_rad: 0.7086036763096978 + } + gain_value { + gain_db: -4.3583 + phi_rad: 0.0 + theta_rad: 0.7094763409356949 + } + gain_value { + gain_db: -4.4648 + phi_rad: 0.0 + theta_rad: 0.7103490055616922 + } + gain_value { + gain_db: -4.5298 + phi_rad: 0.0 + theta_rad: 0.7112216701876893 + } + gain_value { + gain_db: -4.5019 + phi_rad: 0.0 + theta_rad: 0.7120943348136864 + } + gain_value { + gain_db: -4.5853 + phi_rad: 0.0 + theta_rad: 0.7129669994396837 + } + gain_value { + gain_db: -4.6933 + phi_rad: 0.0 + theta_rad: 0.7138396640656808 + } + gain_value { + gain_db: -4.8621 + phi_rad: 0.0 + theta_rad: 0.714712328691678 + } + gain_value { + gain_db: -5.0144 + phi_rad: 0.0 + theta_rad: 0.7155849933176751 + } + gain_value { + gain_db: -5.1699 + phi_rad: 0.0 + theta_rad: 0.7164576579436722 + } + gain_value { + gain_db: -5.3624 + phi_rad: 0.0 + theta_rad: 0.7173303225696694 + } + gain_value { + gain_db: -5.6323 + phi_rad: 0.0 + theta_rad: 0.7182029871956666 + } + gain_value { + gain_db: -5.8421 + phi_rad: 0.0 + theta_rad: 0.7190756518216638 + } + gain_value { + gain_db: -5.9786 + phi_rad: 0.0 + theta_rad: 0.7199483164476609 + } + gain_value { + gain_db: -6.1876 + phi_rad: 0.0 + theta_rad: 0.7208209810736581 + } + gain_value { + gain_db: -6.4785 + phi_rad: 0.0 + theta_rad: 0.7216936456996553 + } + gain_value { + gain_db: -6.5991 + phi_rad: 0.0 + theta_rad: 0.7225663103256524 + } + gain_value { + gain_db: -6.647 + phi_rad: 0.0 + theta_rad: 0.7234389749516497 + } + gain_value { + gain_db: -6.6509 + phi_rad: 0.0 + theta_rad: 0.7243116395776468 + } + gain_value { + gain_db: -6.6494 + phi_rad: 0.0 + theta_rad: 0.7251843042036439 + } + gain_value { + gain_db: -6.5347 + phi_rad: 0.0 + theta_rad: 0.7260569688296411 + } + gain_value { + gain_db: -6.4379 + phi_rad: 0.0 + theta_rad: 0.7269296334556382 + } + gain_value { + gain_db: -6.3175 + phi_rad: 0.0 + theta_rad: 0.7278022980816354 + } + gain_value { + gain_db: -6.1706 + phi_rad: 0.0 + theta_rad: 0.7286749627076325 + } + gain_value { + gain_db: -6.1665 + phi_rad: 0.0 + theta_rad: 0.7295476273336297 + } + gain_value { + gain_db: -6.2609 + phi_rad: 0.0 + theta_rad: 0.730420291959627 + } + gain_value { + gain_db: -6.5555 + phi_rad: 0.0 + theta_rad: 0.7312929565856241 + } + gain_value { + gain_db: -6.9801 + phi_rad: 0.0 + theta_rad: 0.7321656212116213 + } + gain_value { + gain_db: -7.2261 + phi_rad: 0.0 + theta_rad: 0.7330382858376184 + } + gain_value { + gain_db: -7.3171 + phi_rad: 0.0 + theta_rad: 0.7339109504636155 + } + gain_value { + gain_db: -7.1386 + phi_rad: 0.0 + theta_rad: 0.7347836150896128 + } + gain_value { + gain_db: -7.0097 + phi_rad: 0.0 + theta_rad: 0.7356562797156099 + } + gain_value { + gain_db: -6.7089 + phi_rad: 0.0 + theta_rad: 0.7365289443416071 + } + gain_value { + gain_db: -6.4877 + phi_rad: 0.0 + theta_rad: 0.7374016089676042 + } + gain_value { + gain_db: -6.4123 + phi_rad: 0.0 + theta_rad: 0.7382742735936013 + } + gain_value { + gain_db: -6.3287 + phi_rad: 0.0 + theta_rad: 0.7391469382195985 + } + gain_value { + gain_db: -6.4113 + phi_rad: 0.0 + theta_rad: 0.7400196028455958 + } + gain_value { + gain_db: -6.489 + phi_rad: 0.0 + theta_rad: 0.740892267471593 + } + gain_value { + gain_db: -6.5013 + phi_rad: 0.0 + theta_rad: 0.7417649320975901 + } + gain_value { + gain_db: -6.6058 + phi_rad: 0.0 + theta_rad: 0.7426375967235872 + } + gain_value { + gain_db: -6.6242 + phi_rad: 0.0 + theta_rad: 0.7435102613495844 + } + gain_value { + gain_db: -6.523 + phi_rad: 0.0 + theta_rad: 0.7443829259755815 + } + gain_value { + gain_db: -6.3334 + phi_rad: 0.0 + theta_rad: 0.7452555906015788 + } + gain_value { + gain_db: -6.1078 + phi_rad: 0.0 + theta_rad: 0.7461282552275759 + } + gain_value { + gain_db: -6.0284 + phi_rad: 0.0 + theta_rad: 0.747000919853573 + } + gain_value { + gain_db: -6.2169 + phi_rad: 0.0 + theta_rad: 0.7478735844795702 + } + gain_value { + gain_db: -6.7733 + phi_rad: 0.0 + theta_rad: 0.7487462491055673 + } + gain_value { + gain_db: -8.0317 + phi_rad: 0.0 + theta_rad: 0.7496189137315646 + } + gain_value { + gain_db: -9.3392 + phi_rad: 0.0 + theta_rad: 0.7504915783575618 + } + gain_value { + gain_db: -8.6329 + phi_rad: 0.0 + theta_rad: 0.7513642429835589 + } + gain_value { + gain_db: -7.5348 + phi_rad: 0.0 + theta_rad: 0.7522369076095561 + } + gain_value { + gain_db: -7.0888 + phi_rad: 0.0 + theta_rad: 0.7531095722355532 + } + gain_value { + gain_db: -7.0135 + phi_rad: 0.0 + theta_rad: 0.7539822368615504 + } + gain_value { + gain_db: -7.0252 + phi_rad: 0.0 + theta_rad: 0.7548549014875475 + } + gain_value { + gain_db: -7.0754 + phi_rad: 0.0 + theta_rad: 0.7557275661135446 + } + gain_value { + gain_db: -7.3392 + phi_rad: 0.0 + theta_rad: 0.7566002307395419 + } + gain_value { + gain_db: -7.4844 + phi_rad: 0.0 + theta_rad: 0.757472895365539 + } + gain_value { + gain_db: -7.7088 + phi_rad: 0.0 + theta_rad: 0.7583455599915362 + } + gain_value { + gain_db: -7.9806 + phi_rad: 0.0 + theta_rad: 0.7592182246175333 + } + gain_value { + gain_db: -8.1525 + phi_rad: 0.0 + theta_rad: 0.7600908892435305 + } + gain_value { + gain_db: -8.3129 + phi_rad: 0.0 + theta_rad: 0.7609635538695277 + } + gain_value { + gain_db: -8.7777 + phi_rad: 0.0 + theta_rad: 0.7618362184955249 + } + gain_value { + gain_db: -8.9795 + phi_rad: 0.0 + theta_rad: 0.7627088831215221 + } + gain_value { + gain_db: -9.0158 + phi_rad: 0.0 + theta_rad: 0.7635815477475192 + } + gain_value { + gain_db: -8.6196 + phi_rad: 0.0 + theta_rad: 0.7644542123735163 + } + gain_value { + gain_db: -7.8342 + phi_rad: 0.0 + theta_rad: 0.7653268769995135 + } + gain_value { + gain_db: -7.1381 + phi_rad: 0.0 + theta_rad: 0.7661995416255106 + } + gain_value { + gain_db: -6.7303 + phi_rad: 0.0 + theta_rad: 0.7670722062515078 + } + gain_value { + gain_db: -6.4954 + phi_rad: 0.0 + theta_rad: 0.767944870877505 + } + gain_value { + gain_db: -6.5096 + phi_rad: 0.0 + theta_rad: 0.7688175355035021 + } + gain_value { + gain_db: -6.5609 + phi_rad: 0.0 + theta_rad: 0.7696902001294994 + } + gain_value { + gain_db: -6.5629 + phi_rad: 0.0 + theta_rad: 0.7705628647554965 + } + gain_value { + gain_db: -6.3802 + phi_rad: 0.0 + theta_rad: 0.7714355293814937 + } + gain_value { + gain_db: -6.2191 + phi_rad: 0.0 + theta_rad: 0.7723081940074908 + } + gain_value { + gain_db: -6.0906 + phi_rad: 0.0 + theta_rad: 0.773180858633488 + } + gain_value { + gain_db: -6.2955 + phi_rad: 0.0 + theta_rad: 0.7740535232594852 + } + gain_value { + gain_db: -6.6654 + phi_rad: 0.0 + theta_rad: 0.7749261878854823 + } + gain_value { + gain_db: -6.831 + phi_rad: 0.0 + theta_rad: 0.7757988525114795 + } + gain_value { + gain_db: -6.6946 + phi_rad: 0.0 + theta_rad: 0.7766715171374766 + } + gain_value { + gain_db: -6.4939 + phi_rad: 0.0 + theta_rad: 0.7775441817634737 + } + gain_value { + gain_db: -6.2871 + phi_rad: 0.0 + theta_rad: 0.778416846389471 + } + gain_value { + gain_db: -5.9501 + phi_rad: 0.0 + theta_rad: 0.7792895110154682 + } + gain_value { + gain_db: -5.5549 + phi_rad: 0.0 + theta_rad: 0.7801621756414654 + } + gain_value { + gain_db: -5.0714 + phi_rad: 0.0 + theta_rad: 0.7810348402674625 + } + gain_value { + gain_db: -4.6863 + phi_rad: 0.0 + theta_rad: 0.7819075048934596 + } + gain_value { + gain_db: -4.3743 + phi_rad: 0.0 + theta_rad: 0.7827801695194568 + } + gain_value { + gain_db: -4.1817 + phi_rad: 0.0 + theta_rad: 0.783652834145454 + } + gain_value { + gain_db: -4.1925 + phi_rad: 0.0 + theta_rad: 0.7845254987714512 + } + gain_value { + gain_db: -4.2594 + phi_rad: 0.0 + theta_rad: 0.7853981633974483 + } + gain_value { + gain_db: -4.4688 + phi_rad: 0.0 + theta_rad: 0.7862708280234454 + } + gain_value { + gain_db: -4.7267 + phi_rad: 0.0 + theta_rad: 0.7871434926494426 + } + gain_value { + gain_db: -5.0804 + phi_rad: 0.0 + theta_rad: 0.7880161572754397 + } + gain_value { + gain_db: -5.6801 + phi_rad: 0.0 + theta_rad: 0.788888821901437 + } + gain_value { + gain_db: -6.224 + phi_rad: 0.0 + theta_rad: 0.7897614865274342 + } + gain_value { + gain_db: -6.6758 + phi_rad: 0.0 + theta_rad: 0.7906341511534313 + } + gain_value { + gain_db: -6.8091 + phi_rad: 0.0 + theta_rad: 0.7915068157794285 + } + gain_value { + gain_db: -6.5935 + phi_rad: 0.0 + theta_rad: 0.7923794804054256 + } + gain_value { + gain_db: -6.1876 + phi_rad: 0.0 + theta_rad: 0.7932521450314228 + } + gain_value { + gain_db: -5.7504 + phi_rad: 0.0 + theta_rad: 0.7941248096574199 + } + gain_value { + gain_db: -5.3208 + phi_rad: 0.0 + theta_rad: 0.794997474283417 + } + gain_value { + gain_db: -5.106 + phi_rad: 0.0 + theta_rad: 0.7958701389094143 + } + gain_value { + gain_db: -5.1112 + phi_rad: 0.0 + theta_rad: 0.7967428035354114 + } + gain_value { + gain_db: -5.4268 + phi_rad: 0.0 + theta_rad: 0.7976154681614086 + } + gain_value { + gain_db: -5.9362 + phi_rad: 0.0 + theta_rad: 0.7984881327874057 + } + gain_value { + gain_db: -6.6585 + phi_rad: 0.0 + theta_rad: 0.7993607974134029 + } + gain_value { + gain_db: -7.5291 + phi_rad: 0.0 + theta_rad: 0.8002334620394002 + } + gain_value { + gain_db: -8.1811 + phi_rad: 0.0 + theta_rad: 0.8011061266653973 + } + gain_value { + gain_db: -8.2567 + phi_rad: 0.0 + theta_rad: 0.8019787912913945 + } + gain_value { + gain_db: -7.9595 + phi_rad: 0.0 + theta_rad: 0.8028514559173916 + } + gain_value { + gain_db: -7.7759 + phi_rad: 0.0 + theta_rad: 0.8037241205433887 + } + gain_value { + gain_db: -7.6921 + phi_rad: 0.0 + theta_rad: 0.8045967851693859 + } + gain_value { + gain_db: -7.7219 + phi_rad: 0.0 + theta_rad: 0.805469449795383 + } + gain_value { + gain_db: -7.8739 + phi_rad: 0.0 + theta_rad: 0.8063421144213803 + } + gain_value { + gain_db: -7.9577 + phi_rad: 0.0 + theta_rad: 0.8072147790473774 + } + gain_value { + gain_db: -7.9532 + phi_rad: 0.0 + theta_rad: 0.8080874436733745 + } + gain_value { + gain_db: -7.6958 + phi_rad: 0.0 + theta_rad: 0.8089601082993718 + } + gain_value { + gain_db: -7.4747 + phi_rad: 0.0 + theta_rad: 0.8098327729253689 + } + gain_value { + gain_db: -7.2625 + phi_rad: 0.0 + theta_rad: 0.8107054375513661 + } + gain_value { + gain_db: -7.1709 + phi_rad: 0.0 + theta_rad: 0.8115781021773633 + } + gain_value { + gain_db: -7.3226 + phi_rad: 0.0 + theta_rad: 0.8124507668033604 + } + gain_value { + gain_db: -7.6653 + phi_rad: 0.0 + theta_rad: 0.8133234314293576 + } + gain_value { + gain_db: -8.0548 + phi_rad: 0.0 + theta_rad: 0.8141960960553547 + } + gain_value { + gain_db: -8.3595 + phi_rad: 0.0 + theta_rad: 0.8150687606813519 + } + gain_value { + gain_db: -8.5821 + phi_rad: 0.0 + theta_rad: 0.815941425307349 + } + gain_value { + gain_db: -8.7196 + phi_rad: 0.0 + theta_rad: 0.8168140899333461 + } + gain_value { + gain_db: -8.8522 + phi_rad: 0.0 + theta_rad: 0.8176867545593434 + } + gain_value { + gain_db: -8.341 + phi_rad: 0.0 + theta_rad: 0.8185594191853406 + } + gain_value { + gain_db: -7.539 + phi_rad: 0.0 + theta_rad: 0.8194320838113378 + } + gain_value { + gain_db: -6.8289 + phi_rad: 0.0 + theta_rad: 0.8203047484373349 + } + gain_value { + gain_db: -6.3166 + phi_rad: 0.0 + theta_rad: 0.821177413063332 + } + gain_value { + gain_db: -6.0477 + phi_rad: 0.0 + theta_rad: 0.8220500776893293 + } + gain_value { + gain_db: -6.0407 + phi_rad: 0.0 + theta_rad: 0.8229227423153264 + } + gain_value { + gain_db: -6.2952 + phi_rad: 0.0 + theta_rad: 0.8237954069413236 + } + gain_value { + gain_db: -6.7723 + phi_rad: 0.0 + theta_rad: 0.8246680715673207 + } + gain_value { + gain_db: -7.1965 + phi_rad: 0.0 + theta_rad: 0.8255407361933178 + } + gain_value { + gain_db: -7.3731 + phi_rad: 0.0 + theta_rad: 0.826413400819315 + } + gain_value { + gain_db: -7.2099 + phi_rad: 0.0 + theta_rad: 0.8272860654453121 + } + gain_value { + gain_db: -7.0692 + phi_rad: 0.0 + theta_rad: 0.8281587300713095 + } + gain_value { + gain_db: -6.8766 + phi_rad: 0.0 + theta_rad: 0.8290313946973066 + } + gain_value { + gain_db: -6.6501 + phi_rad: 0.0 + theta_rad: 0.8299040593233037 + } + gain_value { + gain_db: -6.4899 + phi_rad: 0.0 + theta_rad: 0.8307767239493009 + } + gain_value { + gain_db: -6.3089 + phi_rad: 0.0 + theta_rad: 0.831649388575298 + } + gain_value { + gain_db: -6.0767 + phi_rad: 0.0 + theta_rad: 0.8325220532012952 + } + gain_value { + gain_db: -5.912 + phi_rad: 0.0 + theta_rad: 0.8333947178272924 + } + gain_value { + gain_db: -5.7437 + phi_rad: 0.0 + theta_rad: 0.8342673824532895 + } + gain_value { + gain_db: -5.5749 + phi_rad: 0.0 + theta_rad: 0.8351400470792867 + } + gain_value { + gain_db: -5.6222 + phi_rad: 0.0 + theta_rad: 0.8360127117052838 + } + gain_value { + gain_db: -5.8401 + phi_rad: 0.0 + theta_rad: 0.836885376331281 + } + gain_value { + gain_db: -6.2559 + phi_rad: 0.0 + theta_rad: 0.8377580409572782 + } + gain_value { + gain_db: -6.739 + phi_rad: 0.0 + theta_rad: 0.8386307055832753 + } + gain_value { + gain_db: -7.2602 + phi_rad: 0.0 + theta_rad: 0.8395033702092726 + } + gain_value { + gain_db: -7.6636 + phi_rad: 0.0 + theta_rad: 0.8403760348352697 + } + gain_value { + gain_db: -7.769 + phi_rad: 0.0 + theta_rad: 0.8412486994612669 + } + gain_value { + gain_db: -7.6137 + phi_rad: 0.0 + theta_rad: 0.842121364087264 + } + gain_value { + gain_db: -7.3214 + phi_rad: 0.0 + theta_rad: 0.8429940287132611 + } + gain_value { + gain_db: -6.9906 + phi_rad: 0.0 + theta_rad: 0.8438666933392583 + } + gain_value { + gain_db: -6.4124 + phi_rad: 0.0 + theta_rad: 0.8447393579652555 + } + gain_value { + gain_db: -5.7954 + phi_rad: 0.0 + theta_rad: 0.8456120225912527 + } + gain_value { + gain_db: -5.3235 + phi_rad: 0.0 + theta_rad: 0.8464846872172498 + } + gain_value { + gain_db: -5.1033 + phi_rad: 0.0 + theta_rad: 0.8473573518432469 + } + gain_value { + gain_db: -5.1744 + phi_rad: 0.0 + theta_rad: 0.8482300164692442 + } + gain_value { + gain_db: -5.5468 + phi_rad: 0.0 + theta_rad: 0.8491026810952413 + } + gain_value { + gain_db: -6.0087 + phi_rad: 0.0 + theta_rad: 0.8499753457212386 + } + gain_value { + gain_db: -6.7659 + phi_rad: 0.0 + theta_rad: 0.8508480103472357 + } + gain_value { + gain_db: -7.5183 + phi_rad: 0.0 + theta_rad: 0.8517206749732328 + } + gain_value { + gain_db: -8.1041 + phi_rad: 0.0 + theta_rad: 0.85259333959923 + } + gain_value { + gain_db: -8.2979 + phi_rad: 0.0 + theta_rad: 0.8534660042252271 + } + gain_value { + gain_db: -7.9837 + phi_rad: 0.0 + theta_rad: 0.8543386688512243 + } + gain_value { + gain_db: -7.2001 + phi_rad: 0.0 + theta_rad: 0.8552113334772214 + } + gain_value { + gain_db: -6.2765 + phi_rad: 0.0 + theta_rad: 0.8560839981032186 + } + gain_value { + gain_db: -5.4891 + phi_rad: 0.0 + theta_rad: 0.8569566627292158 + } + gain_value { + gain_db: -5.0048 + phi_rad: 0.0 + theta_rad: 0.857829327355213 + } + gain_value { + gain_db: -4.7105 + phi_rad: 0.0 + theta_rad: 0.8587019919812102 + } + gain_value { + gain_db: -4.6353 + phi_rad: 0.0 + theta_rad: 0.8595746566072073 + } + gain_value { + gain_db: -4.8769 + phi_rad: 0.0 + theta_rad: 0.8604473212332044 + } + gain_value { + gain_db: -5.2973 + phi_rad: 0.0 + theta_rad: 0.8613199858592017 + } + gain_value { + gain_db: -5.9985 + phi_rad: 0.0 + theta_rad: 0.8621926504851988 + } + gain_value { + gain_db: -6.7298 + phi_rad: 0.0 + theta_rad: 0.863065315111196 + } + gain_value { + gain_db: -7.3654 + phi_rad: 0.0 + theta_rad: 0.8639379797371931 + } + gain_value { + gain_db: -7.5282 + phi_rad: 0.0 + theta_rad: 0.8648106443631902 + } + gain_value { + gain_db: -7.1999 + phi_rad: 0.0 + theta_rad: 0.8656833089891874 + } + gain_value { + gain_db: -6.4572 + phi_rad: 0.0 + theta_rad: 0.8665559736151845 + } + gain_value { + gain_db: -5.5675 + phi_rad: 0.0 + theta_rad: 0.8674286382411819 + } + gain_value { + gain_db: -4.8745 + phi_rad: 0.0 + theta_rad: 0.868301302867179 + } + gain_value { + gain_db: -4.4872 + phi_rad: 0.0 + theta_rad: 0.8691739674931761 + } + gain_value { + gain_db: -4.4561 + phi_rad: 0.0 + theta_rad: 0.8700466321191733 + } + gain_value { + gain_db: -4.7319 + phi_rad: 0.0 + theta_rad: 0.8709192967451704 + } + gain_value { + gain_db: -5.2701 + phi_rad: 0.0 + theta_rad: 0.8717919613711677 + } + gain_value { + gain_db: -5.9707 + phi_rad: 0.0 + theta_rad: 0.8726646259971648 + } + gain_value { + gain_db: -6.6369 + phi_rad: 0.0 + theta_rad: 0.8735372906231619 + } + gain_value { + gain_db: -7.1967 + phi_rad: 0.0 + theta_rad: 0.8744099552491591 + } + gain_value { + gain_db: -7.3723 + phi_rad: 0.0 + theta_rad: 0.8752826198751562 + } + gain_value { + gain_db: -7.2848 + phi_rad: 0.0 + theta_rad: 0.8761552845011534 + } + gain_value { + gain_db: -7.0135 + phi_rad: 0.0 + theta_rad: 0.8770279491271507 + } + gain_value { + gain_db: -6.6563 + phi_rad: 0.0 + theta_rad: 0.8779006137531478 + } + gain_value { + gain_db: -6.3343 + phi_rad: 0.0 + theta_rad: 0.878773278379145 + } + gain_value { + gain_db: -6.2158 + phi_rad: 0.0 + theta_rad: 0.8796459430051421 + } + gain_value { + gain_db: -6.3721 + phi_rad: 0.0 + theta_rad: 0.8805186076311393 + } + gain_value { + gain_db: -6.6598 + phi_rad: 0.0 + theta_rad: 0.8813912722571364 + } + gain_value { + gain_db: -7.0327 + phi_rad: 0.0 + theta_rad: 0.8822639368831335 + } + gain_value { + gain_db: -7.3898 + phi_rad: 0.0 + theta_rad: 0.8831366015091308 + } + gain_value { + gain_db: -7.7899 + phi_rad: 0.0 + theta_rad: 0.8840092661351279 + } + gain_value { + gain_db: -8.4611 + phi_rad: 0.0 + theta_rad: 0.8848819307611251 + } + gain_value { + gain_db: -9.3404 + phi_rad: 0.0 + theta_rad: 0.8857545953871222 + } + gain_value { + gain_db: -10.101 + phi_rad: 0.0 + theta_rad: 0.8866272600131193 + } + gain_value { + gain_db: -9.74 + phi_rad: 0.0 + theta_rad: 0.8874999246391166 + } + gain_value { + gain_db: -9.0159 + phi_rad: 0.0 + theta_rad: 0.8883725892651138 + } + gain_value { + gain_db: -8.3577 + phi_rad: 0.0 + theta_rad: 0.889245253891111 + } + gain_value { + gain_db: -7.9545 + phi_rad: 0.0 + theta_rad: 0.8901179185171081 + } + gain_value { + gain_db: -7.939 + phi_rad: 0.0 + theta_rad: 0.8909905831431052 + } + gain_value { + gain_db: -8.0902 + phi_rad: 0.0 + theta_rad: 0.8918632477691024 + } + gain_value { + gain_db: -8.3511 + phi_rad: 0.0 + theta_rad: 0.8927359123950995 + } + gain_value { + gain_db: -8.7993 + phi_rad: 0.0 + theta_rad: 0.8936085770210968 + } + gain_value { + gain_db: -9.0535 + phi_rad: 0.0 + theta_rad: 0.8944812416470939 + } + gain_value { + gain_db: -9.3041 + phi_rad: 0.0 + theta_rad: 0.895353906273091 + } + gain_value { + gain_db: -9.7188 + phi_rad: 0.0 + theta_rad: 0.8962265708990882 + } + gain_value { + gain_db: -9.9535 + phi_rad: 0.0 + theta_rad: 0.8970992355250854 + } + gain_value { + gain_db: -9.6746 + phi_rad: 0.0 + theta_rad: 0.8979719001510826 + } + gain_value { + gain_db: -9.1573 + phi_rad: 0.0 + theta_rad: 0.8988445647770797 + } + gain_value { + gain_db: -8.6101 + phi_rad: 0.0 + theta_rad: 0.8997172294030769 + } + gain_value { + gain_db: -8.5936 + phi_rad: 0.0 + theta_rad: 0.9005898940290741 + } + gain_value { + gain_db: -8.8653 + phi_rad: 0.0 + theta_rad: 0.9014625586550712 + } + gain_value { + gain_db: -9.545 + phi_rad: 0.0 + theta_rad: 0.9023352232810684 + } + gain_value { + gain_db: -10.131 + phi_rad: 0.0 + theta_rad: 0.9032078879070655 + } + gain_value { + gain_db: -9.9272 + phi_rad: 0.0 + theta_rad: 0.9040805525330626 + } + gain_value { + gain_db: -9.2451 + phi_rad: 0.0 + theta_rad: 0.9049532171590599 + } + gain_value { + gain_db: -8.7167 + phi_rad: 0.0 + theta_rad: 0.905825881785057 + } + gain_value { + gain_db: -8.4287 + phi_rad: 0.0 + theta_rad: 0.9066985464110543 + } + gain_value { + gain_db: -8.1795 + phi_rad: 0.0 + theta_rad: 0.9075712110370514 + } + gain_value { + gain_db: -8.1361 + phi_rad: 0.0 + theta_rad: 0.9084438756630485 + } + gain_value { + gain_db: -8.2142 + phi_rad: 0.0 + theta_rad: 0.9093165402890457 + } + gain_value { + gain_db: -8.4448 + phi_rad: 0.0 + theta_rad: 0.9101892049150428 + } + gain_value { + gain_db: -8.9834 + phi_rad: 0.0 + theta_rad: 0.9110618695410401 + } + gain_value { + gain_db: -10.041 + phi_rad: 0.0 + theta_rad: 0.9119345341670372 + } + gain_value { + gain_db: -11.7 + phi_rad: 0.0 + theta_rad: 0.9128071987930343 + } + gain_value { + gain_db: -12.247 + phi_rad: 0.0 + theta_rad: 0.9136798634190315 + } + gain_value { + gain_db: -11.091 + phi_rad: 0.0 + theta_rad: 0.9145525280450286 + } + gain_value { + gain_db: -9.9752 + phi_rad: 0.0 + theta_rad: 0.9154251926710258 + } + gain_value { + gain_db: -9.3587 + phi_rad: 0.0 + theta_rad: 0.9162978572970231 + } + gain_value { + gain_db: -9.0317 + phi_rad: 0.0 + theta_rad: 0.9171705219230202 + } + gain_value { + gain_db: -8.9271 + phi_rad: 0.0 + theta_rad: 0.9180431865490174 + } + gain_value { + gain_db: -9.0484 + phi_rad: 0.0 + theta_rad: 0.9189158511750145 + } + gain_value { + gain_db: -9.1096 + phi_rad: 0.0 + theta_rad: 0.9197885158010117 + } + gain_value { + gain_db: -9.2861 + phi_rad: 0.0 + theta_rad: 0.9206611804270088 + } + gain_value { + gain_db: -9.7105 + phi_rad: 0.0 + theta_rad: 0.921533845053006 + } + gain_value { + gain_db: -10.4 + phi_rad: 0.0 + theta_rad: 0.9224065096790032 + } + gain_value { + gain_db: -10.892 + phi_rad: 0.0 + theta_rad: 0.9232791743050003 + } + gain_value { + gain_db: -10.855 + phi_rad: 0.0 + theta_rad: 0.9241518389309975 + } + gain_value { + gain_db: -10.572 + phi_rad: 0.0 + theta_rad: 0.9250245035569946 + } + gain_value { + gain_db: -10.674 + phi_rad: 0.0 + theta_rad: 0.9258971681829917 + } + gain_value { + gain_db: -10.476 + phi_rad: 0.0 + theta_rad: 0.9267698328089891 + } + gain_value { + gain_db: -9.6755 + phi_rad: 0.0 + theta_rad: 0.9276424974349862 + } + gain_value { + gain_db: -8.8808 + phi_rad: 0.0 + theta_rad: 0.9285151620609834 + } + gain_value { + gain_db: -8.1785 + phi_rad: 0.0 + theta_rad: 0.9293878266869805 + } + gain_value { + gain_db: -7.6466 + phi_rad: 0.0 + theta_rad: 0.9302604913129776 + } + gain_value { + gain_db: -7.4276 + phi_rad: 0.0 + theta_rad: 0.9311331559389748 + } + gain_value { + gain_db: -7.5708 + phi_rad: 0.0 + theta_rad: 0.9320058205649719 + } + gain_value { + gain_db: -8.1446 + phi_rad: 0.0 + theta_rad: 0.9328784851909692 + } + gain_value { + gain_db: -9.0898 + phi_rad: 0.0 + theta_rad: 0.9337511498169663 + } + gain_value { + gain_db: -10.182 + phi_rad: 0.0 + theta_rad: 0.9346238144429634 + } + gain_value { + gain_db: -11.023 + phi_rad: 0.0 + theta_rad: 0.9354964790689606 + } + gain_value { + gain_db: -10.776 + phi_rad: 0.0 + theta_rad: 0.9363691436949578 + } + gain_value { + gain_db: -9.7995 + phi_rad: 0.0 + theta_rad: 0.937241808320955 + } + gain_value { + gain_db: -8.5652 + phi_rad: 0.0 + theta_rad: 0.9381144729469522 + } + gain_value { + gain_db: -7.339 + phi_rad: 0.0 + theta_rad: 0.9389871375729493 + } + gain_value { + gain_db: -6.3281 + phi_rad: 0.0 + theta_rad: 0.9398598021989465 + } + gain_value { + gain_db: -5.7843 + phi_rad: 0.0 + theta_rad: 0.9407324668249436 + } + gain_value { + gain_db: -5.6196 + phi_rad: 0.0 + theta_rad: 0.9416051314509408 + } + gain_value { + gain_db: -5.9044 + phi_rad: 0.0 + theta_rad: 0.9424777960769379 + } + gain_value { + gain_db: -6.6077 + phi_rad: 0.0 + theta_rad: 0.943350460702935 + } + gain_value { + gain_db: -7.6941 + phi_rad: 0.0 + theta_rad: 0.9442231253289323 + } + gain_value { + gain_db: -8.9729 + phi_rad: 0.0 + theta_rad: 0.9450957899549294 + } + gain_value { + gain_db: -10.167 + phi_rad: 0.0 + theta_rad: 0.9459684545809267 + } + gain_value { + gain_db: -11.415 + phi_rad: 0.0 + theta_rad: 0.9468411192069238 + } + gain_value { + gain_db: -11.66 + phi_rad: 0.0 + theta_rad: 0.9477137838329209 + } + gain_value { + gain_db: -10.332 + phi_rad: 0.0 + theta_rad: 0.9485864484589182 + } + gain_value { + gain_db: -9.1708 + phi_rad: 0.0 + theta_rad: 0.9494591130849153 + } + gain_value { + gain_db: -8.2035 + phi_rad: 0.0 + theta_rad: 0.9503317777109125 + } + gain_value { + gain_db: -7.7268 + phi_rad: 0.0 + theta_rad: 0.9512044423369096 + } + gain_value { + gain_db: -7.5779 + phi_rad: 0.0 + theta_rad: 0.9520771069629067 + } + gain_value { + gain_db: -7.7933 + phi_rad: 0.0 + theta_rad: 0.9529497715889039 + } + gain_value { + gain_db: -8.4269 + phi_rad: 0.0 + theta_rad: 0.953822436214901 + } + gain_value { + gain_db: -9.3305 + phi_rad: 0.0 + theta_rad: 0.9546951008408983 + } + gain_value { + gain_db: -10.8 + phi_rad: 0.0 + theta_rad: 0.9555677654668955 + } + gain_value { + gain_db: -12.475 + phi_rad: 0.0 + theta_rad: 0.9564404300928926 + } + gain_value { + gain_db: -15.177 + phi_rad: 0.0 + theta_rad: 0.9573130947188898 + } + gain_value { + gain_db: -17.127 + phi_rad: 0.0 + theta_rad: 0.9581857593448869 + } + gain_value { + gain_db: -13.406 + phi_rad: 0.0 + theta_rad: 0.9590584239708841 + } + gain_value { + gain_db: -11.328 + phi_rad: 0.0 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -9.9841 + phi_rad: 0.0 + theta_rad: 0.9608037532228784 + } + gain_value { + gain_db: -9.2924 + phi_rad: 0.0 + theta_rad: 0.9616764178488756 + } + gain_value { + gain_db: -9.1818 + phi_rad: 0.0 + theta_rad: 0.9625490824748727 + } + gain_value { + gain_db: -9.5859 + phi_rad: 0.0 + theta_rad: 0.9634217471008699 + } + gain_value { + gain_db: -10.406 + phi_rad: 0.0 + theta_rad: 0.964294411726867 + } + gain_value { + gain_db: -11.593 + phi_rad: 0.0 + theta_rad: 0.9651670763528641 + } + gain_value { + gain_db: -12.795 + phi_rad: 0.0 + theta_rad: 0.9660397409788615 + } + gain_value { + gain_db: -13.284 + phi_rad: 0.0 + theta_rad: 0.9669124056048586 + } + gain_value { + gain_db: -12.749 + phi_rad: 0.0 + theta_rad: 0.9677850702308558 + } + gain_value { + gain_db: -11.847 + phi_rad: 0.0 + theta_rad: 0.9686577348568529 + } + gain_value { + gain_db: -10.564 + phi_rad: 0.0 + theta_rad: 0.96953039948285 + } + gain_value { + gain_db: -9.4064 + phi_rad: 0.0 + theta_rad: 0.9704030641088472 + } + gain_value { + gain_db: -8.6583 + phi_rad: 0.0 + theta_rad: 0.9712757287348444 + } + gain_value { + gain_db: -8.1749 + phi_rad: 0.0 + theta_rad: 0.9721483933608416 + } + gain_value { + gain_db: -8.0897 + phi_rad: 0.0 + theta_rad: 0.9730210579868387 + } + gain_value { + gain_db: -8.3683 + phi_rad: 0.0 + theta_rad: 0.9738937226128358 + } + gain_value { + gain_db: -9.0202 + phi_rad: 0.0 + theta_rad: 0.9747663872388331 + } + gain_value { + gain_db: -9.7916 + phi_rad: 0.0 + theta_rad: 0.9756390518648302 + } + gain_value { + gain_db: -10.549 + phi_rad: 0.0 + theta_rad: 0.9765117164908275 + } + gain_value { + gain_db: -11.132 + phi_rad: 0.0 + theta_rad: 0.9773843811168246 + } + gain_value { + gain_db: -11.248 + phi_rad: 0.0 + theta_rad: 0.9782570457428217 + } + gain_value { + gain_db: -11.031 + phi_rad: 0.0 + theta_rad: 0.9791297103688189 + } + gain_value { + gain_db: -10.238 + phi_rad: 0.0 + theta_rad: 0.980002374994816 + } + gain_value { + gain_db: -9.3625 + phi_rad: 0.0 + theta_rad: 0.9808750396208132 + } + gain_value { + gain_db: -8.6838 + phi_rad: 0.0 + theta_rad: 0.9817477042468103 + } + gain_value { + gain_db: -8.3053 + phi_rad: 0.0 + theta_rad: 0.9826203688728075 + } + gain_value { + gain_db: -8.2864 + phi_rad: 0.0 + theta_rad: 0.9834930334988047 + } + gain_value { + gain_db: -8.6447 + phi_rad: 0.0 + theta_rad: 0.9843656981248018 + } + gain_value { + gain_db: -9.4316 + phi_rad: 0.0 + theta_rad: 0.9852383627507991 + } + gain_value { + gain_db: -10.337 + phi_rad: 0.0 + theta_rad: 0.9861110273767962 + } + gain_value { + gain_db: -11.095 + phi_rad: 0.0 + theta_rad: 0.9869836920027933 + } + gain_value { + gain_db: -11.293 + phi_rad: 0.0 + theta_rad: 0.9878563566287906 + } + gain_value { + gain_db: -11.239 + phi_rad: 0.0 + theta_rad: 0.9887290212547877 + } + gain_value { + gain_db: -10.785 + phi_rad: 0.0 + theta_rad: 0.9896016858807849 + } + gain_value { + gain_db: -10.236 + phi_rad: 0.0 + theta_rad: 0.990474350506782 + } + gain_value { + gain_db: -9.9573 + phi_rad: 0.0 + theta_rad: 0.9913470151327791 + } + gain_value { + gain_db: -10.031 + phi_rad: 0.0 + theta_rad: 0.9922196797587763 + } + gain_value { + gain_db: -10.384 + phi_rad: 0.0 + theta_rad: 0.9930923443847735 + } + gain_value { + gain_db: -10.911 + phi_rad: 0.0 + theta_rad: 0.9939650090107707 + } + gain_value { + gain_db: -11.308 + phi_rad: 0.0 + theta_rad: 0.9948376736367679 + } + gain_value { + gain_db: -11.404 + phi_rad: 0.0 + theta_rad: 0.995710338262765 + } + gain_value { + gain_db: -11.124 + phi_rad: 0.0 + theta_rad: 0.9965830028887622 + } + gain_value { + gain_db: -10.852 + phi_rad: 0.0 + theta_rad: 0.9974556675147593 + } + gain_value { + gain_db: -10.665 + phi_rad: 0.0 + theta_rad: 0.9983283321407566 + } + gain_value { + gain_db: -10.669 + phi_rad: 0.0 + theta_rad: 0.9992009967667537 + } + gain_value { + gain_db: -10.908 + phi_rad: 0.0 + theta_rad: 1.0000736613927508 + } + gain_value { + gain_db: -11.432 + phi_rad: 0.0 + theta_rad: 1.0009463260187481 + } + gain_value { + gain_db: -12.605 + phi_rad: 0.0 + theta_rad: 1.0018189906447452 + } + gain_value { + gain_db: -15.233 + phi_rad: 0.0 + theta_rad: 1.0026916552707423 + } + gain_value { + gain_db: -15.17 + phi_rad: 0.0 + theta_rad: 1.0035643198967394 + } + gain_value { + gain_db: -12.784 + phi_rad: 0.0 + theta_rad: 1.0044369845227366 + } + gain_value { + gain_db: -11.53 + phi_rad: 0.0 + theta_rad: 1.0053096491487339 + } + gain_value { + gain_db: -10.773 + phi_rad: 0.0 + theta_rad: 1.006182313774731 + } + gain_value { + gain_db: -10.319 + phi_rad: 0.0 + theta_rad: 1.007054978400728 + } + gain_value { + gain_db: -9.9895 + phi_rad: 0.0 + theta_rad: 1.0079276430267252 + } + gain_value { + gain_db: -9.9413 + phi_rad: 0.0 + theta_rad: 1.0088003076527223 + } + gain_value { + gain_db: -9.9384 + phi_rad: 0.0 + theta_rad: 1.0096729722787197 + } + gain_value { + gain_db: -10.286 + phi_rad: 0.0 + theta_rad: 1.0105456369047168 + } + gain_value { + gain_db: -11.069 + phi_rad: 0.0 + theta_rad: 1.011418301530714 + } + gain_value { + gain_db: -11.927 + phi_rad: 0.0 + theta_rad: 1.0122909661567112 + } + gain_value { + gain_db: -13.206 + phi_rad: 0.0 + theta_rad: 1.0131636307827083 + } + gain_value { + gain_db: -15.094 + phi_rad: 0.0 + theta_rad: 1.0140362954087054 + } + gain_value { + gain_db: -15.951 + phi_rad: 0.0 + theta_rad: 1.0149089600347025 + } + gain_value { + gain_db: -14.786 + phi_rad: 0.0 + theta_rad: 1.0157816246606999 + } + gain_value { + gain_db: -13.714 + phi_rad: 0.0 + theta_rad: 1.016654289286697 + } + gain_value { + gain_db: -12.799 + phi_rad: 0.0 + theta_rad: 1.017526953912694 + } + gain_value { + gain_db: -11.781 + phi_rad: 0.0 + theta_rad: 1.0183996185386912 + } + gain_value { + gain_db: -11.267 + phi_rad: 0.0 + theta_rad: 1.0192722831646885 + } + gain_value { + gain_db: -11.038 + phi_rad: 0.0 + theta_rad: 1.0201449477906857 + } + gain_value { + gain_db: -10.891 + phi_rad: 0.0 + theta_rad: 1.0210176124166828 + } + gain_value { + gain_db: -11.099 + phi_rad: 0.0 + theta_rad: 1.0218902770426799 + } + gain_value { + gain_db: -11.461 + phi_rad: 0.0 + theta_rad: 1.0227629416686772 + } + gain_value { + gain_db: -11.837 + phi_rad: 0.0 + theta_rad: 1.0236356062946743 + } + gain_value { + gain_db: -12.453 + phi_rad: 0.0 + theta_rad: 1.0245082709206714 + } + gain_value { + gain_db: -13.098 + phi_rad: 0.0 + theta_rad: 1.0253809355466685 + } + gain_value { + gain_db: -13.899 + phi_rad: 0.0 + theta_rad: 1.0262536001726656 + } + gain_value { + gain_db: -14.772 + phi_rad: 0.0 + theta_rad: 1.027126264798663 + } + gain_value { + gain_db: -14.684 + phi_rad: 0.0 + theta_rad: 1.02799892942466 + } + gain_value { + gain_db: -13.326 + phi_rad: 0.0 + theta_rad: 1.0288715940506574 + } + gain_value { + gain_db: -12.419 + phi_rad: 0.0 + theta_rad: 1.0297442586766545 + } + gain_value { + gain_db: -11.916 + phi_rad: 0.0 + theta_rad: 1.0306169233026516 + } + gain_value { + gain_db: -11.774 + phi_rad: 0.0 + theta_rad: 1.0314895879286488 + } + gain_value { + gain_db: -11.514 + phi_rad: 0.0 + theta_rad: 1.0323622525546459 + } + gain_value { + gain_db: -11.456 + phi_rad: 0.0 + theta_rad: 1.0332349171806432 + } + gain_value { + gain_db: -11.418 + phi_rad: 0.0 + theta_rad: 1.0341075818066403 + } + gain_value { + gain_db: -11.739 + phi_rad: 0.0 + theta_rad: 1.0349802464326374 + } + gain_value { + gain_db: -12.086 + phi_rad: 0.0 + theta_rad: 1.0358529110586345 + } + gain_value { + gain_db: -12.652 + phi_rad: 0.0 + theta_rad: 1.0367255756846316 + } + gain_value { + gain_db: -13.254 + phi_rad: 0.0 + theta_rad: 1.037598240310629 + } + gain_value { + gain_db: -13.048 + phi_rad: 0.0 + theta_rad: 1.038470904936626 + } + gain_value { + gain_db: -12.705 + phi_rad: 0.0 + theta_rad: 1.0393435695626232 + } + gain_value { + gain_db: -12.582 + phi_rad: 0.0 + theta_rad: 1.0402162341886205 + } + gain_value { + gain_db: -12.183 + phi_rad: 0.0 + theta_rad: 1.0410888988146176 + } + gain_value { + gain_db: -11.684 + phi_rad: 0.0 + theta_rad: 1.0419615634406147 + } + gain_value { + gain_db: -11.071 + phi_rad: 0.0 + theta_rad: 1.0428342280666119 + } + gain_value { + gain_db: -10.583 + phi_rad: 0.0 + theta_rad: 1.043706892692609 + } + gain_value { + gain_db: -10.416 + phi_rad: 0.0 + theta_rad: 1.0445795573186063 + } + gain_value { + gain_db: -10.368 + phi_rad: 0.0 + theta_rad: 1.0454522219446034 + } + gain_value { + gain_db: -10.669 + phi_rad: 0.0 + theta_rad: 1.0463248865706005 + } + gain_value { + gain_db: -11.108 + phi_rad: 0.0 + theta_rad: 1.0471975511965976 + } + gain_value { + gain_db: -11.394 + phi_rad: 0.0 + theta_rad: 1.0480702158225947 + } + gain_value { + gain_db: -11.13 + phi_rad: 0.0 + theta_rad: 1.048942880448592 + } + gain_value { + gain_db: -11.012 + phi_rad: 0.0 + theta_rad: 1.0498155450745892 + } + gain_value { + gain_db: -10.986 + phi_rad: 0.0 + theta_rad: 1.0506882097005865 + } + gain_value { + gain_db: -11.252 + phi_rad: 0.0 + theta_rad: 1.0515608743265836 + } + gain_value { + gain_db: -11.824 + phi_rad: 0.0 + theta_rad: 1.0524335389525807 + } + gain_value { + gain_db: -12.267 + phi_rad: 0.0 + theta_rad: 1.0533062035785778 + } + gain_value { + gain_db: -12.238 + phi_rad: 0.0 + theta_rad: 1.054178868204575 + } + gain_value { + gain_db: -11.949 + phi_rad: 0.0 + theta_rad: 1.0550515328305723 + } + gain_value { + gain_db: -11.714 + phi_rad: 0.0 + theta_rad: 1.0559241974565694 + } + gain_value { + gain_db: -11.29 + phi_rad: 0.0 + theta_rad: 1.0567968620825665 + } + gain_value { + gain_db: -10.844 + phi_rad: 0.0 + theta_rad: 1.0576695267085636 + } + gain_value { + gain_db: -10.477 + phi_rad: 0.0 + theta_rad: 1.058542191334561 + } + gain_value { + gain_db: -10.54 + phi_rad: 0.0 + theta_rad: 1.059414855960558 + } + gain_value { + gain_db: -10.918 + phi_rad: 0.0 + theta_rad: 1.0602875205865552 + } + gain_value { + gain_db: -11.449 + phi_rad: 0.0 + theta_rad: 1.0611601852125523 + } + gain_value { + gain_db: -11.187 + phi_rad: 0.0 + theta_rad: 1.0620328498385496 + } + gain_value { + gain_db: -10.915 + phi_rad: 0.0 + theta_rad: 1.0629055144645467 + } + gain_value { + gain_db: -10.603 + phi_rad: 0.0 + theta_rad: 1.0637781790905438 + } + gain_value { + gain_db: -10.495 + phi_rad: 0.0 + theta_rad: 1.064650843716541 + } + gain_value { + gain_db: -10.496 + phi_rad: 0.0 + theta_rad: 1.065523508342538 + } + gain_value { + gain_db: -10.654 + phi_rad: 0.0 + theta_rad: 1.0663961729685354 + } + gain_value { + gain_db: -10.984 + phi_rad: 0.0 + theta_rad: 1.0672688375945325 + } + gain_value { + gain_db: -11.467 + phi_rad: 0.0 + theta_rad: 1.0681415022205298 + } + gain_value { + gain_db: -11.737 + phi_rad: 0.0 + theta_rad: 1.069014166846527 + } + gain_value { + gain_db: -12.032 + phi_rad: 0.0 + theta_rad: 1.069886831472524 + } + gain_value { + gain_db: -11.358 + phi_rad: 0.0 + theta_rad: 1.0707594960985212 + } + gain_value { + gain_db: -10.363 + phi_rad: 0.0 + theta_rad: 1.0716321607245183 + } + gain_value { + gain_db: -9.5289 + phi_rad: 0.0 + theta_rad: 1.0725048253505156 + } + gain_value { + gain_db: -8.691 + phi_rad: 0.0 + theta_rad: 1.0733774899765127 + } + gain_value { + gain_db: -8.1762 + phi_rad: 0.0 + theta_rad: 1.0742501546025098 + } + gain_value { + gain_db: -7.9467 + phi_rad: 0.0 + theta_rad: 1.075122819228507 + } + gain_value { + gain_db: -7.9935 + phi_rad: 0.0 + theta_rad: 1.075995483854504 + } + gain_value { + gain_db: -8.3637 + phi_rad: 0.0 + theta_rad: 1.0768681484805014 + } + gain_value { + gain_db: -8.8135 + phi_rad: 0.0 + theta_rad: 1.0777408131064985 + } + gain_value { + gain_db: -9.1712 + phi_rad: 0.0 + theta_rad: 1.0786134777324956 + } + gain_value { + gain_db: -9.4919 + phi_rad: 0.0 + theta_rad: 1.079486142358493 + } + gain_value { + gain_db: -9.5612 + phi_rad: 0.0 + theta_rad: 1.08035880698449 + } + gain_value { + gain_db: -9.1711 + phi_rad: 0.0 + theta_rad: 1.0812314716104872 + } + gain_value { + gain_db: -8.598 + phi_rad: 0.0 + theta_rad: 1.0821041362364843 + } + gain_value { + gain_db: -8.0967 + phi_rad: 0.0 + theta_rad: 1.0829768008624814 + } + gain_value { + gain_db: -7.6287 + phi_rad: 0.0 + theta_rad: 1.0838494654884787 + } + gain_value { + gain_db: -7.2009 + phi_rad: 0.0 + theta_rad: 1.0847221301144758 + } + gain_value { + gain_db: -6.9067 + phi_rad: 0.0 + theta_rad: 1.085594794740473 + } + gain_value { + gain_db: -6.8636 + phi_rad: 0.0 + theta_rad: 1.08646745936647 + } + gain_value { + gain_db: -6.9577 + phi_rad: 0.0 + theta_rad: 1.0873401239924672 + } + gain_value { + gain_db: -7.0595 + phi_rad: 0.0 + theta_rad: 1.0882127886184645 + } + gain_value { + gain_db: -7.1534 + phi_rad: 0.0 + theta_rad: 1.0890854532444616 + } + gain_value { + gain_db: -7.3066 + phi_rad: 0.0 + theta_rad: 1.089958117870459 + } + gain_value { + gain_db: -7.3317 + phi_rad: 0.0 + theta_rad: 1.090830782496456 + } + gain_value { + gain_db: -7.2104 + phi_rad: 0.0 + theta_rad: 1.0917034471224532 + } + gain_value { + gain_db: -7.1951 + phi_rad: 0.0 + theta_rad: 1.0925761117484503 + } + gain_value { + gain_db: -7.1659 + phi_rad: 0.0 + theta_rad: 1.0934487763744474 + } + gain_value { + gain_db: -7.2263 + phi_rad: 0.0 + theta_rad: 1.0943214410004447 + } + gain_value { + gain_db: -7.2622 + phi_rad: 0.0 + theta_rad: 1.0951941056264418 + } + gain_value { + gain_db: -7.2265 + phi_rad: 0.0 + theta_rad: 1.096066770252439 + } + gain_value { + gain_db: -7.4424 + phi_rad: 0.0 + theta_rad: 1.096939434878436 + } + gain_value { + gain_db: -7.653 + phi_rad: 0.0 + theta_rad: 1.0978120995044334 + } + gain_value { + gain_db: -8.021 + phi_rad: 0.0 + theta_rad: 1.0986847641304305 + } + gain_value { + gain_db: -8.4207 + phi_rad: 0.0 + theta_rad: 1.0995574287564276 + } + gain_value { + gain_db: -8.8259 + phi_rad: 0.0 + theta_rad: 1.1004300933824247 + } + gain_value { + gain_db: -9.3983 + phi_rad: 0.0 + theta_rad: 1.101302758008422 + } + gain_value { + gain_db: -9.9549 + phi_rad: 0.0 + theta_rad: 1.1021754226344191 + } + gain_value { + gain_db: -10.254 + phi_rad: 0.0 + theta_rad: 1.1030480872604163 + } + gain_value { + gain_db: -10.262 + phi_rad: 0.0 + theta_rad: 1.1039207518864134 + } + gain_value { + gain_db: -10.17 + phi_rad: 0.0 + theta_rad: 1.1047934165124105 + } + gain_value { + gain_db: -9.6012 + phi_rad: 0.0 + theta_rad: 1.1056660811384078 + } + gain_value { + gain_db: -9.1651 + phi_rad: 0.0 + theta_rad: 1.106538745764405 + } + gain_value { + gain_db: -8.8972 + phi_rad: 0.0 + theta_rad: 1.1074114103904023 + } + gain_value { + gain_db: -8.8468 + phi_rad: 0.0 + theta_rad: 1.1082840750163994 + } + gain_value { + gain_db: -8.9289 + phi_rad: 0.0 + theta_rad: 1.1091567396423965 + } + gain_value { + gain_db: -9.2386 + phi_rad: 0.0 + theta_rad: 1.1100294042683936 + } + gain_value { + gain_db: -9.479 + phi_rad: 0.0 + theta_rad: 1.1109020688943907 + } + gain_value { + gain_db: -10.05 + phi_rad: 0.0 + theta_rad: 1.111774733520388 + } + gain_value { + gain_db: -10.889 + phi_rad: 0.0 + theta_rad: 1.1126473981463851 + } + gain_value { + gain_db: -11.626 + phi_rad: 0.0 + theta_rad: 1.1135200627723822 + } + gain_value { + gain_db: -12.081 + phi_rad: 0.0 + theta_rad: 1.1143927273983794 + } + gain_value { + gain_db: -12.141 + phi_rad: 0.0 + theta_rad: 1.1152653920243765 + } + gain_value { + gain_db: -11.817 + phi_rad: 0.0 + theta_rad: 1.1161380566503738 + } + gain_value { + gain_db: -11.445 + phi_rad: 0.0 + theta_rad: 1.117010721276371 + } + gain_value { + gain_db: -11.11 + phi_rad: 0.0 + theta_rad: 1.117883385902368 + } + gain_value { + gain_db: -11.497 + phi_rad: 0.0 + theta_rad: 1.1187560505283651 + } + gain_value { + gain_db: -12.75 + phi_rad: 0.0 + theta_rad: 1.1196287151543625 + } + gain_value { + gain_db: -14.418 + phi_rad: 0.0 + theta_rad: 1.1205013797803596 + } + gain_value { + gain_db: -13.106 + phi_rad: 0.0 + theta_rad: 1.1213740444063567 + } + gain_value { + gain_db: -11.691 + phi_rad: 0.0 + theta_rad: 1.1222467090323538 + } + gain_value { + gain_db: -10.783 + phi_rad: 0.0 + theta_rad: 1.123119373658351 + } + gain_value { + gain_db: -10.482 + phi_rad: 0.0 + theta_rad: 1.1239920382843482 + } + gain_value { + gain_db: -10.01 + phi_rad: 0.0 + theta_rad: 1.1248647029103453 + } + gain_value { + gain_db: -9.913 + phi_rad: 0.0 + theta_rad: 1.1257373675363425 + } + gain_value { + gain_db: -9.7956 + phi_rad: 0.0 + theta_rad: 1.1266100321623396 + } + gain_value { + gain_db: -9.9721 + phi_rad: 0.0 + theta_rad: 1.1274826967883367 + } + gain_value { + gain_db: -10.309 + phi_rad: 0.0 + theta_rad: 1.1283553614143342 + } + gain_value { + gain_db: -10.757 + phi_rad: 0.0 + theta_rad: 1.1292280260403313 + } + gain_value { + gain_db: -11.699 + phi_rad: 0.0 + theta_rad: 1.1301006906663285 + } + gain_value { + gain_db: -11.919 + phi_rad: 0.0 + theta_rad: 1.1309733552923256 + } + gain_value { + gain_db: -11.961 + phi_rad: 0.0 + theta_rad: 1.1318460199183227 + } + gain_value { + gain_db: -11.781 + phi_rad: 0.0 + theta_rad: 1.13271868454432 + } + gain_value { + gain_db: -11.64 + phi_rad: 0.0 + theta_rad: 1.1335913491703171 + } + gain_value { + gain_db: -11.541 + phi_rad: 0.0 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -12.135 + phi_rad: 0.0 + theta_rad: 1.1353366784223113 + } + gain_value { + gain_db: -12.221 + phi_rad: 0.0 + theta_rad: 1.1362093430483085 + } + gain_value { + gain_db: -12.284 + phi_rad: 0.0 + theta_rad: 1.1370820076743058 + } + gain_value { + gain_db: -11.048 + phi_rad: 0.0 + theta_rad: 1.137954672300303 + } + gain_value { + gain_db: -10.211 + phi_rad: 0.0 + theta_rad: 1.1388273369263 + } + gain_value { + gain_db: -9.4844 + phi_rad: 0.0 + theta_rad: 1.1397000015522971 + } + gain_value { + gain_db: -8.7757 + phi_rad: 0.0 + theta_rad: 1.1405726661782942 + } + gain_value { + gain_db: -8.3895 + phi_rad: 0.0 + theta_rad: 1.1414453308042916 + } + gain_value { + gain_db: -8.3124 + phi_rad: 0.0 + theta_rad: 1.1423179954302887 + } + gain_value { + gain_db: -8.377 + phi_rad: 0.0 + theta_rad: 1.1431906600562858 + } + gain_value { + gain_db: -8.5623 + phi_rad: 0.0 + theta_rad: 1.144063324682283 + } + gain_value { + gain_db: -8.6583 + phi_rad: 0.0 + theta_rad: 1.14493598930828 + } + gain_value { + gain_db: -8.8084 + phi_rad: 0.0 + theta_rad: 1.1458086539342776 + } + gain_value { + gain_db: -8.7229 + phi_rad: 0.0 + theta_rad: 1.1466813185602747 + } + gain_value { + gain_db: -8.6679 + phi_rad: 0.0 + theta_rad: 1.1475539831862718 + } + gain_value { + gain_db: -8.5507 + phi_rad: 0.0 + theta_rad: 1.1484266478122689 + } + gain_value { + gain_db: -8.4549 + phi_rad: 0.0 + theta_rad: 1.149299312438266 + } + gain_value { + gain_db: -8.4386 + phi_rad: 0.0 + theta_rad: 1.1501719770642633 + } + gain_value { + gain_db: -8.3475 + phi_rad: 0.0 + theta_rad: 1.1510446416902604 + } + gain_value { + gain_db: -8.1766 + phi_rad: 0.0 + theta_rad: 1.1519173063162575 + } + gain_value { + gain_db: -7.9478 + phi_rad: 0.0 + theta_rad: 1.1527899709422547 + } + gain_value { + gain_db: -7.8742 + phi_rad: 0.0 + theta_rad: 1.1536626355682518 + } + gain_value { + gain_db: -7.9498 + phi_rad: 0.0 + theta_rad: 1.154535300194249 + } + gain_value { + gain_db: -8.052 + phi_rad: 0.0 + theta_rad: 1.1554079648202462 + } + gain_value { + gain_db: -8.3074 + phi_rad: 0.0 + theta_rad: 1.1562806294462433 + } + gain_value { + gain_db: -8.4778 + phi_rad: 0.0 + theta_rad: 1.1571532940722404 + } + gain_value { + gain_db: -8.7027 + phi_rad: 0.0 + theta_rad: 1.1580259586982375 + } + gain_value { + gain_db: -8.9792 + phi_rad: 0.0 + theta_rad: 1.1588986233242349 + } + gain_value { + gain_db: -9.2118 + phi_rad: 0.0 + theta_rad: 1.159771287950232 + } + gain_value { + gain_db: -9.5493 + phi_rad: 0.0 + theta_rad: 1.160643952576229 + } + gain_value { + gain_db: -9.7813 + phi_rad: 0.0 + theta_rad: 1.1615166172022262 + } + gain_value { + gain_db: -9.8842 + phi_rad: 0.0 + theta_rad: 1.1623892818282233 + } + gain_value { + gain_db: -9.8447 + phi_rad: 0.0 + theta_rad: 1.1632619464542207 + } + gain_value { + gain_db: -9.854 + phi_rad: 0.0 + theta_rad: 1.1641346110802178 + } + gain_value { + gain_db: -9.8275 + phi_rad: 0.0 + theta_rad: 1.1650072757062149 + } + gain_value { + gain_db: -9.9378 + phi_rad: 0.0 + theta_rad: 1.165879940332212 + } + gain_value { + gain_db: -10.19 + phi_rad: 0.0 + theta_rad: 1.166752604958209 + } + gain_value { + gain_db: -10.599 + phi_rad: 0.0 + theta_rad: 1.1676252695842066 + } + gain_value { + gain_db: -10.786 + phi_rad: 0.0 + theta_rad: 1.1684979342102038 + } + gain_value { + gain_db: -10.788 + phi_rad: 0.0 + theta_rad: 1.1693705988362009 + } + gain_value { + gain_db: -10.91 + phi_rad: 0.0 + theta_rad: 1.170243263462198 + } + gain_value { + gain_db: -11.096 + phi_rad: 0.0 + theta_rad: 1.171115928088195 + } + gain_value { + gain_db: -11.068 + phi_rad: 0.0 + theta_rad: 1.1719885927141924 + } + gain_value { + gain_db: -11.09 + phi_rad: 0.0 + theta_rad: 1.1728612573401895 + } + gain_value { + gain_db: -11.171 + phi_rad: 0.0 + theta_rad: 1.1737339219661866 + } + gain_value { + gain_db: -11.448 + phi_rad: 0.0 + theta_rad: 1.1746065865921838 + } + gain_value { + gain_db: -12.075 + phi_rad: 0.0 + theta_rad: 1.1754792512181809 + } + gain_value { + gain_db: -12.973 + phi_rad: 0.0 + theta_rad: 1.1763519158441782 + } + gain_value { + gain_db: -14.034 + phi_rad: 0.0 + theta_rad: 1.1772245804701753 + } + gain_value { + gain_db: -14.998 + phi_rad: 0.0 + theta_rad: 1.1780972450961724 + } + gain_value { + gain_db: -14.466 + phi_rad: 0.0 + theta_rad: 1.1789699097221695 + } + gain_value { + gain_db: -13.639 + phi_rad: 0.0 + theta_rad: 1.1798425743481666 + } + gain_value { + gain_db: -12.978 + phi_rad: 0.0 + theta_rad: 1.180715238974164 + } + gain_value { + gain_db: -12.435 + phi_rad: 0.0 + theta_rad: 1.181587903600161 + } + gain_value { + gain_db: -11.858 + phi_rad: 0.0 + theta_rad: 1.1824605682261582 + } + gain_value { + gain_db: -11.378 + phi_rad: 0.0 + theta_rad: 1.1833332328521553 + } + gain_value { + gain_db: -11.001 + phi_rad: 0.0 + theta_rad: 1.1842058974781524 + } + gain_value { + gain_db: -10.862 + phi_rad: 0.0 + theta_rad: 1.18507856210415 + } + gain_value { + gain_db: -11.092 + phi_rad: 0.0 + theta_rad: 1.185951226730147 + } + gain_value { + gain_db: -11.482 + phi_rad: 0.0 + theta_rad: 1.1868238913561442 + } + gain_value { + gain_db: -12.415 + phi_rad: 0.0 + theta_rad: 1.1876965559821413 + } + gain_value { + gain_db: -13.694 + phi_rad: 0.0 + theta_rad: 1.1885692206081384 + } + gain_value { + gain_db: -15.27 + phi_rad: 0.0 + theta_rad: 1.1894418852341357 + } + gain_value { + gain_db: -17.528 + phi_rad: 0.0 + theta_rad: 1.1903145498601329 + } + gain_value { + gain_db: -15.824 + phi_rad: 0.0 + theta_rad: 1.19118721448613 + } + gain_value { + gain_db: -13.869 + phi_rad: 0.0 + theta_rad: 1.192059879112127 + } + gain_value { + gain_db: -12.618 + phi_rad: 0.0 + theta_rad: 1.1929325437381242 + } + gain_value { + gain_db: -11.774 + phi_rad: 0.0 + theta_rad: 1.1938052083641215 + } + gain_value { + gain_db: -11.046 + phi_rad: 0.0 + theta_rad: 1.1946778729901186 + } + gain_value { + gain_db: -10.769 + phi_rad: 0.0 + theta_rad: 1.1955505376161157 + } + gain_value { + gain_db: -10.563 + phi_rad: 0.0 + theta_rad: 1.1964232022421128 + } + gain_value { + gain_db: -10.753 + phi_rad: 0.0 + theta_rad: 1.19729586686811 + } + gain_value { + gain_db: -11.342 + phi_rad: 0.0 + theta_rad: 1.1981685314941073 + } + gain_value { + gain_db: -11.999 + phi_rad: 0.0 + theta_rad: 1.1990411961201044 + } + gain_value { + gain_db: -13.033 + phi_rad: 0.0 + theta_rad: 1.1999138607461015 + } + gain_value { + gain_db: -14.229 + phi_rad: 0.0 + theta_rad: 1.2007865253720986 + } + gain_value { + gain_db: -14.609 + phi_rad: 0.0 + theta_rad: 1.2016591899980957 + } + gain_value { + gain_db: -13.629 + phi_rad: 0.0 + theta_rad: 1.202531854624093 + } + gain_value { + gain_db: -12.454 + phi_rad: 0.0 + theta_rad: 1.2034045192500902 + } + gain_value { + gain_db: -11.452 + phi_rad: 0.0 + theta_rad: 1.2042771838760873 + } + gain_value { + gain_db: -10.654 + phi_rad: 0.0 + theta_rad: 1.2051498485020844 + } + gain_value { + gain_db: -10.032 + phi_rad: 0.0 + theta_rad: 1.2060225131280815 + } + gain_value { + gain_db: -9.5626 + phi_rad: 0.0 + theta_rad: 1.206895177754079 + } + gain_value { + gain_db: -9.4096 + phi_rad: 0.0 + theta_rad: 1.2077678423800762 + } + gain_value { + gain_db: -9.446 + phi_rad: 0.0 + theta_rad: 1.2086405070060733 + } + gain_value { + gain_db: -9.6401 + phi_rad: 0.0 + theta_rad: 1.2095131716320704 + } + gain_value { + gain_db: -10.141 + phi_rad: 0.0 + theta_rad: 1.2103858362580675 + } + gain_value { + gain_db: -11.154 + phi_rad: 0.0 + theta_rad: 1.2112585008840648 + } + gain_value { + gain_db: -12.3 + phi_rad: 0.0 + theta_rad: 1.212131165510062 + } + gain_value { + gain_db: -12.586 + phi_rad: 0.0 + theta_rad: 1.213003830136059 + } + gain_value { + gain_db: -11.985 + phi_rad: 0.0 + theta_rad: 1.2138764947620562 + } + gain_value { + gain_db: -11.741 + phi_rad: 0.0 + theta_rad: 1.2147491593880533 + } + gain_value { + gain_db: -11.633 + phi_rad: 0.0 + theta_rad: 1.2156218240140506 + } + gain_value { + gain_db: -11.674 + phi_rad: 0.0 + theta_rad: 1.2164944886400477 + } + gain_value { + gain_db: -11.558 + phi_rad: 0.0 + theta_rad: 1.2173671532660448 + } + gain_value { + gain_db: -11.345 + phi_rad: 0.0 + theta_rad: 1.218239817892042 + } + gain_value { + gain_db: -11.071 + phi_rad: 0.0 + theta_rad: 1.219112482518039 + } + gain_value { + gain_db: -11.024 + phi_rad: 0.0 + theta_rad: 1.2199851471440364 + } + gain_value { + gain_db: -10.956 + phi_rad: 0.0 + theta_rad: 1.2208578117700335 + } + gain_value { + gain_db: -10.941 + phi_rad: 0.0 + theta_rad: 1.2217304763960306 + } + gain_value { + gain_db: -11.043 + phi_rad: 0.0 + theta_rad: 1.2226031410220277 + } + gain_value { + gain_db: -11.01 + phi_rad: 0.0 + theta_rad: 1.2234758056480248 + } + gain_value { + gain_db: -11.018 + phi_rad: 0.0 + theta_rad: 1.2243484702740224 + } + gain_value { + gain_db: -11.155 + phi_rad: 0.0 + theta_rad: 1.2252211349000195 + } + gain_value { + gain_db: -11.548 + phi_rad: 0.0 + theta_rad: 1.2260937995260166 + } + gain_value { + gain_db: -12.342 + phi_rad: 0.0 + theta_rad: 1.2269664641520137 + } + gain_value { + gain_db: -13.19 + phi_rad: 0.0 + theta_rad: 1.2278391287780108 + } + gain_value { + gain_db: -14.5 + phi_rad: 0.0 + theta_rad: 1.2287117934040082 + } + gain_value { + gain_db: -17.703 + phi_rad: 0.0 + theta_rad: 1.2295844580300053 + } + gain_value { + gain_db: -16.571 + phi_rad: 0.0 + theta_rad: 1.2304571226560024 + } + gain_value { + gain_db: -14.4 + phi_rad: 0.0 + theta_rad: 1.2313297872819995 + } + gain_value { + gain_db: -12.894 + phi_rad: 0.0 + theta_rad: 1.2322024519079966 + } + gain_value { + gain_db: -11.685 + phi_rad: 0.0 + theta_rad: 1.233075116533994 + } + gain_value { + gain_db: -10.915 + phi_rad: 0.0 + theta_rad: 1.233947781159991 + } + gain_value { + gain_db: -10.667 + phi_rad: 0.0 + theta_rad: 1.2348204457859882 + } + gain_value { + gain_db: -10.558 + phi_rad: 0.0 + theta_rad: 1.2356931104119853 + } + gain_value { + gain_db: -10.579 + phi_rad: 0.0 + theta_rad: 1.2365657750379824 + } + gain_value { + gain_db: -10.733 + phi_rad: 0.0 + theta_rad: 1.2374384396639797 + } + gain_value { + gain_db: -11.088 + phi_rad: 0.0 + theta_rad: 1.2383111042899768 + } + gain_value { + gain_db: -11.552 + phi_rad: 0.0 + theta_rad: 1.239183768915974 + } + gain_value { + gain_db: -11.825 + phi_rad: 0.0 + theta_rad: 1.240056433541971 + } + gain_value { + gain_db: -11.704 + phi_rad: 0.0 + theta_rad: 1.2409290981679681 + } + gain_value { + gain_db: -11.555 + phi_rad: 0.0 + theta_rad: 1.2418017627939655 + } + gain_value { + gain_db: -11.571 + phi_rad: 0.0 + theta_rad: 1.2426744274199626 + } + gain_value { + gain_db: -11.779 + phi_rad: 0.0 + theta_rad: 1.2435470920459597 + } + gain_value { + gain_db: -12.461 + phi_rad: 0.0 + theta_rad: 1.2444197566719568 + } + gain_value { + gain_db: -13.74 + phi_rad: 0.0 + theta_rad: 1.245292421297954 + } + gain_value { + gain_db: -18.349 + phi_rad: 0.0 + theta_rad: 1.2461650859239515 + } + gain_value { + gain_db: -14.445 + phi_rad: 0.0 + theta_rad: 1.2470377505499486 + } + gain_value { + gain_db: -12.451 + phi_rad: 0.0 + theta_rad: 1.2479104151759457 + } + gain_value { + gain_db: -11.435 + phi_rad: 0.0 + theta_rad: 1.2487830798019428 + } + gain_value { + gain_db: -11.167 + phi_rad: 0.0 + theta_rad: 1.24965574442794 + } + gain_value { + gain_db: -10.993 + phi_rad: 0.0 + theta_rad: 1.2505284090539373 + } + gain_value { + gain_db: -11.084 + phi_rad: 0.0 + theta_rad: 1.2514010736799344 + } + gain_value { + gain_db: -10.956 + phi_rad: 0.0 + theta_rad: 1.2522737383059315 + } + gain_value { + gain_db: -10.804 + phi_rad: 0.0 + theta_rad: 1.2531464029319286 + } + gain_value { + gain_db: -10.475 + phi_rad: 0.0 + theta_rad: 1.2540190675579257 + } + gain_value { + gain_db: -10.032 + phi_rad: 0.0 + theta_rad: 1.254891732183923 + } + gain_value { + gain_db: -9.8107 + phi_rad: 0.0 + theta_rad: 1.2557643968099201 + } + gain_value { + gain_db: -9.6938 + phi_rad: 0.0 + theta_rad: 1.2566370614359172 + } + gain_value { + gain_db: -9.8916 + phi_rad: 0.0 + theta_rad: 1.2575097260619144 + } + gain_value { + gain_db: -10.05 + phi_rad: 0.0 + theta_rad: 1.2583823906879115 + } + gain_value { + gain_db: -10.451 + phi_rad: 0.0 + theta_rad: 1.2592550553139088 + } + gain_value { + gain_db: -10.914 + phi_rad: 0.0 + theta_rad: 1.260127719939906 + } + gain_value { + gain_db: -11.351 + phi_rad: 0.0 + theta_rad: 1.261000384565903 + } + gain_value { + gain_db: -11.466 + phi_rad: 0.0 + theta_rad: 1.2618730491919001 + } + gain_value { + gain_db: -11.33 + phi_rad: 0.0 + theta_rad: 1.2627457138178972 + } + gain_value { + gain_db: -11.317 + phi_rad: 0.0 + theta_rad: 1.2636183784438948 + } + gain_value { + gain_db: -11.642 + phi_rad: 0.0 + theta_rad: 1.264491043069892 + } + gain_value { + gain_db: -11.462 + phi_rad: 0.0 + theta_rad: 1.265363707695889 + } + gain_value { + gain_db: -10.859 + phi_rad: 0.0 + theta_rad: 1.2662363723218861 + } + gain_value { + gain_db: -10.084 + phi_rad: 0.0 + theta_rad: 1.2671090369478832 + } + gain_value { + gain_db: -9.7835 + phi_rad: 0.0 + theta_rad: 1.2679817015738806 + } + gain_value { + gain_db: -9.6854 + phi_rad: 0.0 + theta_rad: 1.2688543661998777 + } + gain_value { + gain_db: -9.5456 + phi_rad: 0.0 + theta_rad: 1.2697270308258748 + } + gain_value { + gain_db: -9.4576 + phi_rad: 0.0 + theta_rad: 1.270599695451872 + } + gain_value { + gain_db: -9.3941 + phi_rad: 0.0 + theta_rad: 1.271472360077869 + } + gain_value { + gain_db: -9.374 + phi_rad: 0.0 + theta_rad: 1.2723450247038663 + } + gain_value { + gain_db: -9.4364 + phi_rad: 0.0 + theta_rad: 1.2732176893298635 + } + gain_value { + gain_db: -9.5917 + phi_rad: 0.0 + theta_rad: 1.2740903539558606 + } + gain_value { + gain_db: -9.6041 + phi_rad: 0.0 + theta_rad: 1.2749630185818577 + } + gain_value { + gain_db: -9.6267 + phi_rad: 0.0 + theta_rad: 1.2758356832078548 + } + gain_value { + gain_db: -9.595 + phi_rad: 0.0 + theta_rad: 1.2767083478338521 + } + gain_value { + gain_db: -9.7794 + phi_rad: 0.0 + theta_rad: 1.2775810124598492 + } + gain_value { + gain_db: -10.206 + phi_rad: 0.0 + theta_rad: 1.2784536770858463 + } + gain_value { + gain_db: -11.318 + phi_rad: 0.0 + theta_rad: 1.2793263417118435 + } + gain_value { + gain_db: -12.121 + phi_rad: 0.0 + theta_rad: 1.2801990063378406 + } + gain_value { + gain_db: -11.485 + phi_rad: 0.0 + theta_rad: 1.281071670963838 + } + gain_value { + gain_db: -10.471 + phi_rad: 0.0 + theta_rad: 1.281944335589835 + } + gain_value { + gain_db: -9.7552 + phi_rad: 0.0 + theta_rad: 1.2828170002158321 + } + gain_value { + gain_db: -9.3696 + phi_rad: 0.0 + theta_rad: 1.2836896648418292 + } + gain_value { + gain_db: -9.137 + phi_rad: 0.0 + theta_rad: 1.2845623294678266 + } + gain_value { + gain_db: -8.9594 + phi_rad: 0.0 + theta_rad: 1.285434994093824 + } + gain_value { + gain_db: -8.7336 + phi_rad: 0.0 + theta_rad: 1.286307658719821 + } + gain_value { + gain_db: -8.7591 + phi_rad: 0.0 + theta_rad: 1.2871803233458181 + } + gain_value { + gain_db: -8.5853 + phi_rad: 0.0 + theta_rad: 1.2880529879718152 + } + gain_value { + gain_db: -8.7732 + phi_rad: 0.0 + theta_rad: 1.2889256525978123 + } + gain_value { + gain_db: -8.9882 + phi_rad: 0.0 + theta_rad: 1.2897983172238097 + } + gain_value { + gain_db: -9.0062 + phi_rad: 0.0 + theta_rad: 1.2906709818498068 + } + gain_value { + gain_db: -9.0487 + phi_rad: 0.0 + theta_rad: 1.2915436464758039 + } + gain_value { + gain_db: -9.1025 + phi_rad: 0.0 + theta_rad: 1.292416311101801 + } + gain_value { + gain_db: -9.5541 + phi_rad: 0.0 + theta_rad: 1.293288975727798 + } + gain_value { + gain_db: -9.9613 + phi_rad: 0.0 + theta_rad: 1.2941616403537954 + } + gain_value { + gain_db: -9.6545 + phi_rad: 0.0 + theta_rad: 1.2950343049797925 + } + gain_value { + gain_db: -8.9004 + phi_rad: 0.0 + theta_rad: 1.2959069696057897 + } + gain_value { + gain_db: -8.0359 + phi_rad: 0.0 + theta_rad: 1.2967796342317868 + } + gain_value { + gain_db: -7.5796 + phi_rad: 0.0 + theta_rad: 1.2976522988577839 + } + gain_value { + gain_db: -7.5089 + phi_rad: 0.0 + theta_rad: 1.2985249634837812 + } + gain_value { + gain_db: -7.4118 + phi_rad: 0.0 + theta_rad: 1.2993976281097783 + } + gain_value { + gain_db: -7.324 + phi_rad: 0.0 + theta_rad: 1.3002702927357754 + } + gain_value { + gain_db: -7.2754 + phi_rad: 0.0 + theta_rad: 1.3011429573617725 + } + gain_value { + gain_db: -7.2573 + phi_rad: 0.0 + theta_rad: 1.3020156219877697 + } + gain_value { + gain_db: -7.3094 + phi_rad: 0.0 + theta_rad: 1.3028882866137672 + } + gain_value { + gain_db: -7.3755 + phi_rad: 0.0 + theta_rad: 1.3037609512397643 + } + gain_value { + gain_db: -7.2791 + phi_rad: 0.0 + theta_rad: 1.3046336158657614 + } + gain_value { + gain_db: -7.1461 + phi_rad: 0.0 + theta_rad: 1.3055062804917585 + } + gain_value { + gain_db: -7.1101 + phi_rad: 0.0 + theta_rad: 1.3063789451177557 + } + gain_value { + gain_db: -7.2517 + phi_rad: 0.0 + theta_rad: 1.307251609743753 + } + gain_value { + gain_db: -7.3742 + phi_rad: 0.0 + theta_rad: 1.30812427436975 + } + gain_value { + gain_db: -7.5853 + phi_rad: 0.0 + theta_rad: 1.3089969389957472 + } + gain_value { + gain_db: -8.1132 + phi_rad: 0.0 + theta_rad: 1.3098696036217443 + } + gain_value { + gain_db: -8.9726 + phi_rad: 0.0 + theta_rad: 1.3107422682477414 + } + gain_value { + gain_db: -10.286 + phi_rad: 0.0 + theta_rad: 1.3116149328737388 + } + gain_value { + gain_db: -12.861 + phi_rad: 0.0 + theta_rad: 1.3124875974997359 + } + gain_value { + gain_db: -12.887 + phi_rad: 0.0 + theta_rad: 1.313360262125733 + } + gain_value { + gain_db: -10.848 + phi_rad: 0.0 + theta_rad: 1.31423292675173 + } + gain_value { + gain_db: -9.6119 + phi_rad: 0.0 + theta_rad: 1.3151055913777272 + } + gain_value { + gain_db: -8.7541 + phi_rad: 0.0 + theta_rad: 1.3159782560037245 + } + gain_value { + gain_db: -8.1582 + phi_rad: 0.0 + theta_rad: 1.3168509206297216 + } + gain_value { + gain_db: -7.7137 + phi_rad: 0.0 + theta_rad: 1.3177235852557188 + } + gain_value { + gain_db: -7.4731 + phi_rad: 0.0 + theta_rad: 1.3185962498817159 + } + gain_value { + gain_db: -7.3555 + phi_rad: 0.0 + theta_rad: 1.319468914507713 + } + gain_value { + gain_db: -7.3491 + phi_rad: 0.0 + theta_rad: 1.3203415791337103 + } + gain_value { + gain_db: -7.6241 + phi_rad: 0.0 + theta_rad: 1.3212142437597074 + } + gain_value { + gain_db: -8.1069 + phi_rad: 0.0 + theta_rad: 1.3220869083857045 + } + gain_value { + gain_db: -8.6897 + phi_rad: 0.0 + theta_rad: 1.3229595730117016 + } + gain_value { + gain_db: -9.141 + phi_rad: 0.0 + theta_rad: 1.323832237637699 + } + gain_value { + gain_db: -9.8067 + phi_rad: 0.0 + theta_rad: 1.3247049022636963 + } + gain_value { + gain_db: -10.718 + phi_rad: 0.0 + theta_rad: 1.3255775668896934 + } + gain_value { + gain_db: -11.274 + phi_rad: 0.0 + theta_rad: 1.3264502315156905 + } + gain_value { + gain_db: -11.547 + phi_rad: 0.0 + theta_rad: 1.3273228961416876 + } + gain_value { + gain_db: -11.438 + phi_rad: 0.0 + theta_rad: 1.3281955607676847 + } + gain_value { + gain_db: -11.308 + phi_rad: 0.0 + theta_rad: 1.329068225393682 + } + gain_value { + gain_db: -11.134 + phi_rad: 0.0 + theta_rad: 1.3299408900196792 + } + gain_value { + gain_db: -10.871 + phi_rad: 0.0 + theta_rad: 1.3308135546456763 + } + gain_value { + gain_db: -10.431 + phi_rad: 0.0 + theta_rad: 1.3316862192716734 + } + gain_value { + gain_db: -9.8562 + phi_rad: 0.0 + theta_rad: 1.3325588838976705 + } + gain_value { + gain_db: -8.8959 + phi_rad: 0.0 + theta_rad: 1.3334315485236679 + } + gain_value { + gain_db: -8.0948 + phi_rad: 0.0 + theta_rad: 1.334304213149665 + } + gain_value { + gain_db: -7.5824 + phi_rad: 0.0 + theta_rad: 1.335176877775662 + } + gain_value { + gain_db: -7.2273 + phi_rad: 0.0 + theta_rad: 1.3360495424016592 + } + gain_value { + gain_db: -7.2437 + phi_rad: 0.0 + theta_rad: 1.3369222070276563 + } + gain_value { + gain_db: -7.4083 + phi_rad: 0.0 + theta_rad: 1.3377948716536536 + } + gain_value { + gain_db: -8.0338 + phi_rad: 0.0 + theta_rad: 1.3386675362796507 + } + gain_value { + gain_db: -9.0254 + phi_rad: 0.0 + theta_rad: 1.3395402009056478 + } + gain_value { + gain_db: -10.211 + phi_rad: 0.0 + theta_rad: 1.340412865531645 + } + gain_value { + gain_db: -11.532 + phi_rad: 0.0 + theta_rad: 1.341285530157642 + } + gain_value { + gain_db: -13.066 + phi_rad: 0.0 + theta_rad: 1.3421581947836396 + } + gain_value { + gain_db: -13.906 + phi_rad: 0.0 + theta_rad: 1.3430308594096367 + } + gain_value { + gain_db: -13.422 + phi_rad: 0.0 + theta_rad: 1.3439035240356338 + } + gain_value { + gain_db: -13.447 + phi_rad: 0.0 + theta_rad: 1.344776188661631 + } + gain_value { + gain_db: -14.861 + phi_rad: 0.0 + theta_rad: 1.345648853287628 + } + gain_value { + gain_db: -14.821 + phi_rad: 0.0 + theta_rad: 1.3465215179136254 + } + gain_value { + gain_db: -11.856 + phi_rad: 0.0 + theta_rad: 1.3473941825396225 + } + gain_value { + gain_db: -10.367 + phi_rad: 0.0 + theta_rad: 1.3482668471656196 + } + gain_value { + gain_db: -9.5539 + phi_rad: 0.0 + theta_rad: 1.3491395117916167 + } + gain_value { + gain_db: -8.8868 + phi_rad: 0.0 + theta_rad: 1.3500121764176138 + } + gain_value { + gain_db: -8.7575 + phi_rad: 0.0 + theta_rad: 1.3508848410436112 + } + gain_value { + gain_db: -8.987 + phi_rad: 0.0 + theta_rad: 1.3517575056696083 + } + gain_value { + gain_db: -9.4036 + phi_rad: 0.0 + theta_rad: 1.3526301702956054 + } + gain_value { + gain_db: -9.8702 + phi_rad: 0.0 + theta_rad: 1.3535028349216025 + } + gain_value { + gain_db: -10.191 + phi_rad: 0.0 + theta_rad: 1.3543754995475996 + } + gain_value { + gain_db: -10.496 + phi_rad: 0.0 + theta_rad: 1.355248164173597 + } + gain_value { + gain_db: -10.757 + phi_rad: 0.0 + theta_rad: 1.356120828799594 + } + gain_value { + gain_db: -10.721 + phi_rad: 0.0 + theta_rad: 1.3569934934255912 + } + gain_value { + gain_db: -10.703 + phi_rad: 0.0 + theta_rad: 1.3578661580515883 + } + gain_value { + gain_db: -10.734 + phi_rad: 0.0 + theta_rad: 1.3587388226775854 + } + gain_value { + gain_db: -11.105 + phi_rad: 0.0 + theta_rad: 1.3596114873035827 + } + gain_value { + gain_db: -11.523 + phi_rad: 0.0 + theta_rad: 1.3604841519295798 + } + gain_value { + gain_db: -12.967 + phi_rad: 0.0 + theta_rad: 1.361356816555577 + } + gain_value { + gain_db: -15.544 + phi_rad: 0.0 + theta_rad: 1.362229481181574 + } + gain_value { + gain_db: -13.338 + phi_rad: 0.0 + theta_rad: 1.3631021458075714 + } + gain_value { + gain_db: -11.216 + phi_rad: 0.0 + theta_rad: 1.3639748104335687 + } + gain_value { + gain_db: -10.067 + phi_rad: 0.0 + theta_rad: 1.3648474750595658 + } + gain_value { + gain_db: -9.4854 + phi_rad: 0.0 + theta_rad: 1.365720139685563 + } + gain_value { + gain_db: -9.0821 + phi_rad: 0.0 + theta_rad: 1.36659280431156 + } + gain_value { + gain_db: -9.1378 + phi_rad: 0.0 + theta_rad: 1.3674654689375572 + } + gain_value { + gain_db: -9.4316 + phi_rad: 0.0 + theta_rad: 1.3683381335635545 + } + gain_value { + gain_db: -9.9183 + phi_rad: 0.0 + theta_rad: 1.3692107981895516 + } + gain_value { + gain_db: -10.821 + phi_rad: 0.0 + theta_rad: 1.3700834628155487 + } + gain_value { + gain_db: -11.921 + phi_rad: 0.0 + theta_rad: 1.3709561274415458 + } + gain_value { + gain_db: -13.107 + phi_rad: 0.0 + theta_rad: 1.371828792067543 + } + gain_value { + gain_db: -14.349 + phi_rad: 0.0 + theta_rad: 1.3727014566935403 + } + gain_value { + gain_db: -14.722 + phi_rad: 0.0 + theta_rad: 1.3735741213195374 + } + gain_value { + gain_db: -13.459 + phi_rad: 0.0 + theta_rad: 1.3744467859455345 + } + gain_value { + gain_db: -12.335 + phi_rad: 0.0 + theta_rad: 1.3753194505715316 + } + gain_value { + gain_db: -11.814 + phi_rad: 0.0 + theta_rad: 1.3761921151975287 + } + gain_value { + gain_db: -11.817 + phi_rad: 0.0 + theta_rad: 1.377064779823526 + } + gain_value { + gain_db: -12.187 + phi_rad: 0.0 + theta_rad: 1.3779374444495232 + } + gain_value { + gain_db: -13.118 + phi_rad: 0.0 + theta_rad: 1.3788101090755203 + } + gain_value { + gain_db: -16.876 + phi_rad: 0.0 + theta_rad: 1.3796827737015174 + } + gain_value { + gain_db: -14.46 + phi_rad: 0.0 + theta_rad: 1.3805554383275145 + } + gain_value { + gain_db: -11.83 + phi_rad: 0.0 + theta_rad: 1.381428102953512 + } + gain_value { + gain_db: -10.546 + phi_rad: 0.0 + theta_rad: 1.3823007675795091 + } + gain_value { + gain_db: -10.008 + phi_rad: 0.0 + theta_rad: 1.3831734322055063 + } + gain_value { + gain_db: -9.515 + phi_rad: 0.0 + theta_rad: 1.3840460968315034 + } + gain_value { + gain_db: -9.4342 + phi_rad: 0.0 + theta_rad: 1.3849187614575005 + } + gain_value { + gain_db: -9.8318 + phi_rad: 0.0 + theta_rad: 1.3857914260834978 + } + gain_value { + gain_db: -10.365 + phi_rad: 0.0 + theta_rad: 1.386664090709495 + } + gain_value { + gain_db: -10.688 + phi_rad: 0.0 + theta_rad: 1.387536755335492 + } + gain_value { + gain_db: -11.241 + phi_rad: 0.0 + theta_rad: 1.3884094199614891 + } + gain_value { + gain_db: -11.653 + phi_rad: 0.0 + theta_rad: 1.3892820845874863 + } + gain_value { + gain_db: -11.189 + phi_rad: 0.0 + theta_rad: 1.3901547492134836 + } + gain_value { + gain_db: -10.572 + phi_rad: 0.0 + theta_rad: 1.3910274138394807 + } + gain_value { + gain_db: -9.9941 + phi_rad: 0.0 + theta_rad: 1.3919000784654778 + } + gain_value { + gain_db: -9.4954 + phi_rad: 0.0 + theta_rad: 1.392772743091475 + } + gain_value { + gain_db: -9.1823 + phi_rad: 0.0 + theta_rad: 1.393645407717472 + } + gain_value { + gain_db: -8.8266 + phi_rad: 0.0 + theta_rad: 1.3945180723434694 + } + gain_value { + gain_db: -8.5368 + phi_rad: 0.0 + theta_rad: 1.3953907369694665 + } + gain_value { + gain_db: -8.0639 + phi_rad: 0.0 + theta_rad: 1.3962634015954636 + } + gain_value { + gain_db: -7.7623 + phi_rad: 0.0 + theta_rad: 1.3971360662214607 + } + gain_value { + gain_db: -7.677 + phi_rad: 0.0 + theta_rad: 1.3980087308474578 + } + gain_value { + gain_db: -7.5646 + phi_rad: 0.0 + theta_rad: 1.3988813954734551 + } + gain_value { + gain_db: -7.2407 + phi_rad: 0.0 + theta_rad: 1.3997540600994522 + } + gain_value { + gain_db: -7.1852 + phi_rad: 0.0 + theta_rad: 1.4006267247254494 + } + gain_value { + gain_db: -7.4281 + phi_rad: 0.0 + theta_rad: 1.4014993893514465 + } + gain_value { + gain_db: -7.5779 + phi_rad: 0.0 + theta_rad: 1.4023720539774438 + } + gain_value { + gain_db: -7.8522 + phi_rad: 0.0 + theta_rad: 1.4032447186034411 + } + gain_value { + gain_db: -8.5644 + phi_rad: 0.0 + theta_rad: 1.4041173832294382 + } + gain_value { + gain_db: -9.2243 + phi_rad: 0.0 + theta_rad: 1.4049900478554354 + } + gain_value { + gain_db: -9.7022 + phi_rad: 0.0 + theta_rad: 1.4058627124814325 + } + gain_value { + gain_db: -10.152 + phi_rad: 0.0 + theta_rad: 1.4067353771074296 + } + gain_value { + gain_db: -10.307 + phi_rad: 0.0 + theta_rad: 1.407608041733427 + } + gain_value { + gain_db: -9.9416 + phi_rad: 0.0 + theta_rad: 1.408480706359424 + } + gain_value { + gain_db: -9.2682 + phi_rad: 0.0 + theta_rad: 1.4093533709854211 + } + gain_value { + gain_db: -8.8377 + phi_rad: 0.0 + theta_rad: 1.4102260356114182 + } + gain_value { + gain_db: -8.6324 + phi_rad: 0.0 + theta_rad: 1.4110987002374153 + } + gain_value { + gain_db: -8.2163 + phi_rad: 0.0 + theta_rad: 1.4119713648634127 + } + gain_value { + gain_db: -7.8167 + phi_rad: 0.0 + theta_rad: 1.4128440294894098 + } + gain_value { + gain_db: -7.5653 + phi_rad: 0.0 + theta_rad: 1.413716694115407 + } + gain_value { + gain_db: -7.1058 + phi_rad: 0.0 + theta_rad: 1.414589358741404 + } + gain_value { + gain_db: -6.6547 + phi_rad: 0.0 + theta_rad: 1.4154620233674011 + } + gain_value { + gain_db: -6.4298 + phi_rad: 0.0 + theta_rad: 1.4163346879933985 + } + gain_value { + gain_db: -6.2276 + phi_rad: 0.0 + theta_rad: 1.4172073526193956 + } + gain_value { + gain_db: -6.063 + phi_rad: 0.0 + theta_rad: 1.4180800172453927 + } + gain_value { + gain_db: -5.963 + phi_rad: 0.0 + theta_rad: 1.4189526818713898 + } + gain_value { + gain_db: -6.005 + phi_rad: 0.0 + theta_rad: 1.419825346497387 + } + gain_value { + gain_db: -6.2093 + phi_rad: 0.0 + theta_rad: 1.4206980111233845 + } + gain_value { + gain_db: -6.6208 + phi_rad: 0.0 + theta_rad: 1.4215706757493816 + } + gain_value { + gain_db: -7.0145 + phi_rad: 0.0 + theta_rad: 1.4224433403753787 + } + gain_value { + gain_db: -7.3373 + phi_rad: 0.0 + theta_rad: 1.4233160050013758 + } + gain_value { + gain_db: -7.5336 + phi_rad: 0.0 + theta_rad: 1.424188669627373 + } + gain_value { + gain_db: -7.5417 + phi_rad: 0.0 + theta_rad: 1.4250613342533702 + } + gain_value { + gain_db: -7.3681 + phi_rad: 0.0 + theta_rad: 1.4259339988793673 + } + gain_value { + gain_db: -7.02 + phi_rad: 0.0 + theta_rad: 1.4268066635053644 + } + gain_value { + gain_db: -6.5667 + phi_rad: 0.0 + theta_rad: 1.4276793281313616 + } + gain_value { + gain_db: -6.0717 + phi_rad: 0.0 + theta_rad: 1.4285519927573587 + } + gain_value { + gain_db: -5.4809 + phi_rad: 0.0 + theta_rad: 1.429424657383356 + } + gain_value { + gain_db: -4.9819 + phi_rad: 0.0 + theta_rad: 1.4302973220093531 + } + gain_value { + gain_db: -4.6786 + phi_rad: 0.0 + theta_rad: 1.4311699866353502 + } + gain_value { + gain_db: -4.5051 + phi_rad: 0.0 + theta_rad: 1.4320426512613473 + } + gain_value { + gain_db: -4.4918 + phi_rad: 0.0 + theta_rad: 1.4329153158873444 + } + gain_value { + gain_db: -4.6085 + phi_rad: 0.0 + theta_rad: 1.4337879805133418 + } + gain_value { + gain_db: -4.9483 + phi_rad: 0.0 + theta_rad: 1.4346606451393389 + } + gain_value { + gain_db: -5.468 + phi_rad: 0.0 + theta_rad: 1.435533309765336 + } + gain_value { + gain_db: -6.038 + phi_rad: 0.0 + theta_rad: 1.436405974391333 + } + gain_value { + gain_db: -6.606 + phi_rad: 0.0 + theta_rad: 1.4372786390173302 + } + gain_value { + gain_db: -7.301 + phi_rad: 0.0 + theta_rad: 1.4381513036433275 + } + gain_value { + gain_db: -7.8587 + phi_rad: 0.0 + theta_rad: 1.4390239682693247 + } + gain_value { + gain_db: -8.0138 + phi_rad: 0.0 + theta_rad: 1.4398966328953218 + } + gain_value { + gain_db: -7.88 + phi_rad: 0.0 + theta_rad: 1.4407692975213189 + } + gain_value { + gain_db: -7.5625 + phi_rad: 0.0 + theta_rad: 1.4416419621473162 + } + gain_value { + gain_db: -7.1556 + phi_rad: 0.0 + theta_rad: 1.4425146267733135 + } + gain_value { + gain_db: -6.5718 + phi_rad: 0.0 + theta_rad: 1.4433872913993107 + } + gain_value { + gain_db: -6.0812 + phi_rad: 0.0 + theta_rad: 1.4442599560253078 + } + gain_value { + gain_db: -5.808 + phi_rad: 0.0 + theta_rad: 1.4451326206513049 + } + gain_value { + gain_db: -5.6187 + phi_rad: 0.0 + theta_rad: 1.446005285277302 + } + gain_value { + gain_db: -5.596 + phi_rad: 0.0 + theta_rad: 1.4468779499032993 + } + gain_value { + gain_db: -5.772 + phi_rad: 0.0 + theta_rad: 1.4477506145292964 + } + gain_value { + gain_db: -6.0282 + phi_rad: 0.0 + theta_rad: 1.4486232791552935 + } + gain_value { + gain_db: -6.3737 + phi_rad: 0.0 + theta_rad: 1.4494959437812907 + } + gain_value { + gain_db: -6.8291 + phi_rad: 0.0 + theta_rad: 1.4503686084072878 + } + gain_value { + gain_db: -7.2855 + phi_rad: 0.0 + theta_rad: 1.451241273033285 + } + gain_value { + gain_db: -7.7448 + phi_rad: 0.0 + theta_rad: 1.4521139376592822 + } + gain_value { + gain_db: -8.1145 + phi_rad: 0.0 + theta_rad: 1.4529866022852793 + } + gain_value { + gain_db: -8.1821 + phi_rad: 0.0 + theta_rad: 1.4538592669112764 + } + gain_value { + gain_db: -8.058 + phi_rad: 0.0 + theta_rad: 1.4547319315372735 + } + gain_value { + gain_db: -7.86 + phi_rad: 0.0 + theta_rad: 1.4556045961632709 + } + gain_value { + gain_db: -7.59 + phi_rad: 0.0 + theta_rad: 1.456477260789268 + } + gain_value { + gain_db: -7.2276 + phi_rad: 0.0 + theta_rad: 1.457349925415265 + } + gain_value { + gain_db: -6.8217 + phi_rad: 0.0 + theta_rad: 1.4582225900412622 + } + gain_value { + gain_db: -6.4479 + phi_rad: 0.0 + theta_rad: 1.4590952546672593 + } + gain_value { + gain_db: -6.2165 + phi_rad: 0.0 + theta_rad: 1.4599679192932569 + } + gain_value { + gain_db: -5.9905 + phi_rad: 0.0 + theta_rad: 1.460840583919254 + } + gain_value { + gain_db: -5.9175 + phi_rad: 0.0 + theta_rad: 1.461713248545251 + } + gain_value { + gain_db: -6.0019 + phi_rad: 0.0 + theta_rad: 1.4625859131712482 + } + gain_value { + gain_db: -6.2232 + phi_rad: 0.0 + theta_rad: 1.4634585777972453 + } + gain_value { + gain_db: -6.5455 + phi_rad: 0.0 + theta_rad: 1.4643312424232426 + } + gain_value { + gain_db: -6.9291 + phi_rad: 0.0 + theta_rad: 1.4652039070492398 + } + gain_value { + gain_db: -7.4342 + phi_rad: 0.0 + theta_rad: 1.4660765716752369 + } + gain_value { + gain_db: -8.0357 + phi_rad: 0.0 + theta_rad: 1.466949236301234 + } + gain_value { + gain_db: -8.7923 + phi_rad: 0.0 + theta_rad: 1.467821900927231 + } + gain_value { + gain_db: -9.7145 + phi_rad: 0.0 + theta_rad: 1.4686945655532284 + } + gain_value { + gain_db: -10.43 + phi_rad: 0.0 + theta_rad: 1.4695672301792255 + } + gain_value { + gain_db: -10.725 + phi_rad: 0.0 + theta_rad: 1.4704398948052226 + } + gain_value { + gain_db: -10.542 + phi_rad: 0.0 + theta_rad: 1.4713125594312197 + } + gain_value { + gain_db: -9.9813 + phi_rad: 0.0 + theta_rad: 1.4721852240572169 + } + gain_value { + gain_db: -9.2805 + phi_rad: 0.0 + theta_rad: 1.4730578886832142 + } + gain_value { + gain_db: -8.7243 + phi_rad: 0.0 + theta_rad: 1.4739305533092113 + } + gain_value { + gain_db: -8.2451 + phi_rad: 0.0 + theta_rad: 1.4748032179352084 + } + gain_value { + gain_db: -7.9067 + phi_rad: 0.0 + theta_rad: 1.4756758825612055 + } + gain_value { + gain_db: -7.87 + phi_rad: 0.0 + theta_rad: 1.4765485471872026 + } + gain_value { + gain_db: -7.9579 + phi_rad: 0.0 + theta_rad: 1.4774212118132 + } + gain_value { + gain_db: -8.1782 + phi_rad: 0.0 + theta_rad: 1.478293876439197 + } + gain_value { + gain_db: -8.6054 + phi_rad: 0.0 + theta_rad: 1.4791665410651942 + } + gain_value { + gain_db: -9.1378 + phi_rad: 0.0 + theta_rad: 1.4800392056911915 + } + gain_value { + gain_db: -9.8344 + phi_rad: 0.0 + theta_rad: 1.4809118703171886 + } + gain_value { + gain_db: -10.638 + phi_rad: 0.0 + theta_rad: 1.481784534943186 + } + gain_value { + gain_db: -11.649 + phi_rad: 0.0 + theta_rad: 1.482657199569183 + } + gain_value { + gain_db: -12.446 + phi_rad: 0.0 + theta_rad: 1.4835298641951802 + } + gain_value { + gain_db: -12.732 + phi_rad: 0.0 + theta_rad: 1.4844025288211773 + } + gain_value { + gain_db: -12.909 + phi_rad: 0.0 + theta_rad: 1.4852751934471744 + } + gain_value { + gain_db: -12.815 + phi_rad: 0.0 + theta_rad: 1.4861478580731717 + } + gain_value { + gain_db: -11.754 + phi_rad: 0.0 + theta_rad: 1.4870205226991688 + } + gain_value { + gain_db: -10.453 + phi_rad: 0.0 + theta_rad: 1.487893187325166 + } + gain_value { + gain_db: -9.408 + phi_rad: 0.0 + theta_rad: 1.488765851951163 + } + gain_value { + gain_db: -8.5819 + phi_rad: 0.0 + theta_rad: 1.4896385165771602 + } + gain_value { + gain_db: -7.9218 + phi_rad: 0.0 + theta_rad: 1.4905111812031575 + } + gain_value { + gain_db: -7.6049 + phi_rad: 0.0 + theta_rad: 1.4913838458291546 + } + gain_value { + gain_db: -7.3536 + phi_rad: 0.0 + theta_rad: 1.4922565104551517 + } + gain_value { + gain_db: -7.3521 + phi_rad: 0.0 + theta_rad: 1.4931291750811488 + } + gain_value { + gain_db: -7.6462 + phi_rad: 0.0 + theta_rad: 1.494001839707146 + } + gain_value { + gain_db: -8.1578 + phi_rad: 0.0 + theta_rad: 1.4948745043331433 + } + gain_value { + gain_db: -8.9962 + phi_rad: 0.0 + theta_rad: 1.4957471689591404 + } + gain_value { + gain_db: -9.8641 + phi_rad: 0.0 + theta_rad: 1.4966198335851375 + } + gain_value { + gain_db: -10.699 + phi_rad: 0.0 + theta_rad: 1.4974924982111346 + } + gain_value { + gain_db: -11.503 + phi_rad: 0.0 + theta_rad: 1.4983651628371317 + } + gain_value { + gain_db: -11.947 + phi_rad: 0.0 + theta_rad: 1.4992378274631293 + } + gain_value { + gain_db: -11.95 + phi_rad: 0.0 + theta_rad: 1.5001104920891264 + } + gain_value { + gain_db: -11.854 + phi_rad: 0.0 + theta_rad: 1.5009831567151235 + } + gain_value { + gain_db: -11.347 + phi_rad: 0.0 + theta_rad: 1.5018558213411206 + } + gain_value { + gain_db: -10.079 + phi_rad: 0.0 + theta_rad: 1.5027284859671177 + } + gain_value { + gain_db: -8.9824 + phi_rad: 0.0 + theta_rad: 1.503601150593115 + } + gain_value { + gain_db: -7.9102 + phi_rad: 0.0 + theta_rad: 1.5044738152191122 + } + gain_value { + gain_db: -7.2289 + phi_rad: 0.0 + theta_rad: 1.5053464798451093 + } + gain_value { + gain_db: -6.758 + phi_rad: 0.0 + theta_rad: 1.5062191444711064 + } + gain_value { + gain_db: -6.4972 + phi_rad: 0.0 + theta_rad: 1.5070918090971035 + } + gain_value { + gain_db: -6.577 + phi_rad: 0.0 + theta_rad: 1.5079644737231008 + } + gain_value { + gain_db: -6.9386 + phi_rad: 0.0 + theta_rad: 1.508837138349098 + } + gain_value { + gain_db: -7.7088 + phi_rad: 0.0 + theta_rad: 1.509709802975095 + } + gain_value { + gain_db: -8.6904 + phi_rad: 0.0 + theta_rad: 1.5105824676010922 + } + gain_value { + gain_db: -10.117 + phi_rad: 0.0 + theta_rad: 1.5114551322270893 + } + gain_value { + gain_db: -11.536 + phi_rad: 0.0 + theta_rad: 1.5123277968530866 + } + gain_value { + gain_db: -12.767 + phi_rad: 0.0 + theta_rad: 1.5132004614790837 + } + gain_value { + gain_db: -12.645 + phi_rad: 0.0 + theta_rad: 1.5140731261050808 + } + gain_value { + gain_db: -11.942 + phi_rad: 0.0 + theta_rad: 1.514945790731078 + } + gain_value { + gain_db: -10.949 + phi_rad: 0.0 + theta_rad: 1.515818455357075 + } + gain_value { + gain_db: -9.9152 + phi_rad: 0.0 + theta_rad: 1.5166911199830724 + } + gain_value { + gain_db: -9.0846 + phi_rad: 0.0 + theta_rad: 1.5175637846090695 + } + gain_value { + gain_db: -8.53 + phi_rad: 0.0 + theta_rad: 1.5184364492350666 + } + gain_value { + gain_db: -8.222 + phi_rad: 0.0 + theta_rad: 1.519309113861064 + } + gain_value { + gain_db: -8.0648 + phi_rad: 0.0 + theta_rad: 1.520181778487061 + } + gain_value { + gain_db: -8.2072 + phi_rad: 0.0 + theta_rad: 1.5210544431130584 + } + gain_value { + gain_db: -8.4271 + phi_rad: 0.0 + theta_rad: 1.5219271077390555 + } + gain_value { + gain_db: -8.7453 + phi_rad: 0.0 + theta_rad: 1.5227997723650526 + } + gain_value { + gain_db: -9.1832 + phi_rad: 0.0 + theta_rad: 1.5236724369910497 + } + gain_value { + gain_db: -9.8297 + phi_rad: 0.0 + theta_rad: 1.5245451016170468 + } + gain_value { + gain_db: -10.555 + phi_rad: 0.0 + theta_rad: 1.5254177662430441 + } + gain_value { + gain_db: -11.367 + phi_rad: 0.0 + theta_rad: 1.5262904308690413 + } + gain_value { + gain_db: -12.223 + phi_rad: 0.0 + theta_rad: 1.5271630954950384 + } + gain_value { + gain_db: -12.857 + phi_rad: 0.0 + theta_rad: 1.5280357601210355 + } + gain_value { + gain_db: -13.478 + phi_rad: 0.0 + theta_rad: 1.5289084247470326 + } + gain_value { + gain_db: -13.315 + phi_rad: 0.0 + theta_rad: 1.52978108937303 + } + gain_value { + gain_db: -12.486 + phi_rad: 0.0 + theta_rad: 1.530653753999027 + } + gain_value { + gain_db: -11.56 + phi_rad: 0.0 + theta_rad: 1.5315264186250241 + } + gain_value { + gain_db: -10.802 + phi_rad: 0.0 + theta_rad: 1.5323990832510213 + } + gain_value { + gain_db: -10.123 + phi_rad: 0.0 + theta_rad: 1.5332717478770184 + } + gain_value { + gain_db: -9.7344 + phi_rad: 0.0 + theta_rad: 1.5341444125030157 + } + gain_value { + gain_db: -9.6116 + phi_rad: 0.0 + theta_rad: 1.5350170771290128 + } + gain_value { + gain_db: -9.4764 + phi_rad: 0.0 + theta_rad: 1.53588974175501 + } + gain_value { + gain_db: -9.5548 + phi_rad: 0.0 + theta_rad: 1.536762406381007 + } + gain_value { + gain_db: -9.8354 + phi_rad: 0.0 + theta_rad: 1.5376350710070041 + } + gain_value { + gain_db: -10.176 + phi_rad: 0.0 + theta_rad: 1.5385077356330017 + } + gain_value { + gain_db: -10.333 + phi_rad: 0.0 + theta_rad: 1.5393804002589988 + } + gain_value { + gain_db: -10.652 + phi_rad: 0.0 + theta_rad: 1.540253064884996 + } + gain_value { + gain_db: -11.221 + phi_rad: 0.0 + theta_rad: 1.541125729510993 + } + gain_value { + gain_db: -12.034 + phi_rad: 0.0 + theta_rad: 1.5419983941369901 + } + gain_value { + gain_db: -13.311 + phi_rad: 0.0 + theta_rad: 1.5428710587629875 + } + gain_value { + gain_db: -15.202 + phi_rad: 0.0 + theta_rad: 1.5437437233889846 + } + gain_value { + gain_db: -16.412 + phi_rad: 0.0 + theta_rad: 1.5446163880149817 + } + gain_value { + gain_db: -15.304 + phi_rad: 0.0 + theta_rad: 1.5454890526409788 + } + gain_value { + gain_db: -13.852 + phi_rad: 0.0 + theta_rad: 1.546361717266976 + } + gain_value { + gain_db: -12.717 + phi_rad: 0.0 + theta_rad: 1.5472343818929732 + } + gain_value { + gain_db: -11.657 + phi_rad: 0.0 + theta_rad: 1.5481070465189704 + } + gain_value { + gain_db: -11.123 + phi_rad: 0.0 + theta_rad: 1.5489797111449675 + } + gain_value { + gain_db: -10.607 + phi_rad: 0.0 + theta_rad: 1.5498523757709646 + } + gain_value { + gain_db: -10.59 + phi_rad: 0.0 + theta_rad: 1.5507250403969617 + } + gain_value { + gain_db: -10.863 + phi_rad: 0.0 + theta_rad: 1.551597705022959 + } + gain_value { + gain_db: -11.352 + phi_rad: 0.0 + theta_rad: 1.5524703696489561 + } + gain_value { + gain_db: -12.318 + phi_rad: 0.0 + theta_rad: 1.5533430342749532 + } + gain_value { + gain_db: -13.387 + phi_rad: 0.0 + theta_rad: 1.5542156989009503 + } + gain_value { + gain_db: -14.748 + phi_rad: 0.0 + theta_rad: 1.5550883635269475 + } + gain_value { + gain_db: -16.223 + phi_rad: 0.0 + theta_rad: 1.5559610281529448 + } + gain_value { + gain_db: -18.364 + phi_rad: 0.0 + theta_rad: 1.556833692778942 + } + gain_value { + gain_db: -22.184 + phi_rad: 0.0 + theta_rad: 1.557706357404939 + } + gain_value { + gain_db: -18.439 + phi_rad: 0.0 + theta_rad: 1.5585790220309363 + } + gain_value { + gain_db: -16.329 + phi_rad: 0.0 + theta_rad: 1.5594516866569335 + } + gain_value { + gain_db: -14.814 + phi_rad: 0.0 + theta_rad: 1.5603243512829308 + } + gain_value { + gain_db: -13.692 + phi_rad: 0.0 + theta_rad: 1.561197015908928 + } + gain_value { + gain_db: -13.242 + phi_rad: 0.0 + theta_rad: 1.562069680534925 + } + gain_value { + gain_db: -12.67 + phi_rad: 0.0 + theta_rad: 1.5629423451609221 + } + gain_value { + gain_db: -12.261 + phi_rad: 0.0 + theta_rad: 1.5638150097869192 + } + gain_value { + gain_db: -12.169 + phi_rad: 0.0 + theta_rad: 1.5646876744129166 + } + gain_value { + gain_db: -12.322 + phi_rad: 0.0 + theta_rad: 1.5655603390389137 + } + gain_value { + gain_db: -12.523 + phi_rad: 0.0 + theta_rad: 1.5664330036649108 + } + gain_value { + gain_db: -12.695 + phi_rad: 0.0 + theta_rad: 1.567305668290908 + } + gain_value { + gain_db: -13.396 + phi_rad: 0.0 + theta_rad: 1.568178332916905 + } + gain_value { + gain_db: -14.528 + phi_rad: 0.0 + theta_rad: 1.5690509975429023 + } + gain_value { + gain_db: -16.213 + phi_rad: 0.0 + theta_rad: 1.5699236621688994 + } + gain_value { + gain_db: -18.418 + phi_rad: 0.0 + theta_rad: 1.5707963267948966 + } + gain_value { + gain_db: 43.974 + phi_rad: 1.5707963267948966 + theta_rad: 0.0 + } + gain_value { + gain_db: 44.0 + phi_rad: 1.5707963267948966 + theta_rad: 8.726646259971648E-4 + } + gain_value { + gain_db: 43.976 + phi_rad: 1.5707963267948966 + theta_rad: 0.0017453292519943296 + } + gain_value { + gain_db: 43.931 + phi_rad: 1.5707963267948966 + theta_rad: 0.002617993877991494 + } + gain_value { + gain_db: 43.827 + phi_rad: 1.5707963267948966 + theta_rad: 0.003490658503988659 + } + gain_value { + gain_db: 43.644 + phi_rad: 1.5707963267948966 + theta_rad: 0.004363323129985824 + } + gain_value { + gain_db: 43.47 + phi_rad: 1.5707963267948966 + theta_rad: 0.005235987755982988 + } + gain_value { + gain_db: 43.157 + phi_rad: 1.5707963267948966 + theta_rad: 0.006108652381980153 + } + gain_value { + gain_db: 42.82 + phi_rad: 1.5707963267948966 + theta_rad: 0.006981317007977318 + } + gain_value { + gain_db: 42.327 + phi_rad: 1.5707963267948966 + theta_rad: 0.007853981633974483 + } + gain_value { + gain_db: 41.881 + phi_rad: 1.5707963267948966 + theta_rad: 0.008726646259971648 + } + gain_value { + gain_db: 41.018 + phi_rad: 1.5707963267948966 + theta_rad: 0.009599310885968814 + } + gain_value { + gain_db: 39.985 + phi_rad: 1.5707963267948966 + theta_rad: 0.010471975511965976 + } + gain_value { + gain_db: 39.027 + phi_rad: 1.5707963267948966 + theta_rad: 0.011344640137963142 + } + gain_value { + gain_db: 37.92 + phi_rad: 1.5707963267948966 + theta_rad: 0.012217304763960306 + } + gain_value { + gain_db: 36.749 + phi_rad: 1.5707963267948966 + theta_rad: 0.013089969389957472 + } + gain_value { + gain_db: 35.187 + phi_rad: 1.5707963267948966 + theta_rad: 0.013962634015954637 + } + gain_value { + gain_db: 33.523 + phi_rad: 1.5707963267948966 + theta_rad: 0.014835298641951801 + } + gain_value { + gain_db: 31.566 + phi_rad: 1.5707963267948966 + theta_rad: 0.015707963267948967 + } + gain_value { + gain_db: 29.48 + phi_rad: 1.5707963267948966 + theta_rad: 0.01658062789394613 + } + gain_value { + gain_db: 26.868 + phi_rad: 1.5707963267948966 + theta_rad: 0.017453292519943295 + } + gain_value { + gain_db: 23.292 + phi_rad: 1.5707963267948966 + theta_rad: 0.01832595714594046 + } + gain_value { + gain_db: 18.388 + phi_rad: 1.5707963267948966 + theta_rad: 0.019198621771937627 + } + gain_value { + gain_db: 15.045 + phi_rad: 1.5707963267948966 + theta_rad: 0.02007128639793479 + } + gain_value { + gain_db: 14.486 + phi_rad: 1.5707963267948966 + theta_rad: 0.020943951023931952 + } + gain_value { + gain_db: 15.067 + phi_rad: 1.5707963267948966 + theta_rad: 0.02181661564992912 + } + gain_value { + gain_db: 17.825 + phi_rad: 1.5707963267948966 + theta_rad: 0.022689280275926284 + } + gain_value { + gain_db: 19.636 + phi_rad: 1.5707963267948966 + theta_rad: 0.02356194490192345 + } + gain_value { + gain_db: 20.588 + phi_rad: 1.5707963267948966 + theta_rad: 0.024434609527920613 + } + gain_value { + gain_db: 21.171 + phi_rad: 1.5707963267948966 + theta_rad: 0.02530727415391778 + } + gain_value { + gain_db: 21.429 + phi_rad: 1.5707963267948966 + theta_rad: 0.026179938779914945 + } + gain_value { + gain_db: 21.413 + phi_rad: 1.5707963267948966 + theta_rad: 0.027052603405912107 + } + gain_value { + gain_db: 21.265 + phi_rad: 1.5707963267948966 + theta_rad: 0.027925268031909273 + } + gain_value { + gain_db: 20.777 + phi_rad: 1.5707963267948966 + theta_rad: 0.028797932657906436 + } + gain_value { + gain_db: 20.396 + phi_rad: 1.5707963267948966 + theta_rad: 0.029670597283903602 + } + gain_value { + gain_db: 19.753 + phi_rad: 1.5707963267948966 + theta_rad: 0.030543261909900768 + } + gain_value { + gain_db: 18.96 + phi_rad: 1.5707963267948966 + theta_rad: 0.031415926535897934 + } + gain_value { + gain_db: 18.096 + phi_rad: 1.5707963267948966 + theta_rad: 0.0322885911618951 + } + gain_value { + gain_db: 17.203 + phi_rad: 1.5707963267948966 + theta_rad: 0.03316125578789226 + } + gain_value { + gain_db: 16.225 + phi_rad: 1.5707963267948966 + theta_rad: 0.034033920413889425 + } + gain_value { + gain_db: 14.626 + phi_rad: 1.5707963267948966 + theta_rad: 0.03490658503988659 + } + gain_value { + gain_db: 13.399 + phi_rad: 1.5707963267948966 + theta_rad: 0.03577924966588375 + } + gain_value { + gain_db: 12.835 + phi_rad: 1.5707963267948966 + theta_rad: 0.03665191429188092 + } + gain_value { + gain_db: 13.313 + phi_rad: 1.5707963267948966 + theta_rad: 0.03752457891787808 + } + gain_value { + gain_db: 13.614 + phi_rad: 1.5707963267948966 + theta_rad: 0.038397243543875255 + } + gain_value { + gain_db: 13.686 + phi_rad: 1.5707963267948966 + theta_rad: 0.039269908169872414 + } + gain_value { + gain_db: 13.784 + phi_rad: 1.5707963267948966 + theta_rad: 0.04014257279586958 + } + gain_value { + gain_db: 14.215 + phi_rad: 1.5707963267948966 + theta_rad: 0.041015237421866746 + } + gain_value { + gain_db: 14.965 + phi_rad: 1.5707963267948966 + theta_rad: 0.041887902047863905 + } + gain_value { + gain_db: 15.734 + phi_rad: 1.5707963267948966 + theta_rad: 0.04276056667386108 + } + gain_value { + gain_db: 16.663 + phi_rad: 1.5707963267948966 + theta_rad: 0.04363323129985824 + } + gain_value { + gain_db: 17.472 + phi_rad: 1.5707963267948966 + theta_rad: 0.0445058959258554 + } + gain_value { + gain_db: 18.408 + phi_rad: 1.5707963267948966 + theta_rad: 0.04537856055185257 + } + gain_value { + gain_db: 19.112 + phi_rad: 1.5707963267948966 + theta_rad: 0.046251225177849735 + } + gain_value { + gain_db: 19.678 + phi_rad: 1.5707963267948966 + theta_rad: 0.0471238898038469 + } + gain_value { + gain_db: 20.113 + phi_rad: 1.5707963267948966 + theta_rad: 0.04799655442984406 + } + gain_value { + gain_db: 20.368 + phi_rad: 1.5707963267948966 + theta_rad: 0.048869219055841226 + } + gain_value { + gain_db: 20.441 + phi_rad: 1.5707963267948966 + theta_rad: 0.04974188368183839 + } + gain_value { + gain_db: 20.364 + phi_rad: 1.5707963267948966 + theta_rad: 0.05061454830783556 + } + gain_value { + gain_db: 20.058 + phi_rad: 1.5707963267948966 + theta_rad: 0.051487212933832724 + } + gain_value { + gain_db: 19.728 + phi_rad: 1.5707963267948966 + theta_rad: 0.05235987755982989 + } + gain_value { + gain_db: 19.076 + phi_rad: 1.5707963267948966 + theta_rad: 0.05323254218582705 + } + gain_value { + gain_db: 18.434 + phi_rad: 1.5707963267948966 + theta_rad: 0.054105206811824215 + } + gain_value { + gain_db: 17.389 + phi_rad: 1.5707963267948966 + theta_rad: 0.05497787143782138 + } + gain_value { + gain_db: 16.584 + phi_rad: 1.5707963267948966 + theta_rad: 0.05585053606381855 + } + gain_value { + gain_db: 15.265 + phi_rad: 1.5707963267948966 + theta_rad: 0.05672320068981571 + } + gain_value { + gain_db: 14.258 + phi_rad: 1.5707963267948966 + theta_rad: 0.05759586531581287 + } + gain_value { + gain_db: 13.044 + phi_rad: 1.5707963267948966 + theta_rad: 0.05846852994181004 + } + gain_value { + gain_db: 11.62 + phi_rad: 1.5707963267948966 + theta_rad: 0.059341194567807204 + } + gain_value { + gain_db: 10.284 + phi_rad: 1.5707963267948966 + theta_rad: 0.06021385919380437 + } + gain_value { + gain_db: 9.5321 + phi_rad: 1.5707963267948966 + theta_rad: 0.061086523819801536 + } + gain_value { + gain_db: 9.0861 + phi_rad: 1.5707963267948966 + theta_rad: 0.061959188445798695 + } + gain_value { + gain_db: 9.4133 + phi_rad: 1.5707963267948966 + theta_rad: 0.06283185307179587 + } + gain_value { + gain_db: 10.113 + phi_rad: 1.5707963267948966 + theta_rad: 0.06370451769779303 + } + gain_value { + gain_db: 11.005 + phi_rad: 1.5707963267948966 + theta_rad: 0.0645771823237902 + } + gain_value { + gain_db: 11.929 + phi_rad: 1.5707963267948966 + theta_rad: 0.06544984694978735 + } + gain_value { + gain_db: 12.507 + phi_rad: 1.5707963267948966 + theta_rad: 0.06632251157578452 + } + gain_value { + gain_db: 13.007 + phi_rad: 1.5707963267948966 + theta_rad: 0.06719517620178168 + } + gain_value { + gain_db: 13.335 + phi_rad: 1.5707963267948966 + theta_rad: 0.06806784082777885 + } + gain_value { + gain_db: 13.526 + phi_rad: 1.5707963267948966 + theta_rad: 0.06894050545377602 + } + gain_value { + gain_db: 13.546 + phi_rad: 1.5707963267948966 + theta_rad: 0.06981317007977318 + } + gain_value { + gain_db: 13.531 + phi_rad: 1.5707963267948966 + theta_rad: 0.07068583470577035 + } + gain_value { + gain_db: 13.398 + phi_rad: 1.5707963267948966 + theta_rad: 0.0715584993317675 + } + gain_value { + gain_db: 13.154 + phi_rad: 1.5707963267948966 + theta_rad: 0.07243116395776468 + } + gain_value { + gain_db: 12.827 + phi_rad: 1.5707963267948966 + theta_rad: 0.07330382858376185 + } + gain_value { + gain_db: 12.217 + phi_rad: 1.5707963267948966 + theta_rad: 0.07417649320975901 + } + gain_value { + gain_db: 11.495 + phi_rad: 1.5707963267948966 + theta_rad: 0.07504915783575616 + } + gain_value { + gain_db: 10.537 + phi_rad: 1.5707963267948966 + theta_rad: 0.07592182246175333 + } + gain_value { + gain_db: 9.5845 + phi_rad: 1.5707963267948966 + theta_rad: 0.07679448708775051 + } + gain_value { + gain_db: 8.2994 + phi_rad: 1.5707963267948966 + theta_rad: 0.07766715171374766 + } + gain_value { + gain_db: 6.925 + phi_rad: 1.5707963267948966 + theta_rad: 0.07853981633974483 + } + gain_value { + gain_db: 5.0482 + phi_rad: 1.5707963267948966 + theta_rad: 0.079412480965742 + } + gain_value { + gain_db: 3.1578 + phi_rad: 1.5707963267948966 + theta_rad: 0.08028514559173916 + } + gain_value { + gain_db: 2.1978 + phi_rad: 1.5707963267948966 + theta_rad: 0.08115781021773633 + } + gain_value { + gain_db: 2.7196 + phi_rad: 1.5707963267948966 + theta_rad: 0.08203047484373349 + } + gain_value { + gain_db: 3.4143 + phi_rad: 1.5707963267948966 + theta_rad: 0.08290313946973066 + } + gain_value { + gain_db: 4.4353 + phi_rad: 1.5707963267948966 + theta_rad: 0.08377580409572781 + } + gain_value { + gain_db: 5.4157 + phi_rad: 1.5707963267948966 + theta_rad: 0.08464846872172498 + } + gain_value { + gain_db: 6.3542 + phi_rad: 1.5707963267948966 + theta_rad: 0.08552113334772216 + } + gain_value { + gain_db: 7.0393 + phi_rad: 1.5707963267948966 + theta_rad: 0.08639379797371932 + } + gain_value { + gain_db: 7.7342 + phi_rad: 1.5707963267948966 + theta_rad: 0.08726646259971647 + } + gain_value { + gain_db: 8.1522 + phi_rad: 1.5707963267948966 + theta_rad: 0.08813912722571364 + } + gain_value { + gain_db: 8.562 + phi_rad: 1.5707963267948966 + theta_rad: 0.0890117918517108 + } + gain_value { + gain_db: 8.7905 + phi_rad: 1.5707963267948966 + theta_rad: 0.08988445647770797 + } + gain_value { + gain_db: 8.9218 + phi_rad: 1.5707963267948966 + theta_rad: 0.09075712110370514 + } + gain_value { + gain_db: 8.9712 + phi_rad: 1.5707963267948966 + theta_rad: 0.0916297857297023 + } + gain_value { + gain_db: 8.9217 + phi_rad: 1.5707963267948966 + theta_rad: 0.09250245035569947 + } + gain_value { + gain_db: 8.737 + phi_rad: 1.5707963267948966 + theta_rad: 0.09337511498169662 + } + gain_value { + gain_db: 8.4482 + phi_rad: 1.5707963267948966 + theta_rad: 0.0942477796076938 + } + gain_value { + gain_db: 7.9498 + phi_rad: 1.5707963267948966 + theta_rad: 0.09512044423369097 + } + gain_value { + gain_db: 7.4406 + phi_rad: 1.5707963267948966 + theta_rad: 0.09599310885968812 + } + gain_value { + gain_db: 6.8395 + phi_rad: 1.5707963267948966 + theta_rad: 0.09686577348568529 + } + gain_value { + gain_db: 6.1189 + phi_rad: 1.5707963267948966 + theta_rad: 0.09773843811168245 + } + gain_value { + gain_db: 5.2792 + phi_rad: 1.5707963267948966 + theta_rad: 0.09861110273767963 + } + gain_value { + gain_db: 4.1359 + phi_rad: 1.5707963267948966 + theta_rad: 0.09948376736367678 + } + gain_value { + gain_db: 3.1165 + phi_rad: 1.5707963267948966 + theta_rad: 0.10035643198967395 + } + gain_value { + gain_db: 1.9631 + phi_rad: 1.5707963267948966 + theta_rad: 0.10122909661567112 + } + gain_value { + gain_db: 1.0336 + phi_rad: 1.5707963267948966 + theta_rad: 0.10210176124166827 + } + gain_value { + gain_db: 0.0594 + phi_rad: 1.5707963267948966 + theta_rad: 0.10297442586766545 + } + gain_value { + gain_db: -1.1004 + phi_rad: 1.5707963267948966 + theta_rad: 0.10384709049366261 + } + gain_value { + gain_db: -2.8563 + phi_rad: 1.5707963267948966 + theta_rad: 0.10471975511965978 + } + gain_value { + gain_db: -5.0778 + phi_rad: 1.5707963267948966 + theta_rad: 0.10559241974565693 + } + gain_value { + gain_db: -4.5828 + phi_rad: 1.5707963267948966 + theta_rad: 0.1064650843716541 + } + gain_value { + gain_db: -2.1616 + phi_rad: 1.5707963267948966 + theta_rad: 0.10733774899765128 + } + gain_value { + gain_db: -1.1126 + phi_rad: 1.5707963267948966 + theta_rad: 0.10821041362364843 + } + gain_value { + gain_db: -0.12537 + phi_rad: 1.5707963267948966 + theta_rad: 0.1090830782496456 + } + gain_value { + gain_db: 0.72517 + phi_rad: 1.5707963267948966 + theta_rad: 0.10995574287564276 + } + gain_value { + gain_db: 1.4598 + phi_rad: 1.5707963267948966 + theta_rad: 0.11082840750163991 + } + gain_value { + gain_db: 1.7772 + phi_rad: 1.5707963267948966 + theta_rad: 0.1117010721276371 + } + gain_value { + gain_db: 2.1629 + phi_rad: 1.5707963267948966 + theta_rad: 0.11257373675363426 + } + gain_value { + gain_db: 2.2169 + phi_rad: 1.5707963267948966 + theta_rad: 0.11344640137963143 + } + gain_value { + gain_db: 2.2596 + phi_rad: 1.5707963267948966 + theta_rad: 0.11431906600562858 + } + gain_value { + gain_db: 2.3345 + phi_rad: 1.5707963267948966 + theta_rad: 0.11519173063162574 + } + gain_value { + gain_db: 2.2856 + phi_rad: 1.5707963267948966 + theta_rad: 0.11606439525762292 + } + gain_value { + gain_db: 2.2172 + phi_rad: 1.5707963267948966 + theta_rad: 0.11693705988362008 + } + gain_value { + gain_db: 2.1104 + phi_rad: 1.5707963267948966 + theta_rad: 0.11780972450961724 + } + gain_value { + gain_db: 2.0199 + phi_rad: 1.5707963267948966 + theta_rad: 0.11868238913561441 + } + gain_value { + gain_db: 1.8882 + phi_rad: 1.5707963267948966 + theta_rad: 0.11955505376161157 + } + gain_value { + gain_db: 1.6698 + phi_rad: 1.5707963267948966 + theta_rad: 0.12042771838760874 + } + gain_value { + gain_db: 1.4087 + phi_rad: 1.5707963267948966 + theta_rad: 0.1213003830136059 + } + gain_value { + gain_db: 1.1881 + phi_rad: 1.5707963267948966 + theta_rad: 0.12217304763960307 + } + gain_value { + gain_db: 1.0708 + phi_rad: 1.5707963267948966 + theta_rad: 0.12304571226560022 + } + gain_value { + gain_db: 1.1034 + phi_rad: 1.5707963267948966 + theta_rad: 0.12391837689159739 + } + gain_value { + gain_db: 1.2754 + phi_rad: 1.5707963267948966 + theta_rad: 0.12479104151759457 + } + gain_value { + gain_db: 1.6215 + phi_rad: 1.5707963267948966 + theta_rad: 0.12566370614359174 + } + gain_value { + gain_db: 2.0232 + phi_rad: 1.5707963267948966 + theta_rad: 0.1265363707695889 + } + gain_value { + gain_db: 2.6713 + phi_rad: 1.5707963267948966 + theta_rad: 0.12740903539558607 + } + gain_value { + gain_db: 3.2391 + phi_rad: 1.5707963267948966 + theta_rad: 0.1282817000215832 + } + gain_value { + gain_db: 3.8048 + phi_rad: 1.5707963267948966 + theta_rad: 0.1291543646475804 + } + gain_value { + gain_db: 4.481 + phi_rad: 1.5707963267948966 + theta_rad: 0.13002702927357757 + } + gain_value { + gain_db: 4.9146 + phi_rad: 1.5707963267948966 + theta_rad: 0.1308996938995747 + } + gain_value { + gain_db: 5.4986 + phi_rad: 1.5707963267948966 + theta_rad: 0.13177235852557187 + } + gain_value { + gain_db: 5.88 + phi_rad: 1.5707963267948966 + theta_rad: 0.13264502315156904 + } + gain_value { + gain_db: 6.3766 + phi_rad: 1.5707963267948966 + theta_rad: 0.13351768777756623 + } + gain_value { + gain_db: 6.6809 + phi_rad: 1.5707963267948966 + theta_rad: 0.13439035240356337 + } + gain_value { + gain_db: 6.961 + phi_rad: 1.5707963267948966 + theta_rad: 0.13526301702956053 + } + gain_value { + gain_db: 7.0674 + phi_rad: 1.5707963267948966 + theta_rad: 0.1361356816555577 + } + gain_value { + gain_db: 7.2067 + phi_rad: 1.5707963267948966 + theta_rad: 0.13700834628155487 + } + gain_value { + gain_db: 7.2525 + phi_rad: 1.5707963267948966 + theta_rad: 0.13788101090755203 + } + gain_value { + gain_db: 7.2463 + phi_rad: 1.5707963267948966 + theta_rad: 0.1387536755335492 + } + gain_value { + gain_db: 7.1976 + phi_rad: 1.5707963267948966 + theta_rad: 0.13962634015954636 + } + gain_value { + gain_db: 7.1274 + phi_rad: 1.5707963267948966 + theta_rad: 0.14049900478554353 + } + gain_value { + gain_db: 7.0635 + phi_rad: 1.5707963267948966 + theta_rad: 0.1413716694115407 + } + gain_value { + gain_db: 6.945 + phi_rad: 1.5707963267948966 + theta_rad: 0.14224433403753786 + } + gain_value { + gain_db: 6.7838 + phi_rad: 1.5707963267948966 + theta_rad: 0.143116998663535 + } + gain_value { + gain_db: 6.6837 + phi_rad: 1.5707963267948966 + theta_rad: 0.1439896632895322 + } + gain_value { + gain_db: 6.5768 + phi_rad: 1.5707963267948966 + theta_rad: 0.14486232791552936 + } + gain_value { + gain_db: 6.417 + phi_rad: 1.5707963267948966 + theta_rad: 0.1457349925415265 + } + gain_value { + gain_db: 6.2376 + phi_rad: 1.5707963267948966 + theta_rad: 0.1466076571675237 + } + gain_value { + gain_db: 5.9804 + phi_rad: 1.5707963267948966 + theta_rad: 0.14748032179352083 + } + gain_value { + gain_db: 5.808 + phi_rad: 1.5707963267948966 + theta_rad: 0.14835298641951802 + } + gain_value { + gain_db: 5.5635 + phi_rad: 1.5707963267948966 + theta_rad: 0.1492256510455152 + } + gain_value { + gain_db: 5.42 + phi_rad: 1.5707963267948966 + theta_rad: 0.15009831567151233 + } + gain_value { + gain_db: 5.2534 + phi_rad: 1.5707963267948966 + theta_rad: 0.15097098029750952 + } + gain_value { + gain_db: 5.0504 + phi_rad: 1.5707963267948966 + theta_rad: 0.15184364492350666 + } + gain_value { + gain_db: 4.8129 + phi_rad: 1.5707963267948966 + theta_rad: 0.15271630954950383 + } + gain_value { + gain_db: 4.5734 + phi_rad: 1.5707963267948966 + theta_rad: 0.15358897417550102 + } + gain_value { + gain_db: 4.3522 + phi_rad: 1.5707963267948966 + theta_rad: 0.15446163880149816 + } + gain_value { + gain_db: 4.1219 + phi_rad: 1.5707963267948966 + theta_rad: 0.15533430342749532 + } + gain_value { + gain_db: 3.8443 + phi_rad: 1.5707963267948966 + theta_rad: 0.1562069680534925 + } + gain_value { + gain_db: 3.5325 + phi_rad: 1.5707963267948966 + theta_rad: 0.15707963267948966 + } + gain_value { + gain_db: 3.2145 + phi_rad: 1.5707963267948966 + theta_rad: 0.15795229730548685 + } + gain_value { + gain_db: 2.9435 + phi_rad: 1.5707963267948966 + theta_rad: 0.158824961931484 + } + gain_value { + gain_db: 2.5455 + phi_rad: 1.5707963267948966 + theta_rad: 0.15969762655748115 + } + gain_value { + gain_db: 2.1709 + phi_rad: 1.5707963267948966 + theta_rad: 0.16057029118347832 + } + gain_value { + gain_db: 1.8447 + phi_rad: 1.5707963267948966 + theta_rad: 0.16144295580947549 + } + gain_value { + gain_db: 1.5044 + phi_rad: 1.5707963267948966 + theta_rad: 0.16231562043547265 + } + gain_value { + gain_db: 1.2096 + phi_rad: 1.5707963267948966 + theta_rad: 0.16318828506146982 + } + gain_value { + gain_db: 0.98113 + phi_rad: 1.5707963267948966 + theta_rad: 0.16406094968746698 + } + gain_value { + gain_db: 0.75077 + phi_rad: 1.5707963267948966 + theta_rad: 0.16493361431346412 + } + gain_value { + gain_db: 0.6065 + phi_rad: 1.5707963267948966 + theta_rad: 0.16580627893946132 + } + gain_value { + gain_db: 0.43017 + phi_rad: 1.5707963267948966 + theta_rad: 0.16667894356545848 + } + gain_value { + gain_db: 0.27337 + phi_rad: 1.5707963267948966 + theta_rad: 0.16755160819145562 + } + gain_value { + gain_db: 0.1614 + phi_rad: 1.5707963267948966 + theta_rad: 0.1684242728174528 + } + gain_value { + gain_db: 0.0953 + phi_rad: 1.5707963267948966 + theta_rad: 0.16929693744344995 + } + gain_value { + gain_db: -0.0274 + phi_rad: 1.5707963267948966 + theta_rad: 0.17016960206944712 + } + gain_value { + gain_db: -0.1471 + phi_rad: 1.5707963267948966 + theta_rad: 0.1710422666954443 + } + gain_value { + gain_db: -0.2923 + phi_rad: 1.5707963267948966 + theta_rad: 0.17191493132144145 + } + gain_value { + gain_db: -0.41793 + phi_rad: 1.5707963267948966 + theta_rad: 0.17278759594743864 + } + gain_value { + gain_db: -0.6197 + phi_rad: 1.5707963267948966 + theta_rad: 0.17366026057343578 + } + gain_value { + gain_db: -0.8551 + phi_rad: 1.5707963267948966 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: -1.0629 + phi_rad: 1.5707963267948966 + theta_rad: 0.17540558982543014 + } + gain_value { + gain_db: -1.275 + phi_rad: 1.5707963267948966 + theta_rad: 0.17627825445142728 + } + gain_value { + gain_db: -1.4498 + phi_rad: 1.5707963267948966 + theta_rad: 0.17715091907742445 + } + gain_value { + gain_db: -1.5628 + phi_rad: 1.5707963267948966 + theta_rad: 0.1780235837034216 + } + gain_value { + gain_db: -1.7257 + phi_rad: 1.5707963267948966 + theta_rad: 0.17889624832941878 + } + gain_value { + gain_db: -1.8643 + phi_rad: 1.5707963267948966 + theta_rad: 0.17976891295541594 + } + gain_value { + gain_db: -1.9444 + phi_rad: 1.5707963267948966 + theta_rad: 0.1806415775814131 + } + gain_value { + gain_db: -1.9997 + phi_rad: 1.5707963267948966 + theta_rad: 0.18151424220741028 + } + gain_value { + gain_db: -2.103 + phi_rad: 1.5707963267948966 + theta_rad: 0.18238690683340741 + } + gain_value { + gain_db: -2.1444 + phi_rad: 1.5707963267948966 + theta_rad: 0.1832595714594046 + } + gain_value { + gain_db: -2.3853 + phi_rad: 1.5707963267948966 + theta_rad: 0.18413223608540177 + } + gain_value { + gain_db: -2.8358 + phi_rad: 1.5707963267948966 + theta_rad: 0.18500490071139894 + } + gain_value { + gain_db: -3.4786 + phi_rad: 1.5707963267948966 + theta_rad: 0.1858775653373961 + } + gain_value { + gain_db: -4.4345 + phi_rad: 1.5707963267948966 + theta_rad: 0.18675022996339324 + } + gain_value { + gain_db: -6.0897 + phi_rad: 1.5707963267948966 + theta_rad: 0.18762289458939044 + } + gain_value { + gain_db: -10.62 + phi_rad: 1.5707963267948966 + theta_rad: 0.1884955592153876 + } + gain_value { + gain_db: -6.9752 + phi_rad: 1.5707963267948966 + theta_rad: 0.18936822384138474 + } + gain_value { + gain_db: -5.18 + phi_rad: 1.5707963267948966 + theta_rad: 0.19024088846738194 + } + gain_value { + gain_db: -4.2952 + phi_rad: 1.5707963267948966 + theta_rad: 0.19111355309337907 + } + gain_value { + gain_db: -3.7561 + phi_rad: 1.5707963267948966 + theta_rad: 0.19198621771937624 + } + gain_value { + gain_db: -3.4967 + phi_rad: 1.5707963267948966 + theta_rad: 0.19285888234537343 + } + gain_value { + gain_db: -3.2519 + phi_rad: 1.5707963267948966 + theta_rad: 0.19373154697137057 + } + gain_value { + gain_db: -3.0632 + phi_rad: 1.5707963267948966 + theta_rad: 0.19460421159736774 + } + gain_value { + gain_db: -2.9075 + phi_rad: 1.5707963267948966 + theta_rad: 0.1954768762233649 + } + gain_value { + gain_db: -2.8766 + phi_rad: 1.5707963267948966 + theta_rad: 0.19634954084936207 + } + gain_value { + gain_db: -2.9316 + phi_rad: 1.5707963267948966 + theta_rad: 0.19722220547535926 + } + gain_value { + gain_db: -2.965 + phi_rad: 1.5707963267948966 + theta_rad: 0.1980948701013564 + } + gain_value { + gain_db: -3.0287 + phi_rad: 1.5707963267948966 + theta_rad: 0.19896753472735357 + } + gain_value { + gain_db: -3.1996 + phi_rad: 1.5707963267948966 + theta_rad: 0.19984019935335073 + } + gain_value { + gain_db: -3.3573 + phi_rad: 1.5707963267948966 + theta_rad: 0.2007128639793479 + } + gain_value { + gain_db: -3.5352 + phi_rad: 1.5707963267948966 + theta_rad: 0.20158552860534507 + } + gain_value { + gain_db: -3.6643 + phi_rad: 1.5707963267948966 + theta_rad: 0.20245819323134223 + } + gain_value { + gain_db: -4.2343 + phi_rad: 1.5707963267948966 + theta_rad: 0.2033308578573394 + } + gain_value { + gain_db: -4.8747 + phi_rad: 1.5707963267948966 + theta_rad: 0.20420352248333654 + } + gain_value { + gain_db: -5.9868 + phi_rad: 1.5707963267948966 + theta_rad: 0.20507618710933373 + } + gain_value { + gain_db: -7.8237 + phi_rad: 1.5707963267948966 + theta_rad: 0.2059488517353309 + } + gain_value { + gain_db: -8.5534 + phi_rad: 1.5707963267948966 + theta_rad: 0.20682151636132803 + } + gain_value { + gain_db: -8.1438 + phi_rad: 1.5707963267948966 + theta_rad: 0.20769418098732523 + } + gain_value { + gain_db: -8.5417 + phi_rad: 1.5707963267948966 + theta_rad: 0.20856684561332237 + } + gain_value { + gain_db: -12.306 + phi_rad: 1.5707963267948966 + theta_rad: 0.20943951023931956 + } + gain_value { + gain_db: -7.9443 + phi_rad: 1.5707963267948966 + theta_rad: 0.21031217486531673 + } + gain_value { + gain_db: -5.1815 + phi_rad: 1.5707963267948966 + theta_rad: 0.21118483949131386 + } + gain_value { + gain_db: -3.4291 + phi_rad: 1.5707963267948966 + theta_rad: 0.21205750411731106 + } + gain_value { + gain_db: -2.3146 + phi_rad: 1.5707963267948966 + theta_rad: 0.2129301687433082 + } + gain_value { + gain_db: -1.3354 + phi_rad: 1.5707963267948966 + theta_rad: 0.21380283336930536 + } + gain_value { + gain_db: -0.72493 + phi_rad: 1.5707963267948966 + theta_rad: 0.21467549799530256 + } + gain_value { + gain_db: -0.20483 + phi_rad: 1.5707963267948966 + theta_rad: 0.2155481626212997 + } + gain_value { + gain_db: 0.16043 + phi_rad: 1.5707963267948966 + theta_rad: 0.21642082724729686 + } + gain_value { + gain_db: 0.52073 + phi_rad: 1.5707963267948966 + theta_rad: 0.21729349187329403 + } + gain_value { + gain_db: 0.89417 + phi_rad: 1.5707963267948966 + theta_rad: 0.2181661564992912 + } + gain_value { + gain_db: 1.1359 + phi_rad: 1.5707963267948966 + theta_rad: 0.21903882112528836 + } + gain_value { + gain_db: 1.3336 + phi_rad: 1.5707963267948966 + theta_rad: 0.21991148575128552 + } + gain_value { + gain_db: 1.5791 + phi_rad: 1.5707963267948966 + theta_rad: 0.2207841503772827 + } + gain_value { + gain_db: 1.7993 + phi_rad: 1.5707963267948966 + theta_rad: 0.22165681500327983 + } + gain_value { + gain_db: 1.9762 + phi_rad: 1.5707963267948966 + theta_rad: 0.22252947962927702 + } + gain_value { + gain_db: 2.1079 + phi_rad: 1.5707963267948966 + theta_rad: 0.2234021442552742 + } + gain_value { + gain_db: 2.2239 + phi_rad: 1.5707963267948966 + theta_rad: 0.22427480888127135 + } + gain_value { + gain_db: 2.299 + phi_rad: 1.5707963267948966 + theta_rad: 0.22514747350726852 + } + gain_value { + gain_db: 2.3654 + phi_rad: 1.5707963267948966 + theta_rad: 0.22602013813326566 + } + gain_value { + gain_db: 2.3828 + phi_rad: 1.5707963267948966 + theta_rad: 0.22689280275926285 + } + gain_value { + gain_db: 2.3817 + phi_rad: 1.5707963267948966 + theta_rad: 0.22776546738526002 + } + gain_value { + gain_db: 2.396 + phi_rad: 1.5707963267948966 + theta_rad: 0.22863813201125716 + } + gain_value { + gain_db: 2.3074 + phi_rad: 1.5707963267948966 + theta_rad: 0.22951079663725435 + } + gain_value { + gain_db: 2.265 + phi_rad: 1.5707963267948966 + theta_rad: 0.2303834612632515 + } + gain_value { + gain_db: 2.1863 + phi_rad: 1.5707963267948966 + theta_rad: 0.23125612588924865 + } + gain_value { + gain_db: 2.1617 + phi_rad: 1.5707963267948966 + theta_rad: 0.23212879051524585 + } + gain_value { + gain_db: 2.1394 + phi_rad: 1.5707963267948966 + theta_rad: 0.23300145514124299 + } + gain_value { + gain_db: 2.1532 + phi_rad: 1.5707963267948966 + theta_rad: 0.23387411976724015 + } + gain_value { + gain_db: 2.2615 + phi_rad: 1.5707963267948966 + theta_rad: 0.23474678439323732 + } + gain_value { + gain_db: 2.3962 + phi_rad: 1.5707963267948966 + theta_rad: 0.23561944901923448 + } + gain_value { + gain_db: 2.5819 + phi_rad: 1.5707963267948966 + theta_rad: 0.23649211364523168 + } + gain_value { + gain_db: 2.7908 + phi_rad: 1.5707963267948966 + theta_rad: 0.23736477827122882 + } + gain_value { + gain_db: 3.0224 + phi_rad: 1.5707963267948966 + theta_rad: 0.23823744289722598 + } + gain_value { + gain_db: 3.3289 + phi_rad: 1.5707963267948966 + theta_rad: 0.23911010752322315 + } + gain_value { + gain_db: 3.6101 + phi_rad: 1.5707963267948966 + theta_rad: 0.2399827721492203 + } + gain_value { + gain_db: 3.8349 + phi_rad: 1.5707963267948966 + theta_rad: 0.24085543677521748 + } + gain_value { + gain_db: 4.0671 + phi_rad: 1.5707963267948966 + theta_rad: 0.24172810140121465 + } + gain_value { + gain_db: 4.3433 + phi_rad: 1.5707963267948966 + theta_rad: 0.2426007660272118 + } + gain_value { + gain_db: 4.5779 + phi_rad: 1.5707963267948966 + theta_rad: 0.24347343065320895 + } + gain_value { + gain_db: 4.7395 + phi_rad: 1.5707963267948966 + theta_rad: 0.24434609527920614 + } + gain_value { + gain_db: 4.9612 + phi_rad: 1.5707963267948966 + theta_rad: 0.2452187599052033 + } + gain_value { + gain_db: 5.1231 + phi_rad: 1.5707963267948966 + theta_rad: 0.24609142453120045 + } + gain_value { + gain_db: 5.2382 + phi_rad: 1.5707963267948966 + theta_rad: 0.24696408915719764 + } + gain_value { + gain_db: 5.3185 + phi_rad: 1.5707963267948966 + theta_rad: 0.24783675378319478 + } + gain_value { + gain_db: 5.3387 + phi_rad: 1.5707963267948966 + theta_rad: 0.24870941840919197 + } + gain_value { + gain_db: 5.3458 + phi_rad: 1.5707963267948966 + theta_rad: 0.24958208303518914 + } + gain_value { + gain_db: 5.3313 + phi_rad: 1.5707963267948966 + theta_rad: 0.2504547476611863 + } + gain_value { + gain_db: 5.2628 + phi_rad: 1.5707963267948966 + theta_rad: 0.25132741228718347 + } + gain_value { + gain_db: 5.1245 + phi_rad: 1.5707963267948966 + theta_rad: 0.2522000769131806 + } + gain_value { + gain_db: 4.9702 + phi_rad: 1.5707963267948966 + theta_rad: 0.2530727415391778 + } + gain_value { + gain_db: 4.7168 + phi_rad: 1.5707963267948966 + theta_rad: 0.25394540616517497 + } + gain_value { + gain_db: 4.4601 + phi_rad: 1.5707963267948966 + theta_rad: 0.25481807079117214 + } + gain_value { + gain_db: 4.2266 + phi_rad: 1.5707963267948966 + theta_rad: 0.2556907354171693 + } + gain_value { + gain_db: 3.9568 + phi_rad: 1.5707963267948966 + theta_rad: 0.2565634000431664 + } + gain_value { + gain_db: 3.6941 + phi_rad: 1.5707963267948966 + theta_rad: 0.25743606466916363 + } + gain_value { + gain_db: 3.3814 + phi_rad: 1.5707963267948966 + theta_rad: 0.2583087292951608 + } + gain_value { + gain_db: 2.9959 + phi_rad: 1.5707963267948966 + theta_rad: 0.2591813939211579 + } + gain_value { + gain_db: 2.6121 + phi_rad: 1.5707963267948966 + theta_rad: 0.26005405854715513 + } + gain_value { + gain_db: 2.2224 + phi_rad: 1.5707963267948966 + theta_rad: 0.26092672317315224 + } + gain_value { + gain_db: 1.8804 + phi_rad: 1.5707963267948966 + theta_rad: 0.2617993877991494 + } + gain_value { + gain_db: 1.4087 + phi_rad: 1.5707963267948966 + theta_rad: 0.26267205242514663 + } + gain_value { + gain_db: 1.0163 + phi_rad: 1.5707963267948966 + theta_rad: 0.26354471705114374 + } + gain_value { + gain_db: 0.40717 + phi_rad: 1.5707963267948966 + theta_rad: 0.2644173816771409 + } + gain_value { + gain_db: -0.11527 + phi_rad: 1.5707963267948966 + theta_rad: 0.26529004630313807 + } + gain_value { + gain_db: -0.59763 + phi_rad: 1.5707963267948966 + theta_rad: 0.26616271092913524 + } + gain_value { + gain_db: -1.0891 + phi_rad: 1.5707963267948966 + theta_rad: 0.26703537555513246 + } + gain_value { + gain_db: -1.474 + phi_rad: 1.5707963267948966 + theta_rad: 0.26790804018112957 + } + gain_value { + gain_db: -1.7283 + phi_rad: 1.5707963267948966 + theta_rad: 0.26878070480712674 + } + gain_value { + gain_db: -1.9384 + phi_rad: 1.5707963267948966 + theta_rad: 0.2696533694331239 + } + gain_value { + gain_db: -2.0893 + phi_rad: 1.5707963267948966 + theta_rad: 0.27052603405912107 + } + gain_value { + gain_db: -2.2484 + phi_rad: 1.5707963267948966 + theta_rad: 0.27139869868511823 + } + gain_value { + gain_db: -2.37 + phi_rad: 1.5707963267948966 + theta_rad: 0.2722713633111154 + } + gain_value { + gain_db: -2.4643 + phi_rad: 1.5707963267948966 + theta_rad: 0.27314402793711257 + } + gain_value { + gain_db: -2.5777 + phi_rad: 1.5707963267948966 + theta_rad: 0.27401669256310973 + } + gain_value { + gain_db: -2.7471 + phi_rad: 1.5707963267948966 + theta_rad: 0.2748893571891069 + } + gain_value { + gain_db: -3.0165 + phi_rad: 1.5707963267948966 + theta_rad: 0.27576202181510406 + } + gain_value { + gain_db: -3.3318 + phi_rad: 1.5707963267948966 + theta_rad: 0.27663468644110123 + } + gain_value { + gain_db: -3.6807 + phi_rad: 1.5707963267948966 + theta_rad: 0.2775073510670984 + } + gain_value { + gain_db: -4.0422 + phi_rad: 1.5707963267948966 + theta_rad: 0.27838001569309556 + } + gain_value { + gain_db: -4.4979 + phi_rad: 1.5707963267948966 + theta_rad: 0.2792526803190927 + } + gain_value { + gain_db: -4.8334 + phi_rad: 1.5707963267948966 + theta_rad: 0.2801253449450899 + } + gain_value { + gain_db: -5.1762 + phi_rad: 1.5707963267948966 + theta_rad: 0.28099800957108706 + } + gain_value { + gain_db: -5.3759 + phi_rad: 1.5707963267948966 + theta_rad: 0.28187067419708417 + } + gain_value { + gain_db: -5.5312 + phi_rad: 1.5707963267948966 + theta_rad: 0.2827433388230814 + } + gain_value { + gain_db: -5.7566 + phi_rad: 1.5707963267948966 + theta_rad: 0.28361600344907856 + } + gain_value { + gain_db: -5.8286 + phi_rad: 1.5707963267948966 + theta_rad: 0.2844886680750757 + } + gain_value { + gain_db: -5.8744 + phi_rad: 1.5707963267948966 + theta_rad: 0.2853613327010729 + } + gain_value { + gain_db: -5.9876 + phi_rad: 1.5707963267948966 + theta_rad: 0.28623399732707 + } + gain_value { + gain_db: -6.0816 + phi_rad: 1.5707963267948966 + theta_rad: 0.2871066619530672 + } + gain_value { + gain_db: -6.1811 + phi_rad: 1.5707963267948966 + theta_rad: 0.2879793265790644 + } + gain_value { + gain_db: -6.3134 + phi_rad: 1.5707963267948966 + theta_rad: 0.28885199120506155 + } + gain_value { + gain_db: -6.4282 + phi_rad: 1.5707963267948966 + theta_rad: 0.2897246558310587 + } + gain_value { + gain_db: -6.5285 + phi_rad: 1.5707963267948966 + theta_rad: 0.29059732045705583 + } + gain_value { + gain_db: -6.4251 + phi_rad: 1.5707963267948966 + theta_rad: 0.291469985083053 + } + gain_value { + gain_db: -6.4633 + phi_rad: 1.5707963267948966 + theta_rad: 0.2923426497090502 + } + gain_value { + gain_db: -6.3735 + phi_rad: 1.5707963267948966 + theta_rad: 0.2932153143350474 + } + gain_value { + gain_db: -6.229 + phi_rad: 1.5707963267948966 + theta_rad: 0.29408797896104455 + } + gain_value { + gain_db: -6.1655 + phi_rad: 1.5707963267948966 + theta_rad: 0.29496064358704166 + } + gain_value { + gain_db: -6.0293 + phi_rad: 1.5707963267948966 + theta_rad: 0.2958333082130388 + } + gain_value { + gain_db: -5.8583 + phi_rad: 1.5707963267948966 + theta_rad: 0.29670597283903605 + } + gain_value { + gain_db: -5.7681 + phi_rad: 1.5707963267948966 + theta_rad: 0.2975786374650332 + } + gain_value { + gain_db: -5.7287 + phi_rad: 1.5707963267948966 + theta_rad: 0.2984513020910304 + } + gain_value { + gain_db: -5.7678 + phi_rad: 1.5707963267948966 + theta_rad: 0.2993239667170275 + } + gain_value { + gain_db: -5.8893 + phi_rad: 1.5707963267948966 + theta_rad: 0.30019663134302466 + } + gain_value { + gain_db: -6.0322 + phi_rad: 1.5707963267948966 + theta_rad: 0.3010692959690218 + } + gain_value { + gain_db: -6.3031 + phi_rad: 1.5707963267948966 + theta_rad: 0.30194196059501904 + } + gain_value { + gain_db: -6.5278 + phi_rad: 1.5707963267948966 + theta_rad: 0.3028146252210162 + } + gain_value { + gain_db: -6.8389 + phi_rad: 1.5707963267948966 + theta_rad: 0.3036872898470133 + } + gain_value { + gain_db: -7.2421 + phi_rad: 1.5707963267948966 + theta_rad: 0.3045599544730105 + } + gain_value { + gain_db: -7.5851 + phi_rad: 1.5707963267948966 + theta_rad: 0.30543261909900765 + } + gain_value { + gain_db: -7.9254 + phi_rad: 1.5707963267948966 + theta_rad: 0.3063052837250049 + } + gain_value { + gain_db: -8.1997 + phi_rad: 1.5707963267948966 + theta_rad: 0.30717794835100204 + } + gain_value { + gain_db: -8.4476 + phi_rad: 1.5707963267948966 + theta_rad: 0.30805061297699915 + } + gain_value { + gain_db: -8.474 + phi_rad: 1.5707963267948966 + theta_rad: 0.3089232776029963 + } + gain_value { + gain_db: -8.7259 + phi_rad: 1.5707963267948966 + theta_rad: 0.3097959422289935 + } + gain_value { + gain_db: -8.9339 + phi_rad: 1.5707963267948966 + theta_rad: 0.31066860685499065 + } + gain_value { + gain_db: -9.1799 + phi_rad: 1.5707963267948966 + theta_rad: 0.31154127148098787 + } + gain_value { + gain_db: -9.4865 + phi_rad: 1.5707963267948966 + theta_rad: 0.312413936106985 + } + gain_value { + gain_db: -9.8802 + phi_rad: 1.5707963267948966 + theta_rad: 0.31328660073298215 + } + gain_value { + gain_db: -10.342 + phi_rad: 1.5707963267948966 + theta_rad: 0.3141592653589793 + } + gain_value { + gain_db: -10.977 + phi_rad: 1.5707963267948966 + theta_rad: 0.3150319299849765 + } + gain_value { + gain_db: -11.595 + phi_rad: 1.5707963267948966 + theta_rad: 0.3159045946109737 + } + gain_value { + gain_db: -12.649 + phi_rad: 1.5707963267948966 + theta_rad: 0.3167772592369708 + } + gain_value { + gain_db: -14.179 + phi_rad: 1.5707963267948966 + theta_rad: 0.317649923862968 + } + gain_value { + gain_db: -16.164 + phi_rad: 1.5707963267948966 + theta_rad: 0.31852258848896514 + } + gain_value { + gain_db: -15.554 + phi_rad: 1.5707963267948966 + theta_rad: 0.3193952531149623 + } + gain_value { + gain_db: -13.806 + phi_rad: 1.5707963267948966 + theta_rad: 0.3202679177409595 + } + gain_value { + gain_db: -12.803 + phi_rad: 1.5707963267948966 + theta_rad: 0.32114058236695664 + } + gain_value { + gain_db: -12.135 + phi_rad: 1.5707963267948966 + theta_rad: 0.3220132469929538 + } + gain_value { + gain_db: -11.793 + phi_rad: 1.5707963267948966 + theta_rad: 0.32288591161895097 + } + gain_value { + gain_db: -11.671 + phi_rad: 1.5707963267948966 + theta_rad: 0.32375857624494814 + } + gain_value { + gain_db: -11.939 + phi_rad: 1.5707963267948966 + theta_rad: 0.3246312408709453 + } + gain_value { + gain_db: -12.272 + phi_rad: 1.5707963267948966 + theta_rad: 0.3255039054969424 + } + gain_value { + gain_db: -12.549 + phi_rad: 1.5707963267948966 + theta_rad: 0.32637657012293964 + } + gain_value { + gain_db: -12.535 + phi_rad: 1.5707963267948966 + theta_rad: 0.3272492347489368 + } + gain_value { + gain_db: -12.325 + phi_rad: 1.5707963267948966 + theta_rad: 0.32812189937493397 + } + gain_value { + gain_db: -11.912 + phi_rad: 1.5707963267948966 + theta_rad: 0.32899456400093113 + } + gain_value { + gain_db: -11.492 + phi_rad: 1.5707963267948966 + theta_rad: 0.32986722862692824 + } + gain_value { + gain_db: -11.231 + phi_rad: 1.5707963267948966 + theta_rad: 0.3307398932529254 + } + gain_value { + gain_db: -10.993 + phi_rad: 1.5707963267948966 + theta_rad: 0.33161255787892263 + } + gain_value { + gain_db: -10.847 + phi_rad: 1.5707963267948966 + theta_rad: 0.3324852225049198 + } + gain_value { + gain_db: -10.442 + phi_rad: 1.5707963267948966 + theta_rad: 0.33335788713091696 + } + gain_value { + gain_db: -10.189 + phi_rad: 1.5707963267948966 + theta_rad: 0.3342305517569141 + } + gain_value { + gain_db: -9.5115 + phi_rad: 1.5707963267948966 + theta_rad: 0.33510321638291124 + } + gain_value { + gain_db: -8.8915 + phi_rad: 1.5707963267948966 + theta_rad: 0.33597588100890846 + } + gain_value { + gain_db: -8.4536 + phi_rad: 1.5707963267948966 + theta_rad: 0.3368485456349056 + } + gain_value { + gain_db: -7.8797 + phi_rad: 1.5707963267948966 + theta_rad: 0.3377212102609028 + } + gain_value { + gain_db: -7.2264 + phi_rad: 1.5707963267948966 + theta_rad: 0.3385938748868999 + } + gain_value { + gain_db: -6.7922 + phi_rad: 1.5707963267948966 + theta_rad: 0.33946653951289707 + } + gain_value { + gain_db: -6.3212 + phi_rad: 1.5707963267948966 + theta_rad: 0.34033920413889424 + } + gain_value { + gain_db: -5.8931 + phi_rad: 1.5707963267948966 + theta_rad: 0.34121186876489146 + } + gain_value { + gain_db: -5.5439 + phi_rad: 1.5707963267948966 + theta_rad: 0.3420845333908886 + } + gain_value { + gain_db: -5.258 + phi_rad: 1.5707963267948966 + theta_rad: 0.34295719801688573 + } + gain_value { + gain_db: -5.1158 + phi_rad: 1.5707963267948966 + theta_rad: 0.3438298626428829 + } + gain_value { + gain_db: -4.932 + phi_rad: 1.5707963267948966 + theta_rad: 0.34470252726888007 + } + gain_value { + gain_db: -4.7983 + phi_rad: 1.5707963267948966 + theta_rad: 0.3455751918948773 + } + gain_value { + gain_db: -4.7248 + phi_rad: 1.5707963267948966 + theta_rad: 0.34644785652087445 + } + gain_value { + gain_db: -4.648 + phi_rad: 1.5707963267948966 + theta_rad: 0.34732052114687156 + } + gain_value { + gain_db: -4.5094 + phi_rad: 1.5707963267948966 + theta_rad: 0.34819318577286873 + } + gain_value { + gain_db: -4.3778 + phi_rad: 1.5707963267948966 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -4.2199 + phi_rad: 1.5707963267948966 + theta_rad: 0.34993851502486306 + } + gain_value { + gain_db: -4.0733 + phi_rad: 1.5707963267948966 + theta_rad: 0.3508111796508603 + } + gain_value { + gain_db: -3.8119 + phi_rad: 1.5707963267948966 + theta_rad: 0.3516838442768574 + } + gain_value { + gain_db: -3.4738 + phi_rad: 1.5707963267948966 + theta_rad: 0.35255650890285456 + } + gain_value { + gain_db: -3.2252 + phi_rad: 1.5707963267948966 + theta_rad: 0.3534291735288517 + } + gain_value { + gain_db: -2.9846 + phi_rad: 1.5707963267948966 + theta_rad: 0.3543018381548489 + } + gain_value { + gain_db: -2.7197 + phi_rad: 1.5707963267948966 + theta_rad: 0.3551745027808461 + } + gain_value { + gain_db: -2.4954 + phi_rad: 1.5707963267948966 + theta_rad: 0.3560471674068432 + } + gain_value { + gain_db: -2.2721 + phi_rad: 1.5707963267948966 + theta_rad: 0.3569198320328404 + } + gain_value { + gain_db: -2.1006 + phi_rad: 1.5707963267948966 + theta_rad: 0.35779249665883756 + } + gain_value { + gain_db: -1.9521 + phi_rad: 1.5707963267948966 + theta_rad: 0.3586651612848347 + } + gain_value { + gain_db: -1.7335 + phi_rad: 1.5707963267948966 + theta_rad: 0.3595378259108319 + } + gain_value { + gain_db: -1.564 + phi_rad: 1.5707963267948966 + theta_rad: 0.36041049053682905 + } + gain_value { + gain_db: -1.4509 + phi_rad: 1.5707963267948966 + theta_rad: 0.3612831551628262 + } + gain_value { + gain_db: -1.3547 + phi_rad: 1.5707963267948966 + theta_rad: 0.3621558197888234 + } + gain_value { + gain_db: -1.2766 + phi_rad: 1.5707963267948966 + theta_rad: 0.36302848441482055 + } + gain_value { + gain_db: -1.18 + phi_rad: 1.5707963267948966 + theta_rad: 0.3639011490408177 + } + gain_value { + gain_db: -1.0401 + phi_rad: 1.5707963267948966 + theta_rad: 0.36477381366681483 + } + gain_value { + gain_db: -0.92993 + phi_rad: 1.5707963267948966 + theta_rad: 0.36564647829281205 + } + gain_value { + gain_db: -0.69597 + phi_rad: 1.5707963267948966 + theta_rad: 0.3665191429188092 + } + gain_value { + gain_db: -0.54433 + phi_rad: 1.5707963267948966 + theta_rad: 0.3673918075448064 + } + gain_value { + gain_db: -0.3218 + phi_rad: 1.5707963267948966 + theta_rad: 0.36826447217080355 + } + gain_value { + gain_db: -0.13497 + phi_rad: 1.5707963267948966 + theta_rad: 0.36913713679680066 + } + gain_value { + gain_db: -0.0018333 + phi_rad: 1.5707963267948966 + theta_rad: 0.3700098014227979 + } + gain_value { + gain_db: 0.081067 + phi_rad: 1.5707963267948966 + theta_rad: 0.37088246604879505 + } + gain_value { + gain_db: 0.14467 + phi_rad: 1.5707963267948966 + theta_rad: 0.3717551306747922 + } + gain_value { + gain_db: 0.1623 + phi_rad: 1.5707963267948966 + theta_rad: 0.3726277953007894 + } + gain_value { + gain_db: 0.11263 + phi_rad: 1.5707963267948966 + theta_rad: 0.3735004599267865 + } + gain_value { + gain_db: 0.10013 + phi_rad: 1.5707963267948966 + theta_rad: 0.37437312455278365 + } + gain_value { + gain_db: 0.0339 + phi_rad: 1.5707963267948966 + theta_rad: 0.3752457891787809 + } + gain_value { + gain_db: 0.0062 + phi_rad: 1.5707963267948966 + theta_rad: 0.37611845380477804 + } + gain_value { + gain_db: -0.0080333 + phi_rad: 1.5707963267948966 + theta_rad: 0.3769911184307752 + } + gain_value { + gain_db: -9.3333E-4 + phi_rad: 1.5707963267948966 + theta_rad: 0.3778637830567723 + } + gain_value { + gain_db: 0.0287 + phi_rad: 1.5707963267948966 + theta_rad: 0.3787364476827695 + } + gain_value { + gain_db: 0.098 + phi_rad: 1.5707963267948966 + theta_rad: 0.37960911230876665 + } + gain_value { + gain_db: 0.1936 + phi_rad: 1.5707963267948966 + theta_rad: 0.38048177693476387 + } + gain_value { + gain_db: 0.22297 + phi_rad: 1.5707963267948966 + theta_rad: 0.38135444156076104 + } + gain_value { + gain_db: 0.26973 + phi_rad: 1.5707963267948966 + theta_rad: 0.38222710618675815 + } + gain_value { + gain_db: 0.23647 + phi_rad: 1.5707963267948966 + theta_rad: 0.3830997708127553 + } + gain_value { + gain_db: 0.21603 + phi_rad: 1.5707963267948966 + theta_rad: 0.3839724354387525 + } + gain_value { + gain_db: 0.1383 + phi_rad: 1.5707963267948966 + theta_rad: 0.3848451000647497 + } + gain_value { + gain_db: -0.0028333 + phi_rad: 1.5707963267948966 + theta_rad: 0.38571776469074687 + } + gain_value { + gain_db: -0.10977 + phi_rad: 1.5707963267948966 + theta_rad: 0.386590429316744 + } + gain_value { + gain_db: -0.301 + phi_rad: 1.5707963267948966 + theta_rad: 0.38746309394274114 + } + gain_value { + gain_db: -0.5249 + phi_rad: 1.5707963267948966 + theta_rad: 0.3883357585687383 + } + gain_value { + gain_db: -0.77687 + phi_rad: 1.5707963267948966 + theta_rad: 0.3892084231947355 + } + gain_value { + gain_db: -0.96397 + phi_rad: 1.5707963267948966 + theta_rad: 0.3900810878207327 + } + gain_value { + gain_db: -1.0595 + phi_rad: 1.5707963267948966 + theta_rad: 0.3909537524467298 + } + gain_value { + gain_db: -1.1498 + phi_rad: 1.5707963267948966 + theta_rad: 0.391826417072727 + } + gain_value { + gain_db: -1.226 + phi_rad: 1.5707963267948966 + theta_rad: 0.39269908169872414 + } + gain_value { + gain_db: -1.3204 + phi_rad: 1.5707963267948966 + theta_rad: 0.3935717463247213 + } + gain_value { + gain_db: -1.4804 + phi_rad: 1.5707963267948966 + theta_rad: 0.3944444109507185 + } + gain_value { + gain_db: -1.6916 + phi_rad: 1.5707963267948966 + theta_rad: 0.39531707557671564 + } + gain_value { + gain_db: -1.9557 + phi_rad: 1.5707963267948966 + theta_rad: 0.3961897402027128 + } + gain_value { + gain_db: -2.2875 + phi_rad: 1.5707963267948966 + theta_rad: 0.39706240482870997 + } + gain_value { + gain_db: -2.6082 + phi_rad: 1.5707963267948966 + theta_rad: 0.39793506945470714 + } + gain_value { + gain_db: -3.0625 + phi_rad: 1.5707963267948966 + theta_rad: 0.3988077340807043 + } + gain_value { + gain_db: -3.5228 + phi_rad: 1.5707963267948966 + theta_rad: 0.39968039870670147 + } + gain_value { + gain_db: -3.9739 + phi_rad: 1.5707963267948966 + theta_rad: 0.40055306333269863 + } + gain_value { + gain_db: -4.3551 + phi_rad: 1.5707963267948966 + theta_rad: 0.4014257279586958 + } + gain_value { + gain_db: -4.7634 + phi_rad: 1.5707963267948966 + theta_rad: 0.40229839258469297 + } + gain_value { + gain_db: -5.0 + phi_rad: 1.5707963267948966 + theta_rad: 0.40317105721069013 + } + gain_value { + gain_db: -5.2693 + phi_rad: 1.5707963267948966 + theta_rad: 0.40404372183668724 + } + gain_value { + gain_db: -5.4425 + phi_rad: 1.5707963267948966 + theta_rad: 0.40491638646268446 + } + gain_value { + gain_db: -5.6188 + phi_rad: 1.5707963267948966 + theta_rad: 0.40578905108868163 + } + gain_value { + gain_db: -5.7875 + phi_rad: 1.5707963267948966 + theta_rad: 0.4066617157146788 + } + gain_value { + gain_db: -5.9414 + phi_rad: 1.5707963267948966 + theta_rad: 0.40753438034067596 + } + gain_value { + gain_db: -6.2155 + phi_rad: 1.5707963267948966 + theta_rad: 0.40840704496667307 + } + gain_value { + gain_db: -6.5975 + phi_rad: 1.5707963267948966 + theta_rad: 0.4092797095926703 + } + gain_value { + gain_db: -7.0587 + phi_rad: 1.5707963267948966 + theta_rad: 0.41015237421866746 + } + gain_value { + gain_db: -7.8177 + phi_rad: 1.5707963267948966 + theta_rad: 0.4110250388446646 + } + gain_value { + gain_db: -8.5224 + phi_rad: 1.5707963267948966 + theta_rad: 0.4118977034706618 + } + gain_value { + gain_db: -9.1432 + phi_rad: 1.5707963267948966 + theta_rad: 0.4127703680966589 + } + gain_value { + gain_db: -9.6398 + phi_rad: 1.5707963267948966 + theta_rad: 0.41364303272265607 + } + gain_value { + gain_db: -9.5231 + phi_rad: 1.5707963267948966 + theta_rad: 0.4145156973486533 + } + gain_value { + gain_db: -9.2901 + phi_rad: 1.5707963267948966 + theta_rad: 0.41538836197465046 + } + gain_value { + gain_db: -8.6441 + phi_rad: 1.5707963267948966 + theta_rad: 0.4162610266006476 + } + gain_value { + gain_db: -8.0659 + phi_rad: 1.5707963267948966 + theta_rad: 0.41713369122664473 + } + gain_value { + gain_db: -7.5793 + phi_rad: 1.5707963267948966 + theta_rad: 0.4180063558526419 + } + gain_value { + gain_db: -7.0756 + phi_rad: 1.5707963267948966 + theta_rad: 0.4188790204786391 + } + gain_value { + gain_db: -6.629 + phi_rad: 1.5707963267948966 + theta_rad: 0.4197516851046363 + } + gain_value { + gain_db: -6.4679 + phi_rad: 1.5707963267948966 + theta_rad: 0.42062434973063345 + } + gain_value { + gain_db: -6.3937 + phi_rad: 1.5707963267948966 + theta_rad: 0.42149701435663056 + } + gain_value { + gain_db: -6.515 + phi_rad: 1.5707963267948966 + theta_rad: 0.4223696789826277 + } + gain_value { + gain_db: -6.9123 + phi_rad: 1.5707963267948966 + theta_rad: 0.4232423436086249 + } + gain_value { + gain_db: -7.4081 + phi_rad: 1.5707963267948966 + theta_rad: 0.4241150082346221 + } + gain_value { + gain_db: -8.0584 + phi_rad: 1.5707963267948966 + theta_rad: 0.4249876728606193 + } + gain_value { + gain_db: -8.6544 + phi_rad: 1.5707963267948966 + theta_rad: 0.4258603374866164 + } + gain_value { + gain_db: -9.3062 + phi_rad: 1.5707963267948966 + theta_rad: 0.42673300211261356 + } + gain_value { + gain_db: -9.6929 + phi_rad: 1.5707963267948966 + theta_rad: 0.4276056667386107 + } + gain_value { + gain_db: -9.9535 + phi_rad: 1.5707963267948966 + theta_rad: 0.4284783313646079 + } + gain_value { + gain_db: -10.142 + phi_rad: 1.5707963267948966 + theta_rad: 0.4293509959906051 + } + gain_value { + gain_db: -10.14 + phi_rad: 1.5707963267948966 + theta_rad: 0.4302236606166022 + } + gain_value { + gain_db: -9.9817 + phi_rad: 1.5707963267948966 + theta_rad: 0.4310963252425994 + } + gain_value { + gain_db: -9.8696 + phi_rad: 1.5707963267948966 + theta_rad: 0.43196898986859655 + } + gain_value { + gain_db: -9.7721 + phi_rad: 1.5707963267948966 + theta_rad: 0.4328416544945937 + } + gain_value { + gain_db: -9.376 + phi_rad: 1.5707963267948966 + theta_rad: 0.43371431912059094 + } + gain_value { + gain_db: -9.2969 + phi_rad: 1.5707963267948966 + theta_rad: 0.43458698374658805 + } + gain_value { + gain_db: -9.3254 + phi_rad: 1.5707963267948966 + theta_rad: 0.4354596483725852 + } + gain_value { + gain_db: -9.4689 + phi_rad: 1.5707963267948966 + theta_rad: 0.4363323129985824 + } + gain_value { + gain_db: -9.6349 + phi_rad: 1.5707963267948966 + theta_rad: 0.43720497762457955 + } + gain_value { + gain_db: -9.8346 + phi_rad: 1.5707963267948966 + theta_rad: 0.4380776422505767 + } + gain_value { + gain_db: -10.22 + phi_rad: 1.5707963267948966 + theta_rad: 0.4389503068765739 + } + gain_value { + gain_db: -10.655 + phi_rad: 1.5707963267948966 + theta_rad: 0.43982297150257105 + } + gain_value { + gain_db: -10.956 + phi_rad: 1.5707963267948966 + theta_rad: 0.4406956361285682 + } + gain_value { + gain_db: -11.032 + phi_rad: 1.5707963267948966 + theta_rad: 0.4415683007545654 + } + gain_value { + gain_db: -11.023 + phi_rad: 1.5707963267948966 + theta_rad: 0.44244096538056255 + } + gain_value { + gain_db: -10.974 + phi_rad: 1.5707963267948966 + theta_rad: 0.44331363000655966 + } + gain_value { + gain_db: -10.8 + phi_rad: 1.5707963267948966 + theta_rad: 0.4441862946325569 + } + gain_value { + gain_db: -10.564 + phi_rad: 1.5707963267948966 + theta_rad: 0.44505895925855404 + } + gain_value { + gain_db: -10.423 + phi_rad: 1.5707963267948966 + theta_rad: 0.4459316238845512 + } + gain_value { + gain_db: -10.293 + phi_rad: 1.5707963267948966 + theta_rad: 0.4468042885105484 + } + gain_value { + gain_db: -10.078 + phi_rad: 1.5707963267948966 + theta_rad: 0.4476769531365455 + } + gain_value { + gain_db: -10.108 + phi_rad: 1.5707963267948966 + theta_rad: 0.4485496177625427 + } + gain_value { + gain_db: -10.183 + phi_rad: 1.5707963267948966 + theta_rad: 0.4494222823885399 + } + gain_value { + gain_db: -10.105 + phi_rad: 1.5707963267948966 + theta_rad: 0.45029494701453704 + } + gain_value { + gain_db: -10.114 + phi_rad: 1.5707963267948966 + theta_rad: 0.4511676116405342 + } + gain_value { + gain_db: -9.8757 + phi_rad: 1.5707963267948966 + theta_rad: 0.4520402762665313 + } + gain_value { + gain_db: -9.5007 + phi_rad: 1.5707963267948966 + theta_rad: 0.4529129408925285 + } + gain_value { + gain_db: -9.083 + phi_rad: 1.5707963267948966 + theta_rad: 0.4537856055185257 + } + gain_value { + gain_db: -8.8642 + phi_rad: 1.5707963267948966 + theta_rad: 0.45465827014452287 + } + gain_value { + gain_db: -8.7116 + phi_rad: 1.5707963267948966 + theta_rad: 0.45553093477052004 + } + gain_value { + gain_db: -8.5843 + phi_rad: 1.5707963267948966 + theta_rad: 0.45640359939651715 + } + gain_value { + gain_db: -8.3405 + phi_rad: 1.5707963267948966 + theta_rad: 0.4572762640225143 + } + gain_value { + gain_db: -8.1543 + phi_rad: 1.5707963267948966 + theta_rad: 0.45814892864851153 + } + gain_value { + gain_db: -8.0934 + phi_rad: 1.5707963267948966 + theta_rad: 0.4590215932745087 + } + gain_value { + gain_db: -8.2492 + phi_rad: 1.5707963267948966 + theta_rad: 0.45989425790050587 + } + gain_value { + gain_db: -8.3658 + phi_rad: 1.5707963267948966 + theta_rad: 0.460766922526503 + } + gain_value { + gain_db: -8.461 + phi_rad: 1.5707963267948966 + theta_rad: 0.46163958715250014 + } + gain_value { + gain_db: -8.161 + phi_rad: 1.5707963267948966 + theta_rad: 0.4625122517784973 + } + gain_value { + gain_db: -7.8162 + phi_rad: 1.5707963267948966 + theta_rad: 0.46338491640449453 + } + gain_value { + gain_db: -7.288 + phi_rad: 1.5707963267948966 + theta_rad: 0.4642575810304917 + } + gain_value { + gain_db: -6.6107 + phi_rad: 1.5707963267948966 + theta_rad: 0.4651302456564888 + } + gain_value { + gain_db: -6.0253 + phi_rad: 1.5707963267948966 + theta_rad: 0.46600291028248597 + } + gain_value { + gain_db: -5.7417 + phi_rad: 1.5707963267948966 + theta_rad: 0.46687557490848314 + } + gain_value { + gain_db: -5.269 + phi_rad: 1.5707963267948966 + theta_rad: 0.4677482395344803 + } + gain_value { + gain_db: -4.816 + phi_rad: 1.5707963267948966 + theta_rad: 0.4686209041604775 + } + gain_value { + gain_db: -4.0744 + phi_rad: 1.5707963267948966 + theta_rad: 0.46949356878647464 + } + gain_value { + gain_db: -3.4367 + phi_rad: 1.5707963267948966 + theta_rad: 0.4703662334124718 + } + gain_value { + gain_db: -2.7441 + phi_rad: 1.5707963267948966 + theta_rad: 0.47123889803846897 + } + gain_value { + gain_db: -2.198 + phi_rad: 1.5707963267948966 + theta_rad: 0.47211156266446613 + } + gain_value { + gain_db: -1.6785 + phi_rad: 1.5707963267948966 + theta_rad: 0.47298422729046335 + } + gain_value { + gain_db: -1.226 + phi_rad: 1.5707963267948966 + theta_rad: 0.47385689191646047 + } + gain_value { + gain_db: -0.79483 + phi_rad: 1.5707963267948966 + theta_rad: 0.47472955654245763 + } + gain_value { + gain_db: -0.3762 + phi_rad: 1.5707963267948966 + theta_rad: 0.4756022211684548 + } + gain_value { + gain_db: -0.16217 + phi_rad: 1.5707963267948966 + theta_rad: 0.47647488579445196 + } + gain_value { + gain_db: -0.0041 + phi_rad: 1.5707963267948966 + theta_rad: 0.47734755042044913 + } + gain_value { + gain_db: 0.077167 + phi_rad: 1.5707963267948966 + theta_rad: 0.4782202150464463 + } + gain_value { + gain_db: 0.14457 + phi_rad: 1.5707963267948966 + theta_rad: 0.47909287967244346 + } + gain_value { + gain_db: 0.24563 + phi_rad: 1.5707963267948966 + theta_rad: 0.4799655442984406 + } + gain_value { + gain_db: 0.4628 + phi_rad: 1.5707963267948966 + theta_rad: 0.4808382089244378 + } + gain_value { + gain_db: 0.69327 + phi_rad: 1.5707963267948966 + theta_rad: 0.48171087355043496 + } + gain_value { + gain_db: 0.91647 + phi_rad: 1.5707963267948966 + theta_rad: 0.48258353817643207 + } + gain_value { + gain_db: 1.1109 + phi_rad: 1.5707963267948966 + theta_rad: 0.4834562028024293 + } + gain_value { + gain_db: 1.2828 + phi_rad: 1.5707963267948966 + theta_rad: 0.48432886742842646 + } + gain_value { + gain_db: 1.4255 + phi_rad: 1.5707963267948966 + theta_rad: 0.4852015320544236 + } + gain_value { + gain_db: 1.5766 + phi_rad: 1.5707963267948966 + theta_rad: 0.4860741966804208 + } + gain_value { + gain_db: 1.6602 + phi_rad: 1.5707963267948966 + theta_rad: 0.4869468613064179 + } + gain_value { + gain_db: 1.6669 + phi_rad: 1.5707963267948966 + theta_rad: 0.4878195259324151 + } + gain_value { + gain_db: 1.5753 + phi_rad: 1.5707963267948966 + theta_rad: 0.4886921905584123 + } + gain_value { + gain_db: 1.3759 + phi_rad: 1.5707963267948966 + theta_rad: 0.48956485518440945 + } + gain_value { + gain_db: 1.1401 + phi_rad: 1.5707963267948966 + theta_rad: 0.4904375198104066 + } + gain_value { + gain_db: 0.95023 + phi_rad: 1.5707963267948966 + theta_rad: 0.49131018443640373 + } + gain_value { + gain_db: 0.82783 + phi_rad: 1.5707963267948966 + theta_rad: 0.4921828490624009 + } + gain_value { + gain_db: 0.73313 + phi_rad: 1.5707963267948966 + theta_rad: 0.4930555136883981 + } + gain_value { + gain_db: 0.7676 + phi_rad: 1.5707963267948966 + theta_rad: 0.4939281783143953 + } + gain_value { + gain_db: 0.81917 + phi_rad: 1.5707963267948966 + theta_rad: 0.49480084294039245 + } + gain_value { + gain_db: 0.89043 + phi_rad: 1.5707963267948966 + theta_rad: 0.49567350756638956 + } + gain_value { + gain_db: 0.9623 + phi_rad: 1.5707963267948966 + theta_rad: 0.4965461721923867 + } + gain_value { + gain_db: 1.0134 + phi_rad: 1.5707963267948966 + theta_rad: 0.49741883681838395 + } + gain_value { + gain_db: 1.0088 + phi_rad: 1.5707963267948966 + theta_rad: 0.4982915014443811 + } + gain_value { + gain_db: 0.88437 + phi_rad: 1.5707963267948966 + theta_rad: 0.4991641660703783 + } + gain_value { + gain_db: 0.62883 + phi_rad: 1.5707963267948966 + theta_rad: 0.5000368306963754 + } + gain_value { + gain_db: 0.28337 + phi_rad: 1.5707963267948966 + theta_rad: 0.5009094953223726 + } + gain_value { + gain_db: -0.1568 + phi_rad: 1.5707963267948966 + theta_rad: 0.5017821599483697 + } + gain_value { + gain_db: -0.63717 + phi_rad: 1.5707963267948966 + theta_rad: 0.5026548245743669 + } + gain_value { + gain_db: -1.1613 + phi_rad: 1.5707963267948966 + theta_rad: 0.503527489200364 + } + gain_value { + gain_db: -1.6285 + phi_rad: 1.5707963267948966 + theta_rad: 0.5044001538263612 + } + gain_value { + gain_db: -1.8182 + phi_rad: 1.5707963267948966 + theta_rad: 0.5052728184523584 + } + gain_value { + gain_db: -1.8501 + phi_rad: 1.5707963267948966 + theta_rad: 0.5061454830783556 + } + gain_value { + gain_db: -1.8377 + phi_rad: 1.5707963267948966 + theta_rad: 0.5070181477043527 + } + gain_value { + gain_db: -1.8587 + phi_rad: 1.5707963267948966 + theta_rad: 0.5078908123303499 + } + gain_value { + gain_db: -1.8856 + phi_rad: 1.5707963267948966 + theta_rad: 0.508763476956347 + } + gain_value { + gain_db: -1.9734 + phi_rad: 1.5707963267948966 + theta_rad: 0.5096361415823443 + } + gain_value { + gain_db: -2.2299 + phi_rad: 1.5707963267948966 + theta_rad: 0.5105088062083414 + } + gain_value { + gain_db: -2.6261 + phi_rad: 1.5707963267948966 + theta_rad: 0.5113814708343386 + } + gain_value { + gain_db: -3.0978 + phi_rad: 1.5707963267948966 + theta_rad: 0.5122541354603357 + } + gain_value { + gain_db: -3.7112 + phi_rad: 1.5707963267948966 + theta_rad: 0.5131268000863328 + } + gain_value { + gain_db: -4.168 + phi_rad: 1.5707963267948966 + theta_rad: 0.51399946471233 + } + gain_value { + gain_db: -4.0917 + phi_rad: 1.5707963267948966 + theta_rad: 0.5148721293383273 + } + gain_value { + gain_db: -3.7848 + phi_rad: 1.5707963267948966 + theta_rad: 0.5157447939643244 + } + gain_value { + gain_db: -3.6001 + phi_rad: 1.5707963267948966 + theta_rad: 0.5166174585903216 + } + gain_value { + gain_db: -3.4806 + phi_rad: 1.5707963267948966 + theta_rad: 0.5174901232163187 + } + gain_value { + gain_db: -3.4226 + phi_rad: 1.5707963267948966 + theta_rad: 0.5183627878423158 + } + gain_value { + gain_db: -3.4187 + phi_rad: 1.5707963267948966 + theta_rad: 0.519235452468313 + } + gain_value { + gain_db: -3.4664 + phi_rad: 1.5707963267948966 + theta_rad: 0.5201081170943103 + } + gain_value { + gain_db: -3.559 + phi_rad: 1.5707963267948966 + theta_rad: 0.5209807817203074 + } + gain_value { + gain_db: -3.8496 + phi_rad: 1.5707963267948966 + theta_rad: 0.5218534463463045 + } + gain_value { + gain_db: -4.4685 + phi_rad: 1.5707963267948966 + theta_rad: 0.5227261109723017 + } + gain_value { + gain_db: -5.3179 + phi_rad: 1.5707963267948966 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -6.4321 + phi_rad: 1.5707963267948966 + theta_rad: 0.524471440224296 + } + gain_value { + gain_db: -7.2692 + phi_rad: 1.5707963267948966 + theta_rad: 0.5253441048502933 + } + gain_value { + gain_db: -7.6164 + phi_rad: 1.5707963267948966 + theta_rad: 0.5262167694762904 + } + gain_value { + gain_db: -7.4502 + phi_rad: 1.5707963267948966 + theta_rad: 0.5270894341022875 + } + gain_value { + gain_db: -7.2879 + phi_rad: 1.5707963267948966 + theta_rad: 0.5279620987282847 + } + gain_value { + gain_db: -7.1017 + phi_rad: 1.5707963267948966 + theta_rad: 0.5288347633542818 + } + gain_value { + gain_db: -7.0603 + phi_rad: 1.5707963267948966 + theta_rad: 0.529707427980279 + } + gain_value { + gain_db: -7.0159 + phi_rad: 1.5707963267948966 + theta_rad: 0.5305800926062761 + } + gain_value { + gain_db: -7.0629 + phi_rad: 1.5707963267948966 + theta_rad: 0.5314527572322734 + } + gain_value { + gain_db: -7.3892 + phi_rad: 1.5707963267948966 + theta_rad: 0.5323254218582705 + } + gain_value { + gain_db: -8.0845 + phi_rad: 1.5707963267948966 + theta_rad: 0.5331980864842677 + } + gain_value { + gain_db: -9.1368 + phi_rad: 1.5707963267948966 + theta_rad: 0.5340707511102649 + } + gain_value { + gain_db: -10.461 + phi_rad: 1.5707963267948966 + theta_rad: 0.534943415736262 + } + gain_value { + gain_db: -12.111 + phi_rad: 1.5707963267948966 + theta_rad: 0.5358160803622591 + } + gain_value { + gain_db: -13.838 + phi_rad: 1.5707963267948966 + theta_rad: 0.5366887449882564 + } + gain_value { + gain_db: -15.947 + phi_rad: 1.5707963267948966 + theta_rad: 0.5375614096142535 + } + gain_value { + gain_db: -15.63 + phi_rad: 1.5707963267948966 + theta_rad: 0.5384340742402507 + } + gain_value { + gain_db: -14.098 + phi_rad: 1.5707963267948966 + theta_rad: 0.5393067388662478 + } + gain_value { + gain_db: -12.883 + phi_rad: 1.5707963267948966 + theta_rad: 0.540179403492245 + } + gain_value { + gain_db: -11.699 + phi_rad: 1.5707963267948966 + theta_rad: 0.5410520681182421 + } + gain_value { + gain_db: -10.553 + phi_rad: 1.5707963267948966 + theta_rad: 0.5419247327442394 + } + gain_value { + gain_db: -9.8745 + phi_rad: 1.5707963267948966 + theta_rad: 0.5427973973702365 + } + gain_value { + gain_db: -9.3867 + phi_rad: 1.5707963267948966 + theta_rad: 0.5436700619962336 + } + gain_value { + gain_db: -9.0758 + phi_rad: 1.5707963267948966 + theta_rad: 0.5445427266222308 + } + gain_value { + gain_db: -9.0879 + phi_rad: 1.5707963267948966 + theta_rad: 0.545415391248228 + } + gain_value { + gain_db: -9.5828 + phi_rad: 1.5707963267948966 + theta_rad: 0.5462880558742251 + } + gain_value { + gain_db: -10.315 + phi_rad: 1.5707963267948966 + theta_rad: 0.5471607205002224 + } + gain_value { + gain_db: -11.125 + phi_rad: 1.5707963267948966 + theta_rad: 0.5480333851262195 + } + gain_value { + gain_db: -11.952 + phi_rad: 1.5707963267948966 + theta_rad: 0.5489060497522167 + } + gain_value { + gain_db: -12.654 + phi_rad: 1.5707963267948966 + theta_rad: 0.5497787143782138 + } + gain_value { + gain_db: -13.188 + phi_rad: 1.5707963267948966 + theta_rad: 0.550651379004211 + } + gain_value { + gain_db: -13.205 + phi_rad: 1.5707963267948966 + theta_rad: 0.5515240436302081 + } + gain_value { + gain_db: -12.711 + phi_rad: 1.5707963267948966 + theta_rad: 0.5523967082562052 + } + gain_value { + gain_db: -11.645 + phi_rad: 1.5707963267948966 + theta_rad: 0.5532693728822025 + } + gain_value { + gain_db: -10.61 + phi_rad: 1.5707963267948966 + theta_rad: 0.5541420375081997 + } + gain_value { + gain_db: -9.7566 + phi_rad: 1.5707963267948966 + theta_rad: 0.5550147021341968 + } + gain_value { + gain_db: -9.2281 + phi_rad: 1.5707963267948966 + theta_rad: 0.555887366760194 + } + gain_value { + gain_db: -8.9599 + phi_rad: 1.5707963267948966 + theta_rad: 0.5567600313861911 + } + gain_value { + gain_db: -8.9855 + phi_rad: 1.5707963267948966 + theta_rad: 0.5576326960121882 + } + gain_value { + gain_db: -9.1558 + phi_rad: 1.5707963267948966 + theta_rad: 0.5585053606381855 + } + gain_value { + gain_db: -9.4521 + phi_rad: 1.5707963267948966 + theta_rad: 0.5593780252641826 + } + gain_value { + gain_db: -9.8802 + phi_rad: 1.5707963267948966 + theta_rad: 0.5602506898901798 + } + gain_value { + gain_db: -10.327 + phi_rad: 1.5707963267948966 + theta_rad: 0.5611233545161769 + } + gain_value { + gain_db: -10.971 + phi_rad: 1.5707963267948966 + theta_rad: 0.5619960191421741 + } + gain_value { + gain_db: -11.31 + phi_rad: 1.5707963267948966 + theta_rad: 0.5628686837681712 + } + gain_value { + gain_db: -10.678 + phi_rad: 1.5707963267948966 + theta_rad: 0.5637413483941683 + } + gain_value { + gain_db: -9.045 + phi_rad: 1.5707963267948966 + theta_rad: 0.5646140130201657 + } + gain_value { + gain_db: -7.4683 + phi_rad: 1.5707963267948966 + theta_rad: 0.5654866776461628 + } + gain_value { + gain_db: -6.3451 + phi_rad: 1.5707963267948966 + theta_rad: 0.56635934227216 + } + gain_value { + gain_db: -5.531 + phi_rad: 1.5707963267948966 + theta_rad: 0.5672320068981571 + } + gain_value { + gain_db: -5.1026 + phi_rad: 1.5707963267948966 + theta_rad: 0.5681046715241542 + } + gain_value { + gain_db: -4.9434 + phi_rad: 1.5707963267948966 + theta_rad: 0.5689773361501514 + } + gain_value { + gain_db: -4.8476 + phi_rad: 1.5707963267948966 + theta_rad: 0.5698500007761486 + } + gain_value { + gain_db: -4.8687 + phi_rad: 1.5707963267948966 + theta_rad: 0.5707226654021458 + } + gain_value { + gain_db: -5.0099 + phi_rad: 1.5707963267948966 + theta_rad: 0.5715953300281429 + } + gain_value { + gain_db: -5.2459 + phi_rad: 1.5707963267948966 + theta_rad: 0.57246799465414 + } + gain_value { + gain_db: -5.5034 + phi_rad: 1.5707963267948966 + theta_rad: 0.5733406592801373 + } + gain_value { + gain_db: -5.5502 + phi_rad: 1.5707963267948966 + theta_rad: 0.5742133239061344 + } + gain_value { + gain_db: -5.2646 + phi_rad: 1.5707963267948966 + theta_rad: 0.5750859885321317 + } + gain_value { + gain_db: -4.7589 + phi_rad: 1.5707963267948966 + theta_rad: 0.5759586531581288 + } + gain_value { + gain_db: -4.1321 + phi_rad: 1.5707963267948966 + theta_rad: 0.5768313177841259 + } + gain_value { + gain_db: -3.4746 + phi_rad: 1.5707963267948966 + theta_rad: 0.5777039824101231 + } + gain_value { + gain_db: -2.9305 + phi_rad: 1.5707963267948966 + theta_rad: 0.5785766470361202 + } + gain_value { + gain_db: -2.6021 + phi_rad: 1.5707963267948966 + theta_rad: 0.5794493116621174 + } + gain_value { + gain_db: -2.4378 + phi_rad: 1.5707963267948966 + theta_rad: 0.5803219762881145 + } + gain_value { + gain_db: -2.3761 + phi_rad: 1.5707963267948966 + theta_rad: 0.5811946409141117 + } + gain_value { + gain_db: -2.4219 + phi_rad: 1.5707963267948966 + theta_rad: 0.5820673055401089 + } + gain_value { + gain_db: -2.5775 + phi_rad: 1.5707963267948966 + theta_rad: 0.582939970166106 + } + gain_value { + gain_db: -2.8024 + phi_rad: 1.5707963267948966 + theta_rad: 0.5838126347921033 + } + gain_value { + gain_db: -3.0259 + phi_rad: 1.5707963267948966 + theta_rad: 0.5846852994181004 + } + gain_value { + gain_db: -3.2035 + phi_rad: 1.5707963267948966 + theta_rad: 0.5855579640440975 + } + gain_value { + gain_db: -3.2299 + phi_rad: 1.5707963267948966 + theta_rad: 0.5864306286700948 + } + gain_value { + gain_db: -3.1086 + phi_rad: 1.5707963267948966 + theta_rad: 0.5873032932960919 + } + gain_value { + gain_db: -2.8219 + phi_rad: 1.5707963267948966 + theta_rad: 0.5881759579220891 + } + gain_value { + gain_db: -2.3677 + phi_rad: 1.5707963267948966 + theta_rad: 0.5890486225480862 + } + gain_value { + gain_db: -1.9009 + phi_rad: 1.5707963267948966 + theta_rad: 0.5899212871740833 + } + gain_value { + gain_db: -1.5039 + phi_rad: 1.5707963267948966 + theta_rad: 0.5907939518000805 + } + gain_value { + gain_db: -1.2553 + phi_rad: 1.5707963267948966 + theta_rad: 0.5916666164260777 + } + gain_value { + gain_db: -1.1284 + phi_rad: 1.5707963267948966 + theta_rad: 0.592539281052075 + } + gain_value { + gain_db: -1.1339 + phi_rad: 1.5707963267948966 + theta_rad: 0.5934119456780721 + } + gain_value { + gain_db: -1.2836 + phi_rad: 1.5707963267948966 + theta_rad: 0.5942846103040692 + } + gain_value { + gain_db: -1.581 + phi_rad: 1.5707963267948966 + theta_rad: 0.5951572749300664 + } + gain_value { + gain_db: -1.8571 + phi_rad: 1.5707963267948966 + theta_rad: 0.5960299395560635 + } + gain_value { + gain_db: -1.9518 + phi_rad: 1.5707963267948966 + theta_rad: 0.5969026041820608 + } + gain_value { + gain_db: -1.8914 + phi_rad: 1.5707963267948966 + theta_rad: 0.5977752688080579 + } + gain_value { + gain_db: -1.6971 + phi_rad: 1.5707963267948966 + theta_rad: 0.598647933434055 + } + gain_value { + gain_db: -1.3312 + phi_rad: 1.5707963267948966 + theta_rad: 0.5995205980600522 + } + gain_value { + gain_db: -0.9743 + phi_rad: 1.5707963267948966 + theta_rad: 0.6003932626860493 + } + gain_value { + gain_db: -0.7656 + phi_rad: 1.5707963267948966 + theta_rad: 0.6012659273120465 + } + gain_value { + gain_db: -0.5839 + phi_rad: 1.5707963267948966 + theta_rad: 0.6021385919380436 + } + gain_value { + gain_db: -0.53737 + phi_rad: 1.5707963267948966 + theta_rad: 0.6030112565640408 + } + gain_value { + gain_db: -0.72593 + phi_rad: 1.5707963267948966 + theta_rad: 0.6038839211900381 + } + gain_value { + gain_db: -1.1068 + phi_rad: 1.5707963267948966 + theta_rad: 0.6047565858160352 + } + gain_value { + gain_db: -1.6263 + phi_rad: 1.5707963267948966 + theta_rad: 0.6056292504420324 + } + gain_value { + gain_db: -2.0642 + phi_rad: 1.5707963267948966 + theta_rad: 0.6065019150680295 + } + gain_value { + gain_db: -2.2235 + phi_rad: 1.5707963267948966 + theta_rad: 0.6073745796940266 + } + gain_value { + gain_db: -2.1646 + phi_rad: 1.5707963267948966 + theta_rad: 0.6082472443200239 + } + gain_value { + gain_db: -1.9122 + phi_rad: 1.5707963267948966 + theta_rad: 0.609119908946021 + } + gain_value { + gain_db: -1.7577 + phi_rad: 1.5707963267948966 + theta_rad: 0.6099925735720182 + } + gain_value { + gain_db: -1.6716 + phi_rad: 1.5707963267948966 + theta_rad: 0.6108652381980153 + } + gain_value { + gain_db: -1.6866 + phi_rad: 1.5707963267948966 + theta_rad: 0.6117379028240124 + } + gain_value { + gain_db: -1.8431 + phi_rad: 1.5707963267948966 + theta_rad: 0.6126105674500097 + } + gain_value { + gain_db: -2.1535 + phi_rad: 1.5707963267948966 + theta_rad: 0.6134832320760069 + } + gain_value { + gain_db: -2.6482 + phi_rad: 1.5707963267948966 + theta_rad: 0.6143558967020041 + } + gain_value { + gain_db: -3.3263 + phi_rad: 1.5707963267948966 + theta_rad: 0.6152285613280012 + } + gain_value { + gain_db: -4.4592 + phi_rad: 1.5707963267948966 + theta_rad: 0.6161012259539983 + } + gain_value { + gain_db: -5.1964 + phi_rad: 1.5707963267948966 + theta_rad: 0.6169738905799955 + } + gain_value { + gain_db: -4.8014 + phi_rad: 1.5707963267948966 + theta_rad: 0.6178465552059926 + } + gain_value { + gain_db: -4.096 + phi_rad: 1.5707963267948966 + theta_rad: 0.6187192198319899 + } + gain_value { + gain_db: -3.7423 + phi_rad: 1.5707963267948966 + theta_rad: 0.619591884457987 + } + gain_value { + gain_db: -3.7178 + phi_rad: 1.5707963267948966 + theta_rad: 0.6204645490839841 + } + gain_value { + gain_db: -3.8271 + phi_rad: 1.5707963267948966 + theta_rad: 0.6213372137099813 + } + gain_value { + gain_db: -4.0712 + phi_rad: 1.5707963267948966 + theta_rad: 0.6222098783359784 + } + gain_value { + gain_db: -4.3202 + phi_rad: 1.5707963267948966 + theta_rad: 0.6230825429619757 + } + gain_value { + gain_db: -4.644 + phi_rad: 1.5707963267948966 + theta_rad: 0.6239552075879728 + } + gain_value { + gain_db: -5.1228 + phi_rad: 1.5707963267948966 + theta_rad: 0.62482787221397 + } + gain_value { + gain_db: -5.7022 + phi_rad: 1.5707963267948966 + theta_rad: 0.6257005368399672 + } + gain_value { + gain_db: -5.8688 + phi_rad: 1.5707963267948966 + theta_rad: 0.6265732014659643 + } + gain_value { + gain_db: -5.8506 + phi_rad: 1.5707963267948966 + theta_rad: 0.6274458660919615 + } + gain_value { + gain_db: -5.6164 + phi_rad: 1.5707963267948966 + theta_rad: 0.6283185307179586 + } + gain_value { + gain_db: -5.4265 + phi_rad: 1.5707963267948966 + theta_rad: 0.6291911953439557 + } + gain_value { + gain_db: -5.3852 + phi_rad: 1.5707963267948966 + theta_rad: 0.630063859969953 + } + gain_value { + gain_db: -5.7229 + phi_rad: 1.5707963267948966 + theta_rad: 0.6309365245959501 + } + gain_value { + gain_db: -6.1687 + phi_rad: 1.5707963267948966 + theta_rad: 0.6318091892219474 + } + gain_value { + gain_db: -6.5406 + phi_rad: 1.5707963267948966 + theta_rad: 0.6326818538479445 + } + gain_value { + gain_db: -6.2858 + phi_rad: 1.5707963267948966 + theta_rad: 0.6335545184739416 + } + gain_value { + gain_db: -6.0397 + phi_rad: 1.5707963267948966 + theta_rad: 0.6344271830999388 + } + gain_value { + gain_db: -5.9935 + phi_rad: 1.5707963267948966 + theta_rad: 0.635299847725936 + } + gain_value { + gain_db: -6.2076 + phi_rad: 1.5707963267948966 + theta_rad: 0.6361725123519332 + } + gain_value { + gain_db: -6.2838 + phi_rad: 1.5707963267948966 + theta_rad: 0.6370451769779303 + } + gain_value { + gain_db: -6.2958 + phi_rad: 1.5707963267948966 + theta_rad: 0.6379178416039274 + } + gain_value { + gain_db: -6.3016 + phi_rad: 1.5707963267948966 + theta_rad: 0.6387905062299246 + } + gain_value { + gain_db: -6.3254 + phi_rad: 1.5707963267948966 + theta_rad: 0.6396631708559217 + } + gain_value { + gain_db: -6.2853 + phi_rad: 1.5707963267948966 + theta_rad: 0.640535835481919 + } + gain_value { + gain_db: -6.3441 + phi_rad: 1.5707963267948966 + theta_rad: 0.6414085001079161 + } + gain_value { + gain_db: -6.554 + phi_rad: 1.5707963267948966 + theta_rad: 0.6422811647339133 + } + gain_value { + gain_db: -6.2991 + phi_rad: 1.5707963267948966 + theta_rad: 0.6431538293599105 + } + gain_value { + gain_db: -5.7872 + phi_rad: 1.5707963267948966 + theta_rad: 0.6440264939859076 + } + gain_value { + gain_db: -5.4829 + phi_rad: 1.5707963267948966 + theta_rad: 0.6448991586119048 + } + gain_value { + gain_db: -5.3893 + phi_rad: 1.5707963267948966 + theta_rad: 0.6457718232379019 + } + gain_value { + gain_db: -5.4128 + phi_rad: 1.5707963267948966 + theta_rad: 0.646644487863899 + } + gain_value { + gain_db: -5.3264 + phi_rad: 1.5707963267948966 + theta_rad: 0.6475171524898963 + } + gain_value { + gain_db: -5.2563 + phi_rad: 1.5707963267948966 + theta_rad: 0.6483898171158934 + } + gain_value { + gain_db: -5.3082 + phi_rad: 1.5707963267948966 + theta_rad: 0.6492624817418906 + } + gain_value { + gain_db: -5.5091 + phi_rad: 1.5707963267948966 + theta_rad: 0.6501351463678877 + } + gain_value { + gain_db: -5.7242 + phi_rad: 1.5707963267948966 + theta_rad: 0.6510078109938848 + } + gain_value { + gain_db: -5.9142 + phi_rad: 1.5707963267948966 + theta_rad: 0.6518804756198822 + } + gain_value { + gain_db: -6.0214 + phi_rad: 1.5707963267948966 + theta_rad: 0.6527531402458793 + } + gain_value { + gain_db: -5.9055 + phi_rad: 1.5707963267948966 + theta_rad: 0.6536258048718765 + } + gain_value { + gain_db: -5.5336 + phi_rad: 1.5707963267948966 + theta_rad: 0.6544984694978736 + } + gain_value { + gain_db: -5.2143 + phi_rad: 1.5707963267948966 + theta_rad: 0.6553711341238707 + } + gain_value { + gain_db: -5.0348 + phi_rad: 1.5707963267948966 + theta_rad: 0.6562437987498679 + } + gain_value { + gain_db: -4.9241 + phi_rad: 1.5707963267948966 + theta_rad: 0.657116463375865 + } + gain_value { + gain_db: -4.8589 + phi_rad: 1.5707963267948966 + theta_rad: 0.6579891280018623 + } + gain_value { + gain_db: -4.8806 + phi_rad: 1.5707963267948966 + theta_rad: 0.6588617926278594 + } + gain_value { + gain_db: -4.8271 + phi_rad: 1.5707963267948966 + theta_rad: 0.6597344572538565 + } + gain_value { + gain_db: -4.8955 + phi_rad: 1.5707963267948966 + theta_rad: 0.6606071218798537 + } + gain_value { + gain_db: -4.9626 + phi_rad: 1.5707963267948966 + theta_rad: 0.6614797865058508 + } + gain_value { + gain_db: -5.0223 + phi_rad: 1.5707963267948966 + theta_rad: 0.6623524511318482 + } + gain_value { + gain_db: -4.8825 + phi_rad: 1.5707963267948966 + theta_rad: 0.6632251157578453 + } + gain_value { + gain_db: -4.5618 + phi_rad: 1.5707963267948966 + theta_rad: 0.6640977803838424 + } + gain_value { + gain_db: -4.1102 + phi_rad: 1.5707963267948966 + theta_rad: 0.6649704450098396 + } + gain_value { + gain_db: -3.7544 + phi_rad: 1.5707963267948966 + theta_rad: 0.6658431096358367 + } + gain_value { + gain_db: -3.4373 + phi_rad: 1.5707963267948966 + theta_rad: 0.6667157742618339 + } + gain_value { + gain_db: -3.1603 + phi_rad: 1.5707963267948966 + theta_rad: 0.667588438887831 + } + gain_value { + gain_db: -2.8543 + phi_rad: 1.5707963267948966 + theta_rad: 0.6684611035138281 + } + gain_value { + gain_db: -2.5492 + phi_rad: 1.5707963267948966 + theta_rad: 0.6693337681398254 + } + gain_value { + gain_db: -2.264 + phi_rad: 1.5707963267948966 + theta_rad: 0.6702064327658225 + } + gain_value { + gain_db: -2.1243 + phi_rad: 1.5707963267948966 + theta_rad: 0.6710790973918198 + } + gain_value { + gain_db: -2.1086 + phi_rad: 1.5707963267948966 + theta_rad: 0.6719517620178169 + } + gain_value { + gain_db: -2.0155 + phi_rad: 1.5707963267948966 + theta_rad: 0.672824426643814 + } + gain_value { + gain_db: -1.9126 + phi_rad: 1.5707963267948966 + theta_rad: 0.6736970912698113 + } + gain_value { + gain_db: -1.6692 + phi_rad: 1.5707963267948966 + theta_rad: 0.6745697558958084 + } + gain_value { + gain_db: -1.3541 + phi_rad: 1.5707963267948966 + theta_rad: 0.6754424205218056 + } + gain_value { + gain_db: -1.0766 + phi_rad: 1.5707963267948966 + theta_rad: 0.6763150851478027 + } + gain_value { + gain_db: -0.91433 + phi_rad: 1.5707963267948966 + theta_rad: 0.6771877497737998 + } + gain_value { + gain_db: -0.83407 + phi_rad: 1.5707963267948966 + theta_rad: 0.678060414399797 + } + gain_value { + gain_db: -0.6958 + phi_rad: 1.5707963267948966 + theta_rad: 0.6789330790257941 + } + gain_value { + gain_db: -0.50307 + phi_rad: 1.5707963267948966 + theta_rad: 0.6798057436517914 + } + gain_value { + gain_db: -0.4031 + phi_rad: 1.5707963267948966 + theta_rad: 0.6806784082777885 + } + gain_value { + gain_db: -0.39277 + phi_rad: 1.5707963267948966 + theta_rad: 0.6815510729037857 + } + gain_value { + gain_db: -0.43657 + phi_rad: 1.5707963267948966 + theta_rad: 0.6824237375297829 + } + gain_value { + gain_db: -0.49133 + phi_rad: 1.5707963267948966 + theta_rad: 0.68329640215578 + } + gain_value { + gain_db: -0.5471 + phi_rad: 1.5707963267948966 + theta_rad: 0.6841690667817772 + } + gain_value { + gain_db: -0.50863 + phi_rad: 1.5707963267948966 + theta_rad: 0.6850417314077744 + } + gain_value { + gain_db: -0.4772 + phi_rad: 1.5707963267948966 + theta_rad: 0.6859143960337715 + } + gain_value { + gain_db: -0.46447 + phi_rad: 1.5707963267948966 + theta_rad: 0.6867870606597687 + } + gain_value { + gain_db: -0.54163 + phi_rad: 1.5707963267948966 + theta_rad: 0.6876597252857658 + } + gain_value { + gain_db: -0.68777 + phi_rad: 1.5707963267948966 + theta_rad: 0.688532389911763 + } + gain_value { + gain_db: -0.77677 + phi_rad: 1.5707963267948966 + theta_rad: 0.6894050545377601 + } + gain_value { + gain_db: -0.8158 + phi_rad: 1.5707963267948966 + theta_rad: 0.6902777191637572 + } + gain_value { + gain_db: -0.85487 + phi_rad: 1.5707963267948966 + theta_rad: 0.6911503837897546 + } + gain_value { + gain_db: -0.92957 + phi_rad: 1.5707963267948966 + theta_rad: 0.6920230484157517 + } + gain_value { + gain_db: -1.0672 + phi_rad: 1.5707963267948966 + theta_rad: 0.6928957130417489 + } + gain_value { + gain_db: -1.2236 + phi_rad: 1.5707963267948966 + theta_rad: 0.693768377667746 + } + gain_value { + gain_db: -1.4438 + phi_rad: 1.5707963267948966 + theta_rad: 0.6946410422937431 + } + gain_value { + gain_db: -1.6252 + phi_rad: 1.5707963267948966 + theta_rad: 0.6955137069197403 + } + gain_value { + gain_db: -1.7084 + phi_rad: 1.5707963267948966 + theta_rad: 0.6963863715457375 + } + gain_value { + gain_db: -1.8317 + phi_rad: 1.5707963267948966 + theta_rad: 0.6972590361717347 + } + gain_value { + gain_db: -2.0007 + phi_rad: 1.5707963267948966 + theta_rad: 0.6981317007977318 + } + gain_value { + gain_db: -2.1558 + phi_rad: 1.5707963267948966 + theta_rad: 0.6990043654237289 + } + gain_value { + gain_db: -2.3355 + phi_rad: 1.5707963267948966 + theta_rad: 0.6998770300497261 + } + gain_value { + gain_db: -2.4623 + phi_rad: 1.5707963267948966 + theta_rad: 0.7007496946757232 + } + gain_value { + gain_db: -2.6642 + phi_rad: 1.5707963267948966 + theta_rad: 0.7016223593017206 + } + gain_value { + gain_db: -2.8356 + phi_rad: 1.5707963267948966 + theta_rad: 0.7024950239277177 + } + gain_value { + gain_db: -3.11 + phi_rad: 1.5707963267948966 + theta_rad: 0.7033676885537148 + } + gain_value { + gain_db: -3.4319 + phi_rad: 1.5707963267948966 + theta_rad: 0.704240353179712 + } + gain_value { + gain_db: -3.762 + phi_rad: 1.5707963267948966 + theta_rad: 0.7051130178057091 + } + gain_value { + gain_db: -3.8375 + phi_rad: 1.5707963267948966 + theta_rad: 0.7059856824317063 + } + gain_value { + gain_db: -3.7939 + phi_rad: 1.5707963267948966 + theta_rad: 0.7068583470577035 + } + gain_value { + gain_db: -3.4874 + phi_rad: 1.5707963267948966 + theta_rad: 0.7077310116837006 + } + gain_value { + gain_db: -3.2925 + phi_rad: 1.5707963267948966 + theta_rad: 0.7086036763096978 + } + gain_value { + gain_db: -3.1703 + phi_rad: 1.5707963267948966 + theta_rad: 0.7094763409356949 + } + gain_value { + gain_db: -3.1774 + phi_rad: 1.5707963267948966 + theta_rad: 0.7103490055616922 + } + gain_value { + gain_db: -3.2832 + phi_rad: 1.5707963267948966 + theta_rad: 0.7112216701876893 + } + gain_value { + gain_db: -3.5696 + phi_rad: 1.5707963267948966 + theta_rad: 0.7120943348136864 + } + gain_value { + gain_db: -3.9704 + phi_rad: 1.5707963267948966 + theta_rad: 0.7129669994396837 + } + gain_value { + gain_db: -4.3324 + phi_rad: 1.5707963267948966 + theta_rad: 0.7138396640656808 + } + gain_value { + gain_db: -4.611 + phi_rad: 1.5707963267948966 + theta_rad: 0.714712328691678 + } + gain_value { + gain_db: -4.6272 + phi_rad: 1.5707963267948966 + theta_rad: 0.7155849933176751 + } + gain_value { + gain_db: -4.463 + phi_rad: 1.5707963267948966 + theta_rad: 0.7164576579436722 + } + gain_value { + gain_db: -3.9963 + phi_rad: 1.5707963267948966 + theta_rad: 0.7173303225696694 + } + gain_value { + gain_db: -3.5178 + phi_rad: 1.5707963267948966 + theta_rad: 0.7182029871956666 + } + gain_value { + gain_db: -3.1289 + phi_rad: 1.5707963267948966 + theta_rad: 0.7190756518216638 + } + gain_value { + gain_db: -2.8668 + phi_rad: 1.5707963267948966 + theta_rad: 0.7199483164476609 + } + gain_value { + gain_db: -2.9056 + phi_rad: 1.5707963267948966 + theta_rad: 0.7208209810736581 + } + gain_value { + gain_db: -3.0923 + phi_rad: 1.5707963267948966 + theta_rad: 0.7216936456996553 + } + gain_value { + gain_db: -3.3887 + phi_rad: 1.5707963267948966 + theta_rad: 0.7225663103256524 + } + gain_value { + gain_db: -3.7167 + phi_rad: 1.5707963267948966 + theta_rad: 0.7234389749516497 + } + gain_value { + gain_db: -4.0266 + phi_rad: 1.5707963267948966 + theta_rad: 0.7243116395776468 + } + gain_value { + gain_db: -4.2532 + phi_rad: 1.5707963267948966 + theta_rad: 0.7251843042036439 + } + gain_value { + gain_db: -4.3283 + phi_rad: 1.5707963267948966 + theta_rad: 0.7260569688296411 + } + gain_value { + gain_db: -4.2878 + phi_rad: 1.5707963267948966 + theta_rad: 0.7269296334556382 + } + gain_value { + gain_db: -4.2127 + phi_rad: 1.5707963267948966 + theta_rad: 0.7278022980816354 + } + gain_value { + gain_db: -3.974 + phi_rad: 1.5707963267948966 + theta_rad: 0.7286749627076325 + } + gain_value { + gain_db: -3.776 + phi_rad: 1.5707963267948966 + theta_rad: 0.7295476273336297 + } + gain_value { + gain_db: -3.6541 + phi_rad: 1.5707963267948966 + theta_rad: 0.730420291959627 + } + gain_value { + gain_db: -3.6945 + phi_rad: 1.5707963267948966 + theta_rad: 0.7312929565856241 + } + gain_value { + gain_db: -3.7727 + phi_rad: 1.5707963267948966 + theta_rad: 0.7321656212116213 + } + gain_value { + gain_db: -3.8767 + phi_rad: 1.5707963267948966 + theta_rad: 0.7330382858376184 + } + gain_value { + gain_db: -4.0358 + phi_rad: 1.5707963267948966 + theta_rad: 0.7339109504636155 + } + gain_value { + gain_db: -4.2276 + phi_rad: 1.5707963267948966 + theta_rad: 0.7347836150896128 + } + gain_value { + gain_db: -4.4457 + phi_rad: 1.5707963267948966 + theta_rad: 0.7356562797156099 + } + gain_value { + gain_db: -4.8107 + phi_rad: 1.5707963267948966 + theta_rad: 0.7365289443416071 + } + gain_value { + gain_db: -5.1897 + phi_rad: 1.5707963267948966 + theta_rad: 0.7374016089676042 + } + gain_value { + gain_db: -5.3111 + phi_rad: 1.5707963267948966 + theta_rad: 0.7382742735936013 + } + gain_value { + gain_db: -5.1327 + phi_rad: 1.5707963267948966 + theta_rad: 0.7391469382195985 + } + gain_value { + gain_db: -4.7834 + phi_rad: 1.5707963267948966 + theta_rad: 0.7400196028455958 + } + gain_value { + gain_db: -4.3273 + phi_rad: 1.5707963267948966 + theta_rad: 0.740892267471593 + } + gain_value { + gain_db: -4.0533 + phi_rad: 1.5707963267948966 + theta_rad: 0.7417649320975901 + } + gain_value { + gain_db: -3.8208 + phi_rad: 1.5707963267948966 + theta_rad: 0.7426375967235872 + } + gain_value { + gain_db: -3.5719 + phi_rad: 1.5707963267948966 + theta_rad: 0.7435102613495844 + } + gain_value { + gain_db: -3.4627 + phi_rad: 1.5707963267948966 + theta_rad: 0.7443829259755815 + } + gain_value { + gain_db: -3.5099 + phi_rad: 1.5707963267948966 + theta_rad: 0.7452555906015788 + } + gain_value { + gain_db: -3.7348 + phi_rad: 1.5707963267948966 + theta_rad: 0.7461282552275759 + } + gain_value { + gain_db: -4.0255 + phi_rad: 1.5707963267948966 + theta_rad: 0.747000919853573 + } + gain_value { + gain_db: -4.3733 + phi_rad: 1.5707963267948966 + theta_rad: 0.7478735844795702 + } + gain_value { + gain_db: -4.5554 + phi_rad: 1.5707963267948966 + theta_rad: 0.7487462491055673 + } + gain_value { + gain_db: -4.3802 + phi_rad: 1.5707963267948966 + theta_rad: 0.7496189137315646 + } + gain_value { + gain_db: -4.0607 + phi_rad: 1.5707963267948966 + theta_rad: 0.7504915783575618 + } + gain_value { + gain_db: -3.6734 + phi_rad: 1.5707963267948966 + theta_rad: 0.7513642429835589 + } + gain_value { + gain_db: -3.2842 + phi_rad: 1.5707963267948966 + theta_rad: 0.7522369076095561 + } + gain_value { + gain_db: -3.0227 + phi_rad: 1.5707963267948966 + theta_rad: 0.7531095722355532 + } + gain_value { + gain_db: -2.8661 + phi_rad: 1.5707963267948966 + theta_rad: 0.7539822368615504 + } + gain_value { + gain_db: -2.7808 + phi_rad: 1.5707963267948966 + theta_rad: 0.7548549014875475 + } + gain_value { + gain_db: -2.7958 + phi_rad: 1.5707963267948966 + theta_rad: 0.7557275661135446 + } + gain_value { + gain_db: -2.9428 + phi_rad: 1.5707963267948966 + theta_rad: 0.7566002307395419 + } + gain_value { + gain_db: -3.2174 + phi_rad: 1.5707963267948966 + theta_rad: 0.757472895365539 + } + gain_value { + gain_db: -3.4698 + phi_rad: 1.5707963267948966 + theta_rad: 0.7583455599915362 + } + gain_value { + gain_db: -3.5724 + phi_rad: 1.5707963267948966 + theta_rad: 0.7592182246175333 + } + gain_value { + gain_db: -3.4718 + phi_rad: 1.5707963267948966 + theta_rad: 0.7600908892435305 + } + gain_value { + gain_db: -3.1963 + phi_rad: 1.5707963267948966 + theta_rad: 0.7609635538695277 + } + gain_value { + gain_db: -2.8834 + phi_rad: 1.5707963267948966 + theta_rad: 0.7618362184955249 + } + gain_value { + gain_db: -2.6741 + phi_rad: 1.5707963267948966 + theta_rad: 0.7627088831215221 + } + gain_value { + gain_db: -2.496 + phi_rad: 1.5707963267948966 + theta_rad: 0.7635815477475192 + } + gain_value { + gain_db: -2.3007 + phi_rad: 1.5707963267948966 + theta_rad: 0.7644542123735163 + } + gain_value { + gain_db: -2.1608 + phi_rad: 1.5707963267948966 + theta_rad: 0.7653268769995135 + } + gain_value { + gain_db: -2.0497 + phi_rad: 1.5707963267948966 + theta_rad: 0.7661995416255106 + } + gain_value { + gain_db: -2.0088 + phi_rad: 1.5707963267948966 + theta_rad: 0.7670722062515078 + } + gain_value { + gain_db: -1.9742 + phi_rad: 1.5707963267948966 + theta_rad: 0.767944870877505 + } + gain_value { + gain_db: -2.0067 + phi_rad: 1.5707963267948966 + theta_rad: 0.7688175355035021 + } + gain_value { + gain_db: -2.1753 + phi_rad: 1.5707963267948966 + theta_rad: 0.7696902001294994 + } + gain_value { + gain_db: -2.4143 + phi_rad: 1.5707963267948966 + theta_rad: 0.7705628647554965 + } + gain_value { + gain_db: -2.4907 + phi_rad: 1.5707963267948966 + theta_rad: 0.7714355293814937 + } + gain_value { + gain_db: -2.5075 + phi_rad: 1.5707963267948966 + theta_rad: 0.7723081940074908 + } + gain_value { + gain_db: -2.4994 + phi_rad: 1.5707963267948966 + theta_rad: 0.773180858633488 + } + gain_value { + gain_db: -2.5182 + phi_rad: 1.5707963267948966 + theta_rad: 0.7740535232594852 + } + gain_value { + gain_db: -2.5521 + phi_rad: 1.5707963267948966 + theta_rad: 0.7749261878854823 + } + gain_value { + gain_db: -2.5411 + phi_rad: 1.5707963267948966 + theta_rad: 0.7757988525114795 + } + gain_value { + gain_db: -2.4651 + phi_rad: 1.5707963267948966 + theta_rad: 0.7766715171374766 + } + gain_value { + gain_db: -2.4159 + phi_rad: 1.5707963267948966 + theta_rad: 0.7775441817634737 + } + gain_value { + gain_db: -2.4133 + phi_rad: 1.5707963267948966 + theta_rad: 0.778416846389471 + } + gain_value { + gain_db: -2.6016 + phi_rad: 1.5707963267948966 + theta_rad: 0.7792895110154682 + } + gain_value { + gain_db: -2.8965 + phi_rad: 1.5707963267948966 + theta_rad: 0.7801621756414654 + } + gain_value { + gain_db: -3.128 + phi_rad: 1.5707963267948966 + theta_rad: 0.7810348402674625 + } + gain_value { + gain_db: -3.2379 + phi_rad: 1.5707963267948966 + theta_rad: 0.7819075048934596 + } + gain_value { + gain_db: -3.3909 + phi_rad: 1.5707963267948966 + theta_rad: 0.7827801695194568 + } + gain_value { + gain_db: -3.7165 + phi_rad: 1.5707963267948966 + theta_rad: 0.783652834145454 + } + gain_value { + gain_db: -4.1413 + phi_rad: 1.5707963267948966 + theta_rad: 0.7845254987714512 + } + gain_value { + gain_db: -4.317 + phi_rad: 1.5707963267948966 + theta_rad: 0.7853981633974483 + } + gain_value { + gain_db: -4.5527 + phi_rad: 1.5707963267948966 + theta_rad: 0.7862708280234454 + } + gain_value { + gain_db: -4.5203 + phi_rad: 1.5707963267948966 + theta_rad: 0.7871434926494426 + } + gain_value { + gain_db: -4.6832 + phi_rad: 1.5707963267948966 + theta_rad: 0.7880161572754397 + } + gain_value { + gain_db: -4.9109 + phi_rad: 1.5707963267948966 + theta_rad: 0.788888821901437 + } + gain_value { + gain_db: -4.6977 + phi_rad: 1.5707963267948966 + theta_rad: 0.7897614865274342 + } + gain_value { + gain_db: -4.5228 + phi_rad: 1.5707963267948966 + theta_rad: 0.7906341511534313 + } + gain_value { + gain_db: -4.4464 + phi_rad: 1.5707963267948966 + theta_rad: 0.7915068157794285 + } + gain_value { + gain_db: -4.6683 + phi_rad: 1.5707963267948966 + theta_rad: 0.7923794804054256 + } + gain_value { + gain_db: -4.8389 + phi_rad: 1.5707963267948966 + theta_rad: 0.7932521450314228 + } + gain_value { + gain_db: -5.0676 + phi_rad: 1.5707963267948966 + theta_rad: 0.7941248096574199 + } + gain_value { + gain_db: -5.5875 + phi_rad: 1.5707963267948966 + theta_rad: 0.794997474283417 + } + gain_value { + gain_db: -6.1256 + phi_rad: 1.5707963267948966 + theta_rad: 0.7958701389094143 + } + gain_value { + gain_db: -6.5003 + phi_rad: 1.5707963267948966 + theta_rad: 0.7967428035354114 + } + gain_value { + gain_db: -6.7235 + phi_rad: 1.5707963267948966 + theta_rad: 0.7976154681614086 + } + gain_value { + gain_db: -6.8782 + phi_rad: 1.5707963267948966 + theta_rad: 0.7984881327874057 + } + gain_value { + gain_db: -6.7312 + phi_rad: 1.5707963267948966 + theta_rad: 0.7993607974134029 + } + gain_value { + gain_db: -6.3958 + phi_rad: 1.5707963267948966 + theta_rad: 0.8002334620394002 + } + gain_value { + gain_db: -5.9286 + phi_rad: 1.5707963267948966 + theta_rad: 0.8011061266653973 + } + gain_value { + gain_db: -5.6678 + phi_rad: 1.5707963267948966 + theta_rad: 0.8019787912913945 + } + gain_value { + gain_db: -5.5441 + phi_rad: 1.5707963267948966 + theta_rad: 0.8028514559173916 + } + gain_value { + gain_db: -5.567 + phi_rad: 1.5707963267948966 + theta_rad: 0.8037241205433887 + } + gain_value { + gain_db: -5.8143 + phi_rad: 1.5707963267948966 + theta_rad: 0.8045967851693859 + } + gain_value { + gain_db: -6.2916 + phi_rad: 1.5707963267948966 + theta_rad: 0.805469449795383 + } + gain_value { + gain_db: -6.6289 + phi_rad: 1.5707963267948966 + theta_rad: 0.8063421144213803 + } + gain_value { + gain_db: -7.0029 + phi_rad: 1.5707963267948966 + theta_rad: 0.8072147790473774 + } + gain_value { + gain_db: -7.4933 + phi_rad: 1.5707963267948966 + theta_rad: 0.8080874436733745 + } + gain_value { + gain_db: -7.7492 + phi_rad: 1.5707963267948966 + theta_rad: 0.8089601082993718 + } + gain_value { + gain_db: -7.4757 + phi_rad: 1.5707963267948966 + theta_rad: 0.8098327729253689 + } + gain_value { + gain_db: -7.0452 + phi_rad: 1.5707963267948966 + theta_rad: 0.8107054375513661 + } + gain_value { + gain_db: -6.7426 + phi_rad: 1.5707963267948966 + theta_rad: 0.8115781021773633 + } + gain_value { + gain_db: -6.3998 + phi_rad: 1.5707963267948966 + theta_rad: 0.8124507668033604 + } + gain_value { + gain_db: -6.0125 + phi_rad: 1.5707963267948966 + theta_rad: 0.8133234314293576 + } + gain_value { + gain_db: -5.9136 + phi_rad: 1.5707963267948966 + theta_rad: 0.8141960960553547 + } + gain_value { + gain_db: -5.9694 + phi_rad: 1.5707963267948966 + theta_rad: 0.8150687606813519 + } + gain_value { + gain_db: -6.1722 + phi_rad: 1.5707963267948966 + theta_rad: 0.815941425307349 + } + gain_value { + gain_db: -6.3722 + phi_rad: 1.5707963267948966 + theta_rad: 0.8168140899333461 + } + gain_value { + gain_db: -6.6786 + phi_rad: 1.5707963267948966 + theta_rad: 0.8176867545593434 + } + gain_value { + gain_db: -7.0856 + phi_rad: 1.5707963267948966 + theta_rad: 0.8185594191853406 + } + gain_value { + gain_db: -7.2351 + phi_rad: 1.5707963267948966 + theta_rad: 0.8194320838113378 + } + gain_value { + gain_db: -7.1105 + phi_rad: 1.5707963267948966 + theta_rad: 0.8203047484373349 + } + gain_value { + gain_db: -6.791 + phi_rad: 1.5707963267948966 + theta_rad: 0.821177413063332 + } + gain_value { + gain_db: -6.6551 + phi_rad: 1.5707963267948966 + theta_rad: 0.8220500776893293 + } + gain_value { + gain_db: -6.3906 + phi_rad: 1.5707963267948966 + theta_rad: 0.8229227423153264 + } + gain_value { + gain_db: -6.0696 + phi_rad: 1.5707963267948966 + theta_rad: 0.8237954069413236 + } + gain_value { + gain_db: -5.6852 + phi_rad: 1.5707963267948966 + theta_rad: 0.8246680715673207 + } + gain_value { + gain_db: -5.6245 + phi_rad: 1.5707963267948966 + theta_rad: 0.8255407361933178 + } + gain_value { + gain_db: -5.8092 + phi_rad: 1.5707963267948966 + theta_rad: 0.826413400819315 + } + gain_value { + gain_db: -5.9431 + phi_rad: 1.5707963267948966 + theta_rad: 0.8272860654453121 + } + gain_value { + gain_db: -6.0781 + phi_rad: 1.5707963267948966 + theta_rad: 0.8281587300713095 + } + gain_value { + gain_db: -6.2196 + phi_rad: 1.5707963267948966 + theta_rad: 0.8290313946973066 + } + gain_value { + gain_db: -6.3146 + phi_rad: 1.5707963267948966 + theta_rad: 0.8299040593233037 + } + gain_value { + gain_db: -6.4858 + phi_rad: 1.5707963267948966 + theta_rad: 0.8307767239493009 + } + gain_value { + gain_db: -6.7166 + phi_rad: 1.5707963267948966 + theta_rad: 0.831649388575298 + } + gain_value { + gain_db: -6.595 + phi_rad: 1.5707963267948966 + theta_rad: 0.8325220532012952 + } + gain_value { + gain_db: -6.6911 + phi_rad: 1.5707963267948966 + theta_rad: 0.8333947178272924 + } + gain_value { + gain_db: -7.0231 + phi_rad: 1.5707963267948966 + theta_rad: 0.8342673824532895 + } + gain_value { + gain_db: -6.7334 + phi_rad: 1.5707963267948966 + theta_rad: 0.8351400470792867 + } + gain_value { + gain_db: -6.1454 + phi_rad: 1.5707963267948966 + theta_rad: 0.8360127117052838 + } + gain_value { + gain_db: -5.9912 + phi_rad: 1.5707963267948966 + theta_rad: 0.836885376331281 + } + gain_value { + gain_db: -6.4209 + phi_rad: 1.5707963267948966 + theta_rad: 0.8377580409572782 + } + gain_value { + gain_db: -6.9358 + phi_rad: 1.5707963267948966 + theta_rad: 0.8386307055832753 + } + gain_value { + gain_db: -7.1027 + phi_rad: 1.5707963267948966 + theta_rad: 0.8395033702092726 + } + gain_value { + gain_db: -7.2041 + phi_rad: 1.5707963267948966 + theta_rad: 0.8403760348352697 + } + gain_value { + gain_db: -7.9177 + phi_rad: 1.5707963267948966 + theta_rad: 0.8412486994612669 + } + gain_value { + gain_db: -9.591 + phi_rad: 1.5707963267948966 + theta_rad: 0.842121364087264 + } + gain_value { + gain_db: -9.3433 + phi_rad: 1.5707963267948966 + theta_rad: 0.8429940287132611 + } + gain_value { + gain_db: -7.4615 + phi_rad: 1.5707963267948966 + theta_rad: 0.8438666933392583 + } + gain_value { + gain_db: -6.5206 + phi_rad: 1.5707963267948966 + theta_rad: 0.8447393579652555 + } + gain_value { + gain_db: -6.0813 + phi_rad: 1.5707963267948966 + theta_rad: 0.8456120225912527 + } + gain_value { + gain_db: -6.0052 + phi_rad: 1.5707963267948966 + theta_rad: 0.8464846872172498 + } + gain_value { + gain_db: -6.2175 + phi_rad: 1.5707963267948966 + theta_rad: 0.8473573518432469 + } + gain_value { + gain_db: -6.3525 + phi_rad: 1.5707963267948966 + theta_rad: 0.8482300164692442 + } + gain_value { + gain_db: -6.4345 + phi_rad: 1.5707963267948966 + theta_rad: 0.8491026810952413 + } + gain_value { + gain_db: -6.4054 + phi_rad: 1.5707963267948966 + theta_rad: 0.8499753457212386 + } + gain_value { + gain_db: -6.5982 + phi_rad: 1.5707963267948966 + theta_rad: 0.8508480103472357 + } + gain_value { + gain_db: -6.8814 + phi_rad: 1.5707963267948966 + theta_rad: 0.8517206749732328 + } + gain_value { + gain_db: -6.4133 + phi_rad: 1.5707963267948966 + theta_rad: 0.85259333959923 + } + gain_value { + gain_db: -5.5195 + phi_rad: 1.5707963267948966 + theta_rad: 0.8534660042252271 + } + gain_value { + gain_db: -5.1028 + phi_rad: 1.5707963267948966 + theta_rad: 0.8543386688512243 + } + gain_value { + gain_db: -4.9467 + phi_rad: 1.5707963267948966 + theta_rad: 0.8552113334772214 + } + gain_value { + gain_db: -4.8752 + phi_rad: 1.5707963267948966 + theta_rad: 0.8560839981032186 + } + gain_value { + gain_db: -4.683 + phi_rad: 1.5707963267948966 + theta_rad: 0.8569566627292158 + } + gain_value { + gain_db: -4.6635 + phi_rad: 1.5707963267948966 + theta_rad: 0.857829327355213 + } + gain_value { + gain_db: -4.68 + phi_rad: 1.5707963267948966 + theta_rad: 0.8587019919812102 + } + gain_value { + gain_db: -4.6426 + phi_rad: 1.5707963267948966 + theta_rad: 0.8595746566072073 + } + gain_value { + gain_db: -4.7196 + phi_rad: 1.5707963267948966 + theta_rad: 0.8604473212332044 + } + gain_value { + gain_db: -4.6848 + phi_rad: 1.5707963267948966 + theta_rad: 0.8613199858592017 + } + gain_value { + gain_db: -4.6505 + phi_rad: 1.5707963267948966 + theta_rad: 0.8621926504851988 + } + gain_value { + gain_db: -4.8184 + phi_rad: 1.5707963267948966 + theta_rad: 0.863065315111196 + } + gain_value { + gain_db: -5.0737 + phi_rad: 1.5707963267948966 + theta_rad: 0.8639379797371931 + } + gain_value { + gain_db: -4.9528 + phi_rad: 1.5707963267948966 + theta_rad: 0.8648106443631902 + } + gain_value { + gain_db: -4.7685 + phi_rad: 1.5707963267948966 + theta_rad: 0.8656833089891874 + } + gain_value { + gain_db: -5.0519 + phi_rad: 1.5707963267948966 + theta_rad: 0.8665559736151845 + } + gain_value { + gain_db: -5.2409 + phi_rad: 1.5707963267948966 + theta_rad: 0.8674286382411819 + } + gain_value { + gain_db: -4.8931 + phi_rad: 1.5707963267948966 + theta_rad: 0.868301302867179 + } + gain_value { + gain_db: -4.558 + phi_rad: 1.5707963267948966 + theta_rad: 0.8691739674931761 + } + gain_value { + gain_db: -4.5142 + phi_rad: 1.5707963267948966 + theta_rad: 0.8700466321191733 + } + gain_value { + gain_db: -4.678 + phi_rad: 1.5707963267948966 + theta_rad: 0.8709192967451704 + } + gain_value { + gain_db: -4.8486 + phi_rad: 1.5707963267948966 + theta_rad: 0.8717919613711677 + } + gain_value { + gain_db: -5.2043 + phi_rad: 1.5707963267948966 + theta_rad: 0.8726646259971648 + } + gain_value { + gain_db: -6.121 + phi_rad: 1.5707963267948966 + theta_rad: 0.8735372906231619 + } + gain_value { + gain_db: -7.2043 + phi_rad: 1.5707963267948966 + theta_rad: 0.8744099552491591 + } + gain_value { + gain_db: -7.698 + phi_rad: 1.5707963267948966 + theta_rad: 0.8752826198751562 + } + gain_value { + gain_db: -7.6981 + phi_rad: 1.5707963267948966 + theta_rad: 0.8761552845011534 + } + gain_value { + gain_db: -7.8762 + phi_rad: 1.5707963267948966 + theta_rad: 0.8770279491271507 + } + gain_value { + gain_db: -7.9715 + phi_rad: 1.5707963267948966 + theta_rad: 0.8779006137531478 + } + gain_value { + gain_db: -7.7881 + phi_rad: 1.5707963267948966 + theta_rad: 0.878773278379145 + } + gain_value { + gain_db: -7.645 + phi_rad: 1.5707963267948966 + theta_rad: 0.8796459430051421 + } + gain_value { + gain_db: -7.5585 + phi_rad: 1.5707963267948966 + theta_rad: 0.8805186076311393 + } + gain_value { + gain_db: -7.5745 + phi_rad: 1.5707963267948966 + theta_rad: 0.8813912722571364 + } + gain_value { + gain_db: -7.9232 + phi_rad: 1.5707963267948966 + theta_rad: 0.8822639368831335 + } + gain_value { + gain_db: -8.7012 + phi_rad: 1.5707963267948966 + theta_rad: 0.8831366015091308 + } + gain_value { + gain_db: -9.6653 + phi_rad: 1.5707963267948966 + theta_rad: 0.8840092661351279 + } + gain_value { + gain_db: -11.548 + phi_rad: 1.5707963267948966 + theta_rad: 0.8848819307611251 + } + gain_value { + gain_db: -11.686 + phi_rad: 1.5707963267948966 + theta_rad: 0.8857545953871222 + } + gain_value { + gain_db: -11.16 + phi_rad: 1.5707963267948966 + theta_rad: 0.8866272600131193 + } + gain_value { + gain_db: -11.162 + phi_rad: 1.5707963267948966 + theta_rad: 0.8874999246391166 + } + gain_value { + gain_db: -11.241 + phi_rad: 1.5707963267948966 + theta_rad: 0.8883725892651138 + } + gain_value { + gain_db: -11.268 + phi_rad: 1.5707963267948966 + theta_rad: 0.889245253891111 + } + gain_value { + gain_db: -11.645 + phi_rad: 1.5707963267948966 + theta_rad: 0.8901179185171081 + } + gain_value { + gain_db: -12.089 + phi_rad: 1.5707963267948966 + theta_rad: 0.8909905831431052 + } + gain_value { + gain_db: -12.147 + phi_rad: 1.5707963267948966 + theta_rad: 0.8918632477691024 + } + gain_value { + gain_db: -12.389 + phi_rad: 1.5707963267948966 + theta_rad: 0.8927359123950995 + } + gain_value { + gain_db: -13.596 + phi_rad: 1.5707963267948966 + theta_rad: 0.8936085770210968 + } + gain_value { + gain_db: -13.013 + phi_rad: 1.5707963267948966 + theta_rad: 0.8944812416470939 + } + gain_value { + gain_db: -11.416 + phi_rad: 1.5707963267948966 + theta_rad: 0.895353906273091 + } + gain_value { + gain_db: -10.495 + phi_rad: 1.5707963267948966 + theta_rad: 0.8962265708990882 + } + gain_value { + gain_db: -10.132 + phi_rad: 1.5707963267948966 + theta_rad: 0.8970992355250854 + } + gain_value { + gain_db: -10.23 + phi_rad: 1.5707963267948966 + theta_rad: 0.8979719001510826 + } + gain_value { + gain_db: -10.537 + phi_rad: 1.5707963267948966 + theta_rad: 0.8988445647770797 + } + gain_value { + gain_db: -10.984 + phi_rad: 1.5707963267948966 + theta_rad: 0.8997172294030769 + } + gain_value { + gain_db: -11.145 + phi_rad: 1.5707963267948966 + theta_rad: 0.9005898940290741 + } + gain_value { + gain_db: -11.427 + phi_rad: 1.5707963267948966 + theta_rad: 0.9014625586550712 + } + gain_value { + gain_db: -11.334 + phi_rad: 1.5707963267948966 + theta_rad: 0.9023352232810684 + } + gain_value { + gain_db: -10.226 + phi_rad: 1.5707963267948966 + theta_rad: 0.9032078879070655 + } + gain_value { + gain_db: -9.5809 + phi_rad: 1.5707963267948966 + theta_rad: 0.9040805525330626 + } + gain_value { + gain_db: -9.833 + phi_rad: 1.5707963267948966 + theta_rad: 0.9049532171590599 + } + gain_value { + gain_db: -10.774 + phi_rad: 1.5707963267948966 + theta_rad: 0.905825881785057 + } + gain_value { + gain_db: -11.424 + phi_rad: 1.5707963267948966 + theta_rad: 0.9066985464110543 + } + gain_value { + gain_db: -10.482 + phi_rad: 1.5707963267948966 + theta_rad: 0.9075712110370514 + } + gain_value { + gain_db: -10.129 + phi_rad: 1.5707963267948966 + theta_rad: 0.9084438756630485 + } + gain_value { + gain_db: -10.203 + phi_rad: 1.5707963267948966 + theta_rad: 0.9093165402890457 + } + gain_value { + gain_db: -9.2964 + phi_rad: 1.5707963267948966 + theta_rad: 0.9101892049150428 + } + gain_value { + gain_db: -8.712 + phi_rad: 1.5707963267948966 + theta_rad: 0.9110618695410401 + } + gain_value { + gain_db: -8.9431 + phi_rad: 1.5707963267948966 + theta_rad: 0.9119345341670372 + } + gain_value { + gain_db: -8.4771 + phi_rad: 1.5707963267948966 + theta_rad: 0.9128071987930343 + } + gain_value { + gain_db: -7.2735 + phi_rad: 1.5707963267948966 + theta_rad: 0.9136798634190315 + } + gain_value { + gain_db: -7.0046 + phi_rad: 1.5707963267948966 + theta_rad: 0.9145525280450286 + } + gain_value { + gain_db: -7.4855 + phi_rad: 1.5707963267948966 + theta_rad: 0.9154251926710258 + } + gain_value { + gain_db: -7.1871 + phi_rad: 1.5707963267948966 + theta_rad: 0.9162978572970231 + } + gain_value { + gain_db: -6.8875 + phi_rad: 1.5707963267948966 + theta_rad: 0.9171705219230202 + } + gain_value { + gain_db: -7.2993 + phi_rad: 1.5707963267948966 + theta_rad: 0.9180431865490174 + } + gain_value { + gain_db: -7.7719 + phi_rad: 1.5707963267948966 + theta_rad: 0.9189158511750145 + } + gain_value { + gain_db: -7.3346 + phi_rad: 1.5707963267948966 + theta_rad: 0.9197885158010117 + } + gain_value { + gain_db: -6.8575 + phi_rad: 1.5707963267948966 + theta_rad: 0.9206611804270088 + } + gain_value { + gain_db: -6.8745 + phi_rad: 1.5707963267948966 + theta_rad: 0.921533845053006 + } + gain_value { + gain_db: -6.8276 + phi_rad: 1.5707963267948966 + theta_rad: 0.9224065096790032 + } + gain_value { + gain_db: -6.4367 + phi_rad: 1.5707963267948966 + theta_rad: 0.9232791743050003 + } + gain_value { + gain_db: -6.3381 + phi_rad: 1.5707963267948966 + theta_rad: 0.9241518389309975 + } + gain_value { + gain_db: -6.3441 + phi_rad: 1.5707963267948966 + theta_rad: 0.9250245035569946 + } + gain_value { + gain_db: -6.1701 + phi_rad: 1.5707963267948966 + theta_rad: 0.9258971681829917 + } + gain_value { + gain_db: -6.5009 + phi_rad: 1.5707963267948966 + theta_rad: 0.9267698328089891 + } + gain_value { + gain_db: -7.2565 + phi_rad: 1.5707963267948966 + theta_rad: 0.9276424974349862 + } + gain_value { + gain_db: -7.6414 + phi_rad: 1.5707963267948966 + theta_rad: 0.9285151620609834 + } + gain_value { + gain_db: -7.822 + phi_rad: 1.5707963267948966 + theta_rad: 0.9293878266869805 + } + gain_value { + gain_db: -8.0178 + phi_rad: 1.5707963267948966 + theta_rad: 0.9302604913129776 + } + gain_value { + gain_db: -8.1285 + phi_rad: 1.5707963267948966 + theta_rad: 0.9311331559389748 + } + gain_value { + gain_db: -8.402 + phi_rad: 1.5707963267948966 + theta_rad: 0.9320058205649719 + } + gain_value { + gain_db: -8.603 + phi_rad: 1.5707963267948966 + theta_rad: 0.9328784851909692 + } + gain_value { + gain_db: -8.1995 + phi_rad: 1.5707963267948966 + theta_rad: 0.9337511498169663 + } + gain_value { + gain_db: -7.7847 + phi_rad: 1.5707963267948966 + theta_rad: 0.9346238144429634 + } + gain_value { + gain_db: -7.9465 + phi_rad: 1.5707963267948966 + theta_rad: 0.9354964790689606 + } + gain_value { + gain_db: -8.6593 + phi_rad: 1.5707963267948966 + theta_rad: 0.9363691436949578 + } + gain_value { + gain_db: -9.3702 + phi_rad: 1.5707963267948966 + theta_rad: 0.937241808320955 + } + gain_value { + gain_db: -10.036 + phi_rad: 1.5707963267948966 + theta_rad: 0.9381144729469522 + } + gain_value { + gain_db: -10.945 + phi_rad: 1.5707963267948966 + theta_rad: 0.9389871375729493 + } + gain_value { + gain_db: -11.977 + phi_rad: 1.5707963267948966 + theta_rad: 0.9398598021989465 + } + gain_value { + gain_db: -12.676 + phi_rad: 1.5707963267948966 + theta_rad: 0.9407324668249436 + } + gain_value { + gain_db: -13.262 + phi_rad: 1.5707963267948966 + theta_rad: 0.9416051314509408 + } + gain_value { + gain_db: -13.411 + phi_rad: 1.5707963267948966 + theta_rad: 0.9424777960769379 + } + gain_value { + gain_db: -12.649 + phi_rad: 1.5707963267948966 + theta_rad: 0.943350460702935 + } + gain_value { + gain_db: -11.952 + phi_rad: 1.5707963267948966 + theta_rad: 0.9442231253289323 + } + gain_value { + gain_db: -11.718 + phi_rad: 1.5707963267948966 + theta_rad: 0.9450957899549294 + } + gain_value { + gain_db: -11.643 + phi_rad: 1.5707963267948966 + theta_rad: 0.9459684545809267 + } + gain_value { + gain_db: -11.414 + phi_rad: 1.5707963267948966 + theta_rad: 0.9468411192069238 + } + gain_value { + gain_db: -11.107 + phi_rad: 1.5707963267948966 + theta_rad: 0.9477137838329209 + } + gain_value { + gain_db: -11.044 + phi_rad: 1.5707963267948966 + theta_rad: 0.9485864484589182 + } + gain_value { + gain_db: -11.377 + phi_rad: 1.5707963267948966 + theta_rad: 0.9494591130849153 + } + gain_value { + gain_db: -12.077 + phi_rad: 1.5707963267948966 + theta_rad: 0.9503317777109125 + } + gain_value { + gain_db: -12.52 + phi_rad: 1.5707963267948966 + theta_rad: 0.9512044423369096 + } + gain_value { + gain_db: -12.356 + phi_rad: 1.5707963267948966 + theta_rad: 0.9520771069629067 + } + gain_value { + gain_db: -11.989 + phi_rad: 1.5707963267948966 + theta_rad: 0.9529497715889039 + } + gain_value { + gain_db: -11.712 + phi_rad: 1.5707963267948966 + theta_rad: 0.953822436214901 + } + gain_value { + gain_db: -11.235 + phi_rad: 1.5707963267948966 + theta_rad: 0.9546951008408983 + } + gain_value { + gain_db: -10.851 + phi_rad: 1.5707963267948966 + theta_rad: 0.9555677654668955 + } + gain_value { + gain_db: -10.586 + phi_rad: 1.5707963267948966 + theta_rad: 0.9564404300928926 + } + gain_value { + gain_db: -10.749 + phi_rad: 1.5707963267948966 + theta_rad: 0.9573130947188898 + } + gain_value { + gain_db: -10.942 + phi_rad: 1.5707963267948966 + theta_rad: 0.9581857593448869 + } + gain_value { + gain_db: -11.009 + phi_rad: 1.5707963267948966 + theta_rad: 0.9590584239708841 + } + gain_value { + gain_db: -11.041 + phi_rad: 1.5707963267948966 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -11.083 + phi_rad: 1.5707963267948966 + theta_rad: 0.9608037532228784 + } + gain_value { + gain_db: -11.064 + phi_rad: 1.5707963267948966 + theta_rad: 0.9616764178488756 + } + gain_value { + gain_db: -10.932 + phi_rad: 1.5707963267948966 + theta_rad: 0.9625490824748727 + } + gain_value { + gain_db: -10.832 + phi_rad: 1.5707963267948966 + theta_rad: 0.9634217471008699 + } + gain_value { + gain_db: -10.747 + phi_rad: 1.5707963267948966 + theta_rad: 0.964294411726867 + } + gain_value { + gain_db: -10.463 + phi_rad: 1.5707963267948966 + theta_rad: 0.9651670763528641 + } + gain_value { + gain_db: -10.152 + phi_rad: 1.5707963267948966 + theta_rad: 0.9660397409788615 + } + gain_value { + gain_db: -9.8998 + phi_rad: 1.5707963267948966 + theta_rad: 0.9669124056048586 + } + gain_value { + gain_db: -9.6928 + phi_rad: 1.5707963267948966 + theta_rad: 0.9677850702308558 + } + gain_value { + gain_db: -9.3354 + phi_rad: 1.5707963267948966 + theta_rad: 0.9686577348568529 + } + gain_value { + gain_db: -9.1892 + phi_rad: 1.5707963267948966 + theta_rad: 0.96953039948285 + } + gain_value { + gain_db: -8.9916 + phi_rad: 1.5707963267948966 + theta_rad: 0.9704030641088472 + } + gain_value { + gain_db: -8.7448 + phi_rad: 1.5707963267948966 + theta_rad: 0.9712757287348444 + } + gain_value { + gain_db: -8.9554 + phi_rad: 1.5707963267948966 + theta_rad: 0.9721483933608416 + } + gain_value { + gain_db: -9.1971 + phi_rad: 1.5707963267948966 + theta_rad: 0.9730210579868387 + } + gain_value { + gain_db: -9.0938 + phi_rad: 1.5707963267948966 + theta_rad: 0.9738937226128358 + } + gain_value { + gain_db: -8.9533 + phi_rad: 1.5707963267948966 + theta_rad: 0.9747663872388331 + } + gain_value { + gain_db: -8.7321 + phi_rad: 1.5707963267948966 + theta_rad: 0.9756390518648302 + } + gain_value { + gain_db: -8.3648 + phi_rad: 1.5707963267948966 + theta_rad: 0.9765117164908275 + } + gain_value { + gain_db: -7.9268 + phi_rad: 1.5707963267948966 + theta_rad: 0.9773843811168246 + } + gain_value { + gain_db: -7.5717 + phi_rad: 1.5707963267948966 + theta_rad: 0.9782570457428217 + } + gain_value { + gain_db: -7.2996 + phi_rad: 1.5707963267948966 + theta_rad: 0.9791297103688189 + } + gain_value { + gain_db: -6.9758 + phi_rad: 1.5707963267948966 + theta_rad: 0.980002374994816 + } + gain_value { + gain_db: -6.7872 + phi_rad: 1.5707963267948966 + theta_rad: 0.9808750396208132 + } + gain_value { + gain_db: -6.805 + phi_rad: 1.5707963267948966 + theta_rad: 0.9817477042468103 + } + gain_value { + gain_db: -6.9753 + phi_rad: 1.5707963267948966 + theta_rad: 0.9826203688728075 + } + gain_value { + gain_db: -7.0428 + phi_rad: 1.5707963267948966 + theta_rad: 0.9834930334988047 + } + gain_value { + gain_db: -6.8868 + phi_rad: 1.5707963267948966 + theta_rad: 0.9843656981248018 + } + gain_value { + gain_db: -6.8271 + phi_rad: 1.5707963267948966 + theta_rad: 0.9852383627507991 + } + gain_value { + gain_db: -6.9439 + phi_rad: 1.5707963267948966 + theta_rad: 0.9861110273767962 + } + gain_value { + gain_db: -6.9536 + phi_rad: 1.5707963267948966 + theta_rad: 0.9869836920027933 + } + gain_value { + gain_db: -6.9884 + phi_rad: 1.5707963267948966 + theta_rad: 0.9878563566287906 + } + gain_value { + gain_db: -7.0701 + phi_rad: 1.5707963267948966 + theta_rad: 0.9887290212547877 + } + gain_value { + gain_db: -6.928 + phi_rad: 1.5707963267948966 + theta_rad: 0.9896016858807849 + } + gain_value { + gain_db: -6.8507 + phi_rad: 1.5707963267948966 + theta_rad: 0.990474350506782 + } + gain_value { + gain_db: -6.627 + phi_rad: 1.5707963267948966 + theta_rad: 0.9913470151327791 + } + gain_value { + gain_db: -6.5854 + phi_rad: 1.5707963267948966 + theta_rad: 0.9922196797587763 + } + gain_value { + gain_db: -6.7283 + phi_rad: 1.5707963267948966 + theta_rad: 0.9930923443847735 + } + gain_value { + gain_db: -6.9793 + phi_rad: 1.5707963267948966 + theta_rad: 0.9939650090107707 + } + gain_value { + gain_db: -6.9738 + phi_rad: 1.5707963267948966 + theta_rad: 0.9948376736367679 + } + gain_value { + gain_db: -7.0303 + phi_rad: 1.5707963267948966 + theta_rad: 0.995710338262765 + } + gain_value { + gain_db: -7.2854 + phi_rad: 1.5707963267948966 + theta_rad: 0.9965830028887622 + } + gain_value { + gain_db: -7.4472 + phi_rad: 1.5707963267948966 + theta_rad: 0.9974556675147593 + } + gain_value { + gain_db: -7.539 + phi_rad: 1.5707963267948966 + theta_rad: 0.9983283321407566 + } + gain_value { + gain_db: -7.8097 + phi_rad: 1.5707963267948966 + theta_rad: 0.9992009967667537 + } + gain_value { + gain_db: -8.2805 + phi_rad: 1.5707963267948966 + theta_rad: 1.0000736613927508 + } + gain_value { + gain_db: -8.4783 + phi_rad: 1.5707963267948966 + theta_rad: 1.0009463260187481 + } + gain_value { + gain_db: -8.5183 + phi_rad: 1.5707963267948966 + theta_rad: 1.0018189906447452 + } + gain_value { + gain_db: -8.7098 + phi_rad: 1.5707963267948966 + theta_rad: 1.0026916552707423 + } + gain_value { + gain_db: -8.9769 + phi_rad: 1.5707963267948966 + theta_rad: 1.0035643198967394 + } + gain_value { + gain_db: -8.7345 + phi_rad: 1.5707963267948966 + theta_rad: 1.0044369845227366 + } + gain_value { + gain_db: -8.4923 + phi_rad: 1.5707963267948966 + theta_rad: 1.0053096491487339 + } + gain_value { + gain_db: -8.4443 + phi_rad: 1.5707963267948966 + theta_rad: 1.006182313774731 + } + gain_value { + gain_db: -8.2974 + phi_rad: 1.5707963267948966 + theta_rad: 1.007054978400728 + } + gain_value { + gain_db: -8.2116 + phi_rad: 1.5707963267948966 + theta_rad: 1.0079276430267252 + } + gain_value { + gain_db: -8.3723 + phi_rad: 1.5707963267948966 + theta_rad: 1.0088003076527223 + } + gain_value { + gain_db: -8.7429 + phi_rad: 1.5707963267948966 + theta_rad: 1.0096729722787197 + } + gain_value { + gain_db: -9.3861 + phi_rad: 1.5707963267948966 + theta_rad: 1.0105456369047168 + } + gain_value { + gain_db: -10.038 + phi_rad: 1.5707963267948966 + theta_rad: 1.011418301530714 + } + gain_value { + gain_db: -10.884 + phi_rad: 1.5707963267948966 + theta_rad: 1.0122909661567112 + } + gain_value { + gain_db: -11.579 + phi_rad: 1.5707963267948966 + theta_rad: 1.0131636307827083 + } + gain_value { + gain_db: -11.974 + phi_rad: 1.5707963267948966 + theta_rad: 1.0140362954087054 + } + gain_value { + gain_db: -12.061 + phi_rad: 1.5707963267948966 + theta_rad: 1.0149089600347025 + } + gain_value { + gain_db: -12.359 + phi_rad: 1.5707963267948966 + theta_rad: 1.0157816246606999 + } + gain_value { + gain_db: -12.444 + phi_rad: 1.5707963267948966 + theta_rad: 1.016654289286697 + } + gain_value { + gain_db: -11.874 + phi_rad: 1.5707963267948966 + theta_rad: 1.017526953912694 + } + gain_value { + gain_db: -11.574 + phi_rad: 1.5707963267948966 + theta_rad: 1.0183996185386912 + } + gain_value { + gain_db: -11.581 + phi_rad: 1.5707963267948966 + theta_rad: 1.0192722831646885 + } + gain_value { + gain_db: -12.374 + phi_rad: 1.5707963267948966 + theta_rad: 1.0201449477906857 + } + gain_value { + gain_db: -13.403 + phi_rad: 1.5707963267948966 + theta_rad: 1.0210176124166828 + } + gain_value { + gain_db: -13.675 + phi_rad: 1.5707963267948966 + theta_rad: 1.0218902770426799 + } + gain_value { + gain_db: -12.707 + phi_rad: 1.5707963267948966 + theta_rad: 1.0227629416686772 + } + gain_value { + gain_db: -12.669 + phi_rad: 1.5707963267948966 + theta_rad: 1.0236356062946743 + } + gain_value { + gain_db: -12.329 + phi_rad: 1.5707963267948966 + theta_rad: 1.0245082709206714 + } + gain_value { + gain_db: -12.009 + phi_rad: 1.5707963267948966 + theta_rad: 1.0253809355466685 + } + gain_value { + gain_db: -12.033 + phi_rad: 1.5707963267948966 + theta_rad: 1.0262536001726656 + } + gain_value { + gain_db: -12.125 + phi_rad: 1.5707963267948966 + theta_rad: 1.027126264798663 + } + gain_value { + gain_db: -12.238 + phi_rad: 1.5707963267948966 + theta_rad: 1.02799892942466 + } + gain_value { + gain_db: -11.512 + phi_rad: 1.5707963267948966 + theta_rad: 1.0288715940506574 + } + gain_value { + gain_db: -10.73 + phi_rad: 1.5707963267948966 + theta_rad: 1.0297442586766545 + } + gain_value { + gain_db: -10.275 + phi_rad: 1.5707963267948966 + theta_rad: 1.0306169233026516 + } + gain_value { + gain_db: -9.9805 + phi_rad: 1.5707963267948966 + theta_rad: 1.0314895879286488 + } + gain_value { + gain_db: -9.9853 + phi_rad: 1.5707963267948966 + theta_rad: 1.0323622525546459 + } + gain_value { + gain_db: -10.526 + phi_rad: 1.5707963267948966 + theta_rad: 1.0332349171806432 + } + gain_value { + gain_db: -11.338 + phi_rad: 1.5707963267948966 + theta_rad: 1.0341075818066403 + } + gain_value { + gain_db: -11.417 + phi_rad: 1.5707963267948966 + theta_rad: 1.0349802464326374 + } + gain_value { + gain_db: -11.845 + phi_rad: 1.5707963267948966 + theta_rad: 1.0358529110586345 + } + gain_value { + gain_db: -12.049 + phi_rad: 1.5707963267948966 + theta_rad: 1.0367255756846316 + } + gain_value { + gain_db: -12.117 + phi_rad: 1.5707963267948966 + theta_rad: 1.037598240310629 + } + gain_value { + gain_db: -12.91 + phi_rad: 1.5707963267948966 + theta_rad: 1.038470904936626 + } + gain_value { + gain_db: -14.699 + phi_rad: 1.5707963267948966 + theta_rad: 1.0393435695626232 + } + gain_value { + gain_db: -14.071 + phi_rad: 1.5707963267948966 + theta_rad: 1.0402162341886205 + } + gain_value { + gain_db: -12.334 + phi_rad: 1.5707963267948966 + theta_rad: 1.0410888988146176 + } + gain_value { + gain_db: -11.187 + phi_rad: 1.5707963267948966 + theta_rad: 1.0419615634406147 + } + gain_value { + gain_db: -10.481 + phi_rad: 1.5707963267948966 + theta_rad: 1.0428342280666119 + } + gain_value { + gain_db: -9.9034 + phi_rad: 1.5707963267948966 + theta_rad: 1.043706892692609 + } + gain_value { + gain_db: -9.5862 + phi_rad: 1.5707963267948966 + theta_rad: 1.0445795573186063 + } + gain_value { + gain_db: -9.5701 + phi_rad: 1.5707963267948966 + theta_rad: 1.0454522219446034 + } + gain_value { + gain_db: -9.8033 + phi_rad: 1.5707963267948966 + theta_rad: 1.0463248865706005 + } + gain_value { + gain_db: -10.085 + phi_rad: 1.5707963267948966 + theta_rad: 1.0471975511965976 + } + gain_value { + gain_db: -10.496 + phi_rad: 1.5707963267948966 + theta_rad: 1.0480702158225947 + } + gain_value { + gain_db: -11.116 + phi_rad: 1.5707963267948966 + theta_rad: 1.048942880448592 + } + gain_value { + gain_db: -11.546 + phi_rad: 1.5707963267948966 + theta_rad: 1.0498155450745892 + } + gain_value { + gain_db: -12.039 + phi_rad: 1.5707963267948966 + theta_rad: 1.0506882097005865 + } + gain_value { + gain_db: -12.311 + phi_rad: 1.5707963267948966 + theta_rad: 1.0515608743265836 + } + gain_value { + gain_db: -11.989 + phi_rad: 1.5707963267948966 + theta_rad: 1.0524335389525807 + } + gain_value { + gain_db: -11.184 + phi_rad: 1.5707963267948966 + theta_rad: 1.0533062035785778 + } + gain_value { + gain_db: -10.504 + phi_rad: 1.5707963267948966 + theta_rad: 1.054178868204575 + } + gain_value { + gain_db: -10.015 + phi_rad: 1.5707963267948966 + theta_rad: 1.0550515328305723 + } + gain_value { + gain_db: -9.8632 + phi_rad: 1.5707963267948966 + theta_rad: 1.0559241974565694 + } + gain_value { + gain_db: -9.6565 + phi_rad: 1.5707963267948966 + theta_rad: 1.0567968620825665 + } + gain_value { + gain_db: -9.6832 + phi_rad: 1.5707963267948966 + theta_rad: 1.0576695267085636 + } + gain_value { + gain_db: -9.9842 + phi_rad: 1.5707963267948966 + theta_rad: 1.058542191334561 + } + gain_value { + gain_db: -10.381 + phi_rad: 1.5707963267948966 + theta_rad: 1.059414855960558 + } + gain_value { + gain_db: -11.051 + phi_rad: 1.5707963267948966 + theta_rad: 1.0602875205865552 + } + gain_value { + gain_db: -12.078 + phi_rad: 1.5707963267948966 + theta_rad: 1.0611601852125523 + } + gain_value { + gain_db: -13.146 + phi_rad: 1.5707963267948966 + theta_rad: 1.0620328498385496 + } + gain_value { + gain_db: -14.413 + phi_rad: 1.5707963267948966 + theta_rad: 1.0629055144645467 + } + gain_value { + gain_db: -13.825 + phi_rad: 1.5707963267948966 + theta_rad: 1.0637781790905438 + } + gain_value { + gain_db: -12.627 + phi_rad: 1.5707963267948966 + theta_rad: 1.064650843716541 + } + gain_value { + gain_db: -12.017 + phi_rad: 1.5707963267948966 + theta_rad: 1.065523508342538 + } + gain_value { + gain_db: -11.893 + phi_rad: 1.5707963267948966 + theta_rad: 1.0663961729685354 + } + gain_value { + gain_db: -12.255 + phi_rad: 1.5707963267948966 + theta_rad: 1.0672688375945325 + } + gain_value { + gain_db: -12.966 + phi_rad: 1.5707963267948966 + theta_rad: 1.0681415022205298 + } + gain_value { + gain_db: -13.054 + phi_rad: 1.5707963267948966 + theta_rad: 1.069014166846527 + } + gain_value { + gain_db: -12.842 + phi_rad: 1.5707963267948966 + theta_rad: 1.069886831472524 + } + gain_value { + gain_db: -12.655 + phi_rad: 1.5707963267948966 + theta_rad: 1.0707594960985212 + } + gain_value { + gain_db: -12.877 + phi_rad: 1.5707963267948966 + theta_rad: 1.0716321607245183 + } + gain_value { + gain_db: -13.177 + phi_rad: 1.5707963267948966 + theta_rad: 1.0725048253505156 + } + gain_value { + gain_db: -13.578 + phi_rad: 1.5707963267948966 + theta_rad: 1.0733774899765127 + } + gain_value { + gain_db: -13.676 + phi_rad: 1.5707963267948966 + theta_rad: 1.0742501546025098 + } + gain_value { + gain_db: -14.206 + phi_rad: 1.5707963267948966 + theta_rad: 1.075122819228507 + } + gain_value { + gain_db: -14.55 + phi_rad: 1.5707963267948966 + theta_rad: 1.075995483854504 + } + gain_value { + gain_db: -15.119 + phi_rad: 1.5707963267948966 + theta_rad: 1.0768681484805014 + } + gain_value { + gain_db: -16.314 + phi_rad: 1.5707963267948966 + theta_rad: 1.0777408131064985 + } + gain_value { + gain_db: -18.01 + phi_rad: 1.5707963267948966 + theta_rad: 1.0786134777324956 + } + gain_value { + gain_db: -19.24 + phi_rad: 1.5707963267948966 + theta_rad: 1.079486142358493 + } + gain_value { + gain_db: -19.073 + phi_rad: 1.5707963267948966 + theta_rad: 1.08035880698449 + } + gain_value { + gain_db: -19.485 + phi_rad: 1.5707963267948966 + theta_rad: 1.0812314716104872 + } + gain_value { + gain_db: -19.403 + phi_rad: 1.5707963267948966 + theta_rad: 1.0821041362364843 + } + gain_value { + gain_db: -18.208 + phi_rad: 1.5707963267948966 + theta_rad: 1.0829768008624814 + } + gain_value { + gain_db: -17.696 + phi_rad: 1.5707963267948966 + theta_rad: 1.0838494654884787 + } + gain_value { + gain_db: -17.438 + phi_rad: 1.5707963267948966 + theta_rad: 1.0847221301144758 + } + gain_value { + gain_db: -16.768 + phi_rad: 1.5707963267948966 + theta_rad: 1.085594794740473 + } + gain_value { + gain_db: -16.275 + phi_rad: 1.5707963267948966 + theta_rad: 1.08646745936647 + } + gain_value { + gain_db: -16.527 + phi_rad: 1.5707963267948966 + theta_rad: 1.0873401239924672 + } + gain_value { + gain_db: -17.109 + phi_rad: 1.5707963267948966 + theta_rad: 1.0882127886184645 + } + gain_value { + gain_db: -18.48 + phi_rad: 1.5707963267948966 + theta_rad: 1.0890854532444616 + } + gain_value { + gain_db: -20.142 + phi_rad: 1.5707963267948966 + theta_rad: 1.089958117870459 + } + gain_value { + gain_db: -22.114 + phi_rad: 1.5707963267948966 + theta_rad: 1.090830782496456 + } + gain_value { + gain_db: -23.198 + phi_rad: 1.5707963267948966 + theta_rad: 1.0917034471224532 + } + gain_value { + gain_db: -20.467 + phi_rad: 1.5707963267948966 + theta_rad: 1.0925761117484503 + } + gain_value { + gain_db: -19.06 + phi_rad: 1.5707963267948966 + theta_rad: 1.0934487763744474 + } + gain_value { + gain_db: -17.142 + phi_rad: 1.5707963267948966 + theta_rad: 1.0943214410004447 + } + gain_value { + gain_db: -14.884 + phi_rad: 1.5707963267948966 + theta_rad: 1.0951941056264418 + } + gain_value { + gain_db: -13.191 + phi_rad: 1.5707963267948966 + theta_rad: 1.096066770252439 + } + gain_value { + gain_db: -11.915 + phi_rad: 1.5707963267948966 + theta_rad: 1.096939434878436 + } + gain_value { + gain_db: -11.039 + phi_rad: 1.5707963267948966 + theta_rad: 1.0978120995044334 + } + gain_value { + gain_db: -10.718 + phi_rad: 1.5707963267948966 + theta_rad: 1.0986847641304305 + } + gain_value { + gain_db: -10.945 + phi_rad: 1.5707963267948966 + theta_rad: 1.0995574287564276 + } + gain_value { + gain_db: -11.748 + phi_rad: 1.5707963267948966 + theta_rad: 1.1004300933824247 + } + gain_value { + gain_db: -13.807 + phi_rad: 1.5707963267948966 + theta_rad: 1.101302758008422 + } + gain_value { + gain_db: -15.756 + phi_rad: 1.5707963267948966 + theta_rad: 1.1021754226344191 + } + gain_value { + gain_db: -13.62 + phi_rad: 1.5707963267948966 + theta_rad: 1.1030480872604163 + } + gain_value { + gain_db: -12.741 + phi_rad: 1.5707963267948966 + theta_rad: 1.1039207518864134 + } + gain_value { + gain_db: -12.491 + phi_rad: 1.5707963267948966 + theta_rad: 1.1047934165124105 + } + gain_value { + gain_db: -12.462 + phi_rad: 1.5707963267948966 + theta_rad: 1.1056660811384078 + } + gain_value { + gain_db: -12.292 + phi_rad: 1.5707963267948966 + theta_rad: 1.106538745764405 + } + gain_value { + gain_db: -11.715 + phi_rad: 1.5707963267948966 + theta_rad: 1.1074114103904023 + } + gain_value { + gain_db: -11.266 + phi_rad: 1.5707963267948966 + theta_rad: 1.1082840750163994 + } + gain_value { + gain_db: -11.266 + phi_rad: 1.5707963267948966 + theta_rad: 1.1091567396423965 + } + gain_value { + gain_db: -11.385 + phi_rad: 1.5707963267948966 + theta_rad: 1.1100294042683936 + } + gain_value { + gain_db: -12.143 + phi_rad: 1.5707963267948966 + theta_rad: 1.1109020688943907 + } + gain_value { + gain_db: -13.859 + phi_rad: 1.5707963267948966 + theta_rad: 1.111774733520388 + } + gain_value { + gain_db: -17.317 + phi_rad: 1.5707963267948966 + theta_rad: 1.1126473981463851 + } + gain_value { + gain_db: -13.557 + phi_rad: 1.5707963267948966 + theta_rad: 1.1135200627723822 + } + gain_value { + gain_db: -12.167 + phi_rad: 1.5707963267948966 + theta_rad: 1.1143927273983794 + } + gain_value { + gain_db: -11.69 + phi_rad: 1.5707963267948966 + theta_rad: 1.1152653920243765 + } + gain_value { + gain_db: -11.808 + phi_rad: 1.5707963267948966 + theta_rad: 1.1161380566503738 + } + gain_value { + gain_db: -12.305 + phi_rad: 1.5707963267948966 + theta_rad: 1.117010721276371 + } + gain_value { + gain_db: -13.059 + phi_rad: 1.5707963267948966 + theta_rad: 1.117883385902368 + } + gain_value { + gain_db: -14.246 + phi_rad: 1.5707963267948966 + theta_rad: 1.1187560505283651 + } + gain_value { + gain_db: -16.969 + phi_rad: 1.5707963267948966 + theta_rad: 1.1196287151543625 + } + gain_value { + gain_db: -18.561 + phi_rad: 1.5707963267948966 + theta_rad: 1.1205013797803596 + } + gain_value { + gain_db: -15.94 + phi_rad: 1.5707963267948966 + theta_rad: 1.1213740444063567 + } + gain_value { + gain_db: -15.339 + phi_rad: 1.5707963267948966 + theta_rad: 1.1222467090323538 + } + gain_value { + gain_db: -14.331 + phi_rad: 1.5707963267948966 + theta_rad: 1.123119373658351 + } + gain_value { + gain_db: -12.824 + phi_rad: 1.5707963267948966 + theta_rad: 1.1239920382843482 + } + gain_value { + gain_db: -11.927 + phi_rad: 1.5707963267948966 + theta_rad: 1.1248647029103453 + } + gain_value { + gain_db: -11.697 + phi_rad: 1.5707963267948966 + theta_rad: 1.1257373675363425 + } + gain_value { + gain_db: -11.641 + phi_rad: 1.5707963267948966 + theta_rad: 1.1266100321623396 + } + gain_value { + gain_db: -12.274 + phi_rad: 1.5707963267948966 + theta_rad: 1.1274826967883367 + } + gain_value { + gain_db: -12.639 + phi_rad: 1.5707963267948966 + theta_rad: 1.1283553614143342 + } + gain_value { + gain_db: -12.497 + phi_rad: 1.5707963267948966 + theta_rad: 1.1292280260403313 + } + gain_value { + gain_db: -11.984 + phi_rad: 1.5707963267948966 + theta_rad: 1.1301006906663285 + } + gain_value { + gain_db: -11.768 + phi_rad: 1.5707963267948966 + theta_rad: 1.1309733552923256 + } + gain_value { + gain_db: -11.689 + phi_rad: 1.5707963267948966 + theta_rad: 1.1318460199183227 + } + gain_value { + gain_db: -12.11 + phi_rad: 1.5707963267948966 + theta_rad: 1.13271868454432 + } + gain_value { + gain_db: -12.827 + phi_rad: 1.5707963267948966 + theta_rad: 1.1335913491703171 + } + gain_value { + gain_db: -13.384 + phi_rad: 1.5707963267948966 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -13.479 + phi_rad: 1.5707963267948966 + theta_rad: 1.1353366784223113 + } + gain_value { + gain_db: -13.413 + phi_rad: 1.5707963267948966 + theta_rad: 1.1362093430483085 + } + gain_value { + gain_db: -13.505 + phi_rad: 1.5707963267948966 + theta_rad: 1.1370820076743058 + } + gain_value { + gain_db: -13.864 + phi_rad: 1.5707963267948966 + theta_rad: 1.137954672300303 + } + gain_value { + gain_db: -13.547 + phi_rad: 1.5707963267948966 + theta_rad: 1.1388273369263 + } + gain_value { + gain_db: -12.923 + phi_rad: 1.5707963267948966 + theta_rad: 1.1397000015522971 + } + gain_value { + gain_db: -12.451 + phi_rad: 1.5707963267948966 + theta_rad: 1.1405726661782942 + } + gain_value { + gain_db: -12.234 + phi_rad: 1.5707963267948966 + theta_rad: 1.1414453308042916 + } + gain_value { + gain_db: -12.406 + phi_rad: 1.5707963267948966 + theta_rad: 1.1423179954302887 + } + gain_value { + gain_db: -13.083 + phi_rad: 1.5707963267948966 + theta_rad: 1.1431906600562858 + } + gain_value { + gain_db: -14.443 + phi_rad: 1.5707963267948966 + theta_rad: 1.144063324682283 + } + gain_value { + gain_db: -16.439 + phi_rad: 1.5707963267948966 + theta_rad: 1.14493598930828 + } + gain_value { + gain_db: -19.285 + phi_rad: 1.5707963267948966 + theta_rad: 1.1458086539342776 + } + gain_value { + gain_db: -21.528 + phi_rad: 1.5707963267948966 + theta_rad: 1.1466813185602747 + } + gain_value { + gain_db: -19.657 + phi_rad: 1.5707963267948966 + theta_rad: 1.1475539831862718 + } + gain_value { + gain_db: -16.235 + phi_rad: 1.5707963267948966 + theta_rad: 1.1484266478122689 + } + gain_value { + gain_db: -14.295 + phi_rad: 1.5707963267948966 + theta_rad: 1.149299312438266 + } + gain_value { + gain_db: -12.944 + phi_rad: 1.5707963267948966 + theta_rad: 1.1501719770642633 + } + gain_value { + gain_db: -11.808 + phi_rad: 1.5707963267948966 + theta_rad: 1.1510446416902604 + } + gain_value { + gain_db: -11.14 + phi_rad: 1.5707963267948966 + theta_rad: 1.1519173063162575 + } + gain_value { + gain_db: -10.689 + phi_rad: 1.5707963267948966 + theta_rad: 1.1527899709422547 + } + gain_value { + gain_db: -10.5 + phi_rad: 1.5707963267948966 + theta_rad: 1.1536626355682518 + } + gain_value { + gain_db: -10.637 + phi_rad: 1.5707963267948966 + theta_rad: 1.154535300194249 + } + gain_value { + gain_db: -10.961 + phi_rad: 1.5707963267948966 + theta_rad: 1.1554079648202462 + } + gain_value { + gain_db: -11.636 + phi_rad: 1.5707963267948966 + theta_rad: 1.1562806294462433 + } + gain_value { + gain_db: -12.68 + phi_rad: 1.5707963267948966 + theta_rad: 1.1571532940722404 + } + gain_value { + gain_db: -13.891 + phi_rad: 1.5707963267948966 + theta_rad: 1.1580259586982375 + } + gain_value { + gain_db: -15.107 + phi_rad: 1.5707963267948966 + theta_rad: 1.1588986233242349 + } + gain_value { + gain_db: -15.838 + phi_rad: 1.5707963267948966 + theta_rad: 1.159771287950232 + } + gain_value { + gain_db: -15.496 + phi_rad: 1.5707963267948966 + theta_rad: 1.160643952576229 + } + gain_value { + gain_db: -14.632 + phi_rad: 1.5707963267948966 + theta_rad: 1.1615166172022262 + } + gain_value { + gain_db: -14.139 + phi_rad: 1.5707963267948966 + theta_rad: 1.1623892818282233 + } + gain_value { + gain_db: -13.938 + phi_rad: 1.5707963267948966 + theta_rad: 1.1632619464542207 + } + gain_value { + gain_db: -14.458 + phi_rad: 1.5707963267948966 + theta_rad: 1.1641346110802178 + } + gain_value { + gain_db: -15.015 + phi_rad: 1.5707963267948966 + theta_rad: 1.1650072757062149 + } + gain_value { + gain_db: -13.397 + phi_rad: 1.5707963267948966 + theta_rad: 1.165879940332212 + } + gain_value { + gain_db: -12.202 + phi_rad: 1.5707963267948966 + theta_rad: 1.166752604958209 + } + gain_value { + gain_db: -11.768 + phi_rad: 1.5707963267948966 + theta_rad: 1.1676252695842066 + } + gain_value { + gain_db: -11.874 + phi_rad: 1.5707963267948966 + theta_rad: 1.1684979342102038 + } + gain_value { + gain_db: -12.224 + phi_rad: 1.5707963267948966 + theta_rad: 1.1693705988362009 + } + gain_value { + gain_db: -13.12 + phi_rad: 1.5707963267948966 + theta_rad: 1.170243263462198 + } + gain_value { + gain_db: -13.512 + phi_rad: 1.5707963267948966 + theta_rad: 1.171115928088195 + } + gain_value { + gain_db: -13.773 + phi_rad: 1.5707963267948966 + theta_rad: 1.1719885927141924 + } + gain_value { + gain_db: -15.562 + phi_rad: 1.5707963267948966 + theta_rad: 1.1728612573401895 + } + gain_value { + gain_db: -15.775 + phi_rad: 1.5707963267948966 + theta_rad: 1.1737339219661866 + } + gain_value { + gain_db: -11.988 + phi_rad: 1.5707963267948966 + theta_rad: 1.1746065865921838 + } + gain_value { + gain_db: -10.504 + phi_rad: 1.5707963267948966 + theta_rad: 1.1754792512181809 + } + gain_value { + gain_db: -9.9693 + phi_rad: 1.5707963267948966 + theta_rad: 1.1763519158441782 + } + gain_value { + gain_db: -10.072 + phi_rad: 1.5707963267948966 + theta_rad: 1.1772245804701753 + } + gain_value { + gain_db: -10.611 + phi_rad: 1.5707963267948966 + theta_rad: 1.1780972450961724 + } + gain_value { + gain_db: -11.687 + phi_rad: 1.5707963267948966 + theta_rad: 1.1789699097221695 + } + gain_value { + gain_db: -13.586 + phi_rad: 1.5707963267948966 + theta_rad: 1.1798425743481666 + } + gain_value { + gain_db: -17.475 + phi_rad: 1.5707963267948966 + theta_rad: 1.180715238974164 + } + gain_value { + gain_db: -16.957 + phi_rad: 1.5707963267948966 + theta_rad: 1.181587903600161 + } + gain_value { + gain_db: -14.225 + phi_rad: 1.5707963267948966 + theta_rad: 1.1824605682261582 + } + gain_value { + gain_db: -13.063 + phi_rad: 1.5707963267948966 + theta_rad: 1.1833332328521553 + } + gain_value { + gain_db: -12.863 + phi_rad: 1.5707963267948966 + theta_rad: 1.1842058974781524 + } + gain_value { + gain_db: -13.376 + phi_rad: 1.5707963267948966 + theta_rad: 1.18507856210415 + } + gain_value { + gain_db: -15.037 + phi_rad: 1.5707963267948966 + theta_rad: 1.185951226730147 + } + gain_value { + gain_db: -16.762 + phi_rad: 1.5707963267948966 + theta_rad: 1.1868238913561442 + } + gain_value { + gain_db: -13.999 + phi_rad: 1.5707963267948966 + theta_rad: 1.1876965559821413 + } + gain_value { + gain_db: -12.924 + phi_rad: 1.5707963267948966 + theta_rad: 1.1885692206081384 + } + gain_value { + gain_db: -12.815 + phi_rad: 1.5707963267948966 + theta_rad: 1.1894418852341357 + } + gain_value { + gain_db: -13.48 + phi_rad: 1.5707963267948966 + theta_rad: 1.1903145498601329 + } + gain_value { + gain_db: -14.76 + phi_rad: 1.5707963267948966 + theta_rad: 1.19118721448613 + } + gain_value { + gain_db: -16.579 + phi_rad: 1.5707963267948966 + theta_rad: 1.192059879112127 + } + gain_value { + gain_db: -19.816 + phi_rad: 1.5707963267948966 + theta_rad: 1.1929325437381242 + } + gain_value { + gain_db: -21.894 + phi_rad: 1.5707963267948966 + theta_rad: 1.1938052083641215 + } + gain_value { + gain_db: -20.007 + phi_rad: 1.5707963267948966 + theta_rad: 1.1946778729901186 + } + gain_value { + gain_db: -18.823 + phi_rad: 1.5707963267948966 + theta_rad: 1.1955505376161157 + } + gain_value { + gain_db: -17.718 + phi_rad: 1.5707963267948966 + theta_rad: 1.1964232022421128 + } + gain_value { + gain_db: -18.071 + phi_rad: 1.5707963267948966 + theta_rad: 1.19729586686811 + } + gain_value { + gain_db: -17.904 + phi_rad: 1.5707963267948966 + theta_rad: 1.1981685314941073 + } + gain_value { + gain_db: -16.595 + phi_rad: 1.5707963267948966 + theta_rad: 1.1990411961201044 + } + gain_value { + gain_db: -15.209 + phi_rad: 1.5707963267948966 + theta_rad: 1.1999138607461015 + } + gain_value { + gain_db: -14.23 + phi_rad: 1.5707963267948966 + theta_rad: 1.2007865253720986 + } + gain_value { + gain_db: -13.694 + phi_rad: 1.5707963267948966 + theta_rad: 1.2016591899980957 + } + gain_value { + gain_db: -13.712 + phi_rad: 1.5707963267948966 + theta_rad: 1.202531854624093 + } + gain_value { + gain_db: -13.989 + phi_rad: 1.5707963267948966 + theta_rad: 1.2034045192500902 + } + gain_value { + gain_db: -14.758 + phi_rad: 1.5707963267948966 + theta_rad: 1.2042771838760873 + } + gain_value { + gain_db: -15.547 + phi_rad: 1.5707963267948966 + theta_rad: 1.2051498485020844 + } + gain_value { + gain_db: -16.75 + phi_rad: 1.5707963267948966 + theta_rad: 1.2060225131280815 + } + gain_value { + gain_db: -18.087 + phi_rad: 1.5707963267948966 + theta_rad: 1.206895177754079 + } + gain_value { + gain_db: -22.258 + phi_rad: 1.5707963267948966 + theta_rad: 1.2077678423800762 + } + gain_value { + gain_db: -18.163 + phi_rad: 1.5707963267948966 + theta_rad: 1.2086405070060733 + } + gain_value { + gain_db: -16.041 + phi_rad: 1.5707963267948966 + theta_rad: 1.2095131716320704 + } + gain_value { + gain_db: -15.422 + phi_rad: 1.5707963267948966 + theta_rad: 1.2103858362580675 + } + gain_value { + gain_db: -15.701 + phi_rad: 1.5707963267948966 + theta_rad: 1.2112585008840648 + } + gain_value { + gain_db: -16.391 + phi_rad: 1.5707963267948966 + theta_rad: 1.212131165510062 + } + gain_value { + gain_db: -15.86 + phi_rad: 1.5707963267948966 + theta_rad: 1.213003830136059 + } + gain_value { + gain_db: -14.931 + phi_rad: 1.5707963267948966 + theta_rad: 1.2138764947620562 + } + gain_value { + gain_db: -15.112 + phi_rad: 1.5707963267948966 + theta_rad: 1.2147491593880533 + } + gain_value { + gain_db: -16.833 + phi_rad: 1.5707963267948966 + theta_rad: 1.2156218240140506 + } + gain_value { + gain_db: -15.623 + phi_rad: 1.5707963267948966 + theta_rad: 1.2164944886400477 + } + gain_value { + gain_db: -14.256 + phi_rad: 1.5707963267948966 + theta_rad: 1.2173671532660448 + } + gain_value { + gain_db: -13.774 + phi_rad: 1.5707963267948966 + theta_rad: 1.218239817892042 + } + gain_value { + gain_db: -13.285 + phi_rad: 1.5707963267948966 + theta_rad: 1.219112482518039 + } + gain_value { + gain_db: -12.108 + phi_rad: 1.5707963267948966 + theta_rad: 1.2199851471440364 + } + gain_value { + gain_db: -10.827 + phi_rad: 1.5707963267948966 + theta_rad: 1.2208578117700335 + } + gain_value { + gain_db: -10.297 + phi_rad: 1.5707963267948966 + theta_rad: 1.2217304763960306 + } + gain_value { + gain_db: -10.264 + phi_rad: 1.5707963267948966 + theta_rad: 1.2226031410220277 + } + gain_value { + gain_db: -10.86 + phi_rad: 1.5707963267948966 + theta_rad: 1.2234758056480248 + } + gain_value { + gain_db: -11.527 + phi_rad: 1.5707963267948966 + theta_rad: 1.2243484702740224 + } + gain_value { + gain_db: -12.501 + phi_rad: 1.5707963267948966 + theta_rad: 1.2252211349000195 + } + gain_value { + gain_db: -13.839 + phi_rad: 1.5707963267948966 + theta_rad: 1.2260937995260166 + } + gain_value { + gain_db: -14.753 + phi_rad: 1.5707963267948966 + theta_rad: 1.2269664641520137 + } + gain_value { + gain_db: -14.451 + phi_rad: 1.5707963267948966 + theta_rad: 1.2278391287780108 + } + gain_value { + gain_db: -14.202 + phi_rad: 1.5707963267948966 + theta_rad: 1.2287117934040082 + } + gain_value { + gain_db: -14.551 + phi_rad: 1.5707963267948966 + theta_rad: 1.2295844580300053 + } + gain_value { + gain_db: -15.368 + phi_rad: 1.5707963267948966 + theta_rad: 1.2304571226560024 + } + gain_value { + gain_db: -14.807 + phi_rad: 1.5707963267948966 + theta_rad: 1.2313297872819995 + } + gain_value { + gain_db: -13.141 + phi_rad: 1.5707963267948966 + theta_rad: 1.2322024519079966 + } + gain_value { + gain_db: -12.088 + phi_rad: 1.5707963267948966 + theta_rad: 1.233075116533994 + } + gain_value { + gain_db: -11.629 + phi_rad: 1.5707963267948966 + theta_rad: 1.233947781159991 + } + gain_value { + gain_db: -12.021 + phi_rad: 1.5707963267948966 + theta_rad: 1.2348204457859882 + } + gain_value { + gain_db: -12.645 + phi_rad: 1.5707963267948966 + theta_rad: 1.2356931104119853 + } + gain_value { + gain_db: -13.452 + phi_rad: 1.5707963267948966 + theta_rad: 1.2365657750379824 + } + gain_value { + gain_db: -14.155 + phi_rad: 1.5707963267948966 + theta_rad: 1.2374384396639797 + } + gain_value { + gain_db: -14.442 + phi_rad: 1.5707963267948966 + theta_rad: 1.2383111042899768 + } + gain_value { + gain_db: -14.836 + phi_rad: 1.5707963267948966 + theta_rad: 1.239183768915974 + } + gain_value { + gain_db: -15.617 + phi_rad: 1.5707963267948966 + theta_rad: 1.240056433541971 + } + gain_value { + gain_db: -17.349 + phi_rad: 1.5707963267948966 + theta_rad: 1.2409290981679681 + } + gain_value { + gain_db: -15.727 + phi_rad: 1.5707963267948966 + theta_rad: 1.2418017627939655 + } + gain_value { + gain_db: -13.74 + phi_rad: 1.5707963267948966 + theta_rad: 1.2426744274199626 + } + gain_value { + gain_db: -12.756 + phi_rad: 1.5707963267948966 + theta_rad: 1.2435470920459597 + } + gain_value { + gain_db: -12.242 + phi_rad: 1.5707963267948966 + theta_rad: 1.2444197566719568 + } + gain_value { + gain_db: -12.303 + phi_rad: 1.5707963267948966 + theta_rad: 1.245292421297954 + } + gain_value { + gain_db: -12.832 + phi_rad: 1.5707963267948966 + theta_rad: 1.2461650859239515 + } + gain_value { + gain_db: -14.01 + phi_rad: 1.5707963267948966 + theta_rad: 1.2470377505499486 + } + gain_value { + gain_db: -15.175 + phi_rad: 1.5707963267948966 + theta_rad: 1.2479104151759457 + } + gain_value { + gain_db: -16.262 + phi_rad: 1.5707963267948966 + theta_rad: 1.2487830798019428 + } + gain_value { + gain_db: -17.016 + phi_rad: 1.5707963267948966 + theta_rad: 1.24965574442794 + } + gain_value { + gain_db: -17.03 + phi_rad: 1.5707963267948966 + theta_rad: 1.2505284090539373 + } + gain_value { + gain_db: -18.109 + phi_rad: 1.5707963267948966 + theta_rad: 1.2514010736799344 + } + gain_value { + gain_db: -19.777 + phi_rad: 1.5707963267948966 + theta_rad: 1.2522737383059315 + } + gain_value { + gain_db: -17.291 + phi_rad: 1.5707963267948966 + theta_rad: 1.2531464029319286 + } + gain_value { + gain_db: -16.178 + phi_rad: 1.5707963267948966 + theta_rad: 1.2540190675579257 + } + gain_value { + gain_db: -16.046 + phi_rad: 1.5707963267948966 + theta_rad: 1.254891732183923 + } + gain_value { + gain_db: -17.043 + phi_rad: 1.5707963267948966 + theta_rad: 1.2557643968099201 + } + gain_value { + gain_db: -17.271 + phi_rad: 1.5707963267948966 + theta_rad: 1.2566370614359172 + } + gain_value { + gain_db: -17.596 + phi_rad: 1.5707963267948966 + theta_rad: 1.2575097260619144 + } + gain_value { + gain_db: -18.864 + phi_rad: 1.5707963267948966 + theta_rad: 1.2583823906879115 + } + gain_value { + gain_db: -20.801 + phi_rad: 1.5707963267948966 + theta_rad: 1.2592550553139088 + } + gain_value { + gain_db: -22.666 + phi_rad: 1.5707963267948966 + theta_rad: 1.260127719939906 + } + gain_value { + gain_db: -21.34 + phi_rad: 1.5707963267948966 + theta_rad: 1.261000384565903 + } + gain_value { + gain_db: -19.235 + phi_rad: 1.5707963267948966 + theta_rad: 1.2618730491919001 + } + gain_value { + gain_db: -17.556 + phi_rad: 1.5707963267948966 + theta_rad: 1.2627457138178972 + } + gain_value { + gain_db: -16.242 + phi_rad: 1.5707963267948966 + theta_rad: 1.2636183784438948 + } + gain_value { + gain_db: -15.479 + phi_rad: 1.5707963267948966 + theta_rad: 1.264491043069892 + } + gain_value { + gain_db: -15.292 + phi_rad: 1.5707963267948966 + theta_rad: 1.265363707695889 + } + gain_value { + gain_db: -15.639 + phi_rad: 1.5707963267948966 + theta_rad: 1.2662363723218861 + } + gain_value { + gain_db: -16.562 + phi_rad: 1.5707963267948966 + theta_rad: 1.2671090369478832 + } + gain_value { + gain_db: -17.13 + phi_rad: 1.5707963267948966 + theta_rad: 1.2679817015738806 + } + gain_value { + gain_db: -17.965 + phi_rad: 1.5707963267948966 + theta_rad: 1.2688543661998777 + } + gain_value { + gain_db: -18.116 + phi_rad: 1.5707963267948966 + theta_rad: 1.2697270308258748 + } + gain_value { + gain_db: -18.17 + phi_rad: 1.5707963267948966 + theta_rad: 1.270599695451872 + } + gain_value { + gain_db: -18.947 + phi_rad: 1.5707963267948966 + theta_rad: 1.271472360077869 + } + gain_value { + gain_db: -19.774 + phi_rad: 1.5707963267948966 + theta_rad: 1.2723450247038663 + } + gain_value { + gain_db: -20.926 + phi_rad: 1.5707963267948966 + theta_rad: 1.2732176893298635 + } + gain_value { + gain_db: -22.854 + phi_rad: 1.5707963267948966 + theta_rad: 1.2740903539558606 + } + gain_value { + gain_db: -21.499 + phi_rad: 1.5707963267948966 + theta_rad: 1.2749630185818577 + } + gain_value { + gain_db: -20.547 + phi_rad: 1.5707963267948966 + theta_rad: 1.2758356832078548 + } + gain_value { + gain_db: -19.934 + phi_rad: 1.5707963267948966 + theta_rad: 1.2767083478338521 + } + gain_value { + gain_db: -19.168 + phi_rad: 1.5707963267948966 + theta_rad: 1.2775810124598492 + } + gain_value { + gain_db: -18.781 + phi_rad: 1.5707963267948966 + theta_rad: 1.2784536770858463 + } + gain_value { + gain_db: -18.037 + phi_rad: 1.5707963267948966 + theta_rad: 1.2793263417118435 + } + gain_value { + gain_db: -16.684 + phi_rad: 1.5707963267948966 + theta_rad: 1.2801990063378406 + } + gain_value { + gain_db: -15.467 + phi_rad: 1.5707963267948966 + theta_rad: 1.281071670963838 + } + gain_value { + gain_db: -14.517 + phi_rad: 1.5707963267948966 + theta_rad: 1.281944335589835 + } + gain_value { + gain_db: -13.322 + phi_rad: 1.5707963267948966 + theta_rad: 1.2828170002158321 + } + gain_value { + gain_db: -12.361 + phi_rad: 1.5707963267948966 + theta_rad: 1.2836896648418292 + } + gain_value { + gain_db: -11.823 + phi_rad: 1.5707963267948966 + theta_rad: 1.2845623294678266 + } + gain_value { + gain_db: -11.579 + phi_rad: 1.5707963267948966 + theta_rad: 1.285434994093824 + } + gain_value { + gain_db: -11.6 + phi_rad: 1.5707963267948966 + theta_rad: 1.286307658719821 + } + gain_value { + gain_db: -11.92 + phi_rad: 1.5707963267948966 + theta_rad: 1.2871803233458181 + } + gain_value { + gain_db: -12.631 + phi_rad: 1.5707963267948966 + theta_rad: 1.2880529879718152 + } + gain_value { + gain_db: -13.591 + phi_rad: 1.5707963267948966 + theta_rad: 1.2889256525978123 + } + gain_value { + gain_db: -14.427 + phi_rad: 1.5707963267948966 + theta_rad: 1.2897983172238097 + } + gain_value { + gain_db: -14.684 + phi_rad: 1.5707963267948966 + theta_rad: 1.2906709818498068 + } + gain_value { + gain_db: -14.566 + phi_rad: 1.5707963267948966 + theta_rad: 1.2915436464758039 + } + gain_value { + gain_db: -14.438 + phi_rad: 1.5707963267948966 + theta_rad: 1.292416311101801 + } + gain_value { + gain_db: -15.078 + phi_rad: 1.5707963267948966 + theta_rad: 1.293288975727798 + } + gain_value { + gain_db: -15.38 + phi_rad: 1.5707963267948966 + theta_rad: 1.2941616403537954 + } + gain_value { + gain_db: -14.315 + phi_rad: 1.5707963267948966 + theta_rad: 1.2950343049797925 + } + gain_value { + gain_db: -13.461 + phi_rad: 1.5707963267948966 + theta_rad: 1.2959069696057897 + } + gain_value { + gain_db: -13.271 + phi_rad: 1.5707963267948966 + theta_rad: 1.2967796342317868 + } + gain_value { + gain_db: -13.564 + phi_rad: 1.5707963267948966 + theta_rad: 1.2976522988577839 + } + gain_value { + gain_db: -14.325 + phi_rad: 1.5707963267948966 + theta_rad: 1.2985249634837812 + } + gain_value { + gain_db: -15.133 + phi_rad: 1.5707963267948966 + theta_rad: 1.2993976281097783 + } + gain_value { + gain_db: -15.791 + phi_rad: 1.5707963267948966 + theta_rad: 1.3002702927357754 + } + gain_value { + gain_db: -15.732 + phi_rad: 1.5707963267948966 + theta_rad: 1.3011429573617725 + } + gain_value { + gain_db: -15.373 + phi_rad: 1.5707963267948966 + theta_rad: 1.3020156219877697 + } + gain_value { + gain_db: -15.002 + phi_rad: 1.5707963267948966 + theta_rad: 1.3028882866137672 + } + gain_value { + gain_db: -15.106 + phi_rad: 1.5707963267948966 + theta_rad: 1.3037609512397643 + } + gain_value { + gain_db: -15.279 + phi_rad: 1.5707963267948966 + theta_rad: 1.3046336158657614 + } + gain_value { + gain_db: -16.675 + phi_rad: 1.5707963267948966 + theta_rad: 1.3055062804917585 + } + gain_value { + gain_db: -18.157 + phi_rad: 1.5707963267948966 + theta_rad: 1.3063789451177557 + } + gain_value { + gain_db: -25.667 + phi_rad: 1.5707963267948966 + theta_rad: 1.307251609743753 + } + gain_value { + gain_db: -21.84 + phi_rad: 1.5707963267948966 + theta_rad: 1.30812427436975 + } + gain_value { + gain_db: -22.632 + phi_rad: 1.5707963267948966 + theta_rad: 1.3089969389957472 + } + gain_value { + gain_db: -18.859 + phi_rad: 1.5707963267948966 + theta_rad: 1.3098696036217443 + } + gain_value { + gain_db: -16.886 + phi_rad: 1.5707963267948966 + theta_rad: 1.3107422682477414 + } + gain_value { + gain_db: -15.478 + phi_rad: 1.5707963267948966 + theta_rad: 1.3116149328737388 + } + gain_value { + gain_db: -14.637 + phi_rad: 1.5707963267948966 + theta_rad: 1.3124875974997359 + } + gain_value { + gain_db: -13.671 + phi_rad: 1.5707963267948966 + theta_rad: 1.313360262125733 + } + gain_value { + gain_db: -13.274 + phi_rad: 1.5707963267948966 + theta_rad: 1.31423292675173 + } + gain_value { + gain_db: -12.848 + phi_rad: 1.5707963267948966 + theta_rad: 1.3151055913777272 + } + gain_value { + gain_db: -13.402 + phi_rad: 1.5707963267948966 + theta_rad: 1.3159782560037245 + } + gain_value { + gain_db: -14.175 + phi_rad: 1.5707963267948966 + theta_rad: 1.3168509206297216 + } + gain_value { + gain_db: -14.897 + phi_rad: 1.5707963267948966 + theta_rad: 1.3177235852557188 + } + gain_value { + gain_db: -15.197 + phi_rad: 1.5707963267948966 + theta_rad: 1.3185962498817159 + } + gain_value { + gain_db: -15.22 + phi_rad: 1.5707963267948966 + theta_rad: 1.319468914507713 + } + gain_value { + gain_db: -15.387 + phi_rad: 1.5707963267948966 + theta_rad: 1.3203415791337103 + } + gain_value { + gain_db: -15.892 + phi_rad: 1.5707963267948966 + theta_rad: 1.3212142437597074 + } + gain_value { + gain_db: -16.9 + phi_rad: 1.5707963267948966 + theta_rad: 1.3220869083857045 + } + gain_value { + gain_db: -17.401 + phi_rad: 1.5707963267948966 + theta_rad: 1.3229595730117016 + } + gain_value { + gain_db: -17.277 + phi_rad: 1.5707963267948966 + theta_rad: 1.323832237637699 + } + gain_value { + gain_db: -17.295 + phi_rad: 1.5707963267948966 + theta_rad: 1.3247049022636963 + } + gain_value { + gain_db: -16.998 + phi_rad: 1.5707963267948966 + theta_rad: 1.3255775668896934 + } + gain_value { + gain_db: -16.162 + phi_rad: 1.5707963267948966 + theta_rad: 1.3264502315156905 + } + gain_value { + gain_db: -15.589 + phi_rad: 1.5707963267948966 + theta_rad: 1.3273228961416876 + } + gain_value { + gain_db: -15.733 + phi_rad: 1.5707963267948966 + theta_rad: 1.3281955607676847 + } + gain_value { + gain_db: -16.241 + phi_rad: 1.5707963267948966 + theta_rad: 1.329068225393682 + } + gain_value { + gain_db: -16.803 + phi_rad: 1.5707963267948966 + theta_rad: 1.3299408900196792 + } + gain_value { + gain_db: -17.835 + phi_rad: 1.5707963267948966 + theta_rad: 1.3308135546456763 + } + gain_value { + gain_db: -18.835 + phi_rad: 1.5707963267948966 + theta_rad: 1.3316862192716734 + } + gain_value { + gain_db: -19.309 + phi_rad: 1.5707963267948966 + theta_rad: 1.3325588838976705 + } + gain_value { + gain_db: -19.849 + phi_rad: 1.5707963267948966 + theta_rad: 1.3334315485236679 + } + gain_value { + gain_db: -19.851 + phi_rad: 1.5707963267948966 + theta_rad: 1.334304213149665 + } + gain_value { + gain_db: -16.939 + phi_rad: 1.5707963267948966 + theta_rad: 1.335176877775662 + } + gain_value { + gain_db: -15.218 + phi_rad: 1.5707963267948966 + theta_rad: 1.3360495424016592 + } + gain_value { + gain_db: -14.038 + phi_rad: 1.5707963267948966 + theta_rad: 1.3369222070276563 + } + gain_value { + gain_db: -13.412 + phi_rad: 1.5707963267948966 + theta_rad: 1.3377948716536536 + } + gain_value { + gain_db: -13.114 + phi_rad: 1.5707963267948966 + theta_rad: 1.3386675362796507 + } + gain_value { + gain_db: -13.082 + phi_rad: 1.5707963267948966 + theta_rad: 1.3395402009056478 + } + gain_value { + gain_db: -13.359 + phi_rad: 1.5707963267948966 + theta_rad: 1.340412865531645 + } + gain_value { + gain_db: -13.981 + phi_rad: 1.5707963267948966 + theta_rad: 1.341285530157642 + } + gain_value { + gain_db: -14.709 + phi_rad: 1.5707963267948966 + theta_rad: 1.3421581947836396 + } + gain_value { + gain_db: -15.66 + phi_rad: 1.5707963267948966 + theta_rad: 1.3430308594096367 + } + gain_value { + gain_db: -16.924 + phi_rad: 1.5707963267948966 + theta_rad: 1.3439035240356338 + } + gain_value { + gain_db: -18.147 + phi_rad: 1.5707963267948966 + theta_rad: 1.344776188661631 + } + gain_value { + gain_db: -19.432 + phi_rad: 1.5707963267948966 + theta_rad: 1.345648853287628 + } + gain_value { + gain_db: -22.223 + phi_rad: 1.5707963267948966 + theta_rad: 1.3465215179136254 + } + gain_value { + gain_db: -20.667 + phi_rad: 1.5707963267948966 + theta_rad: 1.3473941825396225 + } + gain_value { + gain_db: -18.198 + phi_rad: 1.5707963267948966 + theta_rad: 1.3482668471656196 + } + gain_value { + gain_db: -17.078 + phi_rad: 1.5707963267948966 + theta_rad: 1.3491395117916167 + } + gain_value { + gain_db: -16.431 + phi_rad: 1.5707963267948966 + theta_rad: 1.3500121764176138 + } + gain_value { + gain_db: -16.147 + phi_rad: 1.5707963267948966 + theta_rad: 1.3508848410436112 + } + gain_value { + gain_db: -14.854 + phi_rad: 1.5707963267948966 + theta_rad: 1.3517575056696083 + } + gain_value { + gain_db: -13.393 + phi_rad: 1.5707963267948966 + theta_rad: 1.3526301702956054 + } + gain_value { + gain_db: -12.676 + phi_rad: 1.5707963267948966 + theta_rad: 1.3535028349216025 + } + gain_value { + gain_db: -12.336 + phi_rad: 1.5707963267948966 + theta_rad: 1.3543754995475996 + } + gain_value { + gain_db: -12.465 + phi_rad: 1.5707963267948966 + theta_rad: 1.355248164173597 + } + gain_value { + gain_db: -13.044 + phi_rad: 1.5707963267948966 + theta_rad: 1.356120828799594 + } + gain_value { + gain_db: -14.254 + phi_rad: 1.5707963267948966 + theta_rad: 1.3569934934255912 + } + gain_value { + gain_db: -16.739 + phi_rad: 1.5707963267948966 + theta_rad: 1.3578661580515883 + } + gain_value { + gain_db: -22.506 + phi_rad: 1.5707963267948966 + theta_rad: 1.3587388226775854 + } + gain_value { + gain_db: -21.135 + phi_rad: 1.5707963267948966 + theta_rad: 1.3596114873035827 + } + gain_value { + gain_db: -20.51 + phi_rad: 1.5707963267948966 + theta_rad: 1.3604841519295798 + } + gain_value { + gain_db: -24.013 + phi_rad: 1.5707963267948966 + theta_rad: 1.361356816555577 + } + gain_value { + gain_db: -22.332 + phi_rad: 1.5707963267948966 + theta_rad: 1.362229481181574 + } + gain_value { + gain_db: -22.228 + phi_rad: 1.5707963267948966 + theta_rad: 1.3631021458075714 + } + gain_value { + gain_db: -24.803 + phi_rad: 1.5707963267948966 + theta_rad: 1.3639748104335687 + } + gain_value { + gain_db: -21.862 + phi_rad: 1.5707963267948966 + theta_rad: 1.3648474750595658 + } + gain_value { + gain_db: -20.262 + phi_rad: 1.5707963267948966 + theta_rad: 1.365720139685563 + } + gain_value { + gain_db: -18.481 + phi_rad: 1.5707963267948966 + theta_rad: 1.36659280431156 + } + gain_value { + gain_db: -16.869 + phi_rad: 1.5707963267948966 + theta_rad: 1.3674654689375572 + } + gain_value { + gain_db: -15.597 + phi_rad: 1.5707963267948966 + theta_rad: 1.3683381335635545 + } + gain_value { + gain_db: -15.266 + phi_rad: 1.5707963267948966 + theta_rad: 1.3692107981895516 + } + gain_value { + gain_db: -14.94 + phi_rad: 1.5707963267948966 + theta_rad: 1.3700834628155487 + } + gain_value { + gain_db: -14.419 + phi_rad: 1.5707963267948966 + theta_rad: 1.3709561274415458 + } + gain_value { + gain_db: -14.949 + phi_rad: 1.5707963267948966 + theta_rad: 1.371828792067543 + } + gain_value { + gain_db: -15.343 + phi_rad: 1.5707963267948966 + theta_rad: 1.3727014566935403 + } + gain_value { + gain_db: -15.979 + phi_rad: 1.5707963267948966 + theta_rad: 1.3735741213195374 + } + gain_value { + gain_db: -16.453 + phi_rad: 1.5707963267948966 + theta_rad: 1.3744467859455345 + } + gain_value { + gain_db: -17.541 + phi_rad: 1.5707963267948966 + theta_rad: 1.3753194505715316 + } + gain_value { + gain_db: -17.982 + phi_rad: 1.5707963267948966 + theta_rad: 1.3761921151975287 + } + gain_value { + gain_db: -18.687 + phi_rad: 1.5707963267948966 + theta_rad: 1.377064779823526 + } + gain_value { + gain_db: -18.571 + phi_rad: 1.5707963267948966 + theta_rad: 1.3779374444495232 + } + gain_value { + gain_db: -17.952 + phi_rad: 1.5707963267948966 + theta_rad: 1.3788101090755203 + } + gain_value { + gain_db: -16.877 + phi_rad: 1.5707963267948966 + theta_rad: 1.3796827737015174 + } + gain_value { + gain_db: -16.574 + phi_rad: 1.5707963267948966 + theta_rad: 1.3805554383275145 + } + gain_value { + gain_db: -16.57 + phi_rad: 1.5707963267948966 + theta_rad: 1.381428102953512 + } + gain_value { + gain_db: -15.095 + phi_rad: 1.5707963267948966 + theta_rad: 1.3823007675795091 + } + gain_value { + gain_db: -14.226 + phi_rad: 1.5707963267948966 + theta_rad: 1.3831734322055063 + } + gain_value { + gain_db: -13.528 + phi_rad: 1.5707963267948966 + theta_rad: 1.3840460968315034 + } + gain_value { + gain_db: -13.166 + phi_rad: 1.5707963267948966 + theta_rad: 1.3849187614575005 + } + gain_value { + gain_db: -13.232 + phi_rad: 1.5707963267948966 + theta_rad: 1.3857914260834978 + } + gain_value { + gain_db: -13.57 + phi_rad: 1.5707963267948966 + theta_rad: 1.386664090709495 + } + gain_value { + gain_db: -13.929 + phi_rad: 1.5707963267948966 + theta_rad: 1.387536755335492 + } + gain_value { + gain_db: -13.87 + phi_rad: 1.5707963267948966 + theta_rad: 1.3884094199614891 + } + gain_value { + gain_db: -13.875 + phi_rad: 1.5707963267948966 + theta_rad: 1.3892820845874863 + } + gain_value { + gain_db: -13.741 + phi_rad: 1.5707963267948966 + theta_rad: 1.3901547492134836 + } + gain_value { + gain_db: -13.634 + phi_rad: 1.5707963267948966 + theta_rad: 1.3910274138394807 + } + gain_value { + gain_db: -13.121 + phi_rad: 1.5707963267948966 + theta_rad: 1.3919000784654778 + } + gain_value { + gain_db: -12.838 + phi_rad: 1.5707963267948966 + theta_rad: 1.392772743091475 + } + gain_value { + gain_db: -12.695 + phi_rad: 1.5707963267948966 + theta_rad: 1.393645407717472 + } + gain_value { + gain_db: -12.88 + phi_rad: 1.5707963267948966 + theta_rad: 1.3945180723434694 + } + gain_value { + gain_db: -12.793 + phi_rad: 1.5707963267948966 + theta_rad: 1.3953907369694665 + } + gain_value { + gain_db: -12.676 + phi_rad: 1.5707963267948966 + theta_rad: 1.3962634015954636 + } + gain_value { + gain_db: -12.655 + phi_rad: 1.5707963267948966 + theta_rad: 1.3971360662214607 + } + gain_value { + gain_db: -12.842 + phi_rad: 1.5707963267948966 + theta_rad: 1.3980087308474578 + } + gain_value { + gain_db: -13.415 + phi_rad: 1.5707963267948966 + theta_rad: 1.3988813954734551 + } + gain_value { + gain_db: -14.354 + phi_rad: 1.5707963267948966 + theta_rad: 1.3997540600994522 + } + gain_value { + gain_db: -15.48 + phi_rad: 1.5707963267948966 + theta_rad: 1.4006267247254494 + } + gain_value { + gain_db: -16.199 + phi_rad: 1.5707963267948966 + theta_rad: 1.4014993893514465 + } + gain_value { + gain_db: -15.89 + phi_rad: 1.5707963267948966 + theta_rad: 1.4023720539774438 + } + gain_value { + gain_db: -15.003 + phi_rad: 1.5707963267948966 + theta_rad: 1.4032447186034411 + } + gain_value { + gain_db: -14.521 + phi_rad: 1.5707963267948966 + theta_rad: 1.4041173832294382 + } + gain_value { + gain_db: -14.144 + phi_rad: 1.5707963267948966 + theta_rad: 1.4049900478554354 + } + gain_value { + gain_db: -13.841 + phi_rad: 1.5707963267948966 + theta_rad: 1.4058627124814325 + } + gain_value { + gain_db: -13.834 + phi_rad: 1.5707963267948966 + theta_rad: 1.4067353771074296 + } + gain_value { + gain_db: -13.435 + phi_rad: 1.5707963267948966 + theta_rad: 1.407608041733427 + } + gain_value { + gain_db: -12.997 + phi_rad: 1.5707963267948966 + theta_rad: 1.408480706359424 + } + gain_value { + gain_db: -12.605 + phi_rad: 1.5707963267948966 + theta_rad: 1.4093533709854211 + } + gain_value { + gain_db: -12.559 + phi_rad: 1.5707963267948966 + theta_rad: 1.4102260356114182 + } + gain_value { + gain_db: -12.798 + phi_rad: 1.5707963267948966 + theta_rad: 1.4110987002374153 + } + gain_value { + gain_db: -13.051 + phi_rad: 1.5707963267948966 + theta_rad: 1.4119713648634127 + } + gain_value { + gain_db: -13.569 + phi_rad: 1.5707963267948966 + theta_rad: 1.4128440294894098 + } + gain_value { + gain_db: -14.309 + phi_rad: 1.5707963267948966 + theta_rad: 1.413716694115407 + } + gain_value { + gain_db: -15.32 + phi_rad: 1.5707963267948966 + theta_rad: 1.414589358741404 + } + gain_value { + gain_db: -17.118 + phi_rad: 1.5707963267948966 + theta_rad: 1.4154620233674011 + } + gain_value { + gain_db: -17.914 + phi_rad: 1.5707963267948966 + theta_rad: 1.4163346879933985 + } + gain_value { + gain_db: -15.947 + phi_rad: 1.5707963267948966 + theta_rad: 1.4172073526193956 + } + gain_value { + gain_db: -15.007 + phi_rad: 1.5707963267948966 + theta_rad: 1.4180800172453927 + } + gain_value { + gain_db: -14.653 + phi_rad: 1.5707963267948966 + theta_rad: 1.4189526818713898 + } + gain_value { + gain_db: -14.805 + phi_rad: 1.5707963267948966 + theta_rad: 1.419825346497387 + } + gain_value { + gain_db: -15.169 + phi_rad: 1.5707963267948966 + theta_rad: 1.4206980111233845 + } + gain_value { + gain_db: -15.143 + phi_rad: 1.5707963267948966 + theta_rad: 1.4215706757493816 + } + gain_value { + gain_db: -15.488 + phi_rad: 1.5707963267948966 + theta_rad: 1.4224433403753787 + } + gain_value { + gain_db: -16.009 + phi_rad: 1.5707963267948966 + theta_rad: 1.4233160050013758 + } + gain_value { + gain_db: -16.781 + phi_rad: 1.5707963267948966 + theta_rad: 1.424188669627373 + } + gain_value { + gain_db: -16.679 + phi_rad: 1.5707963267948966 + theta_rad: 1.4250613342533702 + } + gain_value { + gain_db: -16.009 + phi_rad: 1.5707963267948966 + theta_rad: 1.4259339988793673 + } + gain_value { + gain_db: -15.96 + phi_rad: 1.5707963267948966 + theta_rad: 1.4268066635053644 + } + gain_value { + gain_db: -16.185 + phi_rad: 1.5707963267948966 + theta_rad: 1.4276793281313616 + } + gain_value { + gain_db: -16.484 + phi_rad: 1.5707963267948966 + theta_rad: 1.4285519927573587 + } + gain_value { + gain_db: -17.122 + phi_rad: 1.5707963267948966 + theta_rad: 1.429424657383356 + } + gain_value { + gain_db: -18.234 + phi_rad: 1.5707963267948966 + theta_rad: 1.4302973220093531 + } + gain_value { + gain_db: -20.105 + phi_rad: 1.5707963267948966 + theta_rad: 1.4311699866353502 + } + gain_value { + gain_db: -25.422 + phi_rad: 1.5707963267948966 + theta_rad: 1.4320426512613473 + } + gain_value { + gain_db: -20.328 + phi_rad: 1.5707963267948966 + theta_rad: 1.4329153158873444 + } + gain_value { + gain_db: -17.833 + phi_rad: 1.5707963267948966 + theta_rad: 1.4337879805133418 + } + gain_value { + gain_db: -15.96 + phi_rad: 1.5707963267948966 + theta_rad: 1.4346606451393389 + } + gain_value { + gain_db: -14.645 + phi_rad: 1.5707963267948966 + theta_rad: 1.435533309765336 + } + gain_value { + gain_db: -13.63 + phi_rad: 1.5707963267948966 + theta_rad: 1.436405974391333 + } + gain_value { + gain_db: -12.988 + phi_rad: 1.5707963267948966 + theta_rad: 1.4372786390173302 + } + gain_value { + gain_db: -12.968 + phi_rad: 1.5707963267948966 + theta_rad: 1.4381513036433275 + } + gain_value { + gain_db: -13.478 + phi_rad: 1.5707963267948966 + theta_rad: 1.4390239682693247 + } + gain_value { + gain_db: -14.88 + phi_rad: 1.5707963267948966 + theta_rad: 1.4398966328953218 + } + gain_value { + gain_db: -16.611 + phi_rad: 1.5707963267948966 + theta_rad: 1.4407692975213189 + } + gain_value { + gain_db: -16.688 + phi_rad: 1.5707963267948966 + theta_rad: 1.4416419621473162 + } + gain_value { + gain_db: -16.533 + phi_rad: 1.5707963267948966 + theta_rad: 1.4425146267733135 + } + gain_value { + gain_db: -16.993 + phi_rad: 1.5707963267948966 + theta_rad: 1.4433872913993107 + } + gain_value { + gain_db: -17.452 + phi_rad: 1.5707963267948966 + theta_rad: 1.4442599560253078 + } + gain_value { + gain_db: -17.247 + phi_rad: 1.5707963267948966 + theta_rad: 1.4451326206513049 + } + gain_value { + gain_db: -15.935 + phi_rad: 1.5707963267948966 + theta_rad: 1.446005285277302 + } + gain_value { + gain_db: -14.596 + phi_rad: 1.5707963267948966 + theta_rad: 1.4468779499032993 + } + gain_value { + gain_db: -13.663 + phi_rad: 1.5707963267948966 + theta_rad: 1.4477506145292964 + } + gain_value { + gain_db: -13.255 + phi_rad: 1.5707963267948966 + theta_rad: 1.4486232791552935 + } + gain_value { + gain_db: -13.056 + phi_rad: 1.5707963267948966 + theta_rad: 1.4494959437812907 + } + gain_value { + gain_db: -13.344 + phi_rad: 1.5707963267948966 + theta_rad: 1.4503686084072878 + } + gain_value { + gain_db: -13.793 + phi_rad: 1.5707963267948966 + theta_rad: 1.451241273033285 + } + gain_value { + gain_db: -14.655 + phi_rad: 1.5707963267948966 + theta_rad: 1.4521139376592822 + } + gain_value { + gain_db: -15.859 + phi_rad: 1.5707963267948966 + theta_rad: 1.4529866022852793 + } + gain_value { + gain_db: -17.186 + phi_rad: 1.5707963267948966 + theta_rad: 1.4538592669112764 + } + gain_value { + gain_db: -18.532 + phi_rad: 1.5707963267948966 + theta_rad: 1.4547319315372735 + } + gain_value { + gain_db: -20.739 + phi_rad: 1.5707963267948966 + theta_rad: 1.4556045961632709 + } + gain_value { + gain_db: -20.737 + phi_rad: 1.5707963267948966 + theta_rad: 1.456477260789268 + } + gain_value { + gain_db: -18.648 + phi_rad: 1.5707963267948966 + theta_rad: 1.457349925415265 + } + gain_value { + gain_db: -17.704 + phi_rad: 1.5707963267948966 + theta_rad: 1.4582225900412622 + } + gain_value { + gain_db: -16.489 + phi_rad: 1.5707963267948966 + theta_rad: 1.4590952546672593 + } + gain_value { + gain_db: -15.966 + phi_rad: 1.5707963267948966 + theta_rad: 1.4599679192932569 + } + gain_value { + gain_db: -15.783 + phi_rad: 1.5707963267948966 + theta_rad: 1.460840583919254 + } + gain_value { + gain_db: -15.423 + phi_rad: 1.5707963267948966 + theta_rad: 1.461713248545251 + } + gain_value { + gain_db: -15.426 + phi_rad: 1.5707963267948966 + theta_rad: 1.4625859131712482 + } + gain_value { + gain_db: -15.979 + phi_rad: 1.5707963267948966 + theta_rad: 1.4634585777972453 + } + gain_value { + gain_db: -17.007 + phi_rad: 1.5707963267948966 + theta_rad: 1.4643312424232426 + } + gain_value { + gain_db: -18.54 + phi_rad: 1.5707963267948966 + theta_rad: 1.4652039070492398 + } + gain_value { + gain_db: -18.433 + phi_rad: 1.5707963267948966 + theta_rad: 1.4660765716752369 + } + gain_value { + gain_db: -17.117 + phi_rad: 1.5707963267948966 + theta_rad: 1.466949236301234 + } + gain_value { + gain_db: -15.968 + phi_rad: 1.5707963267948966 + theta_rad: 1.467821900927231 + } + gain_value { + gain_db: -14.627 + phi_rad: 1.5707963267948966 + theta_rad: 1.4686945655532284 + } + gain_value { + gain_db: -13.727 + phi_rad: 1.5707963267948966 + theta_rad: 1.4695672301792255 + } + gain_value { + gain_db: -12.719 + phi_rad: 1.5707963267948966 + theta_rad: 1.4704398948052226 + } + gain_value { + gain_db: -11.922 + phi_rad: 1.5707963267948966 + theta_rad: 1.4713125594312197 + } + gain_value { + gain_db: -11.543 + phi_rad: 1.5707963267948966 + theta_rad: 1.4721852240572169 + } + gain_value { + gain_db: -11.312 + phi_rad: 1.5707963267948966 + theta_rad: 1.4730578886832142 + } + gain_value { + gain_db: -11.454 + phi_rad: 1.5707963267948966 + theta_rad: 1.4739305533092113 + } + gain_value { + gain_db: -12.141 + phi_rad: 1.5707963267948966 + theta_rad: 1.4748032179352084 + } + gain_value { + gain_db: -13.112 + phi_rad: 1.5707963267948966 + theta_rad: 1.4756758825612055 + } + gain_value { + gain_db: -14.485 + phi_rad: 1.5707963267948966 + theta_rad: 1.4765485471872026 + } + gain_value { + gain_db: -16.055 + phi_rad: 1.5707963267948966 + theta_rad: 1.4774212118132 + } + gain_value { + gain_db: -17.87 + phi_rad: 1.5707963267948966 + theta_rad: 1.478293876439197 + } + gain_value { + gain_db: -23.74 + phi_rad: 1.5707963267948966 + theta_rad: 1.4791665410651942 + } + gain_value { + gain_db: -21.266 + phi_rad: 1.5707963267948966 + theta_rad: 1.4800392056911915 + } + gain_value { + gain_db: -16.378 + phi_rad: 1.5707963267948966 + theta_rad: 1.4809118703171886 + } + gain_value { + gain_db: -13.313 + phi_rad: 1.5707963267948966 + theta_rad: 1.481784534943186 + } + gain_value { + gain_db: -11.554 + phi_rad: 1.5707963267948966 + theta_rad: 1.482657199569183 + } + gain_value { + gain_db: -10.452 + phi_rad: 1.5707963267948966 + theta_rad: 1.4835298641951802 + } + gain_value { + gain_db: -9.8511 + phi_rad: 1.5707963267948966 + theta_rad: 1.4844025288211773 + } + gain_value { + gain_db: -9.4576 + phi_rad: 1.5707963267948966 + theta_rad: 1.4852751934471744 + } + gain_value { + gain_db: -9.5018 + phi_rad: 1.5707963267948966 + theta_rad: 1.4861478580731717 + } + gain_value { + gain_db: -9.851 + phi_rad: 1.5707963267948966 + theta_rad: 1.4870205226991688 + } + gain_value { + gain_db: -10.675 + phi_rad: 1.5707963267948966 + theta_rad: 1.487893187325166 + } + gain_value { + gain_db: -12.296 + phi_rad: 1.5707963267948966 + theta_rad: 1.488765851951163 + } + gain_value { + gain_db: -14.499 + phi_rad: 1.5707963267948966 + theta_rad: 1.4896385165771602 + } + gain_value { + gain_db: -19.003 + phi_rad: 1.5707963267948966 + theta_rad: 1.4905111812031575 + } + gain_value { + gain_db: -19.154 + phi_rad: 1.5707963267948966 + theta_rad: 1.4913838458291546 + } + gain_value { + gain_db: -19.946 + phi_rad: 1.5707963267948966 + theta_rad: 1.4922565104551517 + } + gain_value { + gain_db: -17.848 + phi_rad: 1.5707963267948966 + theta_rad: 1.4931291750811488 + } + gain_value { + gain_db: -16.707 + phi_rad: 1.5707963267948966 + theta_rad: 1.494001839707146 + } + gain_value { + gain_db: -17.872 + phi_rad: 1.5707963267948966 + theta_rad: 1.4948745043331433 + } + gain_value { + gain_db: -15.7 + phi_rad: 1.5707963267948966 + theta_rad: 1.4957471689591404 + } + gain_value { + gain_db: -12.69 + phi_rad: 1.5707963267948966 + theta_rad: 1.4966198335851375 + } + gain_value { + gain_db: -11.03 + phi_rad: 1.5707963267948966 + theta_rad: 1.4974924982111346 + } + gain_value { + gain_db: -9.909 + phi_rad: 1.5707963267948966 + theta_rad: 1.4983651628371317 + } + gain_value { + gain_db: -9.4173 + phi_rad: 1.5707963267948966 + theta_rad: 1.4992378274631293 + } + gain_value { + gain_db: -9.2583 + phi_rad: 1.5707963267948966 + theta_rad: 1.5001104920891264 + } + gain_value { + gain_db: -9.2396 + phi_rad: 1.5707963267948966 + theta_rad: 1.5009831567151235 + } + gain_value { + gain_db: -9.6364 + phi_rad: 1.5707963267948966 + theta_rad: 1.5018558213411206 + } + gain_value { + gain_db: -10.436 + phi_rad: 1.5707963267948966 + theta_rad: 1.5027284859671177 + } + gain_value { + gain_db: -11.411 + phi_rad: 1.5707963267948966 + theta_rad: 1.503601150593115 + } + gain_value { + gain_db: -12.398 + phi_rad: 1.5707963267948966 + theta_rad: 1.5044738152191122 + } + gain_value { + gain_db: -13.16 + phi_rad: 1.5707963267948966 + theta_rad: 1.5053464798451093 + } + gain_value { + gain_db: -13.153 + phi_rad: 1.5707963267948966 + theta_rad: 1.5062191444711064 + } + gain_value { + gain_db: -12.683 + phi_rad: 1.5707963267948966 + theta_rad: 1.5070918090971035 + } + gain_value { + gain_db: -12.694 + phi_rad: 1.5707963267948966 + theta_rad: 1.5079644737231008 + } + gain_value { + gain_db: -12.985 + phi_rad: 1.5707963267948966 + theta_rad: 1.508837138349098 + } + gain_value { + gain_db: -12.886 + phi_rad: 1.5707963267948966 + theta_rad: 1.509709802975095 + } + gain_value { + gain_db: -12.21 + phi_rad: 1.5707963267948966 + theta_rad: 1.5105824676010922 + } + gain_value { + gain_db: -11.34 + phi_rad: 1.5707963267948966 + theta_rad: 1.5114551322270893 + } + gain_value { + gain_db: -10.425 + phi_rad: 1.5707963267948966 + theta_rad: 1.5123277968530866 + } + gain_value { + gain_db: -9.6622 + phi_rad: 1.5707963267948966 + theta_rad: 1.5132004614790837 + } + gain_value { + gain_db: -9.1739 + phi_rad: 1.5707963267948966 + theta_rad: 1.5140731261050808 + } + gain_value { + gain_db: -8.905 + phi_rad: 1.5707963267948966 + theta_rad: 1.514945790731078 + } + gain_value { + gain_db: -8.948 + phi_rad: 1.5707963267948966 + theta_rad: 1.515818455357075 + } + gain_value { + gain_db: -9.2432 + phi_rad: 1.5707963267948966 + theta_rad: 1.5166911199830724 + } + gain_value { + gain_db: -9.7918 + phi_rad: 1.5707963267948966 + theta_rad: 1.5175637846090695 + } + gain_value { + gain_db: -10.454 + phi_rad: 1.5707963267948966 + theta_rad: 1.5184364492350666 + } + gain_value { + gain_db: -11.143 + phi_rad: 1.5707963267948966 + theta_rad: 1.519309113861064 + } + gain_value { + gain_db: -11.277 + phi_rad: 1.5707963267948966 + theta_rad: 1.520181778487061 + } + gain_value { + gain_db: -11.171 + phi_rad: 1.5707963267948966 + theta_rad: 1.5210544431130584 + } + gain_value { + gain_db: -11.276 + phi_rad: 1.5707963267948966 + theta_rad: 1.5219271077390555 + } + gain_value { + gain_db: -11.79 + phi_rad: 1.5707963267948966 + theta_rad: 1.5227997723650526 + } + gain_value { + gain_db: -12.31 + phi_rad: 1.5707963267948966 + theta_rad: 1.5236724369910497 + } + gain_value { + gain_db: -12.029 + phi_rad: 1.5707963267948966 + theta_rad: 1.5245451016170468 + } + gain_value { + gain_db: -10.859 + phi_rad: 1.5707963267948966 + theta_rad: 1.5254177662430441 + } + gain_value { + gain_db: -9.9157 + phi_rad: 1.5707963267948966 + theta_rad: 1.5262904308690413 + } + gain_value { + gain_db: -9.2881 + phi_rad: 1.5707963267948966 + theta_rad: 1.5271630954950384 + } + gain_value { + gain_db: -9.0554 + phi_rad: 1.5707963267948966 + theta_rad: 1.5280357601210355 + } + gain_value { + gain_db: -9.0481 + phi_rad: 1.5707963267948966 + theta_rad: 1.5289084247470326 + } + gain_value { + gain_db: -9.4422 + phi_rad: 1.5707963267948966 + theta_rad: 1.52978108937303 + } + gain_value { + gain_db: -10.682 + phi_rad: 1.5707963267948966 + theta_rad: 1.530653753999027 + } + gain_value { + gain_db: -13.398 + phi_rad: 1.5707963267948966 + theta_rad: 1.5315264186250241 + } + gain_value { + gain_db: -15.567 + phi_rad: 1.5707963267948966 + theta_rad: 1.5323990832510213 + } + gain_value { + gain_db: -11.905 + phi_rad: 1.5707963267948966 + theta_rad: 1.5332717478770184 + } + gain_value { + gain_db: -10.89 + phi_rad: 1.5707963267948966 + theta_rad: 1.5341444125030157 + } + gain_value { + gain_db: -10.927 + phi_rad: 1.5707963267948966 + theta_rad: 1.5350170771290128 + } + gain_value { + gain_db: -11.432 + phi_rad: 1.5707963267948966 + theta_rad: 1.53588974175501 + } + gain_value { + gain_db: -12.6 + phi_rad: 1.5707963267948966 + theta_rad: 1.536762406381007 + } + gain_value { + gain_db: -14.195 + phi_rad: 1.5707963267948966 + theta_rad: 1.5376350710070041 + } + gain_value { + gain_db: -12.952 + phi_rad: 1.5707963267948966 + theta_rad: 1.5385077356330017 + } + gain_value { + gain_db: -11.343 + phi_rad: 1.5707963267948966 + theta_rad: 1.5393804002589988 + } + gain_value { + gain_db: -10.505 + phi_rad: 1.5707963267948966 + theta_rad: 1.540253064884996 + } + gain_value { + gain_db: -10.255 + phi_rad: 1.5707963267948966 + theta_rad: 1.541125729510993 + } + gain_value { + gain_db: -10.536 + phi_rad: 1.5707963267948966 + theta_rad: 1.5419983941369901 + } + gain_value { + gain_db: -10.988 + phi_rad: 1.5707963267948966 + theta_rad: 1.5428710587629875 + } + gain_value { + gain_db: -11.107 + phi_rad: 1.5707963267948966 + theta_rad: 1.5437437233889846 + } + gain_value { + gain_db: -10.86 + phi_rad: 1.5707963267948966 + theta_rad: 1.5446163880149817 + } + gain_value { + gain_db: -10.411 + phi_rad: 1.5707963267948966 + theta_rad: 1.5454890526409788 + } + gain_value { + gain_db: -10.177 + phi_rad: 1.5707963267948966 + theta_rad: 1.546361717266976 + } + gain_value { + gain_db: -10.098 + phi_rad: 1.5707963267948966 + theta_rad: 1.5472343818929732 + } + gain_value { + gain_db: -10.367 + phi_rad: 1.5707963267948966 + theta_rad: 1.5481070465189704 + } + gain_value { + gain_db: -11.224 + phi_rad: 1.5707963267948966 + theta_rad: 1.5489797111449675 + } + gain_value { + gain_db: -12.998 + phi_rad: 1.5707963267948966 + theta_rad: 1.5498523757709646 + } + gain_value { + gain_db: -19.251 + phi_rad: 1.5707963267948966 + theta_rad: 1.5507250403969617 + } + gain_value { + gain_db: -14.018 + phi_rad: 1.5707963267948966 + theta_rad: 1.551597705022959 + } + gain_value { + gain_db: -12.282 + phi_rad: 1.5707963267948966 + theta_rad: 1.5524703696489561 + } + gain_value { + gain_db: -11.598 + phi_rad: 1.5707963267948966 + theta_rad: 1.5533430342749532 + } + gain_value { + gain_db: -11.121 + phi_rad: 1.5707963267948966 + theta_rad: 1.5542156989009503 + } + gain_value { + gain_db: -10.828 + phi_rad: 1.5707963267948966 + theta_rad: 1.5550883635269475 + } + gain_value { + gain_db: -10.726 + phi_rad: 1.5707963267948966 + theta_rad: 1.5559610281529448 + } + gain_value { + gain_db: -10.899 + phi_rad: 1.5707963267948966 + theta_rad: 1.556833692778942 + } + gain_value { + gain_db: -10.94 + phi_rad: 1.5707963267948966 + theta_rad: 1.557706357404939 + } + gain_value { + gain_db: -10.957 + phi_rad: 1.5707963267948966 + theta_rad: 1.5585790220309363 + } + gain_value { + gain_db: -11.409 + phi_rad: 1.5707963267948966 + theta_rad: 1.5594516866569335 + } + gain_value { + gain_db: -11.925 + phi_rad: 1.5707963267948966 + theta_rad: 1.5603243512829308 + } + gain_value { + gain_db: -12.979 + phi_rad: 1.5707963267948966 + theta_rad: 1.561197015908928 + } + gain_value { + gain_db: -14.375 + phi_rad: 1.5707963267948966 + theta_rad: 1.562069680534925 + } + gain_value { + gain_db: -15.93 + phi_rad: 1.5707963267948966 + theta_rad: 1.5629423451609221 + } + gain_value { + gain_db: -16.068 + phi_rad: 1.5707963267948966 + theta_rad: 1.5638150097869192 + } + gain_value { + gain_db: -14.402 + phi_rad: 1.5707963267948966 + theta_rad: 1.5646876744129166 + } + gain_value { + gain_db: -12.42 + phi_rad: 1.5707963267948966 + theta_rad: 1.5655603390389137 + } + gain_value { + gain_db: -11.709 + phi_rad: 1.5707963267948966 + theta_rad: 1.5664330036649108 + } + gain_value { + gain_db: -11.406 + phi_rad: 1.5707963267948966 + theta_rad: 1.567305668290908 + } + gain_value { + gain_db: -11.191 + phi_rad: 1.5707963267948966 + theta_rad: 1.568178332916905 + } + gain_value { + gain_db: -11.296 + phi_rad: 1.5707963267948966 + theta_rad: 1.5690509975429023 + } + gain_value { + gain_db: -11.742 + phi_rad: 1.5707963267948966 + theta_rad: 1.5699236621688994 + } + gain_value { + gain_db: -12.22 + phi_rad: 1.5707963267948966 + theta_rad: 1.5707963267948966 + } + gain_value { + gain_db: 44.0 + phi_rad: 3.141592653589793 + theta_rad: 0.0 + } + gain_value { + gain_db: 43.976 + phi_rad: 3.141592653589793 + theta_rad: 8.726646259971648E-4 + } + gain_value { + gain_db: 43.916 + phi_rad: 3.141592653589793 + theta_rad: 0.0017453292519943296 + } + gain_value { + gain_db: 43.808 + phi_rad: 3.141592653589793 + theta_rad: 0.002617993877991494 + } + gain_value { + gain_db: 43.633 + phi_rad: 3.141592653589793 + theta_rad: 0.003490658503988659 + } + gain_value { + gain_db: 43.398 + phi_rad: 3.141592653589793 + theta_rad: 0.004363323129985824 + } + gain_value { + gain_db: 43.075 + phi_rad: 3.141592653589793 + theta_rad: 0.005235987755982988 + } + gain_value { + gain_db: 42.63 + phi_rad: 3.141592653589793 + theta_rad: 0.006108652381980153 + } + gain_value { + gain_db: 42.106 + phi_rad: 3.141592653589793 + theta_rad: 0.006981317007977318 + } + gain_value { + gain_db: 41.433 + phi_rad: 3.141592653589793 + theta_rad: 0.007853981633974483 + } + gain_value { + gain_db: 40.618 + phi_rad: 3.141592653589793 + theta_rad: 0.008726646259971648 + } + gain_value { + gain_db: 39.644 + phi_rad: 3.141592653589793 + theta_rad: 0.009599310885968814 + } + gain_value { + gain_db: 38.639 + phi_rad: 3.141592653589793 + theta_rad: 0.010471975511965976 + } + gain_value { + gain_db: 37.457 + phi_rad: 3.141592653589793 + theta_rad: 0.011344640137963142 + } + gain_value { + gain_db: 35.995 + phi_rad: 3.141592653589793 + theta_rad: 0.012217304763960306 + } + gain_value { + gain_db: 34.567 + phi_rad: 3.141592653589793 + theta_rad: 0.013089969389957472 + } + gain_value { + gain_db: 32.894 + phi_rad: 3.141592653589793 + theta_rad: 0.013962634015954637 + } + gain_value { + gain_db: 31.221 + phi_rad: 3.141592653589793 + theta_rad: 0.014835298641951801 + } + gain_value { + gain_db: 28.973 + phi_rad: 3.141592653589793 + theta_rad: 0.015707963267948967 + } + gain_value { + gain_db: 26.871 + phi_rad: 3.141592653589793 + theta_rad: 0.01658062789394613 + } + gain_value { + gain_db: 24.999 + phi_rad: 3.141592653589793 + theta_rad: 0.017453292519943295 + } + gain_value { + gain_db: 24.241 + phi_rad: 3.141592653589793 + theta_rad: 0.01832595714594046 + } + gain_value { + gain_db: 24.093 + phi_rad: 3.141592653589793 + theta_rad: 0.019198621771937627 + } + gain_value { + gain_db: 24.072 + phi_rad: 3.141592653589793 + theta_rad: 0.02007128639793479 + } + gain_value { + gain_db: 24.117 + phi_rad: 3.141592653589793 + theta_rad: 0.020943951023931952 + } + gain_value { + gain_db: 24.162 + phi_rad: 3.141592653589793 + theta_rad: 0.02181661564992912 + } + gain_value { + gain_db: 24.16 + phi_rad: 3.141592653589793 + theta_rad: 0.022689280275926284 + } + gain_value { + gain_db: 24.083 + phi_rad: 3.141592653589793 + theta_rad: 0.02356194490192345 + } + gain_value { + gain_db: 23.895 + phi_rad: 3.141592653589793 + theta_rad: 0.024434609527920613 + } + gain_value { + gain_db: 23.631 + phi_rad: 3.141592653589793 + theta_rad: 0.02530727415391778 + } + gain_value { + gain_db: 23.282 + phi_rad: 3.141592653589793 + theta_rad: 0.026179938779914945 + } + gain_value { + gain_db: 22.922 + phi_rad: 3.141592653589793 + theta_rad: 0.027052603405912107 + } + gain_value { + gain_db: 22.522 + phi_rad: 3.141592653589793 + theta_rad: 0.027925268031909273 + } + gain_value { + gain_db: 22.122 + phi_rad: 3.141592653589793 + theta_rad: 0.028797932657906436 + } + gain_value { + gain_db: 21.75 + phi_rad: 3.141592653589793 + theta_rad: 0.029670597283903602 + } + gain_value { + gain_db: 21.342 + phi_rad: 3.141592653589793 + theta_rad: 0.030543261909900768 + } + gain_value { + gain_db: 20.94 + phi_rad: 3.141592653589793 + theta_rad: 0.031415926535897934 + } + gain_value { + gain_db: 20.471 + phi_rad: 3.141592653589793 + theta_rad: 0.0322885911618951 + } + gain_value { + gain_db: 19.96 + phi_rad: 3.141592653589793 + theta_rad: 0.03316125578789226 + } + gain_value { + gain_db: 19.398 + phi_rad: 3.141592653589793 + theta_rad: 0.034033920413889425 + } + gain_value { + gain_db: 18.815 + phi_rad: 3.141592653589793 + theta_rad: 0.03490658503988659 + } + gain_value { + gain_db: 18.185 + phi_rad: 3.141592653589793 + theta_rad: 0.03577924966588375 + } + gain_value { + gain_db: 17.595 + phi_rad: 3.141592653589793 + theta_rad: 0.03665191429188092 + } + gain_value { + gain_db: 17.17 + phi_rad: 3.141592653589793 + theta_rad: 0.03752457891787808 + } + gain_value { + gain_db: 16.825 + phi_rad: 3.141592653589793 + theta_rad: 0.038397243543875255 + } + gain_value { + gain_db: 16.566 + phi_rad: 3.141592653589793 + theta_rad: 0.039269908169872414 + } + gain_value { + gain_db: 16.383 + phi_rad: 3.141592653589793 + theta_rad: 0.04014257279586958 + } + gain_value { + gain_db: 16.327 + phi_rad: 3.141592653589793 + theta_rad: 0.041015237421866746 + } + gain_value { + gain_db: 16.376 + phi_rad: 3.141592653589793 + theta_rad: 0.041887902047863905 + } + gain_value { + gain_db: 16.507 + phi_rad: 3.141592653589793 + theta_rad: 0.04276056667386108 + } + gain_value { + gain_db: 16.7 + phi_rad: 3.141592653589793 + theta_rad: 0.04363323129985824 + } + gain_value { + gain_db: 16.892 + phi_rad: 3.141592653589793 + theta_rad: 0.0445058959258554 + } + gain_value { + gain_db: 17.071 + phi_rad: 3.141592653589793 + theta_rad: 0.04537856055185257 + } + gain_value { + gain_db: 17.157 + phi_rad: 3.141592653589793 + theta_rad: 0.046251225177849735 + } + gain_value { + gain_db: 17.155 + phi_rad: 3.141592653589793 + theta_rad: 0.0471238898038469 + } + gain_value { + gain_db: 17.005 + phi_rad: 3.141592653589793 + theta_rad: 0.04799655442984406 + } + gain_value { + gain_db: 16.721 + phi_rad: 3.141592653589793 + theta_rad: 0.048869219055841226 + } + gain_value { + gain_db: 16.278 + phi_rad: 3.141592653589793 + theta_rad: 0.04974188368183839 + } + gain_value { + gain_db: 15.747 + phi_rad: 3.141592653589793 + theta_rad: 0.05061454830783556 + } + gain_value { + gain_db: 15.032 + phi_rad: 3.141592653589793 + theta_rad: 0.051487212933832724 + } + gain_value { + gain_db: 14.278 + phi_rad: 3.141592653589793 + theta_rad: 0.05235987755982989 + } + gain_value { + gain_db: 13.458 + phi_rad: 3.141592653589793 + theta_rad: 0.05323254218582705 + } + gain_value { + gain_db: 12.386 + phi_rad: 3.141592653589793 + theta_rad: 0.054105206811824215 + } + gain_value { + gain_db: 11.44 + phi_rad: 3.141592653589793 + theta_rad: 0.05497787143782138 + } + gain_value { + gain_db: 10.237 + phi_rad: 3.141592653589793 + theta_rad: 0.05585053606381855 + } + gain_value { + gain_db: 9.0555 + phi_rad: 3.141592653589793 + theta_rad: 0.05672320068981571 + } + gain_value { + gain_db: 7.8198 + phi_rad: 3.141592653589793 + theta_rad: 0.05759586531581287 + } + gain_value { + gain_db: 6.7104 + phi_rad: 3.141592653589793 + theta_rad: 0.05846852994181004 + } + gain_value { + gain_db: 5.8155 + phi_rad: 3.141592653589793 + theta_rad: 0.059341194567807204 + } + gain_value { + gain_db: 5.2834 + phi_rad: 3.141592653589793 + theta_rad: 0.06021385919380437 + } + gain_value { + gain_db: 5.0581 + phi_rad: 3.141592653589793 + theta_rad: 0.061086523819801536 + } + gain_value { + gain_db: 5.2608 + phi_rad: 3.141592653589793 + theta_rad: 0.061959188445798695 + } + gain_value { + gain_db: 5.7828 + phi_rad: 3.141592653589793 + theta_rad: 0.06283185307179587 + } + gain_value { + gain_db: 6.3988 + phi_rad: 3.141592653589793 + theta_rad: 0.06370451769779303 + } + gain_value { + gain_db: 6.9616 + phi_rad: 3.141592653589793 + theta_rad: 0.0645771823237902 + } + gain_value { + gain_db: 7.3432 + phi_rad: 3.141592653589793 + theta_rad: 0.06544984694978735 + } + gain_value { + gain_db: 7.6657 + phi_rad: 3.141592653589793 + theta_rad: 0.06632251157578452 + } + gain_value { + gain_db: 7.8709 + phi_rad: 3.141592653589793 + theta_rad: 0.06719517620178168 + } + gain_value { + gain_db: 7.9851 + phi_rad: 3.141592653589793 + theta_rad: 0.06806784082777885 + } + gain_value { + gain_db: 7.992 + phi_rad: 3.141592653589793 + theta_rad: 0.06894050545377602 + } + gain_value { + gain_db: 7.8542 + phi_rad: 3.141592653589793 + theta_rad: 0.06981317007977318 + } + gain_value { + gain_db: 7.5366 + phi_rad: 3.141592653589793 + theta_rad: 0.07068583470577035 + } + gain_value { + gain_db: 7.0636 + phi_rad: 3.141592653589793 + theta_rad: 0.0715584993317675 + } + gain_value { + gain_db: 6.4766 + phi_rad: 3.141592653589793 + theta_rad: 0.07243116395776468 + } + gain_value { + gain_db: 5.7363 + phi_rad: 3.141592653589793 + theta_rad: 0.07330382858376185 + } + gain_value { + gain_db: 5.032 + phi_rad: 3.141592653589793 + theta_rad: 0.07417649320975901 + } + gain_value { + gain_db: 4.0503 + phi_rad: 3.141592653589793 + theta_rad: 0.07504915783575616 + } + gain_value { + gain_db: 3.1399 + phi_rad: 3.141592653589793 + theta_rad: 0.07592182246175333 + } + gain_value { + gain_db: 2.6139 + phi_rad: 3.141592653589793 + theta_rad: 0.07679448708775051 + } + gain_value { + gain_db: 2.7719 + phi_rad: 3.141592653589793 + theta_rad: 0.07766715171374766 + } + gain_value { + gain_db: 3.3723 + phi_rad: 3.141592653589793 + theta_rad: 0.07853981633974483 + } + gain_value { + gain_db: 4.1597 + phi_rad: 3.141592653589793 + theta_rad: 0.079412480965742 + } + gain_value { + gain_db: 4.9349 + phi_rad: 3.141592653589793 + theta_rad: 0.08028514559173916 + } + gain_value { + gain_db: 5.6008 + phi_rad: 3.141592653589793 + theta_rad: 0.08115781021773633 + } + gain_value { + gain_db: 6.061 + phi_rad: 3.141592653589793 + theta_rad: 0.08203047484373349 + } + gain_value { + gain_db: 6.4479 + phi_rad: 3.141592653589793 + theta_rad: 0.08290313946973066 + } + gain_value { + gain_db: 6.7211 + phi_rad: 3.141592653589793 + theta_rad: 0.08377580409572781 + } + gain_value { + gain_db: 6.9185 + phi_rad: 3.141592653589793 + theta_rad: 0.08464846872172498 + } + gain_value { + gain_db: 7.0993 + phi_rad: 3.141592653589793 + theta_rad: 0.08552113334772216 + } + gain_value { + gain_db: 7.3697 + phi_rad: 3.141592653589793 + theta_rad: 0.08639379797371932 + } + gain_value { + gain_db: 7.5697 + phi_rad: 3.141592653589793 + theta_rad: 0.08726646259971647 + } + gain_value { + gain_db: 7.6679 + phi_rad: 3.141592653589793 + theta_rad: 0.08813912722571364 + } + gain_value { + gain_db: 7.5967 + phi_rad: 3.141592653589793 + theta_rad: 0.0890117918517108 + } + gain_value { + gain_db: 7.378 + phi_rad: 3.141592653589793 + theta_rad: 0.08988445647770797 + } + gain_value { + gain_db: 7.133 + phi_rad: 3.141592653589793 + theta_rad: 0.09075712110370514 + } + gain_value { + gain_db: 6.8337 + phi_rad: 3.141592653589793 + theta_rad: 0.0916297857297023 + } + gain_value { + gain_db: 6.423 + phi_rad: 3.141592653589793 + theta_rad: 0.09250245035569947 + } + gain_value { + gain_db: 5.7363 + phi_rad: 3.141592653589793 + theta_rad: 0.09337511498169662 + } + gain_value { + gain_db: 4.9274 + phi_rad: 3.141592653589793 + theta_rad: 0.0942477796076938 + } + gain_value { + gain_db: 3.943 + phi_rad: 3.141592653589793 + theta_rad: 0.09512044423369097 + } + gain_value { + gain_db: 2.8891 + phi_rad: 3.141592653589793 + theta_rad: 0.09599310885968812 + } + gain_value { + gain_db: 1.8682 + phi_rad: 3.141592653589793 + theta_rad: 0.09686577348568529 + } + gain_value { + gain_db: 0.84873 + phi_rad: 3.141592653589793 + theta_rad: 0.09773843811168245 + } + gain_value { + gain_db: -0.25453 + phi_rad: 3.141592653589793 + theta_rad: 0.09861110273767963 + } + gain_value { + gain_db: -1.4126 + phi_rad: 3.141592653589793 + theta_rad: 0.09948376736367678 + } + gain_value { + gain_db: -2.7686 + phi_rad: 3.141592653589793 + theta_rad: 0.10035643198967395 + } + gain_value { + gain_db: -3.723 + phi_rad: 3.141592653589793 + theta_rad: 0.10122909661567112 + } + gain_value { + gain_db: -3.5392 + phi_rad: 3.141592653589793 + theta_rad: 0.10210176124166827 + } + gain_value { + gain_db: -2.3171 + phi_rad: 3.141592653589793 + theta_rad: 0.10297442586766545 + } + gain_value { + gain_db: -1.1191 + phi_rad: 3.141592653589793 + theta_rad: 0.10384709049366261 + } + gain_value { + gain_db: -0.31317 + phi_rad: 3.141592653589793 + theta_rad: 0.10471975511965978 + } + gain_value { + gain_db: 0.16557 + phi_rad: 3.141592653589793 + theta_rad: 0.10559241974565693 + } + gain_value { + gain_db: 0.35993 + phi_rad: 3.141592653589793 + theta_rad: 0.1064650843716541 + } + gain_value { + gain_db: 0.54567 + phi_rad: 3.141592653589793 + theta_rad: 0.10733774899765128 + } + gain_value { + gain_db: 0.71783 + phi_rad: 3.141592653589793 + theta_rad: 0.10821041362364843 + } + gain_value { + gain_db: 0.76537 + phi_rad: 3.141592653589793 + theta_rad: 0.1090830782496456 + } + gain_value { + gain_db: 0.63157 + phi_rad: 3.141592653589793 + theta_rad: 0.10995574287564276 + } + gain_value { + gain_db: 0.36987 + phi_rad: 3.141592653589793 + theta_rad: 0.11082840750163991 + } + gain_value { + gain_db: 0.077033 + phi_rad: 3.141592653589793 + theta_rad: 0.1117010721276371 + } + gain_value { + gain_db: -0.1245 + phi_rad: 3.141592653589793 + theta_rad: 0.11257373675363426 + } + gain_value { + gain_db: -0.32917 + phi_rad: 3.141592653589793 + theta_rad: 0.11344640137963143 + } + gain_value { + gain_db: -0.58207 + phi_rad: 3.141592653589793 + theta_rad: 0.11431906600562858 + } + gain_value { + gain_db: -0.8028 + phi_rad: 3.141592653589793 + theta_rad: 0.11519173063162574 + } + gain_value { + gain_db: -0.95963 + phi_rad: 3.141592653589793 + theta_rad: 0.11606439525762292 + } + gain_value { + gain_db: -0.9561 + phi_rad: 3.141592653589793 + theta_rad: 0.11693705988362008 + } + gain_value { + gain_db: -0.73333 + phi_rad: 3.141592653589793 + theta_rad: 0.11780972450961724 + } + gain_value { + gain_db: -0.3202 + phi_rad: 3.141592653589793 + theta_rad: 0.11868238913561441 + } + gain_value { + gain_db: 0.18883 + phi_rad: 3.141592653589793 + theta_rad: 0.11955505376161157 + } + gain_value { + gain_db: 0.6001 + phi_rad: 3.141592653589793 + theta_rad: 0.12042771838760874 + } + gain_value { + gain_db: 0.82147 + phi_rad: 3.141592653589793 + theta_rad: 0.1213003830136059 + } + gain_value { + gain_db: 1.0148 + phi_rad: 3.141592653589793 + theta_rad: 0.12217304763960307 + } + gain_value { + gain_db: 1.2053 + phi_rad: 3.141592653589793 + theta_rad: 0.12304571226560022 + } + gain_value { + gain_db: 1.3575 + phi_rad: 3.141592653589793 + theta_rad: 0.12391837689159739 + } + gain_value { + gain_db: 1.4942 + phi_rad: 3.141592653589793 + theta_rad: 0.12479104151759457 + } + gain_value { + gain_db: 1.6975 + phi_rad: 3.141592653589793 + theta_rad: 0.12566370614359174 + } + gain_value { + gain_db: 1.9137 + phi_rad: 3.141592653589793 + theta_rad: 0.1265363707695889 + } + gain_value { + gain_db: 2.1149 + phi_rad: 3.141592653589793 + theta_rad: 0.12740903539558607 + } + gain_value { + gain_db: 2.2743 + phi_rad: 3.141592653589793 + theta_rad: 0.1282817000215832 + } + gain_value { + gain_db: 2.403 + phi_rad: 3.141592653589793 + theta_rad: 0.1291543646475804 + } + gain_value { + gain_db: 2.4706 + phi_rad: 3.141592653589793 + theta_rad: 0.13002702927357757 + } + gain_value { + gain_db: 2.4891 + phi_rad: 3.141592653589793 + theta_rad: 0.1308996938995747 + } + gain_value { + gain_db: 2.4134 + phi_rad: 3.141592653589793 + theta_rad: 0.13177235852557187 + } + gain_value { + gain_db: 2.2167 + phi_rad: 3.141592653589793 + theta_rad: 0.13264502315156904 + } + gain_value { + gain_db: 1.9749 + phi_rad: 3.141592653589793 + theta_rad: 0.13351768777756623 + } + gain_value { + gain_db: 1.6727 + phi_rad: 3.141592653589793 + theta_rad: 0.13439035240356337 + } + gain_value { + gain_db: 1.1426 + phi_rad: 3.141592653589793 + theta_rad: 0.13526301702956053 + } + gain_value { + gain_db: 0.5024 + phi_rad: 3.141592653589793 + theta_rad: 0.1361356816555577 + } + gain_value { + gain_db: 0.13423 + phi_rad: 3.141592653589793 + theta_rad: 0.13700834628155487 + } + gain_value { + gain_db: 0.1323 + phi_rad: 3.141592653589793 + theta_rad: 0.13788101090755203 + } + gain_value { + gain_db: 0.18947 + phi_rad: 3.141592653589793 + theta_rad: 0.1387536755335492 + } + gain_value { + gain_db: 0.18677 + phi_rad: 3.141592653589793 + theta_rad: 0.13962634015954636 + } + gain_value { + gain_db: 0.317 + phi_rad: 3.141592653589793 + theta_rad: 0.14049900478554353 + } + gain_value { + gain_db: 0.44613 + phi_rad: 3.141592653589793 + theta_rad: 0.1413716694115407 + } + gain_value { + gain_db: 0.50813 + phi_rad: 3.141592653589793 + theta_rad: 0.14224433403753786 + } + gain_value { + gain_db: 0.5046 + phi_rad: 3.141592653589793 + theta_rad: 0.143116998663535 + } + gain_value { + gain_db: 0.5201 + phi_rad: 3.141592653589793 + theta_rad: 0.1439896632895322 + } + gain_value { + gain_db: 0.4803 + phi_rad: 3.141592653589793 + theta_rad: 0.14486232791552936 + } + gain_value { + gain_db: 0.38597 + phi_rad: 3.141592653589793 + theta_rad: 0.1457349925415265 + } + gain_value { + gain_db: 0.25637 + phi_rad: 3.141592653589793 + theta_rad: 0.1466076571675237 + } + gain_value { + gain_db: 0.11543 + phi_rad: 3.141592653589793 + theta_rad: 0.14748032179352083 + } + gain_value { + gain_db: -0.039267 + phi_rad: 3.141592653589793 + theta_rad: 0.14835298641951802 + } + gain_value { + gain_db: -0.1515 + phi_rad: 3.141592653589793 + theta_rad: 0.1492256510455152 + } + gain_value { + gain_db: -0.2168 + phi_rad: 3.141592653589793 + theta_rad: 0.15009831567151233 + } + gain_value { + gain_db: -0.17253 + phi_rad: 3.141592653589793 + theta_rad: 0.15097098029750952 + } + gain_value { + gain_db: -0.012233 + phi_rad: 3.141592653589793 + theta_rad: 0.15184364492350666 + } + gain_value { + gain_db: 0.2404 + phi_rad: 3.141592653589793 + theta_rad: 0.15271630954950383 + } + gain_value { + gain_db: 0.46733 + phi_rad: 3.141592653589793 + theta_rad: 0.15358897417550102 + } + gain_value { + gain_db: 0.64847 + phi_rad: 3.141592653589793 + theta_rad: 0.15446163880149816 + } + gain_value { + gain_db: 0.8565 + phi_rad: 3.141592653589793 + theta_rad: 0.15533430342749532 + } + gain_value { + gain_db: 1.107 + phi_rad: 3.141592653589793 + theta_rad: 0.1562069680534925 + } + gain_value { + gain_db: 1.3084 + phi_rad: 3.141592653589793 + theta_rad: 0.15707963267948966 + } + gain_value { + gain_db: 1.4603 + phi_rad: 3.141592653589793 + theta_rad: 0.15795229730548685 + } + gain_value { + gain_db: 1.5021 + phi_rad: 3.141592653589793 + theta_rad: 0.158824961931484 + } + gain_value { + gain_db: 1.5438 + phi_rad: 3.141592653589793 + theta_rad: 0.15969762655748115 + } + gain_value { + gain_db: 1.554 + phi_rad: 3.141592653589793 + theta_rad: 0.16057029118347832 + } + gain_value { + gain_db: 1.5789 + phi_rad: 3.141592653589793 + theta_rad: 0.16144295580947549 + } + gain_value { + gain_db: 1.5361 + phi_rad: 3.141592653589793 + theta_rad: 0.16231562043547265 + } + gain_value { + gain_db: 1.3851 + phi_rad: 3.141592653589793 + theta_rad: 0.16318828506146982 + } + gain_value { + gain_db: 1.0435 + phi_rad: 3.141592653589793 + theta_rad: 0.16406094968746698 + } + gain_value { + gain_db: 0.53903 + phi_rad: 3.141592653589793 + theta_rad: 0.16493361431346412 + } + gain_value { + gain_db: -0.12587 + phi_rad: 3.141592653589793 + theta_rad: 0.16580627893946132 + } + gain_value { + gain_db: -0.93523 + phi_rad: 3.141592653589793 + theta_rad: 0.16667894356545848 + } + gain_value { + gain_db: -1.4858 + phi_rad: 3.141592653589793 + theta_rad: 0.16755160819145562 + } + gain_value { + gain_db: -1.624 + phi_rad: 3.141592653589793 + theta_rad: 0.1684242728174528 + } + gain_value { + gain_db: -1.2365 + phi_rad: 3.141592653589793 + theta_rad: 0.16929693744344995 + } + gain_value { + gain_db: -0.76463 + phi_rad: 3.141592653589793 + theta_rad: 0.17016960206944712 + } + gain_value { + gain_db: -0.49487 + phi_rad: 3.141592653589793 + theta_rad: 0.1710422666954443 + } + gain_value { + gain_db: -0.36093 + phi_rad: 3.141592653589793 + theta_rad: 0.17191493132144145 + } + gain_value { + gain_db: -0.3104 + phi_rad: 3.141592653589793 + theta_rad: 0.17278759594743864 + } + gain_value { + gain_db: -0.35113 + phi_rad: 3.141592653589793 + theta_rad: 0.17366026057343578 + } + gain_value { + gain_db: -0.44743 + phi_rad: 3.141592653589793 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: -0.55527 + phi_rad: 3.141592653589793 + theta_rad: 0.17540558982543014 + } + gain_value { + gain_db: -0.63613 + phi_rad: 3.141592653589793 + theta_rad: 0.17627825445142728 + } + gain_value { + gain_db: -0.66933 + phi_rad: 3.141592653589793 + theta_rad: 0.17715091907742445 + } + gain_value { + gain_db: -0.68187 + phi_rad: 3.141592653589793 + theta_rad: 0.1780235837034216 + } + gain_value { + gain_db: -0.5773 + phi_rad: 3.141592653589793 + theta_rad: 0.17889624832941878 + } + gain_value { + gain_db: -0.41947 + phi_rad: 3.141592653589793 + theta_rad: 0.17976891295541594 + } + gain_value { + gain_db: -0.24643 + phi_rad: 3.141592653589793 + theta_rad: 0.1806415775814131 + } + gain_value { + gain_db: -0.13807 + phi_rad: 3.141592653589793 + theta_rad: 0.18151424220741028 + } + gain_value { + gain_db: -0.1089 + phi_rad: 3.141592653589793 + theta_rad: 0.18238690683340741 + } + gain_value { + gain_db: -0.17783 + phi_rad: 3.141592653589793 + theta_rad: 0.1832595714594046 + } + gain_value { + gain_db: -0.30407 + phi_rad: 3.141592653589793 + theta_rad: 0.18413223608540177 + } + gain_value { + gain_db: -0.51617 + phi_rad: 3.141592653589793 + theta_rad: 0.18500490071139894 + } + gain_value { + gain_db: -0.744 + phi_rad: 3.141592653589793 + theta_rad: 0.1858775653373961 + } + gain_value { + gain_db: -1.1115 + phi_rad: 3.141592653589793 + theta_rad: 0.18675022996339324 + } + gain_value { + gain_db: -1.4133 + phi_rad: 3.141592653589793 + theta_rad: 0.18762289458939044 + } + gain_value { + gain_db: -1.8047 + phi_rad: 3.141592653589793 + theta_rad: 0.1884955592153876 + } + gain_value { + gain_db: -2.1759 + phi_rad: 3.141592653589793 + theta_rad: 0.18936822384138474 + } + gain_value { + gain_db: -2.577 + phi_rad: 3.141592653589793 + theta_rad: 0.19024088846738194 + } + gain_value { + gain_db: -3.0234 + phi_rad: 3.141592653589793 + theta_rad: 0.19111355309337907 + } + gain_value { + gain_db: -3.3166 + phi_rad: 3.141592653589793 + theta_rad: 0.19198621771937624 + } + gain_value { + gain_db: -3.5826 + phi_rad: 3.141592653589793 + theta_rad: 0.19285888234537343 + } + gain_value { + gain_db: -3.9594 + phi_rad: 3.141592653589793 + theta_rad: 0.19373154697137057 + } + gain_value { + gain_db: -4.339 + phi_rad: 3.141592653589793 + theta_rad: 0.19460421159736774 + } + gain_value { + gain_db: -4.6269 + phi_rad: 3.141592653589793 + theta_rad: 0.1954768762233649 + } + gain_value { + gain_db: -4.8003 + phi_rad: 3.141592653589793 + theta_rad: 0.19634954084936207 + } + gain_value { + gain_db: -4.9373 + phi_rad: 3.141592653589793 + theta_rad: 0.19722220547535926 + } + gain_value { + gain_db: -5.1408 + phi_rad: 3.141592653589793 + theta_rad: 0.1980948701013564 + } + gain_value { + gain_db: -5.4981 + phi_rad: 3.141592653589793 + theta_rad: 0.19896753472735357 + } + gain_value { + gain_db: -6.0095 + phi_rad: 3.141592653589793 + theta_rad: 0.19984019935335073 + } + gain_value { + gain_db: -6.6767 + phi_rad: 3.141592653589793 + theta_rad: 0.2007128639793479 + } + gain_value { + gain_db: -7.4834 + phi_rad: 3.141592653589793 + theta_rad: 0.20158552860534507 + } + gain_value { + gain_db: -8.9615 + phi_rad: 3.141592653589793 + theta_rad: 0.20245819323134223 + } + gain_value { + gain_db: -8.8837 + phi_rad: 3.141592653589793 + theta_rad: 0.2033308578573394 + } + gain_value { + gain_db: -6.6273 + phi_rad: 3.141592653589793 + theta_rad: 0.20420352248333654 + } + gain_value { + gain_db: -4.9177 + phi_rad: 3.141592653589793 + theta_rad: 0.20507618710933373 + } + gain_value { + gain_db: -3.6865 + phi_rad: 3.141592653589793 + theta_rad: 0.2059488517353309 + } + gain_value { + gain_db: -2.7679 + phi_rad: 3.141592653589793 + theta_rad: 0.20682151636132803 + } + gain_value { + gain_db: -2.0976 + phi_rad: 3.141592653589793 + theta_rad: 0.20769418098732523 + } + gain_value { + gain_db: -1.5375 + phi_rad: 3.141592653589793 + theta_rad: 0.20856684561332237 + } + gain_value { + gain_db: -1.1339 + phi_rad: 3.141592653589793 + theta_rad: 0.20943951023931956 + } + gain_value { + gain_db: -0.93993 + phi_rad: 3.141592653589793 + theta_rad: 0.21031217486531673 + } + gain_value { + gain_db: -0.9818 + phi_rad: 3.141592653589793 + theta_rad: 0.21118483949131386 + } + gain_value { + gain_db: -1.3332 + phi_rad: 3.141592653589793 + theta_rad: 0.21205750411731106 + } + gain_value { + gain_db: -1.9082 + phi_rad: 3.141592653589793 + theta_rad: 0.2129301687433082 + } + gain_value { + gain_db: -2.9673 + phi_rad: 3.141592653589793 + theta_rad: 0.21380283336930536 + } + gain_value { + gain_db: -4.813 + phi_rad: 3.141592653589793 + theta_rad: 0.21467549799530256 + } + gain_value { + gain_db: -6.9744 + phi_rad: 3.141592653589793 + theta_rad: 0.2155481626212997 + } + gain_value { + gain_db: -5.1822 + phi_rad: 3.141592653589793 + theta_rad: 0.21642082724729686 + } + gain_value { + gain_db: -3.7789 + phi_rad: 3.141592653589793 + theta_rad: 0.21729349187329403 + } + gain_value { + gain_db: -2.9906 + phi_rad: 3.141592653589793 + theta_rad: 0.2181661564992912 + } + gain_value { + gain_db: -2.6453 + phi_rad: 3.141592653589793 + theta_rad: 0.21903882112528836 + } + gain_value { + gain_db: -2.6669 + phi_rad: 3.141592653589793 + theta_rad: 0.21991148575128552 + } + gain_value { + gain_db: -2.9519 + phi_rad: 3.141592653589793 + theta_rad: 0.2207841503772827 + } + gain_value { + gain_db: -3.4311 + phi_rad: 3.141592653589793 + theta_rad: 0.22165681500327983 + } + gain_value { + gain_db: -4.0734 + phi_rad: 3.141592653589793 + theta_rad: 0.22252947962927702 + } + gain_value { + gain_db: -4.8897 + phi_rad: 3.141592653589793 + theta_rad: 0.2234021442552742 + } + gain_value { + gain_db: -6.0262 + phi_rad: 3.141592653589793 + theta_rad: 0.22427480888127135 + } + gain_value { + gain_db: -7.0807 + phi_rad: 3.141592653589793 + theta_rad: 0.22514747350726852 + } + gain_value { + gain_db: -8.23 + phi_rad: 3.141592653589793 + theta_rad: 0.22602013813326566 + } + gain_value { + gain_db: -9.7166 + phi_rad: 3.141592653589793 + theta_rad: 0.22689280275926285 + } + gain_value { + gain_db: -11.593 + phi_rad: 3.141592653589793 + theta_rad: 0.22776546738526002 + } + gain_value { + gain_db: -13.491 + phi_rad: 3.141592653589793 + theta_rad: 0.22863813201125716 + } + gain_value { + gain_db: -15.78 + phi_rad: 3.141592653589793 + theta_rad: 0.22951079663725435 + } + gain_value { + gain_db: -13.346 + phi_rad: 3.141592653589793 + theta_rad: 0.2303834612632515 + } + gain_value { + gain_db: -9.8015 + phi_rad: 3.141592653589793 + theta_rad: 0.23125612588924865 + } + gain_value { + gain_db: -7.3508 + phi_rad: 3.141592653589793 + theta_rad: 0.23212879051524585 + } + gain_value { + gain_db: -5.6411 + phi_rad: 3.141592653589793 + theta_rad: 0.23300145514124299 + } + gain_value { + gain_db: -4.311 + phi_rad: 3.141592653589793 + theta_rad: 0.23387411976724015 + } + gain_value { + gain_db: -2.8472 + phi_rad: 3.141592653589793 + theta_rad: 0.23474678439323732 + } + gain_value { + gain_db: -1.7115 + phi_rad: 3.141592653589793 + theta_rad: 0.23561944901923448 + } + gain_value { + gain_db: -0.60113 + phi_rad: 3.141592653589793 + theta_rad: 0.23649211364523168 + } + gain_value { + gain_db: 0.2923 + phi_rad: 3.141592653589793 + theta_rad: 0.23736477827122882 + } + gain_value { + gain_db: 1.1281 + phi_rad: 3.141592653589793 + theta_rad: 0.23823744289722598 + } + gain_value { + gain_db: 1.787 + phi_rad: 3.141592653589793 + theta_rad: 0.23911010752322315 + } + gain_value { + gain_db: 2.3166 + phi_rad: 3.141592653589793 + theta_rad: 0.2399827721492203 + } + gain_value { + gain_db: 2.7579 + phi_rad: 3.141592653589793 + theta_rad: 0.24085543677521748 + } + gain_value { + gain_db: 3.1483 + phi_rad: 3.141592653589793 + theta_rad: 0.24172810140121465 + } + gain_value { + gain_db: 3.4723 + phi_rad: 3.141592653589793 + theta_rad: 0.2426007660272118 + } + gain_value { + gain_db: 3.7095 + phi_rad: 3.141592653589793 + theta_rad: 0.24347343065320895 + } + gain_value { + gain_db: 3.8821 + phi_rad: 3.141592653589793 + theta_rad: 0.24434609527920614 + } + gain_value { + gain_db: 3.9794 + phi_rad: 3.141592653589793 + theta_rad: 0.2452187599052033 + } + gain_value { + gain_db: 4.0253 + phi_rad: 3.141592653589793 + theta_rad: 0.24609142453120045 + } + gain_value { + gain_db: 4.048 + phi_rad: 3.141592653589793 + theta_rad: 0.24696408915719764 + } + gain_value { + gain_db: 4.0292 + phi_rad: 3.141592653589793 + theta_rad: 0.24783675378319478 + } + gain_value { + gain_db: 3.9738 + phi_rad: 3.141592653589793 + theta_rad: 0.24870941840919197 + } + gain_value { + gain_db: 3.88 + phi_rad: 3.141592653589793 + theta_rad: 0.24958208303518914 + } + gain_value { + gain_db: 3.7282 + phi_rad: 3.141592653589793 + theta_rad: 0.2504547476611863 + } + gain_value { + gain_db: 3.5236 + phi_rad: 3.141592653589793 + theta_rad: 0.25132741228718347 + } + gain_value { + gain_db: 3.2934 + phi_rad: 3.141592653589793 + theta_rad: 0.2522000769131806 + } + gain_value { + gain_db: 2.986 + phi_rad: 3.141592653589793 + theta_rad: 0.2530727415391778 + } + gain_value { + gain_db: 2.6803 + phi_rad: 3.141592653589793 + theta_rad: 0.25394540616517497 + } + gain_value { + gain_db: 2.3494 + phi_rad: 3.141592653589793 + theta_rad: 0.25481807079117214 + } + gain_value { + gain_db: 1.9341 + phi_rad: 3.141592653589793 + theta_rad: 0.2556907354171693 + } + gain_value { + gain_db: 1.4419 + phi_rad: 3.141592653589793 + theta_rad: 0.2565634000431664 + } + gain_value { + gain_db: 0.79797 + phi_rad: 3.141592653589793 + theta_rad: 0.25743606466916363 + } + gain_value { + gain_db: 0.19433 + phi_rad: 3.141592653589793 + theta_rad: 0.2583087292951608 + } + gain_value { + gain_db: -0.58113 + phi_rad: 3.141592653589793 + theta_rad: 0.2591813939211579 + } + gain_value { + gain_db: -1.2734 + phi_rad: 3.141592653589793 + theta_rad: 0.26005405854715513 + } + gain_value { + gain_db: -1.9635 + phi_rad: 3.141592653589793 + theta_rad: 0.26092672317315224 + } + gain_value { + gain_db: -2.6833 + phi_rad: 3.141592653589793 + theta_rad: 0.2617993877991494 + } + gain_value { + gain_db: -3.2427 + phi_rad: 3.141592653589793 + theta_rad: 0.26267205242514663 + } + gain_value { + gain_db: -3.8005 + phi_rad: 3.141592653589793 + theta_rad: 0.26354471705114374 + } + gain_value { + gain_db: -4.4592 + phi_rad: 3.141592653589793 + theta_rad: 0.2644173816771409 + } + gain_value { + gain_db: -5.0228 + phi_rad: 3.141592653589793 + theta_rad: 0.26529004630313807 + } + gain_value { + gain_db: -5.417 + phi_rad: 3.141592653589793 + theta_rad: 0.26616271092913524 + } + gain_value { + gain_db: -5.6456 + phi_rad: 3.141592653589793 + theta_rad: 0.26703537555513246 + } + gain_value { + gain_db: -5.7211 + phi_rad: 3.141592653589793 + theta_rad: 0.26790804018112957 + } + gain_value { + gain_db: -5.7859 + phi_rad: 3.141592653589793 + theta_rad: 0.26878070480712674 + } + gain_value { + gain_db: -5.927 + phi_rad: 3.141592653589793 + theta_rad: 0.2696533694331239 + } + gain_value { + gain_db: -6.1505 + phi_rad: 3.141592653589793 + theta_rad: 0.27052603405912107 + } + gain_value { + gain_db: -6.4672 + phi_rad: 3.141592653589793 + theta_rad: 0.27139869868511823 + } + gain_value { + gain_db: -6.8098 + phi_rad: 3.141592653589793 + theta_rad: 0.2722713633111154 + } + gain_value { + gain_db: -6.9899 + phi_rad: 3.141592653589793 + theta_rad: 0.27314402793711257 + } + gain_value { + gain_db: -7.1166 + phi_rad: 3.141592653589793 + theta_rad: 0.27401669256310973 + } + gain_value { + gain_db: -7.2299 + phi_rad: 3.141592653589793 + theta_rad: 0.2748893571891069 + } + gain_value { + gain_db: -7.314 + phi_rad: 3.141592653589793 + theta_rad: 0.27576202181510406 + } + gain_value { + gain_db: -7.4295 + phi_rad: 3.141592653589793 + theta_rad: 0.27663468644110123 + } + gain_value { + gain_db: -7.5285 + phi_rad: 3.141592653589793 + theta_rad: 0.2775073510670984 + } + gain_value { + gain_db: -7.5804 + phi_rad: 3.141592653589793 + theta_rad: 0.27838001569309556 + } + gain_value { + gain_db: -7.4773 + phi_rad: 3.141592653589793 + theta_rad: 0.2792526803190927 + } + gain_value { + gain_db: -7.28 + phi_rad: 3.141592653589793 + theta_rad: 0.2801253449450899 + } + gain_value { + gain_db: -7.0719 + phi_rad: 3.141592653589793 + theta_rad: 0.28099800957108706 + } + gain_value { + gain_db: -6.848 + phi_rad: 3.141592653589793 + theta_rad: 0.28187067419708417 + } + gain_value { + gain_db: -6.6216 + phi_rad: 3.141592653589793 + theta_rad: 0.2827433388230814 + } + gain_value { + gain_db: -6.4805 + phi_rad: 3.141592653589793 + theta_rad: 0.28361600344907856 + } + gain_value { + gain_db: -6.3863 + phi_rad: 3.141592653589793 + theta_rad: 0.2844886680750757 + } + gain_value { + gain_db: -6.1644 + phi_rad: 3.141592653589793 + theta_rad: 0.2853613327010729 + } + gain_value { + gain_db: -5.8884 + phi_rad: 3.141592653589793 + theta_rad: 0.28623399732707 + } + gain_value { + gain_db: -5.6757 + phi_rad: 3.141592653589793 + theta_rad: 0.2871066619530672 + } + gain_value { + gain_db: -5.4962 + phi_rad: 3.141592653589793 + theta_rad: 0.2879793265790644 + } + gain_value { + gain_db: -5.4633 + phi_rad: 3.141592653589793 + theta_rad: 0.28885199120506155 + } + gain_value { + gain_db: -5.3749 + phi_rad: 3.141592653589793 + theta_rad: 0.2897246558310587 + } + gain_value { + gain_db: -5.2094 + phi_rad: 3.141592653589793 + theta_rad: 0.29059732045705583 + } + gain_value { + gain_db: -4.9702 + phi_rad: 3.141592653589793 + theta_rad: 0.291469985083053 + } + gain_value { + gain_db: -4.5923 + phi_rad: 3.141592653589793 + theta_rad: 0.2923426497090502 + } + gain_value { + gain_db: -4.1838 + phi_rad: 3.141592653589793 + theta_rad: 0.2932153143350474 + } + gain_value { + gain_db: -3.8381 + phi_rad: 3.141592653589793 + theta_rad: 0.29408797896104455 + } + gain_value { + gain_db: -3.6054 + phi_rad: 3.141592653589793 + theta_rad: 0.29496064358704166 + } + gain_value { + gain_db: -3.5771 + phi_rad: 3.141592653589793 + theta_rad: 0.2958333082130388 + } + gain_value { + gain_db: -3.6778 + phi_rad: 3.141592653589793 + theta_rad: 0.29670597283903605 + } + gain_value { + gain_db: -3.7532 + phi_rad: 3.141592653589793 + theta_rad: 0.2975786374650332 + } + gain_value { + gain_db: -3.8359 + phi_rad: 3.141592653589793 + theta_rad: 0.2984513020910304 + } + gain_value { + gain_db: -3.859 + phi_rad: 3.141592653589793 + theta_rad: 0.2993239667170275 + } + gain_value { + gain_db: -3.9015 + phi_rad: 3.141592653589793 + theta_rad: 0.30019663134302466 + } + gain_value { + gain_db: -3.9832 + phi_rad: 3.141592653589793 + theta_rad: 0.3010692959690218 + } + gain_value { + gain_db: -4.0722 + phi_rad: 3.141592653589793 + theta_rad: 0.30194196059501904 + } + gain_value { + gain_db: -4.1143 + phi_rad: 3.141592653589793 + theta_rad: 0.3028146252210162 + } + gain_value { + gain_db: -4.1961 + phi_rad: 3.141592653589793 + theta_rad: 0.3036872898470133 + } + gain_value { + gain_db: -4.2052 + phi_rad: 3.141592653589793 + theta_rad: 0.3045599544730105 + } + gain_value { + gain_db: -4.245 + phi_rad: 3.141592653589793 + theta_rad: 0.30543261909900765 + } + gain_value { + gain_db: -4.4037 + phi_rad: 3.141592653589793 + theta_rad: 0.3063052837250049 + } + gain_value { + gain_db: -4.6131 + phi_rad: 3.141592653589793 + theta_rad: 0.30717794835100204 + } + gain_value { + gain_db: -4.9607 + phi_rad: 3.141592653589793 + theta_rad: 0.30805061297699915 + } + gain_value { + gain_db: -5.4464 + phi_rad: 3.141592653589793 + theta_rad: 0.3089232776029963 + } + gain_value { + gain_db: -5.9984 + phi_rad: 3.141592653589793 + theta_rad: 0.3097959422289935 + } + gain_value { + gain_db: -6.4791 + phi_rad: 3.141592653589793 + theta_rad: 0.31066860685499065 + } + gain_value { + gain_db: -6.6197 + phi_rad: 3.141592653589793 + theta_rad: 0.31154127148098787 + } + gain_value { + gain_db: -6.6296 + phi_rad: 3.141592653589793 + theta_rad: 0.312413936106985 + } + gain_value { + gain_db: -6.6644 + phi_rad: 3.141592653589793 + theta_rad: 0.31328660073298215 + } + gain_value { + gain_db: -6.7636 + phi_rad: 3.141592653589793 + theta_rad: 0.3141592653589793 + } + gain_value { + gain_db: -6.9157 + phi_rad: 3.141592653589793 + theta_rad: 0.3150319299849765 + } + gain_value { + gain_db: -7.211 + phi_rad: 3.141592653589793 + theta_rad: 0.3159045946109737 + } + gain_value { + gain_db: -7.8108 + phi_rad: 3.141592653589793 + theta_rad: 0.3167772592369708 + } + gain_value { + gain_db: -7.8417 + phi_rad: 3.141592653589793 + theta_rad: 0.317649923862968 + } + gain_value { + gain_db: -7.5635 + phi_rad: 3.141592653589793 + theta_rad: 0.31852258848896514 + } + gain_value { + gain_db: -7.1518 + phi_rad: 3.141592653589793 + theta_rad: 0.3193952531149623 + } + gain_value { + gain_db: -6.8461 + phi_rad: 3.141592653589793 + theta_rad: 0.3202679177409595 + } + gain_value { + gain_db: -6.8853 + phi_rad: 3.141592653589793 + theta_rad: 0.32114058236695664 + } + gain_value { + gain_db: -7.0833 + phi_rad: 3.141592653589793 + theta_rad: 0.3220132469929538 + } + gain_value { + gain_db: -7.4795 + phi_rad: 3.141592653589793 + theta_rad: 0.32288591161895097 + } + gain_value { + gain_db: -7.8164 + phi_rad: 3.141592653589793 + theta_rad: 0.32375857624494814 + } + gain_value { + gain_db: -8.0459 + phi_rad: 3.141592653589793 + theta_rad: 0.3246312408709453 + } + gain_value { + gain_db: -7.9365 + phi_rad: 3.141592653589793 + theta_rad: 0.3255039054969424 + } + gain_value { + gain_db: -7.7087 + phi_rad: 3.141592653589793 + theta_rad: 0.32637657012293964 + } + gain_value { + gain_db: -7.2391 + phi_rad: 3.141592653589793 + theta_rad: 0.3272492347489368 + } + gain_value { + gain_db: -6.8165 + phi_rad: 3.141592653589793 + theta_rad: 0.32812189937493397 + } + gain_value { + gain_db: -6.307 + phi_rad: 3.141592653589793 + theta_rad: 0.32899456400093113 + } + gain_value { + gain_db: -5.8976 + phi_rad: 3.141592653589793 + theta_rad: 0.32986722862692824 + } + gain_value { + gain_db: -5.4555 + phi_rad: 3.141592653589793 + theta_rad: 0.3307398932529254 + } + gain_value { + gain_db: -5.0927 + phi_rad: 3.141592653589793 + theta_rad: 0.33161255787892263 + } + gain_value { + gain_db: -4.7367 + phi_rad: 3.141592653589793 + theta_rad: 0.3324852225049198 + } + gain_value { + gain_db: -4.4337 + phi_rad: 3.141592653589793 + theta_rad: 0.33335788713091696 + } + gain_value { + gain_db: -4.1831 + phi_rad: 3.141592653589793 + theta_rad: 0.3342305517569141 + } + gain_value { + gain_db: -4.0012 + phi_rad: 3.141592653589793 + theta_rad: 0.33510321638291124 + } + gain_value { + gain_db: -3.7026 + phi_rad: 3.141592653589793 + theta_rad: 0.33597588100890846 + } + gain_value { + gain_db: -3.2818 + phi_rad: 3.141592653589793 + theta_rad: 0.3368485456349056 + } + gain_value { + gain_db: -2.9186 + phi_rad: 3.141592653589793 + theta_rad: 0.3377212102609028 + } + gain_value { + gain_db: -2.5585 + phi_rad: 3.141592653589793 + theta_rad: 0.3385938748868999 + } + gain_value { + gain_db: -2.2406 + phi_rad: 3.141592653589793 + theta_rad: 0.33946653951289707 + } + gain_value { + gain_db: -1.902 + phi_rad: 3.141592653589793 + theta_rad: 0.34033920413889424 + } + gain_value { + gain_db: -1.5583 + phi_rad: 3.141592653589793 + theta_rad: 0.34121186876489146 + } + gain_value { + gain_db: -1.3187 + phi_rad: 3.141592653589793 + theta_rad: 0.3420845333908886 + } + gain_value { + gain_db: -1.2358 + phi_rad: 3.141592653589793 + theta_rad: 0.34295719801688573 + } + gain_value { + gain_db: -1.1917 + phi_rad: 3.141592653589793 + theta_rad: 0.3438298626428829 + } + gain_value { + gain_db: -1.1722 + phi_rad: 3.141592653589793 + theta_rad: 0.34470252726888007 + } + gain_value { + gain_db: -1.1713 + phi_rad: 3.141592653589793 + theta_rad: 0.3455751918948773 + } + gain_value { + gain_db: -1.2309 + phi_rad: 3.141592653589793 + theta_rad: 0.34644785652087445 + } + gain_value { + gain_db: -1.2537 + phi_rad: 3.141592653589793 + theta_rad: 0.34732052114687156 + } + gain_value { + gain_db: -1.2644 + phi_rad: 3.141592653589793 + theta_rad: 0.34819318577286873 + } + gain_value { + gain_db: -1.2497 + phi_rad: 3.141592653589793 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -1.2124 + phi_rad: 3.141592653589793 + theta_rad: 0.34993851502486306 + } + gain_value { + gain_db: -1.1627 + phi_rad: 3.141592653589793 + theta_rad: 0.3508111796508603 + } + gain_value { + gain_db: -1.1167 + phi_rad: 3.141592653589793 + theta_rad: 0.3516838442768574 + } + gain_value { + gain_db: -1.0191 + phi_rad: 3.141592653589793 + theta_rad: 0.35255650890285456 + } + gain_value { + gain_db: -0.94963 + phi_rad: 3.141592653589793 + theta_rad: 0.3534291735288517 + } + gain_value { + gain_db: -0.97903 + phi_rad: 3.141592653589793 + theta_rad: 0.3543018381548489 + } + gain_value { + gain_db: -1.0411 + phi_rad: 3.141592653589793 + theta_rad: 0.3551745027808461 + } + gain_value { + gain_db: -1.1413 + phi_rad: 3.141592653589793 + theta_rad: 0.3560471674068432 + } + gain_value { + gain_db: -1.2905 + phi_rad: 3.141592653589793 + theta_rad: 0.3569198320328404 + } + gain_value { + gain_db: -1.3895 + phi_rad: 3.141592653589793 + theta_rad: 0.35779249665883756 + } + gain_value { + gain_db: -1.4822 + phi_rad: 3.141592653589793 + theta_rad: 0.3586651612848347 + } + gain_value { + gain_db: -1.5707 + phi_rad: 3.141592653589793 + theta_rad: 0.3595378259108319 + } + gain_value { + gain_db: -1.6967 + phi_rad: 3.141592653589793 + theta_rad: 0.36041049053682905 + } + gain_value { + gain_db: -1.8512 + phi_rad: 3.141592653589793 + theta_rad: 0.3612831551628262 + } + gain_value { + gain_db: -1.9227 + phi_rad: 3.141592653589793 + theta_rad: 0.3621558197888234 + } + gain_value { + gain_db: -2.0053 + phi_rad: 3.141592653589793 + theta_rad: 0.36302848441482055 + } + gain_value { + gain_db: -1.9951 + phi_rad: 3.141592653589793 + theta_rad: 0.3639011490408177 + } + gain_value { + gain_db: -1.9618 + phi_rad: 3.141592653589793 + theta_rad: 0.36477381366681483 + } + gain_value { + gain_db: -1.9458 + phi_rad: 3.141592653589793 + theta_rad: 0.36564647829281205 + } + gain_value { + gain_db: -2.06 + phi_rad: 3.141592653589793 + theta_rad: 0.3665191429188092 + } + gain_value { + gain_db: -2.2147 + phi_rad: 3.141592653589793 + theta_rad: 0.3673918075448064 + } + gain_value { + gain_db: -2.4151 + phi_rad: 3.141592653589793 + theta_rad: 0.36826447217080355 + } + gain_value { + gain_db: -2.6575 + phi_rad: 3.141592653589793 + theta_rad: 0.36913713679680066 + } + gain_value { + gain_db: -2.9241 + phi_rad: 3.141592653589793 + theta_rad: 0.3700098014227979 + } + gain_value { + gain_db: -3.1879 + phi_rad: 3.141592653589793 + theta_rad: 0.37088246604879505 + } + gain_value { + gain_db: -3.3916 + phi_rad: 3.141592653589793 + theta_rad: 0.3717551306747922 + } + gain_value { + gain_db: -3.5488 + phi_rad: 3.141592653589793 + theta_rad: 0.3726277953007894 + } + gain_value { + gain_db: -3.6542 + phi_rad: 3.141592653589793 + theta_rad: 0.3735004599267865 + } + gain_value { + gain_db: -3.7144 + phi_rad: 3.141592653589793 + theta_rad: 0.37437312455278365 + } + gain_value { + gain_db: -3.6265 + phi_rad: 3.141592653589793 + theta_rad: 0.3752457891787809 + } + gain_value { + gain_db: -3.5086 + phi_rad: 3.141592653589793 + theta_rad: 0.37611845380477804 + } + gain_value { + gain_db: -3.4354 + phi_rad: 3.141592653589793 + theta_rad: 0.3769911184307752 + } + gain_value { + gain_db: -3.4284 + phi_rad: 3.141592653589793 + theta_rad: 0.3778637830567723 + } + gain_value { + gain_db: -3.5237 + phi_rad: 3.141592653589793 + theta_rad: 0.3787364476827695 + } + gain_value { + gain_db: -3.5465 + phi_rad: 3.141592653589793 + theta_rad: 0.37960911230876665 + } + gain_value { + gain_db: -3.6778 + phi_rad: 3.141592653589793 + theta_rad: 0.38048177693476387 + } + gain_value { + gain_db: -3.9372 + phi_rad: 3.141592653589793 + theta_rad: 0.38135444156076104 + } + gain_value { + gain_db: -4.3509 + phi_rad: 3.141592653589793 + theta_rad: 0.38222710618675815 + } + gain_value { + gain_db: -4.8931 + phi_rad: 3.141592653589793 + theta_rad: 0.3830997708127553 + } + gain_value { + gain_db: -5.3556 + phi_rad: 3.141592653589793 + theta_rad: 0.3839724354387525 + } + gain_value { + gain_db: -5.73 + phi_rad: 3.141592653589793 + theta_rad: 0.3848451000647497 + } + gain_value { + gain_db: -5.9574 + phi_rad: 3.141592653589793 + theta_rad: 0.38571776469074687 + } + gain_value { + gain_db: -6.1517 + phi_rad: 3.141592653589793 + theta_rad: 0.386590429316744 + } + gain_value { + gain_db: -6.2859 + phi_rad: 3.141592653589793 + theta_rad: 0.38746309394274114 + } + gain_value { + gain_db: -6.4289 + phi_rad: 3.141592653589793 + theta_rad: 0.3883357585687383 + } + gain_value { + gain_db: -6.579 + phi_rad: 3.141592653589793 + theta_rad: 0.3892084231947355 + } + gain_value { + gain_db: -6.7922 + phi_rad: 3.141592653589793 + theta_rad: 0.3900810878207327 + } + gain_value { + gain_db: -7.042 + phi_rad: 3.141592653589793 + theta_rad: 0.3909537524467298 + } + gain_value { + gain_db: -7.3836 + phi_rad: 3.141592653589793 + theta_rad: 0.391826417072727 + } + gain_value { + gain_db: -7.8829 + phi_rad: 3.141592653589793 + theta_rad: 0.39269908169872414 + } + gain_value { + gain_db: -8.4843 + phi_rad: 3.141592653589793 + theta_rad: 0.3935717463247213 + } + gain_value { + gain_db: -9.0635 + phi_rad: 3.141592653589793 + theta_rad: 0.3944444109507185 + } + gain_value { + gain_db: -9.4191 + phi_rad: 3.141592653589793 + theta_rad: 0.39531707557671564 + } + gain_value { + gain_db: -9.4791 + phi_rad: 3.141592653589793 + theta_rad: 0.3961897402027128 + } + gain_value { + gain_db: -9.3571 + phi_rad: 3.141592653589793 + theta_rad: 0.39706240482870997 + } + gain_value { + gain_db: -9.1698 + phi_rad: 3.141592653589793 + theta_rad: 0.39793506945470714 + } + gain_value { + gain_db: -8.9834 + phi_rad: 3.141592653589793 + theta_rad: 0.3988077340807043 + } + gain_value { + gain_db: -8.7856 + phi_rad: 3.141592653589793 + theta_rad: 0.39968039870670147 + } + gain_value { + gain_db: -8.5259 + phi_rad: 3.141592653589793 + theta_rad: 0.40055306333269863 + } + gain_value { + gain_db: -8.2178 + phi_rad: 3.141592653589793 + theta_rad: 0.4014257279586958 + } + gain_value { + gain_db: -7.9212 + phi_rad: 3.141592653589793 + theta_rad: 0.40229839258469297 + } + gain_value { + gain_db: -7.7665 + phi_rad: 3.141592653589793 + theta_rad: 0.40317105721069013 + } + gain_value { + gain_db: -7.8006 + phi_rad: 3.141592653589793 + theta_rad: 0.40404372183668724 + } + gain_value { + gain_db: -7.8971 + phi_rad: 3.141592653589793 + theta_rad: 0.40491638646268446 + } + gain_value { + gain_db: -8.104 + phi_rad: 3.141592653589793 + theta_rad: 0.40578905108868163 + } + gain_value { + gain_db: -8.4501 + phi_rad: 3.141592653589793 + theta_rad: 0.4066617157146788 + } + gain_value { + gain_db: -8.6578 + phi_rad: 3.141592653589793 + theta_rad: 0.40753438034067596 + } + gain_value { + gain_db: -8.8727 + phi_rad: 3.141592653589793 + theta_rad: 0.40840704496667307 + } + gain_value { + gain_db: -8.9768 + phi_rad: 3.141592653589793 + theta_rad: 0.4092797095926703 + } + gain_value { + gain_db: -8.882 + phi_rad: 3.141592653589793 + theta_rad: 0.41015237421866746 + } + gain_value { + gain_db: -8.7084 + phi_rad: 3.141592653589793 + theta_rad: 0.4110250388446646 + } + gain_value { + gain_db: -8.4998 + phi_rad: 3.141592653589793 + theta_rad: 0.4118977034706618 + } + gain_value { + gain_db: -8.3178 + phi_rad: 3.141592653589793 + theta_rad: 0.4127703680966589 + } + gain_value { + gain_db: -8.2622 + phi_rad: 3.141592653589793 + theta_rad: 0.41364303272265607 + } + gain_value { + gain_db: -8.2666 + phi_rad: 3.141592653589793 + theta_rad: 0.4145156973486533 + } + gain_value { + gain_db: -8.5327 + phi_rad: 3.141592653589793 + theta_rad: 0.41538836197465046 + } + gain_value { + gain_db: -8.9255 + phi_rad: 3.141592653589793 + theta_rad: 0.4162610266006476 + } + gain_value { + gain_db: -9.4888 + phi_rad: 3.141592653589793 + theta_rad: 0.41713369122664473 + } + gain_value { + gain_db: -10.12 + phi_rad: 3.141592653589793 + theta_rad: 0.4180063558526419 + } + gain_value { + gain_db: -10.542 + phi_rad: 3.141592653589793 + theta_rad: 0.4188790204786391 + } + gain_value { + gain_db: -10.946 + phi_rad: 3.141592653589793 + theta_rad: 0.4197516851046363 + } + gain_value { + gain_db: -11.366 + phi_rad: 3.141592653589793 + theta_rad: 0.42062434973063345 + } + gain_value { + gain_db: -11.777 + phi_rad: 3.141592653589793 + theta_rad: 0.42149701435663056 + } + gain_value { + gain_db: -11.969 + phi_rad: 3.141592653589793 + theta_rad: 0.4223696789826277 + } + gain_value { + gain_db: -11.898 + phi_rad: 3.141592653589793 + theta_rad: 0.4232423436086249 + } + gain_value { + gain_db: -11.22 + phi_rad: 3.141592653589793 + theta_rad: 0.4241150082346221 + } + gain_value { + gain_db: -10.677 + phi_rad: 3.141592653589793 + theta_rad: 0.4249876728606193 + } + gain_value { + gain_db: -10.373 + phi_rad: 3.141592653589793 + theta_rad: 0.4258603374866164 + } + gain_value { + gain_db: -10.183 + phi_rad: 3.141592653589793 + theta_rad: 0.42673300211261356 + } + gain_value { + gain_db: -10.319 + phi_rad: 3.141592653589793 + theta_rad: 0.4276056667386107 + } + gain_value { + gain_db: -10.706 + phi_rad: 3.141592653589793 + theta_rad: 0.4284783313646079 + } + gain_value { + gain_db: -11.114 + phi_rad: 3.141592653589793 + theta_rad: 0.4293509959906051 + } + gain_value { + gain_db: -11.499 + phi_rad: 3.141592653589793 + theta_rad: 0.4302236606166022 + } + gain_value { + gain_db: -11.966 + phi_rad: 3.141592653589793 + theta_rad: 0.4310963252425994 + } + gain_value { + gain_db: -12.262 + phi_rad: 3.141592653589793 + theta_rad: 0.43196898986859655 + } + gain_value { + gain_db: -12.475 + phi_rad: 3.141592653589793 + theta_rad: 0.4328416544945937 + } + gain_value { + gain_db: -12.563 + phi_rad: 3.141592653589793 + theta_rad: 0.43371431912059094 + } + gain_value { + gain_db: -12.656 + phi_rad: 3.141592653589793 + theta_rad: 0.43458698374658805 + } + gain_value { + gain_db: -12.672 + phi_rad: 3.141592653589793 + theta_rad: 0.4354596483725852 + } + gain_value { + gain_db: -12.341 + phi_rad: 3.141592653589793 + theta_rad: 0.4363323129985824 + } + gain_value { + gain_db: -11.988 + phi_rad: 3.141592653589793 + theta_rad: 0.43720497762457955 + } + gain_value { + gain_db: -11.843 + phi_rad: 3.141592653589793 + theta_rad: 0.4380776422505767 + } + gain_value { + gain_db: -11.77 + phi_rad: 3.141592653589793 + theta_rad: 0.4389503068765739 + } + gain_value { + gain_db: -12.176 + phi_rad: 3.141592653589793 + theta_rad: 0.43982297150257105 + } + gain_value { + gain_db: -12.691 + phi_rad: 3.141592653589793 + theta_rad: 0.4406956361285682 + } + gain_value { + gain_db: -13.588 + phi_rad: 3.141592653589793 + theta_rad: 0.4415683007545654 + } + gain_value { + gain_db: -14.855 + phi_rad: 3.141592653589793 + theta_rad: 0.44244096538056255 + } + gain_value { + gain_db: -15.737 + phi_rad: 3.141592653589793 + theta_rad: 0.44331363000655966 + } + gain_value { + gain_db: -14.815 + phi_rad: 3.141592653589793 + theta_rad: 0.4441862946325569 + } + gain_value { + gain_db: -13.836 + phi_rad: 3.141592653589793 + theta_rad: 0.44505895925855404 + } + gain_value { + gain_db: -13.264 + phi_rad: 3.141592653589793 + theta_rad: 0.4459316238845512 + } + gain_value { + gain_db: -12.859 + phi_rad: 3.141592653589793 + theta_rad: 0.4468042885105484 + } + gain_value { + gain_db: -12.512 + phi_rad: 3.141592653589793 + theta_rad: 0.4476769531365455 + } + gain_value { + gain_db: -12.0 + phi_rad: 3.141592653589793 + theta_rad: 0.4485496177625427 + } + gain_value { + gain_db: -11.655 + phi_rad: 3.141592653589793 + theta_rad: 0.4494222823885399 + } + gain_value { + gain_db: -11.302 + phi_rad: 3.141592653589793 + theta_rad: 0.45029494701453704 + } + gain_value { + gain_db: -11.099 + phi_rad: 3.141592653589793 + theta_rad: 0.4511676116405342 + } + gain_value { + gain_db: -11.147 + phi_rad: 3.141592653589793 + theta_rad: 0.4520402762665313 + } + gain_value { + gain_db: -11.44 + phi_rad: 3.141592653589793 + theta_rad: 0.4529129408925285 + } + gain_value { + gain_db: -11.776 + phi_rad: 3.141592653589793 + theta_rad: 0.4537856055185257 + } + gain_value { + gain_db: -11.927 + phi_rad: 3.141592653589793 + theta_rad: 0.45465827014452287 + } + gain_value { + gain_db: -12.028 + phi_rad: 3.141592653589793 + theta_rad: 0.45553093477052004 + } + gain_value { + gain_db: -11.98 + phi_rad: 3.141592653589793 + theta_rad: 0.45640359939651715 + } + gain_value { + gain_db: -11.99 + phi_rad: 3.141592653589793 + theta_rad: 0.4572762640225143 + } + gain_value { + gain_db: -11.393 + phi_rad: 3.141592653589793 + theta_rad: 0.45814892864851153 + } + gain_value { + gain_db: -9.7582 + phi_rad: 3.141592653589793 + theta_rad: 0.4590215932745087 + } + gain_value { + gain_db: -8.3286 + phi_rad: 3.141592653589793 + theta_rad: 0.45989425790050587 + } + gain_value { + gain_db: -7.4239 + phi_rad: 3.141592653589793 + theta_rad: 0.460766922526503 + } + gain_value { + gain_db: -6.8425 + phi_rad: 3.141592653589793 + theta_rad: 0.46163958715250014 + } + gain_value { + gain_db: -6.5007 + phi_rad: 3.141592653589793 + theta_rad: 0.4625122517784973 + } + gain_value { + gain_db: -6.3064 + phi_rad: 3.141592653589793 + theta_rad: 0.46338491640449453 + } + gain_value { + gain_db: -6.1244 + phi_rad: 3.141592653589793 + theta_rad: 0.4642575810304917 + } + gain_value { + gain_db: -5.8022 + phi_rad: 3.141592653589793 + theta_rad: 0.4651302456564888 + } + gain_value { + gain_db: -5.2968 + phi_rad: 3.141592653589793 + theta_rad: 0.46600291028248597 + } + gain_value { + gain_db: -4.782 + phi_rad: 3.141592653589793 + theta_rad: 0.46687557490848314 + } + gain_value { + gain_db: -4.2902 + phi_rad: 3.141592653589793 + theta_rad: 0.4677482395344803 + } + gain_value { + gain_db: -3.7788 + phi_rad: 3.141592653589793 + theta_rad: 0.4686209041604775 + } + gain_value { + gain_db: -3.3233 + phi_rad: 3.141592653589793 + theta_rad: 0.46949356878647464 + } + gain_value { + gain_db: -2.9443 + phi_rad: 3.141592653589793 + theta_rad: 0.4703662334124718 + } + gain_value { + gain_db: -2.5842 + phi_rad: 3.141592653589793 + theta_rad: 0.47123889803846897 + } + gain_value { + gain_db: -2.3993 + phi_rad: 3.141592653589793 + theta_rad: 0.47211156266446613 + } + gain_value { + gain_db: -2.2376 + phi_rad: 3.141592653589793 + theta_rad: 0.47298422729046335 + } + gain_value { + gain_db: -2.2755 + phi_rad: 3.141592653589793 + theta_rad: 0.47385689191646047 + } + gain_value { + gain_db: -2.3683 + phi_rad: 3.141592653589793 + theta_rad: 0.47472955654245763 + } + gain_value { + gain_db: -2.4107 + phi_rad: 3.141592653589793 + theta_rad: 0.4756022211684548 + } + gain_value { + gain_db: -2.2952 + phi_rad: 3.141592653589793 + theta_rad: 0.47647488579445196 + } + gain_value { + gain_db: -2.0367 + phi_rad: 3.141592653589793 + theta_rad: 0.47734755042044913 + } + gain_value { + gain_db: -1.6497 + phi_rad: 3.141592653589793 + theta_rad: 0.4782202150464463 + } + gain_value { + gain_db: -1.2592 + phi_rad: 3.141592653589793 + theta_rad: 0.47909287967244346 + } + gain_value { + gain_db: -0.94477 + phi_rad: 3.141592653589793 + theta_rad: 0.4799655442984406 + } + gain_value { + gain_db: -0.71253 + phi_rad: 3.141592653589793 + theta_rad: 0.4808382089244378 + } + gain_value { + gain_db: -0.62117 + phi_rad: 3.141592653589793 + theta_rad: 0.48171087355043496 + } + gain_value { + gain_db: -0.583 + phi_rad: 3.141592653589793 + theta_rad: 0.48258353817643207 + } + gain_value { + gain_db: -0.628 + phi_rad: 3.141592653589793 + theta_rad: 0.4834562028024293 + } + gain_value { + gain_db: -0.7772 + phi_rad: 3.141592653589793 + theta_rad: 0.48432886742842646 + } + gain_value { + gain_db: -1.0159 + phi_rad: 3.141592653589793 + theta_rad: 0.4852015320544236 + } + gain_value { + gain_db: -1.2485 + phi_rad: 3.141592653589793 + theta_rad: 0.4860741966804208 + } + gain_value { + gain_db: -1.3758 + phi_rad: 3.141592653589793 + theta_rad: 0.4869468613064179 + } + gain_value { + gain_db: -1.3095 + phi_rad: 3.141592653589793 + theta_rad: 0.4878195259324151 + } + gain_value { + gain_db: -1.0838 + phi_rad: 3.141592653589793 + theta_rad: 0.4886921905584123 + } + gain_value { + gain_db: -0.76763 + phi_rad: 3.141592653589793 + theta_rad: 0.48956485518440945 + } + gain_value { + gain_db: -0.55103 + phi_rad: 3.141592653589793 + theta_rad: 0.4904375198104066 + } + gain_value { + gain_db: -0.47933 + phi_rad: 3.141592653589793 + theta_rad: 0.49131018443640373 + } + gain_value { + gain_db: -0.509 + phi_rad: 3.141592653589793 + theta_rad: 0.4921828490624009 + } + gain_value { + gain_db: -0.6595 + phi_rad: 3.141592653589793 + theta_rad: 0.4930555136883981 + } + gain_value { + gain_db: -0.90453 + phi_rad: 3.141592653589793 + theta_rad: 0.4939281783143953 + } + gain_value { + gain_db: -1.2292 + phi_rad: 3.141592653589793 + theta_rad: 0.49480084294039245 + } + gain_value { + gain_db: -1.6778 + phi_rad: 3.141592653589793 + theta_rad: 0.49567350756638956 + } + gain_value { + gain_db: -2.1062 + phi_rad: 3.141592653589793 + theta_rad: 0.4965461721923867 + } + gain_value { + gain_db: -2.4836 + phi_rad: 3.141592653589793 + theta_rad: 0.49741883681838395 + } + gain_value { + gain_db: -2.669 + phi_rad: 3.141592653589793 + theta_rad: 0.4982915014443811 + } + gain_value { + gain_db: -2.6642 + phi_rad: 3.141592653589793 + theta_rad: 0.4991641660703783 + } + gain_value { + gain_db: -2.5526 + phi_rad: 3.141592653589793 + theta_rad: 0.5000368306963754 + } + gain_value { + gain_db: -2.4698 + phi_rad: 3.141592653589793 + theta_rad: 0.5009094953223726 + } + gain_value { + gain_db: -2.4227 + phi_rad: 3.141592653589793 + theta_rad: 0.5017821599483697 + } + gain_value { + gain_db: -2.5371 + phi_rad: 3.141592653589793 + theta_rad: 0.5026548245743669 + } + gain_value { + gain_db: -2.8237 + phi_rad: 3.141592653589793 + theta_rad: 0.503527489200364 + } + gain_value { + gain_db: -3.3829 + phi_rad: 3.141592653589793 + theta_rad: 0.5044001538263612 + } + gain_value { + gain_db: -4.0685 + phi_rad: 3.141592653589793 + theta_rad: 0.5052728184523584 + } + gain_value { + gain_db: -4.7839 + phi_rad: 3.141592653589793 + theta_rad: 0.5061454830783556 + } + gain_value { + gain_db: -5.0564 + phi_rad: 3.141592653589793 + theta_rad: 0.5070181477043527 + } + gain_value { + gain_db: -4.974 + phi_rad: 3.141592653589793 + theta_rad: 0.5078908123303499 + } + gain_value { + gain_db: -4.8532 + phi_rad: 3.141592653589793 + theta_rad: 0.508763476956347 + } + gain_value { + gain_db: -4.8468 + phi_rad: 3.141592653589793 + theta_rad: 0.5096361415823443 + } + gain_value { + gain_db: -4.7774 + phi_rad: 3.141592653589793 + theta_rad: 0.5105088062083414 + } + gain_value { + gain_db: -4.8412 + phi_rad: 3.141592653589793 + theta_rad: 0.5113814708343386 + } + gain_value { + gain_db: -4.7851 + phi_rad: 3.141592653589793 + theta_rad: 0.5122541354603357 + } + gain_value { + gain_db: -4.827 + phi_rad: 3.141592653589793 + theta_rad: 0.5131268000863328 + } + gain_value { + gain_db: -4.9628 + phi_rad: 3.141592653589793 + theta_rad: 0.51399946471233 + } + gain_value { + gain_db: -5.2214 + phi_rad: 3.141592653589793 + theta_rad: 0.5148721293383273 + } + gain_value { + gain_db: -5.5397 + phi_rad: 3.141592653589793 + theta_rad: 0.5157447939643244 + } + gain_value { + gain_db: -5.8054 + phi_rad: 3.141592653589793 + theta_rad: 0.5166174585903216 + } + gain_value { + gain_db: -5.8191 + phi_rad: 3.141592653589793 + theta_rad: 0.5174901232163187 + } + gain_value { + gain_db: -5.7458 + phi_rad: 3.141592653589793 + theta_rad: 0.5183627878423158 + } + gain_value { + gain_db: -5.7382 + phi_rad: 3.141592653589793 + theta_rad: 0.519235452468313 + } + gain_value { + gain_db: -5.9267 + phi_rad: 3.141592653589793 + theta_rad: 0.5201081170943103 + } + gain_value { + gain_db: -6.1029 + phi_rad: 3.141592653589793 + theta_rad: 0.5209807817203074 + } + gain_value { + gain_db: -6.3321 + phi_rad: 3.141592653589793 + theta_rad: 0.5218534463463045 + } + gain_value { + gain_db: -6.5004 + phi_rad: 3.141592653589793 + theta_rad: 0.5227261109723017 + } + gain_value { + gain_db: -6.6567 + phi_rad: 3.141592653589793 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -6.7914 + phi_rad: 3.141592653589793 + theta_rad: 0.524471440224296 + } + gain_value { + gain_db: -6.9309 + phi_rad: 3.141592653589793 + theta_rad: 0.5253441048502933 + } + gain_value { + gain_db: -6.9614 + phi_rad: 3.141592653589793 + theta_rad: 0.5262167694762904 + } + gain_value { + gain_db: -6.8718 + phi_rad: 3.141592653589793 + theta_rad: 0.5270894341022875 + } + gain_value { + gain_db: -6.789 + phi_rad: 3.141592653589793 + theta_rad: 0.5279620987282847 + } + gain_value { + gain_db: -6.8105 + phi_rad: 3.141592653589793 + theta_rad: 0.5288347633542818 + } + gain_value { + gain_db: -6.9722 + phi_rad: 3.141592653589793 + theta_rad: 0.529707427980279 + } + gain_value { + gain_db: -7.0683 + phi_rad: 3.141592653589793 + theta_rad: 0.5305800926062761 + } + gain_value { + gain_db: -7.2356 + phi_rad: 3.141592653589793 + theta_rad: 0.5314527572322734 + } + gain_value { + gain_db: -7.5316 + phi_rad: 3.141592653589793 + theta_rad: 0.5323254218582705 + } + gain_value { + gain_db: -7.7617 + phi_rad: 3.141592653589793 + theta_rad: 0.5331980864842677 + } + gain_value { + gain_db: -7.6363 + phi_rad: 3.141592653589793 + theta_rad: 0.5340707511102649 + } + gain_value { + gain_db: -7.4599 + phi_rad: 3.141592653589793 + theta_rad: 0.534943415736262 + } + gain_value { + gain_db: -7.4797 + phi_rad: 3.141592653589793 + theta_rad: 0.5358160803622591 + } + gain_value { + gain_db: -7.6995 + phi_rad: 3.141592653589793 + theta_rad: 0.5366887449882564 + } + gain_value { + gain_db: -7.9235 + phi_rad: 3.141592653589793 + theta_rad: 0.5375614096142535 + } + gain_value { + gain_db: -7.9351 + phi_rad: 3.141592653589793 + theta_rad: 0.5384340742402507 + } + gain_value { + gain_db: -7.6953 + phi_rad: 3.141592653589793 + theta_rad: 0.5393067388662478 + } + gain_value { + gain_db: -7.2426 + phi_rad: 3.141592653589793 + theta_rad: 0.540179403492245 + } + gain_value { + gain_db: -6.8301 + phi_rad: 3.141592653589793 + theta_rad: 0.5410520681182421 + } + gain_value { + gain_db: -6.4539 + phi_rad: 3.141592653589793 + theta_rad: 0.5419247327442394 + } + gain_value { + gain_db: -6.0507 + phi_rad: 3.141592653589793 + theta_rad: 0.5427973973702365 + } + gain_value { + gain_db: -5.5861 + phi_rad: 3.141592653589793 + theta_rad: 0.5436700619962336 + } + gain_value { + gain_db: -5.2015 + phi_rad: 3.141592653589793 + theta_rad: 0.5445427266222308 + } + gain_value { + gain_db: -4.9687 + phi_rad: 3.141592653589793 + theta_rad: 0.545415391248228 + } + gain_value { + gain_db: -4.9737 + phi_rad: 3.141592653589793 + theta_rad: 0.5462880558742251 + } + gain_value { + gain_db: -5.1421 + phi_rad: 3.141592653589793 + theta_rad: 0.5471607205002224 + } + gain_value { + gain_db: -5.2439 + phi_rad: 3.141592653589793 + theta_rad: 0.5480333851262195 + } + gain_value { + gain_db: -5.2591 + phi_rad: 3.141592653589793 + theta_rad: 0.5489060497522167 + } + gain_value { + gain_db: -5.2085 + phi_rad: 3.141592653589793 + theta_rad: 0.5497787143782138 + } + gain_value { + gain_db: -5.1328 + phi_rad: 3.141592653589793 + theta_rad: 0.550651379004211 + } + gain_value { + gain_db: -5.0457 + phi_rad: 3.141592653589793 + theta_rad: 0.5515240436302081 + } + gain_value { + gain_db: -4.8272 + phi_rad: 3.141592653589793 + theta_rad: 0.5523967082562052 + } + gain_value { + gain_db: -4.6774 + phi_rad: 3.141592653589793 + theta_rad: 0.5532693728822025 + } + gain_value { + gain_db: -4.4955 + phi_rad: 3.141592653589793 + theta_rad: 0.5541420375081997 + } + gain_value { + gain_db: -4.3875 + phi_rad: 3.141592653589793 + theta_rad: 0.5550147021341968 + } + gain_value { + gain_db: -4.3533 + phi_rad: 3.141592653589793 + theta_rad: 0.555887366760194 + } + gain_value { + gain_db: -4.4539 + phi_rad: 3.141592653589793 + theta_rad: 0.5567600313861911 + } + gain_value { + gain_db: -4.5893 + phi_rad: 3.141592653589793 + theta_rad: 0.5576326960121882 + } + gain_value { + gain_db: -4.6456 + phi_rad: 3.141592653589793 + theta_rad: 0.5585053606381855 + } + gain_value { + gain_db: -4.5306 + phi_rad: 3.141592653589793 + theta_rad: 0.5593780252641826 + } + gain_value { + gain_db: -4.3738 + phi_rad: 3.141592653589793 + theta_rad: 0.5602506898901798 + } + gain_value { + gain_db: -4.2222 + phi_rad: 3.141592653589793 + theta_rad: 0.5611233545161769 + } + gain_value { + gain_db: -4.1063 + phi_rad: 3.141592653589793 + theta_rad: 0.5619960191421741 + } + gain_value { + gain_db: -4.0642 + phi_rad: 3.141592653589793 + theta_rad: 0.5628686837681712 + } + gain_value { + gain_db: -4.1221 + phi_rad: 3.141592653589793 + theta_rad: 0.5637413483941683 + } + gain_value { + gain_db: -4.2374 + phi_rad: 3.141592653589793 + theta_rad: 0.5646140130201657 + } + gain_value { + gain_db: -4.2998 + phi_rad: 3.141592653589793 + theta_rad: 0.5654866776461628 + } + gain_value { + gain_db: -4.2684 + phi_rad: 3.141592653589793 + theta_rad: 0.56635934227216 + } + gain_value { + gain_db: -4.157 + phi_rad: 3.141592653589793 + theta_rad: 0.5672320068981571 + } + gain_value { + gain_db: -3.9552 + phi_rad: 3.141592653589793 + theta_rad: 0.5681046715241542 + } + gain_value { + gain_db: -3.7148 + phi_rad: 3.141592653589793 + theta_rad: 0.5689773361501514 + } + gain_value { + gain_db: -3.454 + phi_rad: 3.141592653589793 + theta_rad: 0.5698500007761486 + } + gain_value { + gain_db: -3.3116 + phi_rad: 3.141592653589793 + theta_rad: 0.5707226654021458 + } + gain_value { + gain_db: -3.2346 + phi_rad: 3.141592653589793 + theta_rad: 0.5715953300281429 + } + gain_value { + gain_db: -3.2743 + phi_rad: 3.141592653589793 + theta_rad: 0.57246799465414 + } + gain_value { + gain_db: -3.3677 + phi_rad: 3.141592653589793 + theta_rad: 0.5733406592801373 + } + gain_value { + gain_db: -3.5671 + phi_rad: 3.141592653589793 + theta_rad: 0.5742133239061344 + } + gain_value { + gain_db: -3.7935 + phi_rad: 3.141592653589793 + theta_rad: 0.5750859885321317 + } + gain_value { + gain_db: -3.9475 + phi_rad: 3.141592653589793 + theta_rad: 0.5759586531581288 + } + gain_value { + gain_db: -3.8881 + phi_rad: 3.141592653589793 + theta_rad: 0.5768313177841259 + } + gain_value { + gain_db: -3.6448 + phi_rad: 3.141592653589793 + theta_rad: 0.5777039824101231 + } + gain_value { + gain_db: -3.3547 + phi_rad: 3.141592653589793 + theta_rad: 0.5785766470361202 + } + gain_value { + gain_db: -3.099 + phi_rad: 3.141592653589793 + theta_rad: 0.5794493116621174 + } + gain_value { + gain_db: -2.9027 + phi_rad: 3.141592653589793 + theta_rad: 0.5803219762881145 + } + gain_value { + gain_db: -2.785 + phi_rad: 3.141592653589793 + theta_rad: 0.5811946409141117 + } + gain_value { + gain_db: -2.735 + phi_rad: 3.141592653589793 + theta_rad: 0.5820673055401089 + } + gain_value { + gain_db: -2.744 + phi_rad: 3.141592653589793 + theta_rad: 0.582939970166106 + } + gain_value { + gain_db: -2.7406 + phi_rad: 3.141592653589793 + theta_rad: 0.5838126347921033 + } + gain_value { + gain_db: -2.8319 + phi_rad: 3.141592653589793 + theta_rad: 0.5846852994181004 + } + gain_value { + gain_db: -2.9689 + phi_rad: 3.141592653589793 + theta_rad: 0.5855579640440975 + } + gain_value { + gain_db: -3.1045 + phi_rad: 3.141592653589793 + theta_rad: 0.5864306286700948 + } + gain_value { + gain_db: -3.1433 + phi_rad: 3.141592653589793 + theta_rad: 0.5873032932960919 + } + gain_value { + gain_db: -3.1696 + phi_rad: 3.141592653589793 + theta_rad: 0.5881759579220891 + } + gain_value { + gain_db: -3.1616 + phi_rad: 3.141592653589793 + theta_rad: 0.5890486225480862 + } + gain_value { + gain_db: -3.1063 + phi_rad: 3.141592653589793 + theta_rad: 0.5899212871740833 + } + gain_value { + gain_db: -3.0496 + phi_rad: 3.141592653589793 + theta_rad: 0.5907939518000805 + } + gain_value { + gain_db: -2.978 + phi_rad: 3.141592653589793 + theta_rad: 0.5916666164260777 + } + gain_value { + gain_db: -2.9269 + phi_rad: 3.141592653589793 + theta_rad: 0.592539281052075 + } + gain_value { + gain_db: -2.9182 + phi_rad: 3.141592653589793 + theta_rad: 0.5934119456780721 + } + gain_value { + gain_db: -2.926 + phi_rad: 3.141592653589793 + theta_rad: 0.5942846103040692 + } + gain_value { + gain_db: -3.0244 + phi_rad: 3.141592653589793 + theta_rad: 0.5951572749300664 + } + gain_value { + gain_db: -3.191 + phi_rad: 3.141592653589793 + theta_rad: 0.5960299395560635 + } + gain_value { + gain_db: -3.3871 + phi_rad: 3.141592653589793 + theta_rad: 0.5969026041820608 + } + gain_value { + gain_db: -3.6297 + phi_rad: 3.141592653589793 + theta_rad: 0.5977752688080579 + } + gain_value { + gain_db: -3.9331 + phi_rad: 3.141592653589793 + theta_rad: 0.598647933434055 + } + gain_value { + gain_db: -4.3051 + phi_rad: 3.141592653589793 + theta_rad: 0.5995205980600522 + } + gain_value { + gain_db: -4.7241 + phi_rad: 3.141592653589793 + theta_rad: 0.6003932626860493 + } + gain_value { + gain_db: -4.8756 + phi_rad: 3.141592653589793 + theta_rad: 0.6012659273120465 + } + gain_value { + gain_db: -4.886 + phi_rad: 3.141592653589793 + theta_rad: 0.6021385919380436 + } + gain_value { + gain_db: -4.8462 + phi_rad: 3.141592653589793 + theta_rad: 0.6030112565640408 + } + gain_value { + gain_db: -4.7617 + phi_rad: 3.141592653589793 + theta_rad: 0.6038839211900381 + } + gain_value { + gain_db: -4.713 + phi_rad: 3.141592653589793 + theta_rad: 0.6047565858160352 + } + gain_value { + gain_db: -4.7304 + phi_rad: 3.141592653589793 + theta_rad: 0.6056292504420324 + } + gain_value { + gain_db: -4.9086 + phi_rad: 3.141592653589793 + theta_rad: 0.6065019150680295 + } + gain_value { + gain_db: -5.2574 + phi_rad: 3.141592653589793 + theta_rad: 0.6073745796940266 + } + gain_value { + gain_db: -5.8692 + phi_rad: 3.141592653589793 + theta_rad: 0.6082472443200239 + } + gain_value { + gain_db: -6.5438 + phi_rad: 3.141592653589793 + theta_rad: 0.609119908946021 + } + gain_value { + gain_db: -6.9592 + phi_rad: 3.141592653589793 + theta_rad: 0.6099925735720182 + } + gain_value { + gain_db: -6.9522 + phi_rad: 3.141592653589793 + theta_rad: 0.6108652381980153 + } + gain_value { + gain_db: -6.847 + phi_rad: 3.141592653589793 + theta_rad: 0.6117379028240124 + } + gain_value { + gain_db: -6.7004 + phi_rad: 3.141592653589793 + theta_rad: 0.6126105674500097 + } + gain_value { + gain_db: -6.5501 + phi_rad: 3.141592653589793 + theta_rad: 0.6134832320760069 + } + gain_value { + gain_db: -6.3693 + phi_rad: 3.141592653589793 + theta_rad: 0.6143558967020041 + } + gain_value { + gain_db: -6.0856 + phi_rad: 3.141592653589793 + theta_rad: 0.6152285613280012 + } + gain_value { + gain_db: -5.8659 + phi_rad: 3.141592653589793 + theta_rad: 0.6161012259539983 + } + gain_value { + gain_db: -5.934 + phi_rad: 3.141592653589793 + theta_rad: 0.6169738905799955 + } + gain_value { + gain_db: -6.3281 + phi_rad: 3.141592653589793 + theta_rad: 0.6178465552059926 + } + gain_value { + gain_db: -6.774 + phi_rad: 3.141592653589793 + theta_rad: 0.6187192198319899 + } + gain_value { + gain_db: -7.2051 + phi_rad: 3.141592653589793 + theta_rad: 0.619591884457987 + } + gain_value { + gain_db: -7.4961 + phi_rad: 3.141592653589793 + theta_rad: 0.6204645490839841 + } + gain_value { + gain_db: -7.5865 + phi_rad: 3.141592653589793 + theta_rad: 0.6213372137099813 + } + gain_value { + gain_db: -7.5095 + phi_rad: 3.141592653589793 + theta_rad: 0.6222098783359784 + } + gain_value { + gain_db: -7.4336 + phi_rad: 3.141592653589793 + theta_rad: 0.6230825429619757 + } + gain_value { + gain_db: -7.3252 + phi_rad: 3.141592653589793 + theta_rad: 0.6239552075879728 + } + gain_value { + gain_db: -7.2015 + phi_rad: 3.141592653589793 + theta_rad: 0.62482787221397 + } + gain_value { + gain_db: -7.123 + phi_rad: 3.141592653589793 + theta_rad: 0.6257005368399672 + } + gain_value { + gain_db: -6.9797 + phi_rad: 3.141592653589793 + theta_rad: 0.6265732014659643 + } + gain_value { + gain_db: -6.9706 + phi_rad: 3.141592653589793 + theta_rad: 0.6274458660919615 + } + gain_value { + gain_db: -7.0066 + phi_rad: 3.141592653589793 + theta_rad: 0.6283185307179586 + } + gain_value { + gain_db: -7.205 + phi_rad: 3.141592653589793 + theta_rad: 0.6291911953439557 + } + gain_value { + gain_db: -7.3727 + phi_rad: 3.141592653589793 + theta_rad: 0.630063859969953 + } + gain_value { + gain_db: -7.475 + phi_rad: 3.141592653589793 + theta_rad: 0.6309365245959501 + } + gain_value { + gain_db: -7.2561 + phi_rad: 3.141592653589793 + theta_rad: 0.6318091892219474 + } + gain_value { + gain_db: -6.7382 + phi_rad: 3.141592653589793 + theta_rad: 0.6326818538479445 + } + gain_value { + gain_db: -6.1351 + phi_rad: 3.141592653589793 + theta_rad: 0.6335545184739416 + } + gain_value { + gain_db: -5.7137 + phi_rad: 3.141592653589793 + theta_rad: 0.6344271830999388 + } + gain_value { + gain_db: -5.5073 + phi_rad: 3.141592653589793 + theta_rad: 0.635299847725936 + } + gain_value { + gain_db: -5.5285 + phi_rad: 3.141592653589793 + theta_rad: 0.6361725123519332 + } + gain_value { + gain_db: -5.7802 + phi_rad: 3.141592653589793 + theta_rad: 0.6370451769779303 + } + gain_value { + gain_db: -6.4262 + phi_rad: 3.141592653589793 + theta_rad: 0.6379178416039274 + } + gain_value { + gain_db: -7.2094 + phi_rad: 3.141592653589793 + theta_rad: 0.6387905062299246 + } + gain_value { + gain_db: -7.8738 + phi_rad: 3.141592653589793 + theta_rad: 0.6396631708559217 + } + gain_value { + gain_db: -8.1394 + phi_rad: 3.141592653589793 + theta_rad: 0.640535835481919 + } + gain_value { + gain_db: -7.7566 + phi_rad: 3.141592653589793 + theta_rad: 0.6414085001079161 + } + gain_value { + gain_db: -6.9872 + phi_rad: 3.141592653589793 + theta_rad: 0.6422811647339133 + } + gain_value { + gain_db: -6.2702 + phi_rad: 3.141592653589793 + theta_rad: 0.6431538293599105 + } + gain_value { + gain_db: -5.8721 + phi_rad: 3.141592653589793 + theta_rad: 0.6440264939859076 + } + gain_value { + gain_db: -5.7017 + phi_rad: 3.141592653589793 + theta_rad: 0.6448991586119048 + } + gain_value { + gain_db: -5.7389 + phi_rad: 3.141592653589793 + theta_rad: 0.6457718232379019 + } + gain_value { + gain_db: -5.6604 + phi_rad: 3.141592653589793 + theta_rad: 0.646644487863899 + } + gain_value { + gain_db: -5.2855 + phi_rad: 3.141592653589793 + theta_rad: 0.6475171524898963 + } + gain_value { + gain_db: -4.793 + phi_rad: 3.141592653589793 + theta_rad: 0.6483898171158934 + } + gain_value { + gain_db: -4.4677 + phi_rad: 3.141592653589793 + theta_rad: 0.6492624817418906 + } + gain_value { + gain_db: -4.2579 + phi_rad: 3.141592653589793 + theta_rad: 0.6501351463678877 + } + gain_value { + gain_db: -4.019 + phi_rad: 3.141592653589793 + theta_rad: 0.6510078109938848 + } + gain_value { + gain_db: -3.713 + phi_rad: 3.141592653589793 + theta_rad: 0.6518804756198822 + } + gain_value { + gain_db: -3.3802 + phi_rad: 3.141592653589793 + theta_rad: 0.6527531402458793 + } + gain_value { + gain_db: -3.2256 + phi_rad: 3.141592653589793 + theta_rad: 0.6536258048718765 + } + gain_value { + gain_db: -3.2004 + phi_rad: 3.141592653589793 + theta_rad: 0.6544984694978736 + } + gain_value { + gain_db: -3.3027 + phi_rad: 3.141592653589793 + theta_rad: 0.6553711341238707 + } + gain_value { + gain_db: -3.3527 + phi_rad: 3.141592653589793 + theta_rad: 0.6562437987498679 + } + gain_value { + gain_db: -3.3636 + phi_rad: 3.141592653589793 + theta_rad: 0.657116463375865 + } + gain_value { + gain_db: -3.4292 + phi_rad: 3.141592653589793 + theta_rad: 0.6579891280018623 + } + gain_value { + gain_db: -3.4259 + phi_rad: 3.141592653589793 + theta_rad: 0.6588617926278594 + } + gain_value { + gain_db: -3.4132 + phi_rad: 3.141592653589793 + theta_rad: 0.6597344572538565 + } + gain_value { + gain_db: -3.3545 + phi_rad: 3.141592653589793 + theta_rad: 0.6606071218798537 + } + gain_value { + gain_db: -3.1456 + phi_rad: 3.141592653589793 + theta_rad: 0.6614797865058508 + } + gain_value { + gain_db: -2.9419 + phi_rad: 3.141592653589793 + theta_rad: 0.6623524511318482 + } + gain_value { + gain_db: -2.7902 + phi_rad: 3.141592653589793 + theta_rad: 0.6632251157578453 + } + gain_value { + gain_db: -2.8469 + phi_rad: 3.141592653589793 + theta_rad: 0.6640977803838424 + } + gain_value { + gain_db: -3.0016 + phi_rad: 3.141592653589793 + theta_rad: 0.6649704450098396 + } + gain_value { + gain_db: -3.2017 + phi_rad: 3.141592653589793 + theta_rad: 0.6658431096358367 + } + gain_value { + gain_db: -3.4695 + phi_rad: 3.141592653589793 + theta_rad: 0.6667157742618339 + } + gain_value { + gain_db: -3.7001 + phi_rad: 3.141592653589793 + theta_rad: 0.667588438887831 + } + gain_value { + gain_db: -3.8239 + phi_rad: 3.141592653589793 + theta_rad: 0.6684611035138281 + } + gain_value { + gain_db: -4.0347 + phi_rad: 3.141592653589793 + theta_rad: 0.6693337681398254 + } + gain_value { + gain_db: -4.2057 + phi_rad: 3.141592653589793 + theta_rad: 0.6702064327658225 + } + gain_value { + gain_db: -4.253 + phi_rad: 3.141592653589793 + theta_rad: 0.6710790973918198 + } + gain_value { + gain_db: -4.2818 + phi_rad: 3.141592653589793 + theta_rad: 0.6719517620178169 + } + gain_value { + gain_db: -4.2659 + phi_rad: 3.141592653589793 + theta_rad: 0.672824426643814 + } + gain_value { + gain_db: -4.1817 + phi_rad: 3.141592653589793 + theta_rad: 0.6736970912698113 + } + gain_value { + gain_db: -4.2001 + phi_rad: 3.141592653589793 + theta_rad: 0.6745697558958084 + } + gain_value { + gain_db: -4.2287 + phi_rad: 3.141592653589793 + theta_rad: 0.6754424205218056 + } + gain_value { + gain_db: -4.2192 + phi_rad: 3.141592653589793 + theta_rad: 0.6763150851478027 + } + gain_value { + gain_db: -4.2452 + phi_rad: 3.141592653589793 + theta_rad: 0.6771877497737998 + } + gain_value { + gain_db: -4.2511 + phi_rad: 3.141592653589793 + theta_rad: 0.678060414399797 + } + gain_value { + gain_db: -4.3858 + phi_rad: 3.141592653589793 + theta_rad: 0.6789330790257941 + } + gain_value { + gain_db: -4.4696 + phi_rad: 3.141592653589793 + theta_rad: 0.6798057436517914 + } + gain_value { + gain_db: -4.5522 + phi_rad: 3.141592653589793 + theta_rad: 0.6806784082777885 + } + gain_value { + gain_db: -4.7149 + phi_rad: 3.141592653589793 + theta_rad: 0.6815510729037857 + } + gain_value { + gain_db: -4.8702 + phi_rad: 3.141592653589793 + theta_rad: 0.6824237375297829 + } + gain_value { + gain_db: -5.0057 + phi_rad: 3.141592653589793 + theta_rad: 0.68329640215578 + } + gain_value { + gain_db: -5.0418 + phi_rad: 3.141592653589793 + theta_rad: 0.6841690667817772 + } + gain_value { + gain_db: -4.8859 + phi_rad: 3.141592653589793 + theta_rad: 0.6850417314077744 + } + gain_value { + gain_db: -4.6755 + phi_rad: 3.141592653589793 + theta_rad: 0.6859143960337715 + } + gain_value { + gain_db: -4.4103 + phi_rad: 3.141592653589793 + theta_rad: 0.6867870606597687 + } + gain_value { + gain_db: -4.1626 + phi_rad: 3.141592653589793 + theta_rad: 0.6876597252857658 + } + gain_value { + gain_db: -4.0799 + phi_rad: 3.141592653589793 + theta_rad: 0.688532389911763 + } + gain_value { + gain_db: -4.1258 + phi_rad: 3.141592653589793 + theta_rad: 0.6894050545377601 + } + gain_value { + gain_db: -4.1747 + phi_rad: 3.141592653589793 + theta_rad: 0.6902777191637572 + } + gain_value { + gain_db: -4.2462 + phi_rad: 3.141592653589793 + theta_rad: 0.6911503837897546 + } + gain_value { + gain_db: -4.3211 + phi_rad: 3.141592653589793 + theta_rad: 0.6920230484157517 + } + gain_value { + gain_db: -4.5254 + phi_rad: 3.141592653589793 + theta_rad: 0.6928957130417489 + } + gain_value { + gain_db: -4.8202 + phi_rad: 3.141592653589793 + theta_rad: 0.693768377667746 + } + gain_value { + gain_db: -5.0858 + phi_rad: 3.141592653589793 + theta_rad: 0.6946410422937431 + } + gain_value { + gain_db: -5.272 + phi_rad: 3.141592653589793 + theta_rad: 0.6955137069197403 + } + gain_value { + gain_db: -5.4065 + phi_rad: 3.141592653589793 + theta_rad: 0.6963863715457375 + } + gain_value { + gain_db: -5.4039 + phi_rad: 3.141592653589793 + theta_rad: 0.6972590361717347 + } + gain_value { + gain_db: -5.3768 + phi_rad: 3.141592653589793 + theta_rad: 0.6981317007977318 + } + gain_value { + gain_db: -5.319 + phi_rad: 3.141592653589793 + theta_rad: 0.6990043654237289 + } + gain_value { + gain_db: -5.2719 + phi_rad: 3.141592653589793 + theta_rad: 0.6998770300497261 + } + gain_value { + gain_db: -5.1785 + phi_rad: 3.141592653589793 + theta_rad: 0.7007496946757232 + } + gain_value { + gain_db: -5.0838 + phi_rad: 3.141592653589793 + theta_rad: 0.7016223593017206 + } + gain_value { + gain_db: -5.0238 + phi_rad: 3.141592653589793 + theta_rad: 0.7024950239277177 + } + gain_value { + gain_db: -5.068 + phi_rad: 3.141592653589793 + theta_rad: 0.7033676885537148 + } + gain_value { + gain_db: -5.3367 + phi_rad: 3.141592653589793 + theta_rad: 0.704240353179712 + } + gain_value { + gain_db: -5.5954 + phi_rad: 3.141592653589793 + theta_rad: 0.7051130178057091 + } + gain_value { + gain_db: -5.9854 + phi_rad: 3.141592653589793 + theta_rad: 0.7059856824317063 + } + gain_value { + gain_db: -6.3753 + phi_rad: 3.141592653589793 + theta_rad: 0.7068583470577035 + } + gain_value { + gain_db: -6.5755 + phi_rad: 3.141592653589793 + theta_rad: 0.7077310116837006 + } + gain_value { + gain_db: -6.844 + phi_rad: 3.141592653589793 + theta_rad: 0.7086036763096978 + } + gain_value { + gain_db: -7.1327 + phi_rad: 3.141592653589793 + theta_rad: 0.7094763409356949 + } + gain_value { + gain_db: -7.0743 + phi_rad: 3.141592653589793 + theta_rad: 0.7103490055616922 + } + gain_value { + gain_db: -6.7335 + phi_rad: 3.141592653589793 + theta_rad: 0.7112216701876893 + } + gain_value { + gain_db: -6.4642 + phi_rad: 3.141592653589793 + theta_rad: 0.7120943348136864 + } + gain_value { + gain_db: -6.3843 + phi_rad: 3.141592653589793 + theta_rad: 0.7129669994396837 + } + gain_value { + gain_db: -6.4572 + phi_rad: 3.141592653589793 + theta_rad: 0.7138396640656808 + } + gain_value { + gain_db: -6.5863 + phi_rad: 3.141592653589793 + theta_rad: 0.714712328691678 + } + gain_value { + gain_db: -6.7592 + phi_rad: 3.141592653589793 + theta_rad: 0.7155849933176751 + } + gain_value { + gain_db: -6.8869 + phi_rad: 3.141592653589793 + theta_rad: 0.7164576579436722 + } + gain_value { + gain_db: -7.1117 + phi_rad: 3.141592653589793 + theta_rad: 0.7173303225696694 + } + gain_value { + gain_db: -7.2845 + phi_rad: 3.141592653589793 + theta_rad: 0.7182029871956666 + } + gain_value { + gain_db: -7.404 + phi_rad: 3.141592653589793 + theta_rad: 0.7190756518216638 + } + gain_value { + gain_db: -7.3647 + phi_rad: 3.141592653589793 + theta_rad: 0.7199483164476609 + } + gain_value { + gain_db: -7.1338 + phi_rad: 3.141592653589793 + theta_rad: 0.7208209810736581 + } + gain_value { + gain_db: -6.8945 + phi_rad: 3.141592653589793 + theta_rad: 0.7216936456996553 + } + gain_value { + gain_db: -6.7627 + phi_rad: 3.141592653589793 + theta_rad: 0.7225663103256524 + } + gain_value { + gain_db: -6.7674 + phi_rad: 3.141592653589793 + theta_rad: 0.7234389749516497 + } + gain_value { + gain_db: -6.9106 + phi_rad: 3.141592653589793 + theta_rad: 0.7243116395776468 + } + gain_value { + gain_db: -7.1176 + phi_rad: 3.141592653589793 + theta_rad: 0.7251843042036439 + } + gain_value { + gain_db: -7.2314 + phi_rad: 3.141592653589793 + theta_rad: 0.7260569688296411 + } + gain_value { + gain_db: -7.3682 + phi_rad: 3.141592653589793 + theta_rad: 0.7269296334556382 + } + gain_value { + gain_db: -7.4461 + phi_rad: 3.141592653589793 + theta_rad: 0.7278022980816354 + } + gain_value { + gain_db: -7.5076 + phi_rad: 3.141592653589793 + theta_rad: 0.7286749627076325 + } + gain_value { + gain_db: -7.5024 + phi_rad: 3.141592653589793 + theta_rad: 0.7295476273336297 + } + gain_value { + gain_db: -7.2329 + phi_rad: 3.141592653589793 + theta_rad: 0.730420291959627 + } + gain_value { + gain_db: -6.7929 + phi_rad: 3.141592653589793 + theta_rad: 0.7312929565856241 + } + gain_value { + gain_db: -6.3188 + phi_rad: 3.141592653589793 + theta_rad: 0.7321656212116213 + } + gain_value { + gain_db: -6.1281 + phi_rad: 3.141592653589793 + theta_rad: 0.7330382858376184 + } + gain_value { + gain_db: -6.0681 + phi_rad: 3.141592653589793 + theta_rad: 0.7339109504636155 + } + gain_value { + gain_db: -6.0754 + phi_rad: 3.141592653589793 + theta_rad: 0.7347836150896128 + } + gain_value { + gain_db: -6.2146 + phi_rad: 3.141592653589793 + theta_rad: 0.7356562797156099 + } + gain_value { + gain_db: -6.4733 + phi_rad: 3.141592653589793 + theta_rad: 0.7365289443416071 + } + gain_value { + gain_db: -6.6595 + phi_rad: 3.141592653589793 + theta_rad: 0.7374016089676042 + } + gain_value { + gain_db: -6.9125 + phi_rad: 3.141592653589793 + theta_rad: 0.7382742735936013 + } + gain_value { + gain_db: -7.0845 + phi_rad: 3.141592653589793 + theta_rad: 0.7391469382195985 + } + gain_value { + gain_db: -7.1894 + phi_rad: 3.141592653589793 + theta_rad: 0.7400196028455958 + } + gain_value { + gain_db: -7.0175 + phi_rad: 3.141592653589793 + theta_rad: 0.740892267471593 + } + gain_value { + gain_db: -6.5951 + phi_rad: 3.141592653589793 + theta_rad: 0.7417649320975901 + } + gain_value { + gain_db: -6.3185 + phi_rad: 3.141592653589793 + theta_rad: 0.7426375967235872 + } + gain_value { + gain_db: -6.2605 + phi_rad: 3.141592653589793 + theta_rad: 0.7435102613495844 + } + gain_value { + gain_db: -6.361 + phi_rad: 3.141592653589793 + theta_rad: 0.7443829259755815 + } + gain_value { + gain_db: -6.5082 + phi_rad: 3.141592653589793 + theta_rad: 0.7452555906015788 + } + gain_value { + gain_db: -6.5706 + phi_rad: 3.141592653589793 + theta_rad: 0.7461282552275759 + } + gain_value { + gain_db: -6.6667 + phi_rad: 3.141592653589793 + theta_rad: 0.747000919853573 + } + gain_value { + gain_db: -6.9341 + phi_rad: 3.141592653589793 + theta_rad: 0.7478735844795702 + } + gain_value { + gain_db: -7.3708 + phi_rad: 3.141592653589793 + theta_rad: 0.7487462491055673 + } + gain_value { + gain_db: -7.7981 + phi_rad: 3.141592653589793 + theta_rad: 0.7496189137315646 + } + gain_value { + gain_db: -7.8927 + phi_rad: 3.141592653589793 + theta_rad: 0.7504915783575618 + } + gain_value { + gain_db: -7.9092 + phi_rad: 3.141592653589793 + theta_rad: 0.7513642429835589 + } + gain_value { + gain_db: -7.9731 + phi_rad: 3.141592653589793 + theta_rad: 0.7522369076095561 + } + gain_value { + gain_db: -8.0885 + phi_rad: 3.141592653589793 + theta_rad: 0.7531095722355532 + } + gain_value { + gain_db: -8.2978 + phi_rad: 3.141592653589793 + theta_rad: 0.7539822368615504 + } + gain_value { + gain_db: -8.269 + phi_rad: 3.141592653589793 + theta_rad: 0.7548549014875475 + } + gain_value { + gain_db: -8.1557 + phi_rad: 3.141592653589793 + theta_rad: 0.7557275661135446 + } + gain_value { + gain_db: -7.9624 + phi_rad: 3.141592653589793 + theta_rad: 0.7566002307395419 + } + gain_value { + gain_db: -7.9676 + phi_rad: 3.141592653589793 + theta_rad: 0.757472895365539 + } + gain_value { + gain_db: -8.0774 + phi_rad: 3.141592653589793 + theta_rad: 0.7583455599915362 + } + gain_value { + gain_db: -8.2685 + phi_rad: 3.141592653589793 + theta_rad: 0.7592182246175333 + } + gain_value { + gain_db: -8.4654 + phi_rad: 3.141592653589793 + theta_rad: 0.7600908892435305 + } + gain_value { + gain_db: -8.6445 + phi_rad: 3.141592653589793 + theta_rad: 0.7609635538695277 + } + gain_value { + gain_db: -8.7705 + phi_rad: 3.141592653589793 + theta_rad: 0.7618362184955249 + } + gain_value { + gain_db: -8.9524 + phi_rad: 3.141592653589793 + theta_rad: 0.7627088831215221 + } + gain_value { + gain_db: -9.019 + phi_rad: 3.141592653589793 + theta_rad: 0.7635815477475192 + } + gain_value { + gain_db: -9.0744 + phi_rad: 3.141592653589793 + theta_rad: 0.7644542123735163 + } + gain_value { + gain_db: -8.9183 + phi_rad: 3.141592653589793 + theta_rad: 0.7653268769995135 + } + gain_value { + gain_db: -8.7961 + phi_rad: 3.141592653589793 + theta_rad: 0.7661995416255106 + } + gain_value { + gain_db: -8.5558 + phi_rad: 3.141592653589793 + theta_rad: 0.7670722062515078 + } + gain_value { + gain_db: -8.4925 + phi_rad: 3.141592653589793 + theta_rad: 0.767944870877505 + } + gain_value { + gain_db: -8.6842 + phi_rad: 3.141592653589793 + theta_rad: 0.7688175355035021 + } + gain_value { + gain_db: -8.9503 + phi_rad: 3.141592653589793 + theta_rad: 0.7696902001294994 + } + gain_value { + gain_db: -9.1855 + phi_rad: 3.141592653589793 + theta_rad: 0.7705628647554965 + } + gain_value { + gain_db: -9.5152 + phi_rad: 3.141592653589793 + theta_rad: 0.7714355293814937 + } + gain_value { + gain_db: -10.19 + phi_rad: 3.141592653589793 + theta_rad: 0.7723081940074908 + } + gain_value { + gain_db: -11.111 + phi_rad: 3.141592653589793 + theta_rad: 0.773180858633488 + } + gain_value { + gain_db: -12.201 + phi_rad: 3.141592653589793 + theta_rad: 0.7740535232594852 + } + gain_value { + gain_db: -13.883 + phi_rad: 3.141592653589793 + theta_rad: 0.7749261878854823 + } + gain_value { + gain_db: -14.956 + phi_rad: 3.141592653589793 + theta_rad: 0.7757988525114795 + } + gain_value { + gain_db: -13.41 + phi_rad: 3.141592653589793 + theta_rad: 0.7766715171374766 + } + gain_value { + gain_db: -11.934 + phi_rad: 3.141592653589793 + theta_rad: 0.7775441817634737 + } + gain_value { + gain_db: -11.378 + phi_rad: 3.141592653589793 + theta_rad: 0.778416846389471 + } + gain_value { + gain_db: -10.978 + phi_rad: 3.141592653589793 + theta_rad: 0.7792895110154682 + } + gain_value { + gain_db: -10.682 + phi_rad: 3.141592653589793 + theta_rad: 0.7801621756414654 + } + gain_value { + gain_db: -10.478 + phi_rad: 3.141592653589793 + theta_rad: 0.7810348402674625 + } + gain_value { + gain_db: -10.36 + phi_rad: 3.141592653589793 + theta_rad: 0.7819075048934596 + } + gain_value { + gain_db: -10.683 + phi_rad: 3.141592653589793 + theta_rad: 0.7827801695194568 + } + gain_value { + gain_db: -11.207 + phi_rad: 3.141592653589793 + theta_rad: 0.783652834145454 + } + gain_value { + gain_db: -11.739 + phi_rad: 3.141592653589793 + theta_rad: 0.7845254987714512 + } + gain_value { + gain_db: -11.954 + phi_rad: 3.141592653589793 + theta_rad: 0.7853981633974483 + } + gain_value { + gain_db: -11.874 + phi_rad: 3.141592653589793 + theta_rad: 0.7862708280234454 + } + gain_value { + gain_db: -12.087 + phi_rad: 3.141592653589793 + theta_rad: 0.7871434926494426 + } + gain_value { + gain_db: -12.655 + phi_rad: 3.141592653589793 + theta_rad: 0.7880161572754397 + } + gain_value { + gain_db: -13.172 + phi_rad: 3.141592653589793 + theta_rad: 0.788888821901437 + } + gain_value { + gain_db: -12.952 + phi_rad: 3.141592653589793 + theta_rad: 0.7897614865274342 + } + gain_value { + gain_db: -12.408 + phi_rad: 3.141592653589793 + theta_rad: 0.7906341511534313 + } + gain_value { + gain_db: -12.053 + phi_rad: 3.141592653589793 + theta_rad: 0.7915068157794285 + } + gain_value { + gain_db: -12.118 + phi_rad: 3.141592653589793 + theta_rad: 0.7923794804054256 + } + gain_value { + gain_db: -12.486 + phi_rad: 3.141592653589793 + theta_rad: 0.7932521450314228 + } + gain_value { + gain_db: -12.84 + phi_rad: 3.141592653589793 + theta_rad: 0.7941248096574199 + } + gain_value { + gain_db: -12.63 + phi_rad: 3.141592653589793 + theta_rad: 0.794997474283417 + } + gain_value { + gain_db: -12.623 + phi_rad: 3.141592653589793 + theta_rad: 0.7958701389094143 + } + gain_value { + gain_db: -12.816 + phi_rad: 3.141592653589793 + theta_rad: 0.7967428035354114 + } + gain_value { + gain_db: -12.967 + phi_rad: 3.141592653589793 + theta_rad: 0.7976154681614086 + } + gain_value { + gain_db: -13.316 + phi_rad: 3.141592653589793 + theta_rad: 0.7984881327874057 + } + gain_value { + gain_db: -13.347 + phi_rad: 3.141592653589793 + theta_rad: 0.7993607974134029 + } + gain_value { + gain_db: -13.82 + phi_rad: 3.141592653589793 + theta_rad: 0.8002334620394002 + } + gain_value { + gain_db: -14.121 + phi_rad: 3.141592653589793 + theta_rad: 0.8011061266653973 + } + gain_value { + gain_db: -14.444 + phi_rad: 3.141592653589793 + theta_rad: 0.8019787912913945 + } + gain_value { + gain_db: -13.841 + phi_rad: 3.141592653589793 + theta_rad: 0.8028514559173916 + } + gain_value { + gain_db: -12.609 + phi_rad: 3.141592653589793 + theta_rad: 0.8037241205433887 + } + gain_value { + gain_db: -11.395 + phi_rad: 3.141592653589793 + theta_rad: 0.8045967851693859 + } + gain_value { + gain_db: -10.732 + phi_rad: 3.141592653589793 + theta_rad: 0.805469449795383 + } + gain_value { + gain_db: -10.083 + phi_rad: 3.141592653589793 + theta_rad: 0.8063421144213803 + } + gain_value { + gain_db: -9.939 + phi_rad: 3.141592653589793 + theta_rad: 0.8072147790473774 + } + gain_value { + gain_db: -9.6837 + phi_rad: 3.141592653589793 + theta_rad: 0.8080874436733745 + } + gain_value { + gain_db: -9.5327 + phi_rad: 3.141592653589793 + theta_rad: 0.8089601082993718 + } + gain_value { + gain_db: -9.651 + phi_rad: 3.141592653589793 + theta_rad: 0.8098327729253689 + } + gain_value { + gain_db: -10.031 + phi_rad: 3.141592653589793 + theta_rad: 0.8107054375513661 + } + gain_value { + gain_db: -10.051 + phi_rad: 3.141592653589793 + theta_rad: 0.8115781021773633 + } + gain_value { + gain_db: -9.5866 + phi_rad: 3.141592653589793 + theta_rad: 0.8124507668033604 + } + gain_value { + gain_db: -9.1826 + phi_rad: 3.141592653589793 + theta_rad: 0.8133234314293576 + } + gain_value { + gain_db: -8.3798 + phi_rad: 3.141592653589793 + theta_rad: 0.8141960960553547 + } + gain_value { + gain_db: -7.6295 + phi_rad: 3.141592653589793 + theta_rad: 0.8150687606813519 + } + gain_value { + gain_db: -7.0471 + phi_rad: 3.141592653589793 + theta_rad: 0.815941425307349 + } + gain_value { + gain_db: -6.5978 + phi_rad: 3.141592653589793 + theta_rad: 0.8168140899333461 + } + gain_value { + gain_db: -6.125 + phi_rad: 3.141592653589793 + theta_rad: 0.8176867545593434 + } + gain_value { + gain_db: -5.8122 + phi_rad: 3.141592653589793 + theta_rad: 0.8185594191853406 + } + gain_value { + gain_db: -5.496 + phi_rad: 3.141592653589793 + theta_rad: 0.8194320838113378 + } + gain_value { + gain_db: -5.4244 + phi_rad: 3.141592653589793 + theta_rad: 0.8203047484373349 + } + gain_value { + gain_db: -5.4684 + phi_rad: 3.141592653589793 + theta_rad: 0.821177413063332 + } + gain_value { + gain_db: -5.4585 + phi_rad: 3.141592653589793 + theta_rad: 0.8220500776893293 + } + gain_value { + gain_db: -5.399 + phi_rad: 3.141592653589793 + theta_rad: 0.8229227423153264 + } + gain_value { + gain_db: -5.431 + phi_rad: 3.141592653589793 + theta_rad: 0.8237954069413236 + } + gain_value { + gain_db: -5.4683 + phi_rad: 3.141592653589793 + theta_rad: 0.8246680715673207 + } + gain_value { + gain_db: -5.5315 + phi_rad: 3.141592653589793 + theta_rad: 0.8255407361933178 + } + gain_value { + gain_db: -5.5275 + phi_rad: 3.141592653589793 + theta_rad: 0.826413400819315 + } + gain_value { + gain_db: -5.5383 + phi_rad: 3.141592653589793 + theta_rad: 0.8272860654453121 + } + gain_value { + gain_db: -5.5724 + phi_rad: 3.141592653589793 + theta_rad: 0.8281587300713095 + } + gain_value { + gain_db: -5.5454 + phi_rad: 3.141592653589793 + theta_rad: 0.8290313946973066 + } + gain_value { + gain_db: -5.431 + phi_rad: 3.141592653589793 + theta_rad: 0.8299040593233037 + } + gain_value { + gain_db: -5.1394 + phi_rad: 3.141592653589793 + theta_rad: 0.8307767239493009 + } + gain_value { + gain_db: -4.8584 + phi_rad: 3.141592653589793 + theta_rad: 0.831649388575298 + } + gain_value { + gain_db: -4.778 + phi_rad: 3.141592653589793 + theta_rad: 0.8325220532012952 + } + gain_value { + gain_db: -4.796 + phi_rad: 3.141592653589793 + theta_rad: 0.8333947178272924 + } + gain_value { + gain_db: -4.8974 + phi_rad: 3.141592653589793 + theta_rad: 0.8342673824532895 + } + gain_value { + gain_db: -5.1254 + phi_rad: 3.141592653589793 + theta_rad: 0.8351400470792867 + } + gain_value { + gain_db: -5.4642 + phi_rad: 3.141592653589793 + theta_rad: 0.8360127117052838 + } + gain_value { + gain_db: -5.8785 + phi_rad: 3.141592653589793 + theta_rad: 0.836885376331281 + } + gain_value { + gain_db: -6.339 + phi_rad: 3.141592653589793 + theta_rad: 0.8377580409572782 + } + gain_value { + gain_db: -6.6494 + phi_rad: 3.141592653589793 + theta_rad: 0.8386307055832753 + } + gain_value { + gain_db: -6.7474 + phi_rad: 3.141592653589793 + theta_rad: 0.8395033702092726 + } + gain_value { + gain_db: -6.8398 + phi_rad: 3.141592653589793 + theta_rad: 0.8403760348352697 + } + gain_value { + gain_db: -6.9732 + phi_rad: 3.141592653589793 + theta_rad: 0.8412486994612669 + } + gain_value { + gain_db: -7.1649 + phi_rad: 3.141592653589793 + theta_rad: 0.842121364087264 + } + gain_value { + gain_db: -7.1464 + phi_rad: 3.141592653589793 + theta_rad: 0.8429940287132611 + } + gain_value { + gain_db: -6.9327 + phi_rad: 3.141592653589793 + theta_rad: 0.8438666933392583 + } + gain_value { + gain_db: -6.6855 + phi_rad: 3.141592653589793 + theta_rad: 0.8447393579652555 + } + gain_value { + gain_db: -6.3021 + phi_rad: 3.141592653589793 + theta_rad: 0.8456120225912527 + } + gain_value { + gain_db: -6.1059 + phi_rad: 3.141592653589793 + theta_rad: 0.8464846872172498 + } + gain_value { + gain_db: -6.1888 + phi_rad: 3.141592653589793 + theta_rad: 0.8473573518432469 + } + gain_value { + gain_db: -6.4074 + phi_rad: 3.141592653589793 + theta_rad: 0.8482300164692442 + } + gain_value { + gain_db: -6.6776 + phi_rad: 3.141592653589793 + theta_rad: 0.8491026810952413 + } + gain_value { + gain_db: -7.0898 + phi_rad: 3.141592653589793 + theta_rad: 0.8499753457212386 + } + gain_value { + gain_db: -7.6217 + phi_rad: 3.141592653589793 + theta_rad: 0.8508480103472357 + } + gain_value { + gain_db: -8.3031 + phi_rad: 3.141592653589793 + theta_rad: 0.8517206749732328 + } + gain_value { + gain_db: -9.2079 + phi_rad: 3.141592653589793 + theta_rad: 0.85259333959923 + } + gain_value { + gain_db: -10.428 + phi_rad: 3.141592653589793 + theta_rad: 0.8534660042252271 + } + gain_value { + gain_db: -9.9297 + phi_rad: 3.141592653589793 + theta_rad: 0.8543386688512243 + } + gain_value { + gain_db: -8.3217 + phi_rad: 3.141592653589793 + theta_rad: 0.8552113334772214 + } + gain_value { + gain_db: -7.5093 + phi_rad: 3.141592653589793 + theta_rad: 0.8560839981032186 + } + gain_value { + gain_db: -7.0608 + phi_rad: 3.141592653589793 + theta_rad: 0.8569566627292158 + } + gain_value { + gain_db: -6.973 + phi_rad: 3.141592653589793 + theta_rad: 0.857829327355213 + } + gain_value { + gain_db: -7.0366 + phi_rad: 3.141592653589793 + theta_rad: 0.8587019919812102 + } + gain_value { + gain_db: -7.1894 + phi_rad: 3.141592653589793 + theta_rad: 0.8595746566072073 + } + gain_value { + gain_db: -7.5241 + phi_rad: 3.141592653589793 + theta_rad: 0.8604473212332044 + } + gain_value { + gain_db: -7.8973 + phi_rad: 3.141592653589793 + theta_rad: 0.8613199858592017 + } + gain_value { + gain_db: -8.2604 + phi_rad: 3.141592653589793 + theta_rad: 0.8621926504851988 + } + gain_value { + gain_db: -8.675 + phi_rad: 3.141592653589793 + theta_rad: 0.863065315111196 + } + gain_value { + gain_db: -8.9504 + phi_rad: 3.141592653589793 + theta_rad: 0.8639379797371931 + } + gain_value { + gain_db: -9.2737 + phi_rad: 3.141592653589793 + theta_rad: 0.8648106443631902 + } + gain_value { + gain_db: -9.5232 + phi_rad: 3.141592653589793 + theta_rad: 0.8656833089891874 + } + gain_value { + gain_db: -9.4342 + phi_rad: 3.141592653589793 + theta_rad: 0.8665559736151845 + } + gain_value { + gain_db: -9.4617 + phi_rad: 3.141592653589793 + theta_rad: 0.8674286382411819 + } + gain_value { + gain_db: -9.4628 + phi_rad: 3.141592653589793 + theta_rad: 0.868301302867179 + } + gain_value { + gain_db: -9.4129 + phi_rad: 3.141592653589793 + theta_rad: 0.8691739674931761 + } + gain_value { + gain_db: -9.0824 + phi_rad: 3.141592653589793 + theta_rad: 0.8700466321191733 + } + gain_value { + gain_db: -8.8534 + phi_rad: 3.141592653589793 + theta_rad: 0.8709192967451704 + } + gain_value { + gain_db: -8.912 + phi_rad: 3.141592653589793 + theta_rad: 0.8717919613711677 + } + gain_value { + gain_db: -9.2385 + phi_rad: 3.141592653589793 + theta_rad: 0.8726646259971648 + } + gain_value { + gain_db: -9.6055 + phi_rad: 3.141592653589793 + theta_rad: 0.8735372906231619 + } + gain_value { + gain_db: -10.234 + phi_rad: 3.141592653589793 + theta_rad: 0.8744099552491591 + } + gain_value { + gain_db: -11.093 + phi_rad: 3.141592653589793 + theta_rad: 0.8752826198751562 + } + gain_value { + gain_db: -11.888 + phi_rad: 3.141592653589793 + theta_rad: 0.8761552845011534 + } + gain_value { + gain_db: -12.59 + phi_rad: 3.141592653589793 + theta_rad: 0.8770279491271507 + } + gain_value { + gain_db: -12.855 + phi_rad: 3.141592653589793 + theta_rad: 0.8779006137531478 + } + gain_value { + gain_db: -11.628 + phi_rad: 3.141592653589793 + theta_rad: 0.878773278379145 + } + gain_value { + gain_db: -10.574 + phi_rad: 3.141592653589793 + theta_rad: 0.8796459430051421 + } + gain_value { + gain_db: -9.9685 + phi_rad: 3.141592653589793 + theta_rad: 0.8805186076311393 + } + gain_value { + gain_db: -9.4812 + phi_rad: 3.141592653589793 + theta_rad: 0.8813912722571364 + } + gain_value { + gain_db: -9.3396 + phi_rad: 3.141592653589793 + theta_rad: 0.8822639368831335 + } + gain_value { + gain_db: -9.3206 + phi_rad: 3.141592653589793 + theta_rad: 0.8831366015091308 + } + gain_value { + gain_db: -9.5275 + phi_rad: 3.141592653589793 + theta_rad: 0.8840092661351279 + } + gain_value { + gain_db: -9.5375 + phi_rad: 3.141592653589793 + theta_rad: 0.8848819307611251 + } + gain_value { + gain_db: -9.5917 + phi_rad: 3.141592653589793 + theta_rad: 0.8857545953871222 + } + gain_value { + gain_db: -9.8107 + phi_rad: 3.141592653589793 + theta_rad: 0.8866272600131193 + } + gain_value { + gain_db: -10.179 + phi_rad: 3.141592653589793 + theta_rad: 0.8874999246391166 + } + gain_value { + gain_db: -10.333 + phi_rad: 3.141592653589793 + theta_rad: 0.8883725892651138 + } + gain_value { + gain_db: -10.433 + phi_rad: 3.141592653589793 + theta_rad: 0.889245253891111 + } + gain_value { + gain_db: -10.536 + phi_rad: 3.141592653589793 + theta_rad: 0.8901179185171081 + } + gain_value { + gain_db: -10.479 + phi_rad: 3.141592653589793 + theta_rad: 0.8909905831431052 + } + gain_value { + gain_db: -10.08 + phi_rad: 3.141592653589793 + theta_rad: 0.8918632477691024 + } + gain_value { + gain_db: -9.4327 + phi_rad: 3.141592653589793 + theta_rad: 0.8927359123950995 + } + gain_value { + gain_db: -8.7191 + phi_rad: 3.141592653589793 + theta_rad: 0.8936085770210968 + } + gain_value { + gain_db: -8.052 + phi_rad: 3.141592653589793 + theta_rad: 0.8944812416470939 + } + gain_value { + gain_db: -7.6095 + phi_rad: 3.141592653589793 + theta_rad: 0.895353906273091 + } + gain_value { + gain_db: -7.3089 + phi_rad: 3.141592653589793 + theta_rad: 0.8962265708990882 + } + gain_value { + gain_db: -7.2568 + phi_rad: 3.141592653589793 + theta_rad: 0.8970992355250854 + } + gain_value { + gain_db: -7.1788 + phi_rad: 3.141592653589793 + theta_rad: 0.8979719001510826 + } + gain_value { + gain_db: -7.3193 + phi_rad: 3.141592653589793 + theta_rad: 0.8988445647770797 + } + gain_value { + gain_db: -7.7908 + phi_rad: 3.141592653589793 + theta_rad: 0.8997172294030769 + } + gain_value { + gain_db: -8.4339 + phi_rad: 3.141592653589793 + theta_rad: 0.9005898940290741 + } + gain_value { + gain_db: -9.0102 + phi_rad: 3.141592653589793 + theta_rad: 0.9014625586550712 + } + gain_value { + gain_db: -9.2725 + phi_rad: 3.141592653589793 + theta_rad: 0.9023352232810684 + } + gain_value { + gain_db: -9.4726 + phi_rad: 3.141592653589793 + theta_rad: 0.9032078879070655 + } + gain_value { + gain_db: -9.7327 + phi_rad: 3.141592653589793 + theta_rad: 0.9040805525330626 + } + gain_value { + gain_db: -9.542 + phi_rad: 3.141592653589793 + theta_rad: 0.9049532171590599 + } + gain_value { + gain_db: -8.8455 + phi_rad: 3.141592653589793 + theta_rad: 0.905825881785057 + } + gain_value { + gain_db: -8.0259 + phi_rad: 3.141592653589793 + theta_rad: 0.9066985464110543 + } + gain_value { + gain_db: -7.2542 + phi_rad: 3.141592653589793 + theta_rad: 0.9075712110370514 + } + gain_value { + gain_db: -7.219 + phi_rad: 3.141592653589793 + theta_rad: 0.9084438756630485 + } + gain_value { + gain_db: -7.7092 + phi_rad: 3.141592653589793 + theta_rad: 0.9093165402890457 + } + gain_value { + gain_db: -8.5762 + phi_rad: 3.141592653589793 + theta_rad: 0.9101892049150428 + } + gain_value { + gain_db: -9.8296 + phi_rad: 3.141592653589793 + theta_rad: 0.9110618695410401 + } + gain_value { + gain_db: -11.068 + phi_rad: 3.141592653589793 + theta_rad: 0.9119345341670372 + } + gain_value { + gain_db: -12.689 + phi_rad: 3.141592653589793 + theta_rad: 0.9128071987930343 + } + gain_value { + gain_db: -14.586 + phi_rad: 3.141592653589793 + theta_rad: 0.9136798634190315 + } + gain_value { + gain_db: -12.293 + phi_rad: 3.141592653589793 + theta_rad: 0.9145525280450286 + } + gain_value { + gain_db: -10.226 + phi_rad: 3.141592653589793 + theta_rad: 0.9154251926710258 + } + gain_value { + gain_db: -8.9891 + phi_rad: 3.141592653589793 + theta_rad: 0.9162978572970231 + } + gain_value { + gain_db: -8.3651 + phi_rad: 3.141592653589793 + theta_rad: 0.9171705219230202 + } + gain_value { + gain_db: -8.4072 + phi_rad: 3.141592653589793 + theta_rad: 0.9180431865490174 + } + gain_value { + gain_db: -8.5933 + phi_rad: 3.141592653589793 + theta_rad: 0.9189158511750145 + } + gain_value { + gain_db: -8.789 + phi_rad: 3.141592653589793 + theta_rad: 0.9197885158010117 + } + gain_value { + gain_db: -8.9409 + phi_rad: 3.141592653589793 + theta_rad: 0.9206611804270088 + } + gain_value { + gain_db: -9.0155 + phi_rad: 3.141592653589793 + theta_rad: 0.921533845053006 + } + gain_value { + gain_db: -9.0438 + phi_rad: 3.141592653589793 + theta_rad: 0.9224065096790032 + } + gain_value { + gain_db: -9.0901 + phi_rad: 3.141592653589793 + theta_rad: 0.9232791743050003 + } + gain_value { + gain_db: -8.7552 + phi_rad: 3.141592653589793 + theta_rad: 0.9241518389309975 + } + gain_value { + gain_db: -8.0645 + phi_rad: 3.141592653589793 + theta_rad: 0.9250245035569946 + } + gain_value { + gain_db: -7.5288 + phi_rad: 3.141592653589793 + theta_rad: 0.9258971681829917 + } + gain_value { + gain_db: -7.2465 + phi_rad: 3.141592653589793 + theta_rad: 0.9267698328089891 + } + gain_value { + gain_db: -7.3446 + phi_rad: 3.141592653589793 + theta_rad: 0.9276424974349862 + } + gain_value { + gain_db: -7.7032 + phi_rad: 3.141592653589793 + theta_rad: 0.9285151620609834 + } + gain_value { + gain_db: -8.1811 + phi_rad: 3.141592653589793 + theta_rad: 0.9293878266869805 + } + gain_value { + gain_db: -8.8179 + phi_rad: 3.141592653589793 + theta_rad: 0.9302604913129776 + } + gain_value { + gain_db: -9.5981 + phi_rad: 3.141592653589793 + theta_rad: 0.9311331559389748 + } + gain_value { + gain_db: -10.329 + phi_rad: 3.141592653589793 + theta_rad: 0.9320058205649719 + } + gain_value { + gain_db: -10.458 + phi_rad: 3.141592653589793 + theta_rad: 0.9328784851909692 + } + gain_value { + gain_db: -10.031 + phi_rad: 3.141592653589793 + theta_rad: 0.9337511498169663 + } + gain_value { + gain_db: -9.2283 + phi_rad: 3.141592653589793 + theta_rad: 0.9346238144429634 + } + gain_value { + gain_db: -8.5154 + phi_rad: 3.141592653589793 + theta_rad: 0.9354964790689606 + } + gain_value { + gain_db: -7.9138 + phi_rad: 3.141592653589793 + theta_rad: 0.9363691436949578 + } + gain_value { + gain_db: -7.6583 + phi_rad: 3.141592653589793 + theta_rad: 0.937241808320955 + } + gain_value { + gain_db: -7.6226 + phi_rad: 3.141592653589793 + theta_rad: 0.9381144729469522 + } + gain_value { + gain_db: -8.0085 + phi_rad: 3.141592653589793 + theta_rad: 0.9389871375729493 + } + gain_value { + gain_db: -8.8368 + phi_rad: 3.141592653589793 + theta_rad: 0.9398598021989465 + } + gain_value { + gain_db: -10.075 + phi_rad: 3.141592653589793 + theta_rad: 0.9407324668249436 + } + gain_value { + gain_db: -11.23 + phi_rad: 3.141592653589793 + theta_rad: 0.9416051314509408 + } + gain_value { + gain_db: -12.117 + phi_rad: 3.141592653589793 + theta_rad: 0.9424777960769379 + } + gain_value { + gain_db: -12.503 + phi_rad: 3.141592653589793 + theta_rad: 0.943350460702935 + } + gain_value { + gain_db: -12.331 + phi_rad: 3.141592653589793 + theta_rad: 0.9442231253289323 + } + gain_value { + gain_db: -11.508 + phi_rad: 3.141592653589793 + theta_rad: 0.9450957899549294 + } + gain_value { + gain_db: -10.229 + phi_rad: 3.141592653589793 + theta_rad: 0.9459684545809267 + } + gain_value { + gain_db: -9.2435 + phi_rad: 3.141592653589793 + theta_rad: 0.9468411192069238 + } + gain_value { + gain_db: -8.6791 + phi_rad: 3.141592653589793 + theta_rad: 0.9477137838329209 + } + gain_value { + gain_db: -8.4212 + phi_rad: 3.141592653589793 + theta_rad: 0.9485864484589182 + } + gain_value { + gain_db: -8.545 + phi_rad: 3.141592653589793 + theta_rad: 0.9494591130849153 + } + gain_value { + gain_db: -8.6457 + phi_rad: 3.141592653589793 + theta_rad: 0.9503317777109125 + } + gain_value { + gain_db: -8.9264 + phi_rad: 3.141592653589793 + theta_rad: 0.9512044423369096 + } + gain_value { + gain_db: -9.5944 + phi_rad: 3.141592653589793 + theta_rad: 0.9520771069629067 + } + gain_value { + gain_db: -10.316 + phi_rad: 3.141592653589793 + theta_rad: 0.9529497715889039 + } + gain_value { + gain_db: -10.533 + phi_rad: 3.141592653589793 + theta_rad: 0.953822436214901 + } + gain_value { + gain_db: -10.312 + phi_rad: 3.141592653589793 + theta_rad: 0.9546951008408983 + } + gain_value { + gain_db: -9.5646 + phi_rad: 3.141592653589793 + theta_rad: 0.9555677654668955 + } + gain_value { + gain_db: -8.8517 + phi_rad: 3.141592653589793 + theta_rad: 0.9564404300928926 + } + gain_value { + gain_db: -8.3289 + phi_rad: 3.141592653589793 + theta_rad: 0.9573130947188898 + } + gain_value { + gain_db: -7.9377 + phi_rad: 3.141592653589793 + theta_rad: 0.9581857593448869 + } + gain_value { + gain_db: -7.5646 + phi_rad: 3.141592653589793 + theta_rad: 0.9590584239708841 + } + gain_value { + gain_db: -7.4048 + phi_rad: 3.141592653589793 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -7.6308 + phi_rad: 3.141592653589793 + theta_rad: 0.9608037532228784 + } + gain_value { + gain_db: -8.1677 + phi_rad: 3.141592653589793 + theta_rad: 0.9616764178488756 + } + gain_value { + gain_db: -9.1277 + phi_rad: 3.141592653589793 + theta_rad: 0.9625490824748727 + } + gain_value { + gain_db: -10.034 + phi_rad: 3.141592653589793 + theta_rad: 0.9634217471008699 + } + gain_value { + gain_db: -11.079 + phi_rad: 3.141592653589793 + theta_rad: 0.964294411726867 + } + gain_value { + gain_db: -13.025 + phi_rad: 3.141592653589793 + theta_rad: 0.9651670763528641 + } + gain_value { + gain_db: -17.178 + phi_rad: 3.141592653589793 + theta_rad: 0.9660397409788615 + } + gain_value { + gain_db: -12.649 + phi_rad: 3.141592653589793 + theta_rad: 0.9669124056048586 + } + gain_value { + gain_db: -10.7 + phi_rad: 3.141592653589793 + theta_rad: 0.9677850702308558 + } + gain_value { + gain_db: -9.6943 + phi_rad: 3.141592653589793 + theta_rad: 0.9686577348568529 + } + gain_value { + gain_db: -9.0231 + phi_rad: 3.141592653589793 + theta_rad: 0.96953039948285 + } + gain_value { + gain_db: -8.8209 + phi_rad: 3.141592653589793 + theta_rad: 0.9704030641088472 + } + gain_value { + gain_db: -9.203 + phi_rad: 3.141592653589793 + theta_rad: 0.9712757287348444 + } + gain_value { + gain_db: -10.196 + phi_rad: 3.141592653589793 + theta_rad: 0.9721483933608416 + } + gain_value { + gain_db: -11.666 + phi_rad: 3.141592653589793 + theta_rad: 0.9730210579868387 + } + gain_value { + gain_db: -13.787 + phi_rad: 3.141592653589793 + theta_rad: 0.9738937226128358 + } + gain_value { + gain_db: -16.362 + phi_rad: 3.141592653589793 + theta_rad: 0.9747663872388331 + } + gain_value { + gain_db: -17.385 + phi_rad: 3.141592653589793 + theta_rad: 0.9756390518648302 + } + gain_value { + gain_db: -15.022 + phi_rad: 3.141592653589793 + theta_rad: 0.9765117164908275 + } + gain_value { + gain_db: -12.54 + phi_rad: 3.141592653589793 + theta_rad: 0.9773843811168246 + } + gain_value { + gain_db: -11.041 + phi_rad: 3.141592653589793 + theta_rad: 0.9782570457428217 + } + gain_value { + gain_db: -10.196 + phi_rad: 3.141592653589793 + theta_rad: 0.9791297103688189 + } + gain_value { + gain_db: -9.9655 + phi_rad: 3.141592653589793 + theta_rad: 0.980002374994816 + } + gain_value { + gain_db: -9.8147 + phi_rad: 3.141592653589793 + theta_rad: 0.9808750396208132 + } + gain_value { + gain_db: -10.147 + phi_rad: 3.141592653589793 + theta_rad: 0.9817477042468103 + } + gain_value { + gain_db: -10.642 + phi_rad: 3.141592653589793 + theta_rad: 0.9826203688728075 + } + gain_value { + gain_db: -11.244 + phi_rad: 3.141592653589793 + theta_rad: 0.9834930334988047 + } + gain_value { + gain_db: -10.85 + phi_rad: 3.141592653589793 + theta_rad: 0.9843656981248018 + } + gain_value { + gain_db: -9.9296 + phi_rad: 3.141592653589793 + theta_rad: 0.9852383627507991 + } + gain_value { + gain_db: -9.1275 + phi_rad: 3.141592653589793 + theta_rad: 0.9861110273767962 + } + gain_value { + gain_db: -8.5442 + phi_rad: 3.141592653589793 + theta_rad: 0.9869836920027933 + } + gain_value { + gain_db: -8.0782 + phi_rad: 3.141592653589793 + theta_rad: 0.9878563566287906 + } + gain_value { + gain_db: -7.6479 + phi_rad: 3.141592653589793 + theta_rad: 0.9887290212547877 + } + gain_value { + gain_db: -7.2797 + phi_rad: 3.141592653589793 + theta_rad: 0.9896016858807849 + } + gain_value { + gain_db: -6.9335 + phi_rad: 3.141592653589793 + theta_rad: 0.990474350506782 + } + gain_value { + gain_db: -6.8312 + phi_rad: 3.141592653589793 + theta_rad: 0.9913470151327791 + } + gain_value { + gain_db: -6.991 + phi_rad: 3.141592653589793 + theta_rad: 0.9922196797587763 + } + gain_value { + gain_db: -7.1315 + phi_rad: 3.141592653589793 + theta_rad: 0.9930923443847735 + } + gain_value { + gain_db: -7.2653 + phi_rad: 3.141592653589793 + theta_rad: 0.9939650090107707 + } + gain_value { + gain_db: -7.5087 + phi_rad: 3.141592653589793 + theta_rad: 0.9948376736367679 + } + gain_value { + gain_db: -7.735 + phi_rad: 3.141592653589793 + theta_rad: 0.995710338262765 + } + gain_value { + gain_db: -7.8032 + phi_rad: 3.141592653589793 + theta_rad: 0.9965830028887622 + } + gain_value { + gain_db: -7.771 + phi_rad: 3.141592653589793 + theta_rad: 0.9974556675147593 + } + gain_value { + gain_db: -7.7966 + phi_rad: 3.141592653589793 + theta_rad: 0.9983283321407566 + } + gain_value { + gain_db: -7.9986 + phi_rad: 3.141592653589793 + theta_rad: 0.9992009967667537 + } + gain_value { + gain_db: -8.3407 + phi_rad: 3.141592653589793 + theta_rad: 1.0000736613927508 + } + gain_value { + gain_db: -8.6849 + phi_rad: 3.141592653589793 + theta_rad: 1.0009463260187481 + } + gain_value { + gain_db: -9.2491 + phi_rad: 3.141592653589793 + theta_rad: 1.0018189906447452 + } + gain_value { + gain_db: -10.298 + phi_rad: 3.141592653589793 + theta_rad: 1.0026916552707423 + } + gain_value { + gain_db: -12.132 + phi_rad: 3.141592653589793 + theta_rad: 1.0035643198967394 + } + gain_value { + gain_db: -14.432 + phi_rad: 3.141592653589793 + theta_rad: 1.0044369845227366 + } + gain_value { + gain_db: -13.413 + phi_rad: 3.141592653589793 + theta_rad: 1.0053096491487339 + } + gain_value { + gain_db: -11.927 + phi_rad: 3.141592653589793 + theta_rad: 1.006182313774731 + } + gain_value { + gain_db: -11.044 + phi_rad: 3.141592653589793 + theta_rad: 1.007054978400728 + } + gain_value { + gain_db: -10.45 + phi_rad: 3.141592653589793 + theta_rad: 1.0079276430267252 + } + gain_value { + gain_db: -10.259 + phi_rad: 3.141592653589793 + theta_rad: 1.0088003076527223 + } + gain_value { + gain_db: -10.578 + phi_rad: 3.141592653589793 + theta_rad: 1.0096729722787197 + } + gain_value { + gain_db: -11.522 + phi_rad: 3.141592653589793 + theta_rad: 1.0105456369047168 + } + gain_value { + gain_db: -12.989 + phi_rad: 3.141592653589793 + theta_rad: 1.011418301530714 + } + gain_value { + gain_db: -14.84 + phi_rad: 3.141592653589793 + theta_rad: 1.0122909661567112 + } + gain_value { + gain_db: -16.286 + phi_rad: 3.141592653589793 + theta_rad: 1.0131636307827083 + } + gain_value { + gain_db: -15.689 + phi_rad: 3.141592653589793 + theta_rad: 1.0140362954087054 + } + gain_value { + gain_db: -14.553 + phi_rad: 3.141592653589793 + theta_rad: 1.0149089600347025 + } + gain_value { + gain_db: -13.613 + phi_rad: 3.141592653589793 + theta_rad: 1.0157816246606999 + } + gain_value { + gain_db: -12.827 + phi_rad: 3.141592653589793 + theta_rad: 1.016654289286697 + } + gain_value { + gain_db: -11.844 + phi_rad: 3.141592653589793 + theta_rad: 1.017526953912694 + } + gain_value { + gain_db: -10.978 + phi_rad: 3.141592653589793 + theta_rad: 1.0183996185386912 + } + gain_value { + gain_db: -10.437 + phi_rad: 3.141592653589793 + theta_rad: 1.0192722831646885 + } + gain_value { + gain_db: -10.38 + phi_rad: 3.141592653589793 + theta_rad: 1.0201449477906857 + } + gain_value { + gain_db: -10.605 + phi_rad: 3.141592653589793 + theta_rad: 1.0210176124166828 + } + gain_value { + gain_db: -11.222 + phi_rad: 3.141592653589793 + theta_rad: 1.0218902770426799 + } + gain_value { + gain_db: -12.398 + phi_rad: 3.141592653589793 + theta_rad: 1.0227629416686772 + } + gain_value { + gain_db: -13.824 + phi_rad: 3.141592653589793 + theta_rad: 1.0236356062946743 + } + gain_value { + gain_db: -14.77 + phi_rad: 3.141592653589793 + theta_rad: 1.0245082709206714 + } + gain_value { + gain_db: -15.398 + phi_rad: 3.141592653589793 + theta_rad: 1.0253809355466685 + } + gain_value { + gain_db: -13.98 + phi_rad: 3.141592653589793 + theta_rad: 1.0262536001726656 + } + gain_value { + gain_db: -12.26 + phi_rad: 3.141592653589793 + theta_rad: 1.027126264798663 + } + gain_value { + gain_db: -10.742 + phi_rad: 3.141592653589793 + theta_rad: 1.02799892942466 + } + gain_value { + gain_db: -9.57 + phi_rad: 3.141592653589793 + theta_rad: 1.0288715940506574 + } + gain_value { + gain_db: -8.807 + phi_rad: 3.141592653589793 + theta_rad: 1.0297442586766545 + } + gain_value { + gain_db: -8.4945 + phi_rad: 3.141592653589793 + theta_rad: 1.0306169233026516 + } + gain_value { + gain_db: -8.6959 + phi_rad: 3.141592653589793 + theta_rad: 1.0314895879286488 + } + gain_value { + gain_db: -9.4852 + phi_rad: 3.141592653589793 + theta_rad: 1.0323622525546459 + } + gain_value { + gain_db: -10.549 + phi_rad: 3.141592653589793 + theta_rad: 1.0332349171806432 + } + gain_value { + gain_db: -11.81 + phi_rad: 3.141592653589793 + theta_rad: 1.0341075818066403 + } + gain_value { + gain_db: -12.478 + phi_rad: 3.141592653589793 + theta_rad: 1.0349802464326374 + } + gain_value { + gain_db: -12.549 + phi_rad: 3.141592653589793 + theta_rad: 1.0358529110586345 + } + gain_value { + gain_db: -11.704 + phi_rad: 3.141592653589793 + theta_rad: 1.0367255756846316 + } + gain_value { + gain_db: -10.627 + phi_rad: 3.141592653589793 + theta_rad: 1.037598240310629 + } + gain_value { + gain_db: -9.45 + phi_rad: 3.141592653589793 + theta_rad: 1.038470904936626 + } + gain_value { + gain_db: -8.6744 + phi_rad: 3.141592653589793 + theta_rad: 1.0393435695626232 + } + gain_value { + gain_db: -8.2422 + phi_rad: 3.141592653589793 + theta_rad: 1.0402162341886205 + } + gain_value { + gain_db: -8.1855 + phi_rad: 3.141592653589793 + theta_rad: 1.0410888988146176 + } + gain_value { + gain_db: -8.2325 + phi_rad: 3.141592653589793 + theta_rad: 1.0419615634406147 + } + gain_value { + gain_db: -8.5624 + phi_rad: 3.141592653589793 + theta_rad: 1.0428342280666119 + } + gain_value { + gain_db: -8.9945 + phi_rad: 3.141592653589793 + theta_rad: 1.043706892692609 + } + gain_value { + gain_db: -9.9486 + phi_rad: 3.141592653589793 + theta_rad: 1.0445795573186063 + } + gain_value { + gain_db: -11.488 + phi_rad: 3.141592653589793 + theta_rad: 1.0454522219446034 + } + gain_value { + gain_db: -12.74 + phi_rad: 3.141592653589793 + theta_rad: 1.0463248865706005 + } + gain_value { + gain_db: -12.927 + phi_rad: 3.141592653589793 + theta_rad: 1.0471975511965976 + } + gain_value { + gain_db: -12.715 + phi_rad: 3.141592653589793 + theta_rad: 1.0480702158225947 + } + gain_value { + gain_db: -12.669 + phi_rad: 3.141592653589793 + theta_rad: 1.048942880448592 + } + gain_value { + gain_db: -12.646 + phi_rad: 3.141592653589793 + theta_rad: 1.0498155450745892 + } + gain_value { + gain_db: -12.86 + phi_rad: 3.141592653589793 + theta_rad: 1.0506882097005865 + } + gain_value { + gain_db: -13.025 + phi_rad: 3.141592653589793 + theta_rad: 1.0515608743265836 + } + gain_value { + gain_db: -13.655 + phi_rad: 3.141592653589793 + theta_rad: 1.0524335389525807 + } + gain_value { + gain_db: -14.798 + phi_rad: 3.141592653589793 + theta_rad: 1.0533062035785778 + } + gain_value { + gain_db: -15.96 + phi_rad: 3.141592653589793 + theta_rad: 1.054178868204575 + } + gain_value { + gain_db: -15.064 + phi_rad: 3.141592653589793 + theta_rad: 1.0550515328305723 + } + gain_value { + gain_db: -13.869 + phi_rad: 3.141592653589793 + theta_rad: 1.0559241974565694 + } + gain_value { + gain_db: -13.328 + phi_rad: 3.141592653589793 + theta_rad: 1.0567968620825665 + } + gain_value { + gain_db: -13.404 + phi_rad: 3.141592653589793 + theta_rad: 1.0576695267085636 + } + gain_value { + gain_db: -13.641 + phi_rad: 3.141592653589793 + theta_rad: 1.058542191334561 + } + gain_value { + gain_db: -13.848 + phi_rad: 3.141592653589793 + theta_rad: 1.059414855960558 + } + gain_value { + gain_db: -12.828 + phi_rad: 3.141592653589793 + theta_rad: 1.0602875205865552 + } + gain_value { + gain_db: -11.735 + phi_rad: 3.141592653589793 + theta_rad: 1.0611601852125523 + } + gain_value { + gain_db: -11.208 + phi_rad: 3.141592653589793 + theta_rad: 1.0620328498385496 + } + gain_value { + gain_db: -11.086 + phi_rad: 3.141592653589793 + theta_rad: 1.0629055144645467 + } + gain_value { + gain_db: -11.18 + phi_rad: 3.141592653589793 + theta_rad: 1.0637781790905438 + } + gain_value { + gain_db: -11.334 + phi_rad: 3.141592653589793 + theta_rad: 1.064650843716541 + } + gain_value { + gain_db: -11.627 + phi_rad: 3.141592653589793 + theta_rad: 1.065523508342538 + } + gain_value { + gain_db: -11.952 + phi_rad: 3.141592653589793 + theta_rad: 1.0663961729685354 + } + gain_value { + gain_db: -11.968 + phi_rad: 3.141592653589793 + theta_rad: 1.0672688375945325 + } + gain_value { + gain_db: -11.815 + phi_rad: 3.141592653589793 + theta_rad: 1.0681415022205298 + } + gain_value { + gain_db: -11.547 + phi_rad: 3.141592653589793 + theta_rad: 1.069014166846527 + } + gain_value { + gain_db: -11.307 + phi_rad: 3.141592653589793 + theta_rad: 1.069886831472524 + } + gain_value { + gain_db: -11.06 + phi_rad: 3.141592653589793 + theta_rad: 1.0707594960985212 + } + gain_value { + gain_db: -10.681 + phi_rad: 3.141592653589793 + theta_rad: 1.0716321607245183 + } + gain_value { + gain_db: -10.236 + phi_rad: 3.141592653589793 + theta_rad: 1.0725048253505156 + } + gain_value { + gain_db: -9.8803 + phi_rad: 3.141592653589793 + theta_rad: 1.0733774899765127 + } + gain_value { + gain_db: -9.542 + phi_rad: 3.141592653589793 + theta_rad: 1.0742501546025098 + } + gain_value { + gain_db: -9.3568 + phi_rad: 3.141592653589793 + theta_rad: 1.075122819228507 + } + gain_value { + gain_db: -9.1409 + phi_rad: 3.141592653589793 + theta_rad: 1.075995483854504 + } + gain_value { + gain_db: -8.9048 + phi_rad: 3.141592653589793 + theta_rad: 1.0768681484805014 + } + gain_value { + gain_db: -8.4864 + phi_rad: 3.141592653589793 + theta_rad: 1.0777408131064985 + } + gain_value { + gain_db: -8.1126 + phi_rad: 3.141592653589793 + theta_rad: 1.0786134777324956 + } + gain_value { + gain_db: -7.8408 + phi_rad: 3.141592653589793 + theta_rad: 1.079486142358493 + } + gain_value { + gain_db: -7.6696 + phi_rad: 3.141592653589793 + theta_rad: 1.08035880698449 + } + gain_value { + gain_db: -7.4827 + phi_rad: 3.141592653589793 + theta_rad: 1.0812314716104872 + } + gain_value { + gain_db: -7.3884 + phi_rad: 3.141592653589793 + theta_rad: 1.0821041362364843 + } + gain_value { + gain_db: -7.3332 + phi_rad: 3.141592653589793 + theta_rad: 1.0829768008624814 + } + gain_value { + gain_db: -7.2726 + phi_rad: 3.141592653589793 + theta_rad: 1.0838494654884787 + } + gain_value { + gain_db: -7.6846 + phi_rad: 3.141592653589793 + theta_rad: 1.0847221301144758 + } + gain_value { + gain_db: -8.0256 + phi_rad: 3.141592653589793 + theta_rad: 1.085594794740473 + } + gain_value { + gain_db: -8.4984 + phi_rad: 3.141592653589793 + theta_rad: 1.08646745936647 + } + gain_value { + gain_db: -9.0437 + phi_rad: 3.141592653589793 + theta_rad: 1.0873401239924672 + } + gain_value { + gain_db: -9.7209 + phi_rad: 3.141592653589793 + theta_rad: 1.0882127886184645 + } + gain_value { + gain_db: -10.561 + phi_rad: 3.141592653589793 + theta_rad: 1.0890854532444616 + } + gain_value { + gain_db: -11.005 + phi_rad: 3.141592653589793 + theta_rad: 1.089958117870459 + } + gain_value { + gain_db: -9.7219 + phi_rad: 3.141592653589793 + theta_rad: 1.090830782496456 + } + gain_value { + gain_db: -8.1519 + phi_rad: 3.141592653589793 + theta_rad: 1.0917034471224532 + } + gain_value { + gain_db: -7.2935 + phi_rad: 3.141592653589793 + theta_rad: 1.0925761117484503 + } + gain_value { + gain_db: -6.882 + phi_rad: 3.141592653589793 + theta_rad: 1.0934487763744474 + } + gain_value { + gain_db: -6.9168 + phi_rad: 3.141592653589793 + theta_rad: 1.0943214410004447 + } + gain_value { + gain_db: -7.2813 + phi_rad: 3.141592653589793 + theta_rad: 1.0951941056264418 + } + gain_value { + gain_db: -7.9133 + phi_rad: 3.141592653589793 + theta_rad: 1.096066770252439 + } + gain_value { + gain_db: -8.9041 + phi_rad: 3.141592653589793 + theta_rad: 1.096939434878436 + } + gain_value { + gain_db: -10.081 + phi_rad: 3.141592653589793 + theta_rad: 1.0978120995044334 + } + gain_value { + gain_db: -11.348 + phi_rad: 3.141592653589793 + theta_rad: 1.0986847641304305 + } + gain_value { + gain_db: -12.436 + phi_rad: 3.141592653589793 + theta_rad: 1.0995574287564276 + } + gain_value { + gain_db: -11.584 + phi_rad: 3.141592653589793 + theta_rad: 1.1004300933824247 + } + gain_value { + gain_db: -9.9531 + phi_rad: 3.141592653589793 + theta_rad: 1.101302758008422 + } + gain_value { + gain_db: -8.9599 + phi_rad: 3.141592653589793 + theta_rad: 1.1021754226344191 + } + gain_value { + gain_db: -8.0725 + phi_rad: 3.141592653589793 + theta_rad: 1.1030480872604163 + } + gain_value { + gain_db: -7.4462 + phi_rad: 3.141592653589793 + theta_rad: 1.1039207518864134 + } + gain_value { + gain_db: -7.2105 + phi_rad: 3.141592653589793 + theta_rad: 1.1047934165124105 + } + gain_value { + gain_db: -7.4555 + phi_rad: 3.141592653589793 + theta_rad: 1.1056660811384078 + } + gain_value { + gain_db: -7.95 + phi_rad: 3.141592653589793 + theta_rad: 1.106538745764405 + } + gain_value { + gain_db: -8.5686 + phi_rad: 3.141592653589793 + theta_rad: 1.1074114103904023 + } + gain_value { + gain_db: -9.4056 + phi_rad: 3.141592653589793 + theta_rad: 1.1082840750163994 + } + gain_value { + gain_db: -10.135 + phi_rad: 3.141592653589793 + theta_rad: 1.1091567396423965 + } + gain_value { + gain_db: -10.284 + phi_rad: 3.141592653589793 + theta_rad: 1.1100294042683936 + } + gain_value { + gain_db: -10.118 + phi_rad: 3.141592653589793 + theta_rad: 1.1109020688943907 + } + gain_value { + gain_db: -9.7697 + phi_rad: 3.141592653589793 + theta_rad: 1.111774733520388 + } + gain_value { + gain_db: -9.7162 + phi_rad: 3.141592653589793 + theta_rad: 1.1126473981463851 + } + gain_value { + gain_db: -9.5787 + phi_rad: 3.141592653589793 + theta_rad: 1.1135200627723822 + } + gain_value { + gain_db: -9.4312 + phi_rad: 3.141592653589793 + theta_rad: 1.1143927273983794 + } + gain_value { + gain_db: -9.5017 + phi_rad: 3.141592653589793 + theta_rad: 1.1152653920243765 + } + gain_value { + gain_db: -10.164 + phi_rad: 3.141592653589793 + theta_rad: 1.1161380566503738 + } + gain_value { + gain_db: -11.2 + phi_rad: 3.141592653589793 + theta_rad: 1.117010721276371 + } + gain_value { + gain_db: -13.242 + phi_rad: 3.141592653589793 + theta_rad: 1.117883385902368 + } + gain_value { + gain_db: -14.699 + phi_rad: 3.141592653589793 + theta_rad: 1.1187560505283651 + } + gain_value { + gain_db: -13.181 + phi_rad: 3.141592653589793 + theta_rad: 1.1196287151543625 + } + gain_value { + gain_db: -12.826 + phi_rad: 3.141592653589793 + theta_rad: 1.1205013797803596 + } + gain_value { + gain_db: -12.362 + phi_rad: 3.141592653589793 + theta_rad: 1.1213740444063567 + } + gain_value { + gain_db: -11.75 + phi_rad: 3.141592653589793 + theta_rad: 1.1222467090323538 + } + gain_value { + gain_db: -11.174 + phi_rad: 3.141592653589793 + theta_rad: 1.123119373658351 + } + gain_value { + gain_db: -11.197 + phi_rad: 3.141592653589793 + theta_rad: 1.1239920382843482 + } + gain_value { + gain_db: -11.871 + phi_rad: 3.141592653589793 + theta_rad: 1.1248647029103453 + } + gain_value { + gain_db: -12.744 + phi_rad: 3.141592653589793 + theta_rad: 1.1257373675363425 + } + gain_value { + gain_db: -12.826 + phi_rad: 3.141592653589793 + theta_rad: 1.1266100321623396 + } + gain_value { + gain_db: -11.332 + phi_rad: 3.141592653589793 + theta_rad: 1.1274826967883367 + } + gain_value { + gain_db: -9.9681 + phi_rad: 3.141592653589793 + theta_rad: 1.1283553614143342 + } + gain_value { + gain_db: -9.109 + phi_rad: 3.141592653589793 + theta_rad: 1.1292280260403313 + } + gain_value { + gain_db: -8.563 + phi_rad: 3.141592653589793 + theta_rad: 1.1301006906663285 + } + gain_value { + gain_db: -8.2055 + phi_rad: 3.141592653589793 + theta_rad: 1.1309733552923256 + } + gain_value { + gain_db: -7.5991 + phi_rad: 3.141592653589793 + theta_rad: 1.1318460199183227 + } + gain_value { + gain_db: -7.1197 + phi_rad: 3.141592653589793 + theta_rad: 1.13271868454432 + } + gain_value { + gain_db: -7.0811 + phi_rad: 3.141592653589793 + theta_rad: 1.1335913491703171 + } + gain_value { + gain_db: -7.3094 + phi_rad: 3.141592653589793 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -7.807 + phi_rad: 3.141592653589793 + theta_rad: 1.1353366784223113 + } + gain_value { + gain_db: -8.3283 + phi_rad: 3.141592653589793 + theta_rad: 1.1362093430483085 + } + gain_value { + gain_db: -8.7458 + phi_rad: 3.141592653589793 + theta_rad: 1.1370820076743058 + } + gain_value { + gain_db: -8.7861 + phi_rad: 3.141592653589793 + theta_rad: 1.137954672300303 + } + gain_value { + gain_db: -8.6719 + phi_rad: 3.141592653589793 + theta_rad: 1.1388273369263 + } + gain_value { + gain_db: -8.7328 + phi_rad: 3.141592653589793 + theta_rad: 1.1397000015522971 + } + gain_value { + gain_db: -9.0286 + phi_rad: 3.141592653589793 + theta_rad: 1.1405726661782942 + } + gain_value { + gain_db: -9.0633 + phi_rad: 3.141592653589793 + theta_rad: 1.1414453308042916 + } + gain_value { + gain_db: -8.855 + phi_rad: 3.141592653589793 + theta_rad: 1.1423179954302887 + } + gain_value { + gain_db: -8.729 + phi_rad: 3.141592653589793 + theta_rad: 1.1431906600562858 + } + gain_value { + gain_db: -8.8192 + phi_rad: 3.141592653589793 + theta_rad: 1.144063324682283 + } + gain_value { + gain_db: -8.8375 + phi_rad: 3.141592653589793 + theta_rad: 1.14493598930828 + } + gain_value { + gain_db: -8.5158 + phi_rad: 3.141592653589793 + theta_rad: 1.1458086539342776 + } + gain_value { + gain_db: -8.195 + phi_rad: 3.141592653589793 + theta_rad: 1.1466813185602747 + } + gain_value { + gain_db: -7.8422 + phi_rad: 3.141592653589793 + theta_rad: 1.1475539831862718 + } + gain_value { + gain_db: -7.4479 + phi_rad: 3.141592653589793 + theta_rad: 1.1484266478122689 + } + gain_value { + gain_db: -7.1931 + phi_rad: 3.141592653589793 + theta_rad: 1.149299312438266 + } + gain_value { + gain_db: -6.9706 + phi_rad: 3.141592653589793 + theta_rad: 1.1501719770642633 + } + gain_value { + gain_db: -6.7574 + phi_rad: 3.141592653589793 + theta_rad: 1.1510446416902604 + } + gain_value { + gain_db: -6.5899 + phi_rad: 3.141592653589793 + theta_rad: 1.1519173063162575 + } + gain_value { + gain_db: -6.659 + phi_rad: 3.141592653589793 + theta_rad: 1.1527899709422547 + } + gain_value { + gain_db: -6.8601 + phi_rad: 3.141592653589793 + theta_rad: 1.1536626355682518 + } + gain_value { + gain_db: -7.1099 + phi_rad: 3.141592653589793 + theta_rad: 1.154535300194249 + } + gain_value { + gain_db: -7.1775 + phi_rad: 3.141592653589793 + theta_rad: 1.1554079648202462 + } + gain_value { + gain_db: -6.9618 + phi_rad: 3.141592653589793 + theta_rad: 1.1562806294462433 + } + gain_value { + gain_db: -6.6636 + phi_rad: 3.141592653589793 + theta_rad: 1.1571532940722404 + } + gain_value { + gain_db: -6.6145 + phi_rad: 3.141592653589793 + theta_rad: 1.1580259586982375 + } + gain_value { + gain_db: -6.6869 + phi_rad: 3.141592653589793 + theta_rad: 1.1588986233242349 + } + gain_value { + gain_db: -6.6553 + phi_rad: 3.141592653589793 + theta_rad: 1.159771287950232 + } + gain_value { + gain_db: -6.5808 + phi_rad: 3.141592653589793 + theta_rad: 1.160643952576229 + } + gain_value { + gain_db: -6.3519 + phi_rad: 3.141592653589793 + theta_rad: 1.1615166172022262 + } + gain_value { + gain_db: -6.1784 + phi_rad: 3.141592653589793 + theta_rad: 1.1623892818282233 + } + gain_value { + gain_db: -6.1258 + phi_rad: 3.141592653589793 + theta_rad: 1.1632619464542207 + } + gain_value { + gain_db: -6.2192 + phi_rad: 3.141592653589793 + theta_rad: 1.1641346110802178 + } + gain_value { + gain_db: -6.6448 + phi_rad: 3.141592653589793 + theta_rad: 1.1650072757062149 + } + gain_value { + gain_db: -7.275 + phi_rad: 3.141592653589793 + theta_rad: 1.165879940332212 + } + gain_value { + gain_db: -7.9758 + phi_rad: 3.141592653589793 + theta_rad: 1.166752604958209 + } + gain_value { + gain_db: -8.7221 + phi_rad: 3.141592653589793 + theta_rad: 1.1676252695842066 + } + gain_value { + gain_db: -9.5704 + phi_rad: 3.141592653589793 + theta_rad: 1.1684979342102038 + } + gain_value { + gain_db: -10.828 + phi_rad: 3.141592653589793 + theta_rad: 1.1693705988362009 + } + gain_value { + gain_db: -12.579 + phi_rad: 3.141592653589793 + theta_rad: 1.170243263462198 + } + gain_value { + gain_db: -13.716 + phi_rad: 3.141592653589793 + theta_rad: 1.171115928088195 + } + gain_value { + gain_db: -13.668 + phi_rad: 3.141592653589793 + theta_rad: 1.1719885927141924 + } + gain_value { + gain_db: -12.685 + phi_rad: 3.141592653589793 + theta_rad: 1.1728612573401895 + } + gain_value { + gain_db: -11.799 + phi_rad: 3.141592653589793 + theta_rad: 1.1737339219661866 + } + gain_value { + gain_db: -11.174 + phi_rad: 3.141592653589793 + theta_rad: 1.1746065865921838 + } + gain_value { + gain_db: -10.7 + phi_rad: 3.141592653589793 + theta_rad: 1.1754792512181809 + } + gain_value { + gain_db: -10.305 + phi_rad: 3.141592653589793 + theta_rad: 1.1763519158441782 + } + gain_value { + gain_db: -10.032 + phi_rad: 3.141592653589793 + theta_rad: 1.1772245804701753 + } + gain_value { + gain_db: -10.085 + phi_rad: 3.141592653589793 + theta_rad: 1.1780972450961724 + } + gain_value { + gain_db: -10.384 + phi_rad: 3.141592653589793 + theta_rad: 1.1789699097221695 + } + gain_value { + gain_db: -11.252 + phi_rad: 3.141592653589793 + theta_rad: 1.1798425743481666 + } + gain_value { + gain_db: -12.549 + phi_rad: 3.141592653589793 + theta_rad: 1.180715238974164 + } + gain_value { + gain_db: -13.749 + phi_rad: 3.141592653589793 + theta_rad: 1.181587903600161 + } + gain_value { + gain_db: -14.731 + phi_rad: 3.141592653589793 + theta_rad: 1.1824605682261582 + } + gain_value { + gain_db: -15.497 + phi_rad: 3.141592653589793 + theta_rad: 1.1833332328521553 + } + gain_value { + gain_db: -15.867 + phi_rad: 3.141592653589793 + theta_rad: 1.1842058974781524 + } + gain_value { + gain_db: -15.108 + phi_rad: 3.141592653589793 + theta_rad: 1.18507856210415 + } + gain_value { + gain_db: -12.94 + phi_rad: 3.141592653589793 + theta_rad: 1.185951226730147 + } + gain_value { + gain_db: -11.56 + phi_rad: 3.141592653589793 + theta_rad: 1.1868238913561442 + } + gain_value { + gain_db: -10.703 + phi_rad: 3.141592653589793 + theta_rad: 1.1876965559821413 + } + gain_value { + gain_db: -10.247 + phi_rad: 3.141592653589793 + theta_rad: 1.1885692206081384 + } + gain_value { + gain_db: -10.067 + phi_rad: 3.141592653589793 + theta_rad: 1.1894418852341357 + } + gain_value { + gain_db: -9.7304 + phi_rad: 3.141592653589793 + theta_rad: 1.1903145498601329 + } + gain_value { + gain_db: -9.3823 + phi_rad: 3.141592653589793 + theta_rad: 1.19118721448613 + } + gain_value { + gain_db: -9.3929 + phi_rad: 3.141592653589793 + theta_rad: 1.192059879112127 + } + gain_value { + gain_db: -9.7944 + phi_rad: 3.141592653589793 + theta_rad: 1.1929325437381242 + } + gain_value { + gain_db: -10.345 + phi_rad: 3.141592653589793 + theta_rad: 1.1938052083641215 + } + gain_value { + gain_db: -10.933 + phi_rad: 3.141592653589793 + theta_rad: 1.1946778729901186 + } + gain_value { + gain_db: -11.422 + phi_rad: 3.141592653589793 + theta_rad: 1.1955505376161157 + } + gain_value { + gain_db: -11.968 + phi_rad: 3.141592653589793 + theta_rad: 1.1964232022421128 + } + gain_value { + gain_db: -12.84 + phi_rad: 3.141592653589793 + theta_rad: 1.19729586686811 + } + gain_value { + gain_db: -13.585 + phi_rad: 3.141592653589793 + theta_rad: 1.1981685314941073 + } + gain_value { + gain_db: -13.151 + phi_rad: 3.141592653589793 + theta_rad: 1.1990411961201044 + } + gain_value { + gain_db: -11.882 + phi_rad: 3.141592653589793 + theta_rad: 1.1999138607461015 + } + gain_value { + gain_db: -11.166 + phi_rad: 3.141592653589793 + theta_rad: 1.2007865253720986 + } + gain_value { + gain_db: -10.606 + phi_rad: 3.141592653589793 + theta_rad: 1.2016591899980957 + } + gain_value { + gain_db: -10.216 + phi_rad: 3.141592653589793 + theta_rad: 1.202531854624093 + } + gain_value { + gain_db: -9.8321 + phi_rad: 3.141592653589793 + theta_rad: 1.2034045192500902 + } + gain_value { + gain_db: -9.3385 + phi_rad: 3.141592653589793 + theta_rad: 1.2042771838760873 + } + gain_value { + gain_db: -9.082 + phi_rad: 3.141592653589793 + theta_rad: 1.2051498485020844 + } + gain_value { + gain_db: -8.9048 + phi_rad: 3.141592653589793 + theta_rad: 1.2060225131280815 + } + gain_value { + gain_db: -9.108 + phi_rad: 3.141592653589793 + theta_rad: 1.206895177754079 + } + gain_value { + gain_db: -9.447 + phi_rad: 3.141592653589793 + theta_rad: 1.2077678423800762 + } + gain_value { + gain_db: -9.7138 + phi_rad: 3.141592653589793 + theta_rad: 1.2086405070060733 + } + gain_value { + gain_db: -9.9323 + phi_rad: 3.141592653589793 + theta_rad: 1.2095131716320704 + } + gain_value { + gain_db: -10.355 + phi_rad: 3.141592653589793 + theta_rad: 1.2103858362580675 + } + gain_value { + gain_db: -10.705 + phi_rad: 3.141592653589793 + theta_rad: 1.2112585008840648 + } + gain_value { + gain_db: -11.017 + phi_rad: 3.141592653589793 + theta_rad: 1.212131165510062 + } + gain_value { + gain_db: -11.29 + phi_rad: 3.141592653589793 + theta_rad: 1.213003830136059 + } + gain_value { + gain_db: -11.334 + phi_rad: 3.141592653589793 + theta_rad: 1.2138764947620562 + } + gain_value { + gain_db: -11.306 + phi_rad: 3.141592653589793 + theta_rad: 1.2147491593880533 + } + gain_value { + gain_db: -11.356 + phi_rad: 3.141592653589793 + theta_rad: 1.2156218240140506 + } + gain_value { + gain_db: -11.236 + phi_rad: 3.141592653589793 + theta_rad: 1.2164944886400477 + } + gain_value { + gain_db: -11.329 + phi_rad: 3.141592653589793 + theta_rad: 1.2173671532660448 + } + gain_value { + gain_db: -11.559 + phi_rad: 3.141592653589793 + theta_rad: 1.218239817892042 + } + gain_value { + gain_db: -11.688 + phi_rad: 3.141592653589793 + theta_rad: 1.219112482518039 + } + gain_value { + gain_db: -11.671 + phi_rad: 3.141592653589793 + theta_rad: 1.2199851471440364 + } + gain_value { + gain_db: -11.357 + phi_rad: 3.141592653589793 + theta_rad: 1.2208578117700335 + } + gain_value { + gain_db: -11.07 + phi_rad: 3.141592653589793 + theta_rad: 1.2217304763960306 + } + gain_value { + gain_db: -11.115 + phi_rad: 3.141592653589793 + theta_rad: 1.2226031410220277 + } + gain_value { + gain_db: -11.186 + phi_rad: 3.141592653589793 + theta_rad: 1.2234758056480248 + } + gain_value { + gain_db: -11.304 + phi_rad: 3.141592653589793 + theta_rad: 1.2243484702740224 + } + gain_value { + gain_db: -11.483 + phi_rad: 3.141592653589793 + theta_rad: 1.2252211349000195 + } + gain_value { + gain_db: -11.831 + phi_rad: 3.141592653589793 + theta_rad: 1.2260937995260166 + } + gain_value { + gain_db: -12.142 + phi_rad: 3.141592653589793 + theta_rad: 1.2269664641520137 + } + gain_value { + gain_db: -11.895 + phi_rad: 3.141592653589793 + theta_rad: 1.2278391287780108 + } + gain_value { + gain_db: -11.008 + phi_rad: 3.141592653589793 + theta_rad: 1.2287117934040082 + } + gain_value { + gain_db: -10.253 + phi_rad: 3.141592653589793 + theta_rad: 1.2295844580300053 + } + gain_value { + gain_db: -9.7688 + phi_rad: 3.141592653589793 + theta_rad: 1.2304571226560024 + } + gain_value { + gain_db: -9.4612 + phi_rad: 3.141592653589793 + theta_rad: 1.2313297872819995 + } + gain_value { + gain_db: -9.3877 + phi_rad: 3.141592653589793 + theta_rad: 1.2322024519079966 + } + gain_value { + gain_db: -9.582 + phi_rad: 3.141592653589793 + theta_rad: 1.233075116533994 + } + gain_value { + gain_db: -9.8192 + phi_rad: 3.141592653589793 + theta_rad: 1.233947781159991 + } + gain_value { + gain_db: -10.306 + phi_rad: 3.141592653589793 + theta_rad: 1.2348204457859882 + } + gain_value { + gain_db: -11.043 + phi_rad: 3.141592653589793 + theta_rad: 1.2356931104119853 + } + gain_value { + gain_db: -11.884 + phi_rad: 3.141592653589793 + theta_rad: 1.2365657750379824 + } + gain_value { + gain_db: -12.875 + phi_rad: 3.141592653589793 + theta_rad: 1.2374384396639797 + } + gain_value { + gain_db: -13.942 + phi_rad: 3.141592653589793 + theta_rad: 1.2383111042899768 + } + gain_value { + gain_db: -14.553 + phi_rad: 3.141592653589793 + theta_rad: 1.239183768915974 + } + gain_value { + gain_db: -15.447 + phi_rad: 3.141592653589793 + theta_rad: 1.240056433541971 + } + gain_value { + gain_db: -15.723 + phi_rad: 3.141592653589793 + theta_rad: 1.2409290981679681 + } + gain_value { + gain_db: -14.845 + phi_rad: 3.141592653589793 + theta_rad: 1.2418017627939655 + } + gain_value { + gain_db: -14.341 + phi_rad: 3.141592653589793 + theta_rad: 1.2426744274199626 + } + gain_value { + gain_db: -14.299 + phi_rad: 3.141592653589793 + theta_rad: 1.2435470920459597 + } + gain_value { + gain_db: -14.813 + phi_rad: 3.141592653589793 + theta_rad: 1.2444197566719568 + } + gain_value { + gain_db: -15.422 + phi_rad: 3.141592653589793 + theta_rad: 1.245292421297954 + } + gain_value { + gain_db: -16.118 + phi_rad: 3.141592653589793 + theta_rad: 1.2461650859239515 + } + gain_value { + gain_db: -16.218 + phi_rad: 3.141592653589793 + theta_rad: 1.2470377505499486 + } + gain_value { + gain_db: -15.768 + phi_rad: 3.141592653589793 + theta_rad: 1.2479104151759457 + } + gain_value { + gain_db: -15.274 + phi_rad: 3.141592653589793 + theta_rad: 1.2487830798019428 + } + gain_value { + gain_db: -15.379 + phi_rad: 3.141592653589793 + theta_rad: 1.24965574442794 + } + gain_value { + gain_db: -15.623 + phi_rad: 3.141592653589793 + theta_rad: 1.2505284090539373 + } + gain_value { + gain_db: -15.905 + phi_rad: 3.141592653589793 + theta_rad: 1.2514010736799344 + } + gain_value { + gain_db: -15.827 + phi_rad: 3.141592653589793 + theta_rad: 1.2522737383059315 + } + gain_value { + gain_db: -14.664 + phi_rad: 3.141592653589793 + theta_rad: 1.2531464029319286 + } + gain_value { + gain_db: -14.049 + phi_rad: 3.141592653589793 + theta_rad: 1.2540190675579257 + } + gain_value { + gain_db: -13.494 + phi_rad: 3.141592653589793 + theta_rad: 1.254891732183923 + } + gain_value { + gain_db: -13.08 + phi_rad: 3.141592653589793 + theta_rad: 1.2557643968099201 + } + gain_value { + gain_db: -13.022 + phi_rad: 3.141592653589793 + theta_rad: 1.2566370614359172 + } + gain_value { + gain_db: -13.213 + phi_rad: 3.141592653589793 + theta_rad: 1.2575097260619144 + } + gain_value { + gain_db: -13.922 + phi_rad: 3.141592653589793 + theta_rad: 1.2583823906879115 + } + gain_value { + gain_db: -15.071 + phi_rad: 3.141592653589793 + theta_rad: 1.2592550553139088 + } + gain_value { + gain_db: -14.642 + phi_rad: 3.141592653589793 + theta_rad: 1.260127719939906 + } + gain_value { + gain_db: -12.648 + phi_rad: 3.141592653589793 + theta_rad: 1.261000384565903 + } + gain_value { + gain_db: -11.405 + phi_rad: 3.141592653589793 + theta_rad: 1.2618730491919001 + } + gain_value { + gain_db: -10.666 + phi_rad: 3.141592653589793 + theta_rad: 1.2627457138178972 + } + gain_value { + gain_db: -10.087 + phi_rad: 3.141592653589793 + theta_rad: 1.2636183784438948 + } + gain_value { + gain_db: -9.9716 + phi_rad: 3.141592653589793 + theta_rad: 1.264491043069892 + } + gain_value { + gain_db: -10.339 + phi_rad: 3.141592653589793 + theta_rad: 1.265363707695889 + } + gain_value { + gain_db: -11.174 + phi_rad: 3.141592653589793 + theta_rad: 1.2662363723218861 + } + gain_value { + gain_db: -12.292 + phi_rad: 3.141592653589793 + theta_rad: 1.2671090369478832 + } + gain_value { + gain_db: -13.528 + phi_rad: 3.141592653589793 + theta_rad: 1.2679817015738806 + } + gain_value { + gain_db: -14.748 + phi_rad: 3.141592653589793 + theta_rad: 1.2688543661998777 + } + gain_value { + gain_db: -13.695 + phi_rad: 3.141592653589793 + theta_rad: 1.2697270308258748 + } + gain_value { + gain_db: -12.687 + phi_rad: 3.141592653589793 + theta_rad: 1.270599695451872 + } + gain_value { + gain_db: -12.086 + phi_rad: 3.141592653589793 + theta_rad: 1.271472360077869 + } + gain_value { + gain_db: -11.787 + phi_rad: 3.141592653589793 + theta_rad: 1.2723450247038663 + } + gain_value { + gain_db: -10.944 + phi_rad: 3.141592653589793 + theta_rad: 1.2732176893298635 + } + gain_value { + gain_db: -10.066 + phi_rad: 3.141592653589793 + theta_rad: 1.2740903539558606 + } + gain_value { + gain_db: -9.3177 + phi_rad: 3.141592653589793 + theta_rad: 1.2749630185818577 + } + gain_value { + gain_db: -8.9126 + phi_rad: 3.141592653589793 + theta_rad: 1.2758356832078548 + } + gain_value { + gain_db: -8.8121 + phi_rad: 3.141592653589793 + theta_rad: 1.2767083478338521 + } + gain_value { + gain_db: -9.0503 + phi_rad: 3.141592653589793 + theta_rad: 1.2775810124598492 + } + gain_value { + gain_db: -9.7449 + phi_rad: 3.141592653589793 + theta_rad: 1.2784536770858463 + } + gain_value { + gain_db: -10.79 + phi_rad: 3.141592653589793 + theta_rad: 1.2793263417118435 + } + gain_value { + gain_db: -12.089 + phi_rad: 3.141592653589793 + theta_rad: 1.2801990063378406 + } + gain_value { + gain_db: -13.493 + phi_rad: 3.141592653589793 + theta_rad: 1.281071670963838 + } + gain_value { + gain_db: -11.882 + phi_rad: 3.141592653589793 + theta_rad: 1.281944335589835 + } + gain_value { + gain_db: -10.37 + phi_rad: 3.141592653589793 + theta_rad: 1.2828170002158321 + } + gain_value { + gain_db: -9.5657 + phi_rad: 3.141592653589793 + theta_rad: 1.2836896648418292 + } + gain_value { + gain_db: -9.0291 + phi_rad: 3.141592653589793 + theta_rad: 1.2845623294678266 + } + gain_value { + gain_db: -8.7907 + phi_rad: 3.141592653589793 + theta_rad: 1.285434994093824 + } + gain_value { + gain_db: -9.0508 + phi_rad: 3.141592653589793 + theta_rad: 1.286307658719821 + } + gain_value { + gain_db: -9.5923 + phi_rad: 3.141592653589793 + theta_rad: 1.2871803233458181 + } + gain_value { + gain_db: -10.171 + phi_rad: 3.141592653589793 + theta_rad: 1.2880529879718152 + } + gain_value { + gain_db: -10.57 + phi_rad: 3.141592653589793 + theta_rad: 1.2889256525978123 + } + gain_value { + gain_db: -10.766 + phi_rad: 3.141592653589793 + theta_rad: 1.2897983172238097 + } + gain_value { + gain_db: -10.858 + phi_rad: 3.141592653589793 + theta_rad: 1.2906709818498068 + } + gain_value { + gain_db: -10.6 + phi_rad: 3.141592653589793 + theta_rad: 1.2915436464758039 + } + gain_value { + gain_db: -10.344 + phi_rad: 3.141592653589793 + theta_rad: 1.292416311101801 + } + gain_value { + gain_db: -9.6733 + phi_rad: 3.141592653589793 + theta_rad: 1.293288975727798 + } + gain_value { + gain_db: -8.8631 + phi_rad: 3.141592653589793 + theta_rad: 1.2941616403537954 + } + gain_value { + gain_db: -7.9939 + phi_rad: 3.141592653589793 + theta_rad: 1.2950343049797925 + } + gain_value { + gain_db: -7.2318 + phi_rad: 3.141592653589793 + theta_rad: 1.2959069696057897 + } + gain_value { + gain_db: -6.8905 + phi_rad: 3.141592653589793 + theta_rad: 1.2967796342317868 + } + gain_value { + gain_db: -6.6739 + phi_rad: 3.141592653589793 + theta_rad: 1.2976522988577839 + } + gain_value { + gain_db: -6.4613 + phi_rad: 3.141592653589793 + theta_rad: 1.2985249634837812 + } + gain_value { + gain_db: -6.5885 + phi_rad: 3.141592653589793 + theta_rad: 1.2993976281097783 + } + gain_value { + gain_db: -7.0394 + phi_rad: 3.141592653589793 + theta_rad: 1.3002702927357754 + } + gain_value { + gain_db: -7.5562 + phi_rad: 3.141592653589793 + theta_rad: 1.3011429573617725 + } + gain_value { + gain_db: -8.052 + phi_rad: 3.141592653589793 + theta_rad: 1.3020156219877697 + } + gain_value { + gain_db: -8.6882 + phi_rad: 3.141592653589793 + theta_rad: 1.3028882866137672 + } + gain_value { + gain_db: -9.2618 + phi_rad: 3.141592653589793 + theta_rad: 1.3037609512397643 + } + gain_value { + gain_db: -10.193 + phi_rad: 3.141592653589793 + theta_rad: 1.3046336158657614 + } + gain_value { + gain_db: -10.595 + phi_rad: 3.141592653589793 + theta_rad: 1.3055062804917585 + } + gain_value { + gain_db: -9.775 + phi_rad: 3.141592653589793 + theta_rad: 1.3063789451177557 + } + gain_value { + gain_db: -9.0257 + phi_rad: 3.141592653589793 + theta_rad: 1.307251609743753 + } + gain_value { + gain_db: -8.363 + phi_rad: 3.141592653589793 + theta_rad: 1.30812427436975 + } + gain_value { + gain_db: -7.7669 + phi_rad: 3.141592653589793 + theta_rad: 1.3089969389957472 + } + gain_value { + gain_db: -7.4124 + phi_rad: 3.141592653589793 + theta_rad: 1.3098696036217443 + } + gain_value { + gain_db: -7.1643 + phi_rad: 3.141592653589793 + theta_rad: 1.3107422682477414 + } + gain_value { + gain_db: -6.917 + phi_rad: 3.141592653589793 + theta_rad: 1.3116149328737388 + } + gain_value { + gain_db: -6.693 + phi_rad: 3.141592653589793 + theta_rad: 1.3124875974997359 + } + gain_value { + gain_db: -6.4222 + phi_rad: 3.141592653589793 + theta_rad: 1.313360262125733 + } + gain_value { + gain_db: -6.5427 + phi_rad: 3.141592653589793 + theta_rad: 1.31423292675173 + } + gain_value { + gain_db: -6.8055 + phi_rad: 3.141592653589793 + theta_rad: 1.3151055913777272 + } + gain_value { + gain_db: -7.1347 + phi_rad: 3.141592653589793 + theta_rad: 1.3159782560037245 + } + gain_value { + gain_db: -7.3706 + phi_rad: 3.141592653589793 + theta_rad: 1.3168509206297216 + } + gain_value { + gain_db: -7.8521 + phi_rad: 3.141592653589793 + theta_rad: 1.3177235852557188 + } + gain_value { + gain_db: -8.436 + phi_rad: 3.141592653589793 + theta_rad: 1.3185962498817159 + } + gain_value { + gain_db: -8.8129 + phi_rad: 3.141592653589793 + theta_rad: 1.319468914507713 + } + gain_value { + gain_db: -9.2839 + phi_rad: 3.141592653589793 + theta_rad: 1.3203415791337103 + } + gain_value { + gain_db: -9.8002 + phi_rad: 3.141592653589793 + theta_rad: 1.3212142437597074 + } + gain_value { + gain_db: -10.087 + phi_rad: 3.141592653589793 + theta_rad: 1.3220869083857045 + } + gain_value { + gain_db: -10.234 + phi_rad: 3.141592653589793 + theta_rad: 1.3229595730117016 + } + gain_value { + gain_db: -10.163 + phi_rad: 3.141592653589793 + theta_rad: 1.323832237637699 + } + gain_value { + gain_db: -9.6373 + phi_rad: 3.141592653589793 + theta_rad: 1.3247049022636963 + } + gain_value { + gain_db: -8.7096 + phi_rad: 3.141592653589793 + theta_rad: 1.3255775668896934 + } + gain_value { + gain_db: -7.8505 + phi_rad: 3.141592653589793 + theta_rad: 1.3264502315156905 + } + gain_value { + gain_db: -7.4652 + phi_rad: 3.141592653589793 + theta_rad: 1.3273228961416876 + } + gain_value { + gain_db: -7.6098 + phi_rad: 3.141592653589793 + theta_rad: 1.3281955607676847 + } + gain_value { + gain_db: -8.248 + phi_rad: 3.141592653589793 + theta_rad: 1.329068225393682 + } + gain_value { + gain_db: -9.2818 + phi_rad: 3.141592653589793 + theta_rad: 1.3299408900196792 + } + gain_value { + gain_db: -10.014 + phi_rad: 3.141592653589793 + theta_rad: 1.3308135546456763 + } + gain_value { + gain_db: -10.82 + phi_rad: 3.141592653589793 + theta_rad: 1.3316862192716734 + } + gain_value { + gain_db: -11.866 + phi_rad: 3.141592653589793 + theta_rad: 1.3325588838976705 + } + gain_value { + gain_db: -13.274 + phi_rad: 3.141592653589793 + theta_rad: 1.3334315485236679 + } + gain_value { + gain_db: -13.984 + phi_rad: 3.141592653589793 + theta_rad: 1.334304213149665 + } + gain_value { + gain_db: -13.537 + phi_rad: 3.141592653589793 + theta_rad: 1.335176877775662 + } + gain_value { + gain_db: -12.536 + phi_rad: 3.141592653589793 + theta_rad: 1.3360495424016592 + } + gain_value { + gain_db: -11.686 + phi_rad: 3.141592653589793 + theta_rad: 1.3369222070276563 + } + gain_value { + gain_db: -10.953 + phi_rad: 3.141592653589793 + theta_rad: 1.3377948716536536 + } + gain_value { + gain_db: -10.749 + phi_rad: 3.141592653589793 + theta_rad: 1.3386675362796507 + } + gain_value { + gain_db: -10.87 + phi_rad: 3.141592653589793 + theta_rad: 1.3395402009056478 + } + gain_value { + gain_db: -11.144 + phi_rad: 3.141592653589793 + theta_rad: 1.340412865531645 + } + gain_value { + gain_db: -11.28 + phi_rad: 3.141592653589793 + theta_rad: 1.341285530157642 + } + gain_value { + gain_db: -11.977 + phi_rad: 3.141592653589793 + theta_rad: 1.3421581947836396 + } + gain_value { + gain_db: -12.728 + phi_rad: 3.141592653589793 + theta_rad: 1.3430308594096367 + } + gain_value { + gain_db: -13.47 + phi_rad: 3.141592653589793 + theta_rad: 1.3439035240356338 + } + gain_value { + gain_db: -13.895 + phi_rad: 3.141592653589793 + theta_rad: 1.344776188661631 + } + gain_value { + gain_db: -13.896 + phi_rad: 3.141592653589793 + theta_rad: 1.345648853287628 + } + gain_value { + gain_db: -14.464 + phi_rad: 3.141592653589793 + theta_rad: 1.3465215179136254 + } + gain_value { + gain_db: -15.223 + phi_rad: 3.141592653589793 + theta_rad: 1.3473941825396225 + } + gain_value { + gain_db: -15.197 + phi_rad: 3.141592653589793 + theta_rad: 1.3482668471656196 + } + gain_value { + gain_db: -13.879 + phi_rad: 3.141592653589793 + theta_rad: 1.3491395117916167 + } + gain_value { + gain_db: -12.441 + phi_rad: 3.141592653589793 + theta_rad: 1.3500121764176138 + } + gain_value { + gain_db: -11.457 + phi_rad: 3.141592653589793 + theta_rad: 1.3508848410436112 + } + gain_value { + gain_db: -10.769 + phi_rad: 3.141592653589793 + theta_rad: 1.3517575056696083 + } + gain_value { + gain_db: -10.686 + phi_rad: 3.141592653589793 + theta_rad: 1.3526301702956054 + } + gain_value { + gain_db: -11.081 + phi_rad: 3.141592653589793 + theta_rad: 1.3535028349216025 + } + gain_value { + gain_db: -11.877 + phi_rad: 3.141592653589793 + theta_rad: 1.3543754995475996 + } + gain_value { + gain_db: -12.427 + phi_rad: 3.141592653589793 + theta_rad: 1.355248164173597 + } + gain_value { + gain_db: -13.026 + phi_rad: 3.141592653589793 + theta_rad: 1.356120828799594 + } + gain_value { + gain_db: -13.405 + phi_rad: 3.141592653589793 + theta_rad: 1.3569934934255912 + } + gain_value { + gain_db: -14.014 + phi_rad: 3.141592653589793 + theta_rad: 1.3578661580515883 + } + gain_value { + gain_db: -14.313 + phi_rad: 3.141592653589793 + theta_rad: 1.3587388226775854 + } + gain_value { + gain_db: -14.721 + phi_rad: 3.141592653589793 + theta_rad: 1.3596114873035827 + } + gain_value { + gain_db: -15.573 + phi_rad: 3.141592653589793 + theta_rad: 1.3604841519295798 + } + gain_value { + gain_db: -17.744 + phi_rad: 3.141592653589793 + theta_rad: 1.361356816555577 + } + gain_value { + gain_db: -21.143 + phi_rad: 3.141592653589793 + theta_rad: 1.362229481181574 + } + gain_value { + gain_db: -16.158 + phi_rad: 3.141592653589793 + theta_rad: 1.3631021458075714 + } + gain_value { + gain_db: -14.489 + phi_rad: 3.141592653589793 + theta_rad: 1.3639748104335687 + } + gain_value { + gain_db: -13.897 + phi_rad: 3.141592653589793 + theta_rad: 1.3648474750595658 + } + gain_value { + gain_db: -13.884 + phi_rad: 3.141592653589793 + theta_rad: 1.365720139685563 + } + gain_value { + gain_db: -13.631 + phi_rad: 3.141592653589793 + theta_rad: 1.36659280431156 + } + gain_value { + gain_db: -12.626 + phi_rad: 3.141592653589793 + theta_rad: 1.3674654689375572 + } + gain_value { + gain_db: -11.689 + phi_rad: 3.141592653589793 + theta_rad: 1.3683381335635545 + } + gain_value { + gain_db: -10.957 + phi_rad: 3.141592653589793 + theta_rad: 1.3692107981895516 + } + gain_value { + gain_db: -10.628 + phi_rad: 3.141592653589793 + theta_rad: 1.3700834628155487 + } + gain_value { + gain_db: -10.762 + phi_rad: 3.141592653589793 + theta_rad: 1.3709561274415458 + } + gain_value { + gain_db: -11.176 + phi_rad: 3.141592653589793 + theta_rad: 1.371828792067543 + } + gain_value { + gain_db: -11.935 + phi_rad: 3.141592653589793 + theta_rad: 1.3727014566935403 + } + gain_value { + gain_db: -12.911 + phi_rad: 3.141592653589793 + theta_rad: 1.3735741213195374 + } + gain_value { + gain_db: -13.947 + phi_rad: 3.141592653589793 + theta_rad: 1.3744467859455345 + } + gain_value { + gain_db: -15.085 + phi_rad: 3.141592653589793 + theta_rad: 1.3753194505715316 + } + gain_value { + gain_db: -16.774 + phi_rad: 3.141592653589793 + theta_rad: 1.3761921151975287 + } + gain_value { + gain_db: -18.341 + phi_rad: 3.141592653589793 + theta_rad: 1.377064779823526 + } + gain_value { + gain_db: -18.95 + phi_rad: 3.141592653589793 + theta_rad: 1.3779374444495232 + } + gain_value { + gain_db: -15.326 + phi_rad: 3.141592653589793 + theta_rad: 1.3788101090755203 + } + gain_value { + gain_db: -13.311 + phi_rad: 3.141592653589793 + theta_rad: 1.3796827737015174 + } + gain_value { + gain_db: -11.856 + phi_rad: 3.141592653589793 + theta_rad: 1.3805554383275145 + } + gain_value { + gain_db: -10.736 + phi_rad: 3.141592653589793 + theta_rad: 1.381428102953512 + } + gain_value { + gain_db: -9.8345 + phi_rad: 3.141592653589793 + theta_rad: 1.3823007675795091 + } + gain_value { + gain_db: -9.306 + phi_rad: 3.141592653589793 + theta_rad: 1.3831734322055063 + } + gain_value { + gain_db: -9.1771 + phi_rad: 3.141592653589793 + theta_rad: 1.3840460968315034 + } + gain_value { + gain_db: -9.4692 + phi_rad: 3.141592653589793 + theta_rad: 1.3849187614575005 + } + gain_value { + gain_db: -9.9925 + phi_rad: 3.141592653589793 + theta_rad: 1.3857914260834978 + } + gain_value { + gain_db: -10.42 + phi_rad: 3.141592653589793 + theta_rad: 1.386664090709495 + } + gain_value { + gain_db: -10.753 + phi_rad: 3.141592653589793 + theta_rad: 1.387536755335492 + } + gain_value { + gain_db: -10.939 + phi_rad: 3.141592653589793 + theta_rad: 1.3884094199614891 + } + gain_value { + gain_db: -10.69 + phi_rad: 3.141592653589793 + theta_rad: 1.3892820845874863 + } + gain_value { + gain_db: -10.106 + phi_rad: 3.141592653589793 + theta_rad: 1.3901547492134836 + } + gain_value { + gain_db: -9.7838 + phi_rad: 3.141592653589793 + theta_rad: 1.3910274138394807 + } + gain_value { + gain_db: -10.074 + phi_rad: 3.141592653589793 + theta_rad: 1.3919000784654778 + } + gain_value { + gain_db: -11.167 + phi_rad: 3.141592653589793 + theta_rad: 1.392772743091475 + } + gain_value { + gain_db: -12.367 + phi_rad: 3.141592653589793 + theta_rad: 1.393645407717472 + } + gain_value { + gain_db: -12.025 + phi_rad: 3.141592653589793 + theta_rad: 1.3945180723434694 + } + gain_value { + gain_db: -10.365 + phi_rad: 3.141592653589793 + theta_rad: 1.3953907369694665 + } + gain_value { + gain_db: -8.9755 + phi_rad: 3.141592653589793 + theta_rad: 1.3962634015954636 + } + gain_value { + gain_db: -8.0975 + phi_rad: 3.141592653589793 + theta_rad: 1.3971360662214607 + } + gain_value { + gain_db: -7.3406 + phi_rad: 3.141592653589793 + theta_rad: 1.3980087308474578 + } + gain_value { + gain_db: -6.8169 + phi_rad: 3.141592653589793 + theta_rad: 1.3988813954734551 + } + gain_value { + gain_db: -6.4393 + phi_rad: 3.141592653589793 + theta_rad: 1.3997540600994522 + } + gain_value { + gain_db: -6.348 + phi_rad: 3.141592653589793 + theta_rad: 1.4006267247254494 + } + gain_value { + gain_db: -6.3246 + phi_rad: 3.141592653589793 + theta_rad: 1.4014993893514465 + } + gain_value { + gain_db: -6.6214 + phi_rad: 3.141592653589793 + theta_rad: 1.4023720539774438 + } + gain_value { + gain_db: -7.0381 + phi_rad: 3.141592653589793 + theta_rad: 1.4032447186034411 + } + gain_value { + gain_db: -7.6075 + phi_rad: 3.141592653589793 + theta_rad: 1.4041173832294382 + } + gain_value { + gain_db: -8.4195 + phi_rad: 3.141592653589793 + theta_rad: 1.4049900478554354 + } + gain_value { + gain_db: -9.0034 + phi_rad: 3.141592653589793 + theta_rad: 1.4058627124814325 + } + gain_value { + gain_db: -9.1246 + phi_rad: 3.141592653589793 + theta_rad: 1.4067353771074296 + } + gain_value { + gain_db: -8.846 + phi_rad: 3.141592653589793 + theta_rad: 1.407608041733427 + } + gain_value { + gain_db: -8.7034 + phi_rad: 3.141592653589793 + theta_rad: 1.408480706359424 + } + gain_value { + gain_db: -8.5129 + phi_rad: 3.141592653589793 + theta_rad: 1.4093533709854211 + } + gain_value { + gain_db: -7.9672 + phi_rad: 3.141592653589793 + theta_rad: 1.4102260356114182 + } + gain_value { + gain_db: -7.507 + phi_rad: 3.141592653589793 + theta_rad: 1.4110987002374153 + } + gain_value { + gain_db: -6.9487 + phi_rad: 3.141592653589793 + theta_rad: 1.4119713648634127 + } + gain_value { + gain_db: -6.5541 + phi_rad: 3.141592653589793 + theta_rad: 1.4128440294894098 + } + gain_value { + gain_db: -6.3598 + phi_rad: 3.141592653589793 + theta_rad: 1.413716694115407 + } + gain_value { + gain_db: -6.2677 + phi_rad: 3.141592653589793 + theta_rad: 1.414589358741404 + } + gain_value { + gain_db: -6.3597 + phi_rad: 3.141592653589793 + theta_rad: 1.4154620233674011 + } + gain_value { + gain_db: -6.5095 + phi_rad: 3.141592653589793 + theta_rad: 1.4163346879933985 + } + gain_value { + gain_db: -6.5958 + phi_rad: 3.141592653589793 + theta_rad: 1.4172073526193956 + } + gain_value { + gain_db: -6.5931 + phi_rad: 3.141592653589793 + theta_rad: 1.4180800172453927 + } + gain_value { + gain_db: -6.7526 + phi_rad: 3.141592653589793 + theta_rad: 1.4189526818713898 + } + gain_value { + gain_db: -7.0566 + phi_rad: 3.141592653589793 + theta_rad: 1.419825346497387 + } + gain_value { + gain_db: -7.3158 + phi_rad: 3.141592653589793 + theta_rad: 1.4206980111233845 + } + gain_value { + gain_db: -7.699 + phi_rad: 3.141592653589793 + theta_rad: 1.4215706757493816 + } + gain_value { + gain_db: -8.0741 + phi_rad: 3.141592653589793 + theta_rad: 1.4224433403753787 + } + gain_value { + gain_db: -8.5626 + phi_rad: 3.141592653589793 + theta_rad: 1.4233160050013758 + } + gain_value { + gain_db: -9.0275 + phi_rad: 3.141592653589793 + theta_rad: 1.424188669627373 + } + gain_value { + gain_db: -9.1439 + phi_rad: 3.141592653589793 + theta_rad: 1.4250613342533702 + } + gain_value { + gain_db: -8.9792 + phi_rad: 3.141592653589793 + theta_rad: 1.4259339988793673 + } + gain_value { + gain_db: -8.686 + phi_rad: 3.141592653589793 + theta_rad: 1.4268066635053644 + } + gain_value { + gain_db: -8.0854 + phi_rad: 3.141592653589793 + theta_rad: 1.4276793281313616 + } + gain_value { + gain_db: -7.652 + phi_rad: 3.141592653589793 + theta_rad: 1.4285519927573587 + } + gain_value { + gain_db: -7.1027 + phi_rad: 3.141592653589793 + theta_rad: 1.429424657383356 + } + gain_value { + gain_db: -6.5964 + phi_rad: 3.141592653589793 + theta_rad: 1.4302973220093531 + } + gain_value { + gain_db: -6.075 + phi_rad: 3.141592653589793 + theta_rad: 1.4311699866353502 + } + gain_value { + gain_db: -5.84 + phi_rad: 3.141592653589793 + theta_rad: 1.4320426512613473 + } + gain_value { + gain_db: -5.7996 + phi_rad: 3.141592653589793 + theta_rad: 1.4329153158873444 + } + gain_value { + gain_db: -5.9591 + phi_rad: 3.141592653589793 + theta_rad: 1.4337879805133418 + } + gain_value { + gain_db: -6.2903 + phi_rad: 3.141592653589793 + theta_rad: 1.4346606451393389 + } + gain_value { + gain_db: -6.6758 + phi_rad: 3.141592653589793 + theta_rad: 1.435533309765336 + } + gain_value { + gain_db: -6.9855 + phi_rad: 3.141592653589793 + theta_rad: 1.436405974391333 + } + gain_value { + gain_db: -7.2138 + phi_rad: 3.141592653589793 + theta_rad: 1.4372786390173302 + } + gain_value { + gain_db: -7.4782 + phi_rad: 3.141592653589793 + theta_rad: 1.4381513036433275 + } + gain_value { + gain_db: -7.7151 + phi_rad: 3.141592653589793 + theta_rad: 1.4390239682693247 + } + gain_value { + gain_db: -8.0217 + phi_rad: 3.141592653589793 + theta_rad: 1.4398966328953218 + } + gain_value { + gain_db: -8.3797 + phi_rad: 3.141592653589793 + theta_rad: 1.4407692975213189 + } + gain_value { + gain_db: -8.8877 + phi_rad: 3.141592653589793 + theta_rad: 1.4416419621473162 + } + gain_value { + gain_db: -9.832 + phi_rad: 3.141592653589793 + theta_rad: 1.4425146267733135 + } + gain_value { + gain_db: -11.603 + phi_rad: 3.141592653589793 + theta_rad: 1.4433872913993107 + } + gain_value { + gain_db: -10.716 + phi_rad: 3.141592653589793 + theta_rad: 1.4442599560253078 + } + gain_value { + gain_db: -9.0659 + phi_rad: 3.141592653589793 + theta_rad: 1.4451326206513049 + } + gain_value { + gain_db: -8.2144 + phi_rad: 3.141592653589793 + theta_rad: 1.446005285277302 + } + gain_value { + gain_db: -7.866 + phi_rad: 3.141592653589793 + theta_rad: 1.4468779499032993 + } + gain_value { + gain_db: -7.7189 + phi_rad: 3.141592653589793 + theta_rad: 1.4477506145292964 + } + gain_value { + gain_db: -7.6428 + phi_rad: 3.141592653589793 + theta_rad: 1.4486232791552935 + } + gain_value { + gain_db: -7.6259 + phi_rad: 3.141592653589793 + theta_rad: 1.4494959437812907 + } + gain_value { + gain_db: -7.7703 + phi_rad: 3.141592653589793 + theta_rad: 1.4503686084072878 + } + gain_value { + gain_db: -7.9736 + phi_rad: 3.141592653589793 + theta_rad: 1.451241273033285 + } + gain_value { + gain_db: -8.2641 + phi_rad: 3.141592653589793 + theta_rad: 1.4521139376592822 + } + gain_value { + gain_db: -8.3674 + phi_rad: 3.141592653589793 + theta_rad: 1.4529866022852793 + } + gain_value { + gain_db: -8.2635 + phi_rad: 3.141592653589793 + theta_rad: 1.4538592669112764 + } + gain_value { + gain_db: -8.3428 + phi_rad: 3.141592653589793 + theta_rad: 1.4547319315372735 + } + gain_value { + gain_db: -8.5113 + phi_rad: 3.141592653589793 + theta_rad: 1.4556045961632709 + } + gain_value { + gain_db: -9.1405 + phi_rad: 3.141592653589793 + theta_rad: 1.456477260789268 + } + gain_value { + gain_db: -9.6156 + phi_rad: 3.141592653589793 + theta_rad: 1.457349925415265 + } + gain_value { + gain_db: -9.3468 + phi_rad: 3.141592653589793 + theta_rad: 1.4582225900412622 + } + gain_value { + gain_db: -8.9726 + phi_rad: 3.141592653589793 + theta_rad: 1.4590952546672593 + } + gain_value { + gain_db: -8.4732 + phi_rad: 3.141592653589793 + theta_rad: 1.4599679192932569 + } + gain_value { + gain_db: -7.9336 + phi_rad: 3.141592653589793 + theta_rad: 1.460840583919254 + } + gain_value { + gain_db: -7.5993 + phi_rad: 3.141592653589793 + theta_rad: 1.461713248545251 + } + gain_value { + gain_db: -7.5255 + phi_rad: 3.141592653589793 + theta_rad: 1.4625859131712482 + } + gain_value { + gain_db: -7.385 + phi_rad: 3.141592653589793 + theta_rad: 1.4634585777972453 + } + gain_value { + gain_db: -7.4558 + phi_rad: 3.141592653589793 + theta_rad: 1.4643312424232426 + } + gain_value { + gain_db: -7.7217 + phi_rad: 3.141592653589793 + theta_rad: 1.4652039070492398 + } + gain_value { + gain_db: -8.0225 + phi_rad: 3.141592653589793 + theta_rad: 1.4660765716752369 + } + gain_value { + gain_db: -8.1238 + phi_rad: 3.141592653589793 + theta_rad: 1.466949236301234 + } + gain_value { + gain_db: -8.149 + phi_rad: 3.141592653589793 + theta_rad: 1.467821900927231 + } + gain_value { + gain_db: -8.19 + phi_rad: 3.141592653589793 + theta_rad: 1.4686945655532284 + } + gain_value { + gain_db: -8.3122 + phi_rad: 3.141592653589793 + theta_rad: 1.4695672301792255 + } + gain_value { + gain_db: -8.4287 + phi_rad: 3.141592653589793 + theta_rad: 1.4704398948052226 + } + gain_value { + gain_db: -8.3644 + phi_rad: 3.141592653589793 + theta_rad: 1.4713125594312197 + } + gain_value { + gain_db: -8.1803 + phi_rad: 3.141592653589793 + theta_rad: 1.4721852240572169 + } + gain_value { + gain_db: -8.0321 + phi_rad: 3.141592653589793 + theta_rad: 1.4730578886832142 + } + gain_value { + gain_db: -7.8313 + phi_rad: 3.141592653589793 + theta_rad: 1.4739305533092113 + } + gain_value { + gain_db: -7.4965 + phi_rad: 3.141592653589793 + theta_rad: 1.4748032179352084 + } + gain_value { + gain_db: -7.3761 + phi_rad: 3.141592653589793 + theta_rad: 1.4756758825612055 + } + gain_value { + gain_db: -7.3992 + phi_rad: 3.141592653589793 + theta_rad: 1.4765485471872026 + } + gain_value { + gain_db: -7.5648 + phi_rad: 3.141592653589793 + theta_rad: 1.4774212118132 + } + gain_value { + gain_db: -7.708 + phi_rad: 3.141592653589793 + theta_rad: 1.478293876439197 + } + gain_value { + gain_db: -7.9401 + phi_rad: 3.141592653589793 + theta_rad: 1.4791665410651942 + } + gain_value { + gain_db: -8.0532 + phi_rad: 3.141592653589793 + theta_rad: 1.4800392056911915 + } + gain_value { + gain_db: -7.8902 + phi_rad: 3.141592653589793 + theta_rad: 1.4809118703171886 + } + gain_value { + gain_db: -7.8583 + phi_rad: 3.141592653589793 + theta_rad: 1.481784534943186 + } + gain_value { + gain_db: -7.808 + phi_rad: 3.141592653589793 + theta_rad: 1.482657199569183 + } + gain_value { + gain_db: -7.8085 + phi_rad: 3.141592653589793 + theta_rad: 1.4835298641951802 + } + gain_value { + gain_db: -7.8644 + phi_rad: 3.141592653589793 + theta_rad: 1.4844025288211773 + } + gain_value { + gain_db: -7.9509 + phi_rad: 3.141592653589793 + theta_rad: 1.4852751934471744 + } + gain_value { + gain_db: -8.0909 + phi_rad: 3.141592653589793 + theta_rad: 1.4861478580731717 + } + gain_value { + gain_db: -8.4682 + phi_rad: 3.141592653589793 + theta_rad: 1.4870205226991688 + } + gain_value { + gain_db: -8.7795 + phi_rad: 3.141592653589793 + theta_rad: 1.487893187325166 + } + gain_value { + gain_db: -8.8621 + phi_rad: 3.141592653589793 + theta_rad: 1.488765851951163 + } + gain_value { + gain_db: -8.4369 + phi_rad: 3.141592653589793 + theta_rad: 1.4896385165771602 + } + gain_value { + gain_db: -7.8921 + phi_rad: 3.141592653589793 + theta_rad: 1.4905111812031575 + } + gain_value { + gain_db: -7.3716 + phi_rad: 3.141592653589793 + theta_rad: 1.4913838458291546 + } + gain_value { + gain_db: -7.0068 + phi_rad: 3.141592653589793 + theta_rad: 1.4922565104551517 + } + gain_value { + gain_db: -6.933 + phi_rad: 3.141592653589793 + theta_rad: 1.4931291750811488 + } + gain_value { + gain_db: -7.1445 + phi_rad: 3.141592653589793 + theta_rad: 1.494001839707146 + } + gain_value { + gain_db: -7.5606 + phi_rad: 3.141592653589793 + theta_rad: 1.4948745043331433 + } + gain_value { + gain_db: -7.9536 + phi_rad: 3.141592653589793 + theta_rad: 1.4957471689591404 + } + gain_value { + gain_db: -8.4572 + phi_rad: 3.141592653589793 + theta_rad: 1.4966198335851375 + } + gain_value { + gain_db: -8.9015 + phi_rad: 3.141592653589793 + theta_rad: 1.4974924982111346 + } + gain_value { + gain_db: -9.2134 + phi_rad: 3.141592653589793 + theta_rad: 1.4983651628371317 + } + gain_value { + gain_db: -9.5954 + phi_rad: 3.141592653589793 + theta_rad: 1.4992378274631293 + } + gain_value { + gain_db: -9.7943 + phi_rad: 3.141592653589793 + theta_rad: 1.5001104920891264 + } + gain_value { + gain_db: -10.028 + phi_rad: 3.141592653589793 + theta_rad: 1.5009831567151235 + } + gain_value { + gain_db: -10.12 + phi_rad: 3.141592653589793 + theta_rad: 1.5018558213411206 + } + gain_value { + gain_db: -9.7366 + phi_rad: 3.141592653589793 + theta_rad: 1.5027284859671177 + } + gain_value { + gain_db: -9.2446 + phi_rad: 3.141592653589793 + theta_rad: 1.503601150593115 + } + gain_value { + gain_db: -8.9559 + phi_rad: 3.141592653589793 + theta_rad: 1.5044738152191122 + } + gain_value { + gain_db: -8.7468 + phi_rad: 3.141592653589793 + theta_rad: 1.5053464798451093 + } + gain_value { + gain_db: -8.8142 + phi_rad: 3.141592653589793 + theta_rad: 1.5062191444711064 + } + gain_value { + gain_db: -9.0264 + phi_rad: 3.141592653589793 + theta_rad: 1.5070918090971035 + } + gain_value { + gain_db: -9.4402 + phi_rad: 3.141592653589793 + theta_rad: 1.5079644737231008 + } + gain_value { + gain_db: -9.8953 + phi_rad: 3.141592653589793 + theta_rad: 1.508837138349098 + } + gain_value { + gain_db: -10.069 + phi_rad: 3.141592653589793 + theta_rad: 1.509709802975095 + } + gain_value { + gain_db: -10.12 + phi_rad: 3.141592653589793 + theta_rad: 1.5105824676010922 + } + gain_value { + gain_db: -10.464 + phi_rad: 3.141592653589793 + theta_rad: 1.5114551322270893 + } + gain_value { + gain_db: -11.217 + phi_rad: 3.141592653589793 + theta_rad: 1.5123277968530866 + } + gain_value { + gain_db: -11.784 + phi_rad: 3.141592653589793 + theta_rad: 1.5132004614790837 + } + gain_value { + gain_db: -11.906 + phi_rad: 3.141592653589793 + theta_rad: 1.5140731261050808 + } + gain_value { + gain_db: -11.811 + phi_rad: 3.141592653589793 + theta_rad: 1.514945790731078 + } + gain_value { + gain_db: -11.353 + phi_rad: 3.141592653589793 + theta_rad: 1.515818455357075 + } + gain_value { + gain_db: -10.78 + phi_rad: 3.141592653589793 + theta_rad: 1.5166911199830724 + } + gain_value { + gain_db: -10.164 + phi_rad: 3.141592653589793 + theta_rad: 1.5175637846090695 + } + gain_value { + gain_db: -9.9207 + phi_rad: 3.141592653589793 + theta_rad: 1.5184364492350666 + } + gain_value { + gain_db: -9.9376 + phi_rad: 3.141592653589793 + theta_rad: 1.519309113861064 + } + gain_value { + gain_db: -10.051 + phi_rad: 3.141592653589793 + theta_rad: 1.520181778487061 + } + gain_value { + gain_db: -10.223 + phi_rad: 3.141592653589793 + theta_rad: 1.5210544431130584 + } + gain_value { + gain_db: -10.716 + phi_rad: 3.141592653589793 + theta_rad: 1.5219271077390555 + } + gain_value { + gain_db: -11.032 + phi_rad: 3.141592653589793 + theta_rad: 1.5227997723650526 + } + gain_value { + gain_db: -11.318 + phi_rad: 3.141592653589793 + theta_rad: 1.5236724369910497 + } + gain_value { + gain_db: -11.764 + phi_rad: 3.141592653589793 + theta_rad: 1.5245451016170468 + } + gain_value { + gain_db: -12.467 + phi_rad: 3.141592653589793 + theta_rad: 1.5254177662430441 + } + gain_value { + gain_db: -13.095 + phi_rad: 3.141592653589793 + theta_rad: 1.5262904308690413 + } + gain_value { + gain_db: -13.99 + phi_rad: 3.141592653589793 + theta_rad: 1.5271630954950384 + } + gain_value { + gain_db: -14.554 + phi_rad: 3.141592653589793 + theta_rad: 1.5280357601210355 + } + gain_value { + gain_db: -14.22 + phi_rad: 3.141592653589793 + theta_rad: 1.5289084247470326 + } + gain_value { + gain_db: -13.768 + phi_rad: 3.141592653589793 + theta_rad: 1.52978108937303 + } + gain_value { + gain_db: -13.111 + phi_rad: 3.141592653589793 + theta_rad: 1.530653753999027 + } + gain_value { + gain_db: -12.583 + phi_rad: 3.141592653589793 + theta_rad: 1.5315264186250241 + } + gain_value { + gain_db: -12.369 + phi_rad: 3.141592653589793 + theta_rad: 1.5323990832510213 + } + gain_value { + gain_db: -12.647 + phi_rad: 3.141592653589793 + theta_rad: 1.5332717478770184 + } + gain_value { + gain_db: -13.282 + phi_rad: 3.141592653589793 + theta_rad: 1.5341444125030157 + } + gain_value { + gain_db: -13.461 + phi_rad: 3.141592653589793 + theta_rad: 1.5350170771290128 + } + gain_value { + gain_db: -13.626 + phi_rad: 3.141592653589793 + theta_rad: 1.53588974175501 + } + gain_value { + gain_db: -13.598 + phi_rad: 3.141592653589793 + theta_rad: 1.536762406381007 + } + gain_value { + gain_db: -13.489 + phi_rad: 3.141592653589793 + theta_rad: 1.5376350710070041 + } + gain_value { + gain_db: -13.983 + phi_rad: 3.141592653589793 + theta_rad: 1.5385077356330017 + } + gain_value { + gain_db: -15.014 + phi_rad: 3.141592653589793 + theta_rad: 1.5393804002589988 + } + gain_value { + gain_db: -16.523 + phi_rad: 3.141592653589793 + theta_rad: 1.540253064884996 + } + gain_value { + gain_db: -18.6 + phi_rad: 3.141592653589793 + theta_rad: 1.541125729510993 + } + gain_value { + gain_db: -19.975 + phi_rad: 3.141592653589793 + theta_rad: 1.5419983941369901 + } + gain_value { + gain_db: -19.661 + phi_rad: 3.141592653589793 + theta_rad: 1.5428710587629875 + } + gain_value { + gain_db: -18.437 + phi_rad: 3.141592653589793 + theta_rad: 1.5437437233889846 + } + gain_value { + gain_db: -17.497 + phi_rad: 3.141592653589793 + theta_rad: 1.5446163880149817 + } + gain_value { + gain_db: -16.66 + phi_rad: 3.141592653589793 + theta_rad: 1.5454890526409788 + } + gain_value { + gain_db: -15.761 + phi_rad: 3.141592653589793 + theta_rad: 1.546361717266976 + } + gain_value { + gain_db: -15.053 + phi_rad: 3.141592653589793 + theta_rad: 1.5472343818929732 + } + gain_value { + gain_db: -14.751 + phi_rad: 3.141592653589793 + theta_rad: 1.5481070465189704 + } + gain_value { + gain_db: -15.367 + phi_rad: 3.141592653589793 + theta_rad: 1.5489797111449675 + } + gain_value { + gain_db: -16.093 + phi_rad: 3.141592653589793 + theta_rad: 1.5498523757709646 + } + gain_value { + gain_db: -15.972 + phi_rad: 3.141592653589793 + theta_rad: 1.5507250403969617 + } + gain_value { + gain_db: -15.385 + phi_rad: 3.141592653589793 + theta_rad: 1.551597705022959 + } + gain_value { + gain_db: -15.311 + phi_rad: 3.141592653589793 + theta_rad: 1.5524703696489561 + } + gain_value { + gain_db: -15.152 + phi_rad: 3.141592653589793 + theta_rad: 1.5533430342749532 + } + gain_value { + gain_db: -15.403 + phi_rad: 3.141592653589793 + theta_rad: 1.5542156989009503 + } + gain_value { + gain_db: -15.222 + phi_rad: 3.141592653589793 + theta_rad: 1.5550883635269475 + } + gain_value { + gain_db: -15.82 + phi_rad: 3.141592653589793 + theta_rad: 1.5559610281529448 + } + gain_value { + gain_db: -16.614 + phi_rad: 3.141592653589793 + theta_rad: 1.556833692778942 + } + gain_value { + gain_db: -16.537 + phi_rad: 3.141592653589793 + theta_rad: 1.557706357404939 + } + gain_value { + gain_db: -16.006 + phi_rad: 3.141592653589793 + theta_rad: 1.5585790220309363 + } + gain_value { + gain_db: -15.829 + phi_rad: 3.141592653589793 + theta_rad: 1.5594516866569335 + } + gain_value { + gain_db: -16.002 + phi_rad: 3.141592653589793 + theta_rad: 1.5603243512829308 + } + gain_value { + gain_db: -16.816 + phi_rad: 3.141592653589793 + theta_rad: 1.561197015908928 + } + gain_value { + gain_db: -17.381 + phi_rad: 3.141592653589793 + theta_rad: 1.562069680534925 + } + gain_value { + gain_db: -16.127 + phi_rad: 3.141592653589793 + theta_rad: 1.5629423451609221 + } + gain_value { + gain_db: -14.028 + phi_rad: 3.141592653589793 + theta_rad: 1.5638150097869192 + } + gain_value { + gain_db: -12.902 + phi_rad: 3.141592653589793 + theta_rad: 1.5646876744129166 + } + gain_value { + gain_db: -12.09 + phi_rad: 3.141592653589793 + theta_rad: 1.5655603390389137 + } + gain_value { + gain_db: -11.789 + phi_rad: 3.141592653589793 + theta_rad: 1.5664330036649108 + } + gain_value { + gain_db: -11.969 + phi_rad: 3.141592653589793 + theta_rad: 1.567305668290908 + } + gain_value { + gain_db: -11.981 + phi_rad: 3.141592653589793 + theta_rad: 1.568178332916905 + } + gain_value { + gain_db: -12.175 + phi_rad: 3.141592653589793 + theta_rad: 1.5690509975429023 + } + gain_value { + gain_db: -12.548 + phi_rad: 3.141592653589793 + theta_rad: 1.5699236621688994 + } + gain_value { + gain_db: -12.861 + phi_rad: 3.141592653589793 + theta_rad: 1.5707963267948966 + } + gain_value { + gain_db: 43.974 + phi_rad: 4.71238898038469 + theta_rad: 0.0 + } + gain_value { + gain_db: 43.898 + phi_rad: 4.71238898038469 + theta_rad: 8.726646259971648E-4 + } + gain_value { + gain_db: 43.784 + phi_rad: 4.71238898038469 + theta_rad: 0.0017453292519943296 + } + gain_value { + gain_db: 43.555 + phi_rad: 4.71238898038469 + theta_rad: 0.002617993877991494 + } + gain_value { + gain_db: 43.349 + phi_rad: 4.71238898038469 + theta_rad: 0.003490658503988659 + } + gain_value { + gain_db: 42.989 + phi_rad: 4.71238898038469 + theta_rad: 0.004363323129985824 + } + gain_value { + gain_db: 42.48 + phi_rad: 4.71238898038469 + theta_rad: 0.005235987755982988 + } + gain_value { + gain_db: 42.037 + phi_rad: 4.71238898038469 + theta_rad: 0.006108652381980153 + } + gain_value { + gain_db: 41.449 + phi_rad: 4.71238898038469 + theta_rad: 0.006981317007977318 + } + gain_value { + gain_db: 40.647 + phi_rad: 4.71238898038469 + theta_rad: 0.007853981633974483 + } + gain_value { + gain_db: 39.902 + phi_rad: 4.71238898038469 + theta_rad: 0.008726646259971648 + } + gain_value { + gain_db: 38.73 + phi_rad: 4.71238898038469 + theta_rad: 0.009599310885968814 + } + gain_value { + gain_db: 37.867 + phi_rad: 4.71238898038469 + theta_rad: 0.010471975511965976 + } + gain_value { + gain_db: 36.257 + phi_rad: 4.71238898038469 + theta_rad: 0.011344640137963142 + } + gain_value { + gain_db: 35.183 + phi_rad: 4.71238898038469 + theta_rad: 0.012217304763960306 + } + gain_value { + gain_db: 33.301 + phi_rad: 4.71238898038469 + theta_rad: 0.013089969389957472 + } + gain_value { + gain_db: 31.432 + phi_rad: 4.71238898038469 + theta_rad: 0.013962634015954637 + } + gain_value { + gain_db: 29.207 + phi_rad: 4.71238898038469 + theta_rad: 0.014835298641951801 + } + gain_value { + gain_db: 26.352 + phi_rad: 4.71238898038469 + theta_rad: 0.015707963267948967 + } + gain_value { + gain_db: 23.529 + phi_rad: 4.71238898038469 + theta_rad: 0.01658062789394613 + } + gain_value { + gain_db: 18.854 + phi_rad: 4.71238898038469 + theta_rad: 0.017453292519943295 + } + gain_value { + gain_db: 10.619 + phi_rad: 4.71238898038469 + theta_rad: 0.01832595714594046 + } + gain_value { + gain_db: 14.304 + phi_rad: 4.71238898038469 + theta_rad: 0.019198621771937627 + } + gain_value { + gain_db: 14.719 + phi_rad: 4.71238898038469 + theta_rad: 0.02007128639793479 + } + gain_value { + gain_db: 16.702 + phi_rad: 4.71238898038469 + theta_rad: 0.020943951023931952 + } + gain_value { + gain_db: 18.907 + phi_rad: 4.71238898038469 + theta_rad: 0.02181661564992912 + } + gain_value { + gain_db: 20.193 + phi_rad: 4.71238898038469 + theta_rad: 0.022689280275926284 + } + gain_value { + gain_db: 20.992 + phi_rad: 4.71238898038469 + theta_rad: 0.02356194490192345 + } + gain_value { + gain_db: 21.378 + phi_rad: 4.71238898038469 + theta_rad: 0.024434609527920613 + } + gain_value { + gain_db: 21.505 + phi_rad: 4.71238898038469 + theta_rad: 0.02530727415391778 + } + gain_value { + gain_db: 21.465 + phi_rad: 4.71238898038469 + theta_rad: 0.026179938779914945 + } + gain_value { + gain_db: 21.268 + phi_rad: 4.71238898038469 + theta_rad: 0.027052603405912107 + } + gain_value { + gain_db: 20.955 + phi_rad: 4.71238898038469 + theta_rad: 0.027925268031909273 + } + gain_value { + gain_db: 20.519 + phi_rad: 4.71238898038469 + theta_rad: 0.028797932657906436 + } + gain_value { + gain_db: 19.941 + phi_rad: 4.71238898038469 + theta_rad: 0.029670597283903602 + } + gain_value { + gain_db: 19.263 + phi_rad: 4.71238898038469 + theta_rad: 0.030543261909900768 + } + gain_value { + gain_db: 18.665 + phi_rad: 4.71238898038469 + theta_rad: 0.031415926535897934 + } + gain_value { + gain_db: 17.857 + phi_rad: 4.71238898038469 + theta_rad: 0.0322885911618951 + } + gain_value { + gain_db: 17.061 + phi_rad: 4.71238898038469 + theta_rad: 0.03316125578789226 + } + gain_value { + gain_db: 16.308 + phi_rad: 4.71238898038469 + theta_rad: 0.034033920413889425 + } + gain_value { + gain_db: 15.757 + phi_rad: 4.71238898038469 + theta_rad: 0.03490658503988659 + } + gain_value { + gain_db: 15.248 + phi_rad: 4.71238898038469 + theta_rad: 0.03577924966588375 + } + gain_value { + gain_db: 14.879 + phi_rad: 4.71238898038469 + theta_rad: 0.03665191429188092 + } + gain_value { + gain_db: 14.641 + phi_rad: 4.71238898038469 + theta_rad: 0.03752457891787808 + } + gain_value { + gain_db: 14.551 + phi_rad: 4.71238898038469 + theta_rad: 0.038397243543875255 + } + gain_value { + gain_db: 14.658 + phi_rad: 4.71238898038469 + theta_rad: 0.039269908169872414 + } + gain_value { + gain_db: 14.966 + phi_rad: 4.71238898038469 + theta_rad: 0.04014257279586958 + } + gain_value { + gain_db: 15.366 + phi_rad: 4.71238898038469 + theta_rad: 0.041015237421866746 + } + gain_value { + gain_db: 15.867 + phi_rad: 4.71238898038469 + theta_rad: 0.041887902047863905 + } + gain_value { + gain_db: 16.408 + phi_rad: 4.71238898038469 + theta_rad: 0.04276056667386108 + } + gain_value { + gain_db: 17.022 + phi_rad: 4.71238898038469 + theta_rad: 0.04363323129985824 + } + gain_value { + gain_db: 17.586 + phi_rad: 4.71238898038469 + theta_rad: 0.0445058959258554 + } + gain_value { + gain_db: 18.063 + phi_rad: 4.71238898038469 + theta_rad: 0.04537856055185257 + } + gain_value { + gain_db: 18.383 + phi_rad: 4.71238898038469 + theta_rad: 0.046251225177849735 + } + gain_value { + gain_db: 18.572 + phi_rad: 4.71238898038469 + theta_rad: 0.0471238898038469 + } + gain_value { + gain_db: 18.598 + phi_rad: 4.71238898038469 + theta_rad: 0.04799655442984406 + } + gain_value { + gain_db: 18.475 + phi_rad: 4.71238898038469 + theta_rad: 0.048869219055841226 + } + gain_value { + gain_db: 18.256 + phi_rad: 4.71238898038469 + theta_rad: 0.04974188368183839 + } + gain_value { + gain_db: 17.867 + phi_rad: 4.71238898038469 + theta_rad: 0.05061454830783556 + } + gain_value { + gain_db: 17.273 + phi_rad: 4.71238898038469 + theta_rad: 0.051487212933832724 + } + gain_value { + gain_db: 16.66 + phi_rad: 4.71238898038469 + theta_rad: 0.05235987755982989 + } + gain_value { + gain_db: 15.893 + phi_rad: 4.71238898038469 + theta_rad: 0.05323254218582705 + } + gain_value { + gain_db: 14.99 + phi_rad: 4.71238898038469 + theta_rad: 0.054105206811824215 + } + gain_value { + gain_db: 14.033 + phi_rad: 4.71238898038469 + theta_rad: 0.05497787143782138 + } + gain_value { + gain_db: 12.84 + phi_rad: 4.71238898038469 + theta_rad: 0.05585053606381855 + } + gain_value { + gain_db: 11.719 + phi_rad: 4.71238898038469 + theta_rad: 0.05672320068981571 + } + gain_value { + gain_db: 10.634 + phi_rad: 4.71238898038469 + theta_rad: 0.05759586531581287 + } + gain_value { + gain_db: 9.4658 + phi_rad: 4.71238898038469 + theta_rad: 0.05846852994181004 + } + gain_value { + gain_db: 9.1675 + phi_rad: 4.71238898038469 + theta_rad: 0.059341194567807204 + } + gain_value { + gain_db: 8.9962 + phi_rad: 4.71238898038469 + theta_rad: 0.06021385919380437 + } + gain_value { + gain_db: 9.323 + phi_rad: 4.71238898038469 + theta_rad: 0.061086523819801536 + } + gain_value { + gain_db: 9.8932 + phi_rad: 4.71238898038469 + theta_rad: 0.061959188445798695 + } + gain_value { + gain_db: 10.711 + phi_rad: 4.71238898038469 + theta_rad: 0.06283185307179587 + } + gain_value { + gain_db: 11.365 + phi_rad: 4.71238898038469 + theta_rad: 0.06370451769779303 + } + gain_value { + gain_db: 11.863 + phi_rad: 4.71238898038469 + theta_rad: 0.0645771823237902 + } + gain_value { + gain_db: 12.3 + phi_rad: 4.71238898038469 + theta_rad: 0.06544984694978735 + } + gain_value { + gain_db: 12.573 + phi_rad: 4.71238898038469 + theta_rad: 0.06632251157578452 + } + gain_value { + gain_db: 12.726 + phi_rad: 4.71238898038469 + theta_rad: 0.06719517620178168 + } + gain_value { + gain_db: 12.775 + phi_rad: 4.71238898038469 + theta_rad: 0.06806784082777885 + } + gain_value { + gain_db: 12.718 + phi_rad: 4.71238898038469 + theta_rad: 0.06894050545377602 + } + gain_value { + gain_db: 12.544 + phi_rad: 4.71238898038469 + theta_rad: 0.06981317007977318 + } + gain_value { + gain_db: 12.277 + phi_rad: 4.71238898038469 + theta_rad: 0.07068583470577035 + } + gain_value { + gain_db: 11.881 + phi_rad: 4.71238898038469 + theta_rad: 0.0715584993317675 + } + gain_value { + gain_db: 11.343 + phi_rad: 4.71238898038469 + theta_rad: 0.07243116395776468 + } + gain_value { + gain_db: 10.656 + phi_rad: 4.71238898038469 + theta_rad: 0.07330382858376185 + } + gain_value { + gain_db: 9.7901 + phi_rad: 4.71238898038469 + theta_rad: 0.07417649320975901 + } + gain_value { + gain_db: 8.9139 + phi_rad: 4.71238898038469 + theta_rad: 0.07504915783575616 + } + gain_value { + gain_db: 7.8878 + phi_rad: 4.71238898038469 + theta_rad: 0.07592182246175333 + } + gain_value { + gain_db: 6.4359 + phi_rad: 4.71238898038469 + theta_rad: 0.07679448708775051 + } + gain_value { + gain_db: 4.9817 + phi_rad: 4.71238898038469 + theta_rad: 0.07766715171374766 + } + gain_value { + gain_db: 4.1744 + phi_rad: 4.71238898038469 + theta_rad: 0.07853981633974483 + } + gain_value { + gain_db: 4.2648 + phi_rad: 4.71238898038469 + theta_rad: 0.079412480965742 + } + gain_value { + gain_db: 4.7623 + phi_rad: 4.71238898038469 + theta_rad: 0.08028514559173916 + } + gain_value { + gain_db: 5.3997 + phi_rad: 4.71238898038469 + theta_rad: 0.08115781021773633 + } + gain_value { + gain_db: 6.0791 + phi_rad: 4.71238898038469 + theta_rad: 0.08203047484373349 + } + gain_value { + gain_db: 6.7535 + phi_rad: 4.71238898038469 + theta_rad: 0.08290313946973066 + } + gain_value { + gain_db: 7.1966 + phi_rad: 4.71238898038469 + theta_rad: 0.08377580409572781 + } + gain_value { + gain_db: 7.5811 + phi_rad: 4.71238898038469 + theta_rad: 0.08464846872172498 + } + gain_value { + gain_db: 7.8349 + phi_rad: 4.71238898038469 + theta_rad: 0.08552113334772216 + } + gain_value { + gain_db: 7.9707 + phi_rad: 4.71238898038469 + theta_rad: 0.08639379797371932 + } + gain_value { + gain_db: 8.0643 + phi_rad: 4.71238898038469 + theta_rad: 0.08726646259971647 + } + gain_value { + gain_db: 8.0532 + phi_rad: 4.71238898038469 + theta_rad: 0.08813912722571364 + } + gain_value { + gain_db: 7.9634 + phi_rad: 4.71238898038469 + theta_rad: 0.0890117918517108 + } + gain_value { + gain_db: 7.827 + phi_rad: 4.71238898038469 + theta_rad: 0.08988445647770797 + } + gain_value { + gain_db: 7.5627 + phi_rad: 4.71238898038469 + theta_rad: 0.09075712110370514 + } + gain_value { + gain_db: 7.2609 + phi_rad: 4.71238898038469 + theta_rad: 0.0916297857297023 + } + gain_value { + gain_db: 6.9051 + phi_rad: 4.71238898038469 + theta_rad: 0.09250245035569947 + } + gain_value { + gain_db: 6.342 + phi_rad: 4.71238898038469 + theta_rad: 0.09337511498169662 + } + gain_value { + gain_db: 5.6563 + phi_rad: 4.71238898038469 + theta_rad: 0.0942477796076938 + } + gain_value { + gain_db: 4.6979 + phi_rad: 4.71238898038469 + theta_rad: 0.09512044423369097 + } + gain_value { + gain_db: 3.7644 + phi_rad: 4.71238898038469 + theta_rad: 0.09599310885968812 + } + gain_value { + gain_db: 2.563 + phi_rad: 4.71238898038469 + theta_rad: 0.09686577348568529 + } + gain_value { + gain_db: 1.2179 + phi_rad: 4.71238898038469 + theta_rad: 0.09773843811168245 + } + gain_value { + gain_db: -0.10837 + phi_rad: 4.71238898038469 + theta_rad: 0.09861110273767963 + } + gain_value { + gain_db: -1.6888 + phi_rad: 4.71238898038469 + theta_rad: 0.09948376736367678 + } + gain_value { + gain_db: -3.1982 + phi_rad: 4.71238898038469 + theta_rad: 0.10035643198967395 + } + gain_value { + gain_db: -3.7142 + phi_rad: 4.71238898038469 + theta_rad: 0.10122909661567112 + } + gain_value { + gain_db: -3.4735 + phi_rad: 4.71238898038469 + theta_rad: 0.10210176124166827 + } + gain_value { + gain_db: -3.1944 + phi_rad: 4.71238898038469 + theta_rad: 0.10297442586766545 + } + gain_value { + gain_db: -2.8686 + phi_rad: 4.71238898038469 + theta_rad: 0.10384709049366261 + } + gain_value { + gain_db: -2.5845 + phi_rad: 4.71238898038469 + theta_rad: 0.10471975511965978 + } + gain_value { + gain_db: -2.3892 + phi_rad: 4.71238898038469 + theta_rad: 0.10559241974565693 + } + gain_value { + gain_db: -2.1284 + phi_rad: 4.71238898038469 + theta_rad: 0.1064650843716541 + } + gain_value { + gain_db: -1.9728 + phi_rad: 4.71238898038469 + theta_rad: 0.10733774899765128 + } + gain_value { + gain_db: -2.003 + phi_rad: 4.71238898038469 + theta_rad: 0.10821041362364843 + } + gain_value { + gain_db: -2.0886 + phi_rad: 4.71238898038469 + theta_rad: 0.1090830782496456 + } + gain_value { + gain_db: -2.2927 + phi_rad: 4.71238898038469 + theta_rad: 0.10995574287564276 + } + gain_value { + gain_db: -2.4405 + phi_rad: 4.71238898038469 + theta_rad: 0.11082840750163991 + } + gain_value { + gain_db: -2.6865 + phi_rad: 4.71238898038469 + theta_rad: 0.1117010721276371 + } + gain_value { + gain_db: -3.0325 + phi_rad: 4.71238898038469 + theta_rad: 0.11257373675363426 + } + gain_value { + gain_db: -3.3064 + phi_rad: 4.71238898038469 + theta_rad: 0.11344640137963143 + } + gain_value { + gain_db: -3.4282 + phi_rad: 4.71238898038469 + theta_rad: 0.11431906600562858 + } + gain_value { + gain_db: -3.2961 + phi_rad: 4.71238898038469 + theta_rad: 0.11519173063162574 + } + gain_value { + gain_db: -2.9857 + phi_rad: 4.71238898038469 + theta_rad: 0.11606439525762292 + } + gain_value { + gain_db: -2.494 + phi_rad: 4.71238898038469 + theta_rad: 0.11693705988362008 + } + gain_value { + gain_db: -2.0232 + phi_rad: 4.71238898038469 + theta_rad: 0.11780972450961724 + } + gain_value { + gain_db: -1.6498 + phi_rad: 4.71238898038469 + theta_rad: 0.11868238913561441 + } + gain_value { + gain_db: -1.3765 + phi_rad: 4.71238898038469 + theta_rad: 0.11955505376161157 + } + gain_value { + gain_db: -1.0415 + phi_rad: 4.71238898038469 + theta_rad: 0.12042771838760874 + } + gain_value { + gain_db: -0.65623 + phi_rad: 4.71238898038469 + theta_rad: 0.1213003830136059 + } + gain_value { + gain_db: -0.22493 + phi_rad: 4.71238898038469 + theta_rad: 0.12217304763960307 + } + gain_value { + gain_db: 0.3664 + phi_rad: 4.71238898038469 + theta_rad: 0.12304571226560022 + } + gain_value { + gain_db: 0.82083 + phi_rad: 4.71238898038469 + theta_rad: 0.12391837689159739 + } + gain_value { + gain_db: 1.4477 + phi_rad: 4.71238898038469 + theta_rad: 0.12479104151759457 + } + gain_value { + gain_db: 2.2231 + phi_rad: 4.71238898038469 + theta_rad: 0.12566370614359174 + } + gain_value { + gain_db: 3.0684 + phi_rad: 4.71238898038469 + theta_rad: 0.1265363707695889 + } + gain_value { + gain_db: 3.7962 + phi_rad: 4.71238898038469 + theta_rad: 0.12740903539558607 + } + gain_value { + gain_db: 4.5604 + phi_rad: 4.71238898038469 + theta_rad: 0.1282817000215832 + } + gain_value { + gain_db: 5.1523 + phi_rad: 4.71238898038469 + theta_rad: 0.1291543646475804 + } + gain_value { + gain_db: 5.61 + phi_rad: 4.71238898038469 + theta_rad: 0.13002702927357757 + } + gain_value { + gain_db: 6.0486 + phi_rad: 4.71238898038469 + theta_rad: 0.1308996938995747 + } + gain_value { + gain_db: 6.374 + phi_rad: 4.71238898038469 + theta_rad: 0.13177235852557187 + } + gain_value { + gain_db: 6.5055 + phi_rad: 4.71238898038469 + theta_rad: 0.13264502315156904 + } + gain_value { + gain_db: 6.6143 + phi_rad: 4.71238898038469 + theta_rad: 0.13351768777756623 + } + gain_value { + gain_db: 6.6038 + phi_rad: 4.71238898038469 + theta_rad: 0.13439035240356337 + } + gain_value { + gain_db: 6.518 + phi_rad: 4.71238898038469 + theta_rad: 0.13526301702956053 + } + gain_value { + gain_db: 6.3683 + phi_rad: 4.71238898038469 + theta_rad: 0.1361356816555577 + } + gain_value { + gain_db: 6.1471 + phi_rad: 4.71238898038469 + theta_rad: 0.13700834628155487 + } + gain_value { + gain_db: 5.8918 + phi_rad: 4.71238898038469 + theta_rad: 0.13788101090755203 + } + gain_value { + gain_db: 5.6675 + phi_rad: 4.71238898038469 + theta_rad: 0.1387536755335492 + } + gain_value { + gain_db: 5.3539 + phi_rad: 4.71238898038469 + theta_rad: 0.13962634015954636 + } + gain_value { + gain_db: 5.1049 + phi_rad: 4.71238898038469 + theta_rad: 0.14049900478554353 + } + gain_value { + gain_db: 4.7765 + phi_rad: 4.71238898038469 + theta_rad: 0.1413716694115407 + } + gain_value { + gain_db: 4.4969 + phi_rad: 4.71238898038469 + theta_rad: 0.14224433403753786 + } + gain_value { + gain_db: 4.1846 + phi_rad: 4.71238898038469 + theta_rad: 0.143116998663535 + } + gain_value { + gain_db: 3.8702 + phi_rad: 4.71238898038469 + theta_rad: 0.1439896632895322 + } + gain_value { + gain_db: 3.6425 + phi_rad: 4.71238898038469 + theta_rad: 0.14486232791552936 + } + gain_value { + gain_db: 3.3967 + phi_rad: 4.71238898038469 + theta_rad: 0.1457349925415265 + } + gain_value { + gain_db: 3.2009 + phi_rad: 4.71238898038469 + theta_rad: 0.1466076571675237 + } + gain_value { + gain_db: 3.0143 + phi_rad: 4.71238898038469 + theta_rad: 0.14748032179352083 + } + gain_value { + gain_db: 2.9385 + phi_rad: 4.71238898038469 + theta_rad: 0.14835298641951802 + } + gain_value { + gain_db: 2.7901 + phi_rad: 4.71238898038469 + theta_rad: 0.1492256510455152 + } + gain_value { + gain_db: 2.6496 + phi_rad: 4.71238898038469 + theta_rad: 0.15009831567151233 + } + gain_value { + gain_db: 2.544 + phi_rad: 4.71238898038469 + theta_rad: 0.15097098029750952 + } + gain_value { + gain_db: 2.4562 + phi_rad: 4.71238898038469 + theta_rad: 0.15184364492350666 + } + gain_value { + gain_db: 2.371 + phi_rad: 4.71238898038469 + theta_rad: 0.15271630954950383 + } + gain_value { + gain_db: 2.2031 + phi_rad: 4.71238898038469 + theta_rad: 0.15358897417550102 + } + gain_value { + gain_db: 1.9595 + phi_rad: 4.71238898038469 + theta_rad: 0.15446163880149816 + } + gain_value { + gain_db: 1.6593 + phi_rad: 4.71238898038469 + theta_rad: 0.15533430342749532 + } + gain_value { + gain_db: 1.2904 + phi_rad: 4.71238898038469 + theta_rad: 0.1562069680534925 + } + gain_value { + gain_db: 0.77817 + phi_rad: 4.71238898038469 + theta_rad: 0.15707963267948966 + } + gain_value { + gain_db: 0.22893 + phi_rad: 4.71238898038469 + theta_rad: 0.15795229730548685 + } + gain_value { + gain_db: -0.34143 + phi_rad: 4.71238898038469 + theta_rad: 0.158824961931484 + } + gain_value { + gain_db: -0.89277 + phi_rad: 4.71238898038469 + theta_rad: 0.15969762655748115 + } + gain_value { + gain_db: -1.3541 + phi_rad: 4.71238898038469 + theta_rad: 0.16057029118347832 + } + gain_value { + gain_db: -1.5407 + phi_rad: 4.71238898038469 + theta_rad: 0.16144295580947549 + } + gain_value { + gain_db: -1.6748 + phi_rad: 4.71238898038469 + theta_rad: 0.16231562043547265 + } + gain_value { + gain_db: -1.6788 + phi_rad: 4.71238898038469 + theta_rad: 0.16318828506146982 + } + gain_value { + gain_db: -1.6929 + phi_rad: 4.71238898038469 + theta_rad: 0.16406094968746698 + } + gain_value { + gain_db: -1.5927 + phi_rad: 4.71238898038469 + theta_rad: 0.16493361431346412 + } + gain_value { + gain_db: -1.4729 + phi_rad: 4.71238898038469 + theta_rad: 0.16580627893946132 + } + gain_value { + gain_db: -1.4185 + phi_rad: 4.71238898038469 + theta_rad: 0.16667894356545848 + } + gain_value { + gain_db: -1.3527 + phi_rad: 4.71238898038469 + theta_rad: 0.16755160819145562 + } + gain_value { + gain_db: -1.2416 + phi_rad: 4.71238898038469 + theta_rad: 0.1684242728174528 + } + gain_value { + gain_db: -1.1754 + phi_rad: 4.71238898038469 + theta_rad: 0.16929693744344995 + } + gain_value { + gain_db: -1.0632 + phi_rad: 4.71238898038469 + theta_rad: 0.17016960206944712 + } + gain_value { + gain_db: -0.98177 + phi_rad: 4.71238898038469 + theta_rad: 0.1710422666954443 + } + gain_value { + gain_db: -0.9377 + phi_rad: 4.71238898038469 + theta_rad: 0.17191493132144145 + } + gain_value { + gain_db: -0.9642 + phi_rad: 4.71238898038469 + theta_rad: 0.17278759594743864 + } + gain_value { + gain_db: -1.0184 + phi_rad: 4.71238898038469 + theta_rad: 0.17366026057343578 + } + gain_value { + gain_db: -1.217 + phi_rad: 4.71238898038469 + theta_rad: 0.17453292519943295 + } + gain_value { + gain_db: -1.3961 + phi_rad: 4.71238898038469 + theta_rad: 0.17540558982543014 + } + gain_value { + gain_db: -1.6943 + phi_rad: 4.71238898038469 + theta_rad: 0.17627825445142728 + } + gain_value { + gain_db: -2.0353 + phi_rad: 4.71238898038469 + theta_rad: 0.17715091907742445 + } + gain_value { + gain_db: -2.4797 + phi_rad: 4.71238898038469 + theta_rad: 0.1780235837034216 + } + gain_value { + gain_db: -3.0065 + phi_rad: 4.71238898038469 + theta_rad: 0.17889624832941878 + } + gain_value { + gain_db: -3.7011 + phi_rad: 4.71238898038469 + theta_rad: 0.17976891295541594 + } + gain_value { + gain_db: -4.5921 + phi_rad: 4.71238898038469 + theta_rad: 0.1806415775814131 + } + gain_value { + gain_db: -5.8528 + phi_rad: 4.71238898038469 + theta_rad: 0.18151424220741028 + } + gain_value { + gain_db: -7.4594 + phi_rad: 4.71238898038469 + theta_rad: 0.18238690683340741 + } + gain_value { + gain_db: -12.661 + phi_rad: 4.71238898038469 + theta_rad: 0.1832595714594046 + } + gain_value { + gain_db: -8.0033 + phi_rad: 4.71238898038469 + theta_rad: 0.18413223608540177 + } + gain_value { + gain_db: -6.2055 + phi_rad: 4.71238898038469 + theta_rad: 0.18500490071139894 + } + gain_value { + gain_db: -5.2179 + phi_rad: 4.71238898038469 + theta_rad: 0.1858775653373961 + } + gain_value { + gain_db: -4.8806 + phi_rad: 4.71238898038469 + theta_rad: 0.18675022996339324 + } + gain_value { + gain_db: -4.6062 + phi_rad: 4.71238898038469 + theta_rad: 0.18762289458939044 + } + gain_value { + gain_db: -4.398 + phi_rad: 4.71238898038469 + theta_rad: 0.1884955592153876 + } + gain_value { + gain_db: -4.2334 + phi_rad: 4.71238898038469 + theta_rad: 0.18936822384138474 + } + gain_value { + gain_db: -4.1728 + phi_rad: 4.71238898038469 + theta_rad: 0.19024088846738194 + } + gain_value { + gain_db: -4.2478 + phi_rad: 4.71238898038469 + theta_rad: 0.19111355309337907 + } + gain_value { + gain_db: -4.4015 + phi_rad: 4.71238898038469 + theta_rad: 0.19198621771937624 + } + gain_value { + gain_db: -4.3927 + phi_rad: 4.71238898038469 + theta_rad: 0.19285888234537343 + } + gain_value { + gain_db: -4.4244 + phi_rad: 4.71238898038469 + theta_rad: 0.19373154697137057 + } + gain_value { + gain_db: -4.3915 + phi_rad: 4.71238898038469 + theta_rad: 0.19460421159736774 + } + gain_value { + gain_db: -4.4111 + phi_rad: 4.71238898038469 + theta_rad: 0.1954768762233649 + } + gain_value { + gain_db: -4.5052 + phi_rad: 4.71238898038469 + theta_rad: 0.19634954084936207 + } + gain_value { + gain_db: -4.8302 + phi_rad: 4.71238898038469 + theta_rad: 0.19722220547535926 + } + gain_value { + gain_db: -5.1702 + phi_rad: 4.71238898038469 + theta_rad: 0.1980948701013564 + } + gain_value { + gain_db: -5.7456 + phi_rad: 4.71238898038469 + theta_rad: 0.19896753472735357 + } + gain_value { + gain_db: -6.3949 + phi_rad: 4.71238898038469 + theta_rad: 0.19984019935335073 + } + gain_value { + gain_db: -6.544 + phi_rad: 4.71238898038469 + theta_rad: 0.2007128639793479 + } + gain_value { + gain_db: -6.323 + phi_rad: 4.71238898038469 + theta_rad: 0.20158552860534507 + } + gain_value { + gain_db: -6.2806 + phi_rad: 4.71238898038469 + theta_rad: 0.20245819323134223 + } + gain_value { + gain_db: -6.4178 + phi_rad: 4.71238898038469 + theta_rad: 0.2033308578573394 + } + gain_value { + gain_db: -5.9776 + phi_rad: 4.71238898038469 + theta_rad: 0.20420352248333654 + } + gain_value { + gain_db: -4.3973 + phi_rad: 4.71238898038469 + theta_rad: 0.20507618710933373 + } + gain_value { + gain_db: -2.8748 + phi_rad: 4.71238898038469 + theta_rad: 0.2059488517353309 + } + gain_value { + gain_db: -1.6541 + phi_rad: 4.71238898038469 + theta_rad: 0.20682151636132803 + } + gain_value { + gain_db: -0.7205 + phi_rad: 4.71238898038469 + theta_rad: 0.20769418098732523 + } + gain_value { + gain_db: -0.14423 + phi_rad: 4.71238898038469 + theta_rad: 0.20856684561332237 + } + gain_value { + gain_db: 0.25317 + phi_rad: 4.71238898038469 + theta_rad: 0.20943951023931956 + } + gain_value { + gain_db: 0.5569 + phi_rad: 4.71238898038469 + theta_rad: 0.21031217486531673 + } + gain_value { + gain_db: 0.68477 + phi_rad: 4.71238898038469 + theta_rad: 0.21118483949131386 + } + gain_value { + gain_db: 0.6888 + phi_rad: 4.71238898038469 + theta_rad: 0.21205750411731106 + } + gain_value { + gain_db: 0.64897 + phi_rad: 4.71238898038469 + theta_rad: 0.2129301687433082 + } + gain_value { + gain_db: 0.49673 + phi_rad: 4.71238898038469 + theta_rad: 0.21380283336930536 + } + gain_value { + gain_db: 0.29457 + phi_rad: 4.71238898038469 + theta_rad: 0.21467549799530256 + } + gain_value { + gain_db: 0.084667 + phi_rad: 4.71238898038469 + theta_rad: 0.2155481626212997 + } + gain_value { + gain_db: -0.053467 + phi_rad: 4.71238898038469 + theta_rad: 0.21642082724729686 + } + gain_value { + gain_db: 0.0013 + phi_rad: 4.71238898038469 + theta_rad: 0.21729349187329403 + } + gain_value { + gain_db: 0.22313 + phi_rad: 4.71238898038469 + theta_rad: 0.2181661564992912 + } + gain_value { + gain_db: 0.5609 + phi_rad: 4.71238898038469 + theta_rad: 0.21903882112528836 + } + gain_value { + gain_db: 0.90557 + phi_rad: 4.71238898038469 + theta_rad: 0.21991148575128552 + } + gain_value { + gain_db: 1.1772 + phi_rad: 4.71238898038469 + theta_rad: 0.2207841503772827 + } + gain_value { + gain_db: 1.4777 + phi_rad: 4.71238898038469 + theta_rad: 0.22165681500327983 + } + gain_value { + gain_db: 1.6658 + phi_rad: 4.71238898038469 + theta_rad: 0.22252947962927702 + } + gain_value { + gain_db: 1.862 + phi_rad: 4.71238898038469 + theta_rad: 0.2234021442552742 + } + gain_value { + gain_db: 1.9838 + phi_rad: 4.71238898038469 + theta_rad: 0.22427480888127135 + } + gain_value { + gain_db: 2.0248 + phi_rad: 4.71238898038469 + theta_rad: 0.22514747350726852 + } + gain_value { + gain_db: 1.9929 + phi_rad: 4.71238898038469 + theta_rad: 0.22602013813326566 + } + gain_value { + gain_db: 1.9197 + phi_rad: 4.71238898038469 + theta_rad: 0.22689280275926285 + } + gain_value { + gain_db: 1.8101 + phi_rad: 4.71238898038469 + theta_rad: 0.22776546738526002 + } + gain_value { + gain_db: 1.7421 + phi_rad: 4.71238898038469 + theta_rad: 0.22863813201125716 + } + gain_value { + gain_db: 1.7301 + phi_rad: 4.71238898038469 + theta_rad: 0.22951079663725435 + } + gain_value { + gain_db: 1.816 + phi_rad: 4.71238898038469 + theta_rad: 0.2303834612632515 + } + gain_value { + gain_db: 1.9904 + phi_rad: 4.71238898038469 + theta_rad: 0.23125612588924865 + } + gain_value { + gain_db: 2.2129 + phi_rad: 4.71238898038469 + theta_rad: 0.23212879051524585 + } + gain_value { + gain_db: 2.5543 + phi_rad: 4.71238898038469 + theta_rad: 0.23300145514124299 + } + gain_value { + gain_db: 2.8389 + phi_rad: 4.71238898038469 + theta_rad: 0.23387411976724015 + } + gain_value { + gain_db: 3.1565 + phi_rad: 4.71238898038469 + theta_rad: 0.23474678439323732 + } + gain_value { + gain_db: 3.4627 + phi_rad: 4.71238898038469 + theta_rad: 0.23561944901923448 + } + gain_value { + gain_db: 3.7842 + phi_rad: 4.71238898038469 + theta_rad: 0.23649211364523168 + } + gain_value { + gain_db: 4.0223 + phi_rad: 4.71238898038469 + theta_rad: 0.23736477827122882 + } + gain_value { + gain_db: 4.1375 + phi_rad: 4.71238898038469 + theta_rad: 0.23823744289722598 + } + gain_value { + gain_db: 4.3227 + phi_rad: 4.71238898038469 + theta_rad: 0.23911010752322315 + } + gain_value { + gain_db: 4.4893 + phi_rad: 4.71238898038469 + theta_rad: 0.2399827721492203 + } + gain_value { + gain_db: 4.6775 + phi_rad: 4.71238898038469 + theta_rad: 0.24085543677521748 + } + gain_value { + gain_db: 4.8266 + phi_rad: 4.71238898038469 + theta_rad: 0.24172810140121465 + } + gain_value { + gain_db: 5.0048 + phi_rad: 4.71238898038469 + theta_rad: 0.2426007660272118 + } + gain_value { + gain_db: 5.1084 + phi_rad: 4.71238898038469 + theta_rad: 0.24347343065320895 + } + gain_value { + gain_db: 5.2902 + phi_rad: 4.71238898038469 + theta_rad: 0.24434609527920614 + } + gain_value { + gain_db: 5.3981 + phi_rad: 4.71238898038469 + theta_rad: 0.2452187599052033 + } + gain_value { + gain_db: 5.5306 + phi_rad: 4.71238898038469 + theta_rad: 0.24609142453120045 + } + gain_value { + gain_db: 5.6153 + phi_rad: 4.71238898038469 + theta_rad: 0.24696408915719764 + } + gain_value { + gain_db: 5.6505 + phi_rad: 4.71238898038469 + theta_rad: 0.24783675378319478 + } + gain_value { + gain_db: 5.609 + phi_rad: 4.71238898038469 + theta_rad: 0.24870941840919197 + } + gain_value { + gain_db: 5.4817 + phi_rad: 4.71238898038469 + theta_rad: 0.24958208303518914 + } + gain_value { + gain_db: 5.3394 + phi_rad: 4.71238898038469 + theta_rad: 0.2504547476611863 + } + gain_value { + gain_db: 5.1664 + phi_rad: 4.71238898038469 + theta_rad: 0.25132741228718347 + } + gain_value { + gain_db: 4.9144 + phi_rad: 4.71238898038469 + theta_rad: 0.2522000769131806 + } + gain_value { + gain_db: 4.6895 + phi_rad: 4.71238898038469 + theta_rad: 0.2530727415391778 + } + gain_value { + gain_db: 4.3658 + phi_rad: 4.71238898038469 + theta_rad: 0.25394540616517497 + } + gain_value { + gain_db: 3.9613 + phi_rad: 4.71238898038469 + theta_rad: 0.25481807079117214 + } + gain_value { + gain_db: 3.6341 + phi_rad: 4.71238898038469 + theta_rad: 0.2556907354171693 + } + gain_value { + gain_db: 3.2206 + phi_rad: 4.71238898038469 + theta_rad: 0.2565634000431664 + } + gain_value { + gain_db: 3.0062 + phi_rad: 4.71238898038469 + theta_rad: 0.25743606466916363 + } + gain_value { + gain_db: 2.7423 + phi_rad: 4.71238898038469 + theta_rad: 0.2583087292951608 + } + gain_value { + gain_db: 2.4523 + phi_rad: 4.71238898038469 + theta_rad: 0.2591813939211579 + } + gain_value { + gain_db: 2.187 + phi_rad: 4.71238898038469 + theta_rad: 0.26005405854715513 + } + gain_value { + gain_db: 1.9188 + phi_rad: 4.71238898038469 + theta_rad: 0.26092672317315224 + } + gain_value { + gain_db: 1.4959 + phi_rad: 4.71238898038469 + theta_rad: 0.2617993877991494 + } + gain_value { + gain_db: 1.1279 + phi_rad: 4.71238898038469 + theta_rad: 0.26267205242514663 + } + gain_value { + gain_db: 0.7564 + phi_rad: 4.71238898038469 + theta_rad: 0.26354471705114374 + } + gain_value { + gain_db: 0.3469 + phi_rad: 4.71238898038469 + theta_rad: 0.2644173816771409 + } + gain_value { + gain_db: 0.084933 + phi_rad: 4.71238898038469 + theta_rad: 0.26529004630313807 + } + gain_value { + gain_db: -0.24037 + phi_rad: 4.71238898038469 + theta_rad: 0.26616271092913524 + } + gain_value { + gain_db: -0.51853 + phi_rad: 4.71238898038469 + theta_rad: 0.26703537555513246 + } + gain_value { + gain_db: -0.70787 + phi_rad: 4.71238898038469 + theta_rad: 0.26790804018112957 + } + gain_value { + gain_db: -0.76343 + phi_rad: 4.71238898038469 + theta_rad: 0.26878070480712674 + } + gain_value { + gain_db: -0.75073 + phi_rad: 4.71238898038469 + theta_rad: 0.2696533694331239 + } + gain_value { + gain_db: -0.72693 + phi_rad: 4.71238898038469 + theta_rad: 0.27052603405912107 + } + gain_value { + gain_db: -0.66803 + phi_rad: 4.71238898038469 + theta_rad: 0.27139869868511823 + } + gain_value { + gain_db: -0.71073 + phi_rad: 4.71238898038469 + theta_rad: 0.2722713633111154 + } + gain_value { + gain_db: -0.87617 + phi_rad: 4.71238898038469 + theta_rad: 0.27314402793711257 + } + gain_value { + gain_db: -1.0722 + phi_rad: 4.71238898038469 + theta_rad: 0.27401669256310973 + } + gain_value { + gain_db: -1.346 + phi_rad: 4.71238898038469 + theta_rad: 0.2748893571891069 + } + gain_value { + gain_db: -1.6576 + phi_rad: 4.71238898038469 + theta_rad: 0.27576202181510406 + } + gain_value { + gain_db: -2.0297 + phi_rad: 4.71238898038469 + theta_rad: 0.27663468644110123 + } + gain_value { + gain_db: -2.3209 + phi_rad: 4.71238898038469 + theta_rad: 0.2775073510670984 + } + gain_value { + gain_db: -2.5376 + phi_rad: 4.71238898038469 + theta_rad: 0.27838001569309556 + } + gain_value { + gain_db: -2.6248 + phi_rad: 4.71238898038469 + theta_rad: 0.2792526803190927 + } + gain_value { + gain_db: -2.6661 + phi_rad: 4.71238898038469 + theta_rad: 0.2801253449450899 + } + gain_value { + gain_db: -2.5925 + phi_rad: 4.71238898038469 + theta_rad: 0.28099800957108706 + } + gain_value { + gain_db: -2.4967 + phi_rad: 4.71238898038469 + theta_rad: 0.28187067419708417 + } + gain_value { + gain_db: -2.3537 + phi_rad: 4.71238898038469 + theta_rad: 0.2827433388230814 + } + gain_value { + gain_db: -2.2224 + phi_rad: 4.71238898038469 + theta_rad: 0.28361600344907856 + } + gain_value { + gain_db: -2.203 + phi_rad: 4.71238898038469 + theta_rad: 0.2844886680750757 + } + gain_value { + gain_db: -2.2312 + phi_rad: 4.71238898038469 + theta_rad: 0.2853613327010729 + } + gain_value { + gain_db: -2.2845 + phi_rad: 4.71238898038469 + theta_rad: 0.28623399732707 + } + gain_value { + gain_db: -2.3629 + phi_rad: 4.71238898038469 + theta_rad: 0.2871066619530672 + } + gain_value { + gain_db: -2.3505 + phi_rad: 4.71238898038469 + theta_rad: 0.2879793265790644 + } + gain_value { + gain_db: -2.3624 + phi_rad: 4.71238898038469 + theta_rad: 0.28885199120506155 + } + gain_value { + gain_db: -2.3237 + phi_rad: 4.71238898038469 + theta_rad: 0.2897246558310587 + } + gain_value { + gain_db: -2.2452 + phi_rad: 4.71238898038469 + theta_rad: 0.29059732045705583 + } + gain_value { + gain_db: -2.0867 + phi_rad: 4.71238898038469 + theta_rad: 0.291469985083053 + } + gain_value { + gain_db: -1.9254 + phi_rad: 4.71238898038469 + theta_rad: 0.2923426497090502 + } + gain_value { + gain_db: -1.7226 + phi_rad: 4.71238898038469 + theta_rad: 0.2932153143350474 + } + gain_value { + gain_db: -1.5487 + phi_rad: 4.71238898038469 + theta_rad: 0.29408797896104455 + } + gain_value { + gain_db: -1.4753 + phi_rad: 4.71238898038469 + theta_rad: 0.29496064358704166 + } + gain_value { + gain_db: -1.4371 + phi_rad: 4.71238898038469 + theta_rad: 0.2958333082130388 + } + gain_value { + gain_db: -1.4745 + phi_rad: 4.71238898038469 + theta_rad: 0.29670597283903605 + } + gain_value { + gain_db: -1.5651 + phi_rad: 4.71238898038469 + theta_rad: 0.2975786374650332 + } + gain_value { + gain_db: -1.7774 + phi_rad: 4.71238898038469 + theta_rad: 0.2984513020910304 + } + gain_value { + gain_db: -2.0173 + phi_rad: 4.71238898038469 + theta_rad: 0.2993239667170275 + } + gain_value { + gain_db: -2.3036 + phi_rad: 4.71238898038469 + theta_rad: 0.30019663134302466 + } + gain_value { + gain_db: -2.5499 + phi_rad: 4.71238898038469 + theta_rad: 0.3010692959690218 + } + gain_value { + gain_db: -2.8782 + phi_rad: 4.71238898038469 + theta_rad: 0.30194196059501904 + } + gain_value { + gain_db: -3.1788 + phi_rad: 4.71238898038469 + theta_rad: 0.3028146252210162 + } + gain_value { + gain_db: -3.6117 + phi_rad: 4.71238898038469 + theta_rad: 0.3036872898470133 + } + gain_value { + gain_db: -3.9985 + phi_rad: 4.71238898038469 + theta_rad: 0.3045599544730105 + } + gain_value { + gain_db: -4.539 + phi_rad: 4.71238898038469 + theta_rad: 0.30543261909900765 + } + gain_value { + gain_db: -5.1656 + phi_rad: 4.71238898038469 + theta_rad: 0.3063052837250049 + } + gain_value { + gain_db: -6.0697 + phi_rad: 4.71238898038469 + theta_rad: 0.30717794835100204 + } + gain_value { + gain_db: -7.0463 + phi_rad: 4.71238898038469 + theta_rad: 0.30805061297699915 + } + gain_value { + gain_db: -8.6476 + phi_rad: 4.71238898038469 + theta_rad: 0.3089232776029963 + } + gain_value { + gain_db: -10.733 + phi_rad: 4.71238898038469 + theta_rad: 0.3097959422289935 + } + gain_value { + gain_db: -11.304 + phi_rad: 4.71238898038469 + theta_rad: 0.31066860685499065 + } + gain_value { + gain_db: -9.9454 + phi_rad: 4.71238898038469 + theta_rad: 0.31154127148098787 + } + gain_value { + gain_db: -9.2068 + phi_rad: 4.71238898038469 + theta_rad: 0.312413936106985 + } + gain_value { + gain_db: -8.6451 + phi_rad: 4.71238898038469 + theta_rad: 0.31328660073298215 + } + gain_value { + gain_db: -8.5355 + phi_rad: 4.71238898038469 + theta_rad: 0.3141592653589793 + } + gain_value { + gain_db: -8.5131 + phi_rad: 4.71238898038469 + theta_rad: 0.3150319299849765 + } + gain_value { + gain_db: -8.6499 + phi_rad: 4.71238898038469 + theta_rad: 0.3159045946109737 + } + gain_value { + gain_db: -8.8938 + phi_rad: 4.71238898038469 + theta_rad: 0.3167772592369708 + } + gain_value { + gain_db: -9.1696 + phi_rad: 4.71238898038469 + theta_rad: 0.317649923862968 + } + gain_value { + gain_db: -9.633 + phi_rad: 4.71238898038469 + theta_rad: 0.31852258848896514 + } + gain_value { + gain_db: -10.151 + phi_rad: 4.71238898038469 + theta_rad: 0.3193952531149623 + } + gain_value { + gain_db: -10.684 + phi_rad: 4.71238898038469 + theta_rad: 0.3202679177409595 + } + gain_value { + gain_db: -11.207 + phi_rad: 4.71238898038469 + theta_rad: 0.32114058236695664 + } + gain_value { + gain_db: -11.783 + phi_rad: 4.71238898038469 + theta_rad: 0.3220132469929538 + } + gain_value { + gain_db: -11.998 + phi_rad: 4.71238898038469 + theta_rad: 0.32288591161895097 + } + gain_value { + gain_db: -12.051 + phi_rad: 4.71238898038469 + theta_rad: 0.32375857624494814 + } + gain_value { + gain_db: -11.616 + phi_rad: 4.71238898038469 + theta_rad: 0.3246312408709453 + } + gain_value { + gain_db: -11.426 + phi_rad: 4.71238898038469 + theta_rad: 0.3255039054969424 + } + gain_value { + gain_db: -11.235 + phi_rad: 4.71238898038469 + theta_rad: 0.32637657012293964 + } + gain_value { + gain_db: -11.198 + phi_rad: 4.71238898038469 + theta_rad: 0.3272492347489368 + } + gain_value { + gain_db: -11.24 + phi_rad: 4.71238898038469 + theta_rad: 0.32812189937493397 + } + gain_value { + gain_db: -11.276 + phi_rad: 4.71238898038469 + theta_rad: 0.32899456400093113 + } + gain_value { + gain_db: -10.914 + phi_rad: 4.71238898038469 + theta_rad: 0.32986722862692824 + } + gain_value { + gain_db: -10.383 + phi_rad: 4.71238898038469 + theta_rad: 0.3307398932529254 + } + gain_value { + gain_db: -9.6759 + phi_rad: 4.71238898038469 + theta_rad: 0.33161255787892263 + } + gain_value { + gain_db: -9.2428 + phi_rad: 4.71238898038469 + theta_rad: 0.3324852225049198 + } + gain_value { + gain_db: -8.9076 + phi_rad: 4.71238898038469 + theta_rad: 0.33335788713091696 + } + gain_value { + gain_db: -8.6993 + phi_rad: 4.71238898038469 + theta_rad: 0.3342305517569141 + } + gain_value { + gain_db: -8.5436 + phi_rad: 4.71238898038469 + theta_rad: 0.33510321638291124 + } + gain_value { + gain_db: -8.4459 + phi_rad: 4.71238898038469 + theta_rad: 0.33597588100890846 + } + gain_value { + gain_db: -8.4688 + phi_rad: 4.71238898038469 + theta_rad: 0.3368485456349056 + } + gain_value { + gain_db: -8.3788 + phi_rad: 4.71238898038469 + theta_rad: 0.3377212102609028 + } + gain_value { + gain_db: -7.8945 + phi_rad: 4.71238898038469 + theta_rad: 0.3385938748868999 + } + gain_value { + gain_db: -7.1895 + phi_rad: 4.71238898038469 + theta_rad: 0.33946653951289707 + } + gain_value { + gain_db: -6.5427 + phi_rad: 4.71238898038469 + theta_rad: 0.34033920413889424 + } + gain_value { + gain_db: -5.8907 + phi_rad: 4.71238898038469 + theta_rad: 0.34121186876489146 + } + gain_value { + gain_db: -5.4168 + phi_rad: 4.71238898038469 + theta_rad: 0.3420845333908886 + } + gain_value { + gain_db: -4.9346 + phi_rad: 4.71238898038469 + theta_rad: 0.34295719801688573 + } + gain_value { + gain_db: -4.6346 + phi_rad: 4.71238898038469 + theta_rad: 0.3438298626428829 + } + gain_value { + gain_db: -4.3631 + phi_rad: 4.71238898038469 + theta_rad: 0.34470252726888007 + } + gain_value { + gain_db: -4.2046 + phi_rad: 4.71238898038469 + theta_rad: 0.3455751918948773 + } + gain_value { + gain_db: -4.2247 + phi_rad: 4.71238898038469 + theta_rad: 0.34644785652087445 + } + gain_value { + gain_db: -4.2589 + phi_rad: 4.71238898038469 + theta_rad: 0.34732052114687156 + } + gain_value { + gain_db: -4.2766 + phi_rad: 4.71238898038469 + theta_rad: 0.34819318577286873 + } + gain_value { + gain_db: -4.296 + phi_rad: 4.71238898038469 + theta_rad: 0.3490658503988659 + } + gain_value { + gain_db: -4.1562 + phi_rad: 4.71238898038469 + theta_rad: 0.34993851502486306 + } + gain_value { + gain_db: -3.9798 + phi_rad: 4.71238898038469 + theta_rad: 0.3508111796508603 + } + gain_value { + gain_db: -3.655 + phi_rad: 4.71238898038469 + theta_rad: 0.3516838442768574 + } + gain_value { + gain_db: -3.3458 + phi_rad: 4.71238898038469 + theta_rad: 0.35255650890285456 + } + gain_value { + gain_db: -3.0917 + phi_rad: 4.71238898038469 + theta_rad: 0.3534291735288517 + } + gain_value { + gain_db: -2.7994 + phi_rad: 4.71238898038469 + theta_rad: 0.3543018381548489 + } + gain_value { + gain_db: -2.5882 + phi_rad: 4.71238898038469 + theta_rad: 0.3551745027808461 + } + gain_value { + gain_db: -2.4494 + phi_rad: 4.71238898038469 + theta_rad: 0.3560471674068432 + } + gain_value { + gain_db: -2.3923 + phi_rad: 4.71238898038469 + theta_rad: 0.3569198320328404 + } + gain_value { + gain_db: -2.3605 + phi_rad: 4.71238898038469 + theta_rad: 0.35779249665883756 + } + gain_value { + gain_db: -2.3848 + phi_rad: 4.71238898038469 + theta_rad: 0.3586651612848347 + } + gain_value { + gain_db: -2.4393 + phi_rad: 4.71238898038469 + theta_rad: 0.3595378259108319 + } + gain_value { + gain_db: -2.4703 + phi_rad: 4.71238898038469 + theta_rad: 0.36041049053682905 + } + gain_value { + gain_db: -2.5312 + phi_rad: 4.71238898038469 + theta_rad: 0.3612831551628262 + } + gain_value { + gain_db: -2.5184 + phi_rad: 4.71238898038469 + theta_rad: 0.3621558197888234 + } + gain_value { + gain_db: -2.4195 + phi_rad: 4.71238898038469 + theta_rad: 0.36302848441482055 + } + gain_value { + gain_db: -2.2382 + phi_rad: 4.71238898038469 + theta_rad: 0.3639011490408177 + } + gain_value { + gain_db: -2.005 + phi_rad: 4.71238898038469 + theta_rad: 0.36477381366681483 + } + gain_value { + gain_db: -1.7563 + phi_rad: 4.71238898038469 + theta_rad: 0.36564647829281205 + } + gain_value { + gain_db: -1.5231 + phi_rad: 4.71238898038469 + theta_rad: 0.3665191429188092 + } + gain_value { + gain_db: -1.2699 + phi_rad: 4.71238898038469 + theta_rad: 0.3673918075448064 + } + gain_value { + gain_db: -1.1017 + phi_rad: 4.71238898038469 + theta_rad: 0.36826447217080355 + } + gain_value { + gain_db: -0.9747 + phi_rad: 4.71238898038469 + theta_rad: 0.36913713679680066 + } + gain_value { + gain_db: -0.85753 + phi_rad: 4.71238898038469 + theta_rad: 0.3700098014227979 + } + gain_value { + gain_db: -0.80977 + phi_rad: 4.71238898038469 + theta_rad: 0.37088246604879505 + } + gain_value { + gain_db: -0.79693 + phi_rad: 4.71238898038469 + theta_rad: 0.3717551306747922 + } + gain_value { + gain_db: -0.8756 + phi_rad: 4.71238898038469 + theta_rad: 0.3726277953007894 + } + gain_value { + gain_db: -0.93043 + phi_rad: 4.71238898038469 + theta_rad: 0.3735004599267865 + } + gain_value { + gain_db: -0.97243 + phi_rad: 4.71238898038469 + theta_rad: 0.37437312455278365 + } + gain_value { + gain_db: -0.89747 + phi_rad: 4.71238898038469 + theta_rad: 0.3752457891787809 + } + gain_value { + gain_db: -0.7479 + phi_rad: 4.71238898038469 + theta_rad: 0.37611845380477804 + } + gain_value { + gain_db: -0.50813 + phi_rad: 4.71238898038469 + theta_rad: 0.3769911184307752 + } + gain_value { + gain_db: -0.27413 + phi_rad: 4.71238898038469 + theta_rad: 0.3778637830567723 + } + gain_value { + gain_db: -0.0616 + phi_rad: 4.71238898038469 + theta_rad: 0.3787364476827695 + } + gain_value { + gain_db: 0.071733 + phi_rad: 4.71238898038469 + theta_rad: 0.37960911230876665 + } + gain_value { + gain_db: 0.086667 + phi_rad: 4.71238898038469 + theta_rad: 0.38048177693476387 + } + gain_value { + gain_db: 0.0357 + phi_rad: 4.71238898038469 + theta_rad: 0.38135444156076104 + } + gain_value { + gain_db: -0.083933 + phi_rad: 4.71238898038469 + theta_rad: 0.38222710618675815 + } + gain_value { + gain_db: -0.22927 + phi_rad: 4.71238898038469 + theta_rad: 0.3830997708127553 + } + gain_value { + gain_db: -0.39223 + phi_rad: 4.71238898038469 + theta_rad: 0.3839724354387525 + } + gain_value { + gain_db: -0.48833 + phi_rad: 4.71238898038469 + theta_rad: 0.3848451000647497 + } + gain_value { + gain_db: -0.55573 + phi_rad: 4.71238898038469 + theta_rad: 0.38571776469074687 + } + gain_value { + gain_db: -0.55197 + phi_rad: 4.71238898038469 + theta_rad: 0.386590429316744 + } + gain_value { + gain_db: -0.49313 + phi_rad: 4.71238898038469 + theta_rad: 0.38746309394274114 + } + gain_value { + gain_db: -0.4977 + phi_rad: 4.71238898038469 + theta_rad: 0.3883357585687383 + } + gain_value { + gain_db: -0.54513 + phi_rad: 4.71238898038469 + theta_rad: 0.3892084231947355 + } + gain_value { + gain_db: -0.67603 + phi_rad: 4.71238898038469 + theta_rad: 0.3900810878207327 + } + gain_value { + gain_db: -0.86623 + phi_rad: 4.71238898038469 + theta_rad: 0.3909537524467298 + } + gain_value { + gain_db: -1.1396 + phi_rad: 4.71238898038469 + theta_rad: 0.391826417072727 + } + gain_value { + gain_db: -1.3608 + phi_rad: 4.71238898038469 + theta_rad: 0.39269908169872414 + } + gain_value { + gain_db: -1.5546 + phi_rad: 4.71238898038469 + theta_rad: 0.3935717463247213 + } + gain_value { + gain_db: -1.6999 + phi_rad: 4.71238898038469 + theta_rad: 0.3944444109507185 + } + gain_value { + gain_db: -1.7795 + phi_rad: 4.71238898038469 + theta_rad: 0.39531707557671564 + } + gain_value { + gain_db: -1.847 + phi_rad: 4.71238898038469 + theta_rad: 0.3961897402027128 + } + gain_value { + gain_db: -2.0363 + phi_rad: 4.71238898038469 + theta_rad: 0.39706240482870997 + } + gain_value { + gain_db: -2.2018 + phi_rad: 4.71238898038469 + theta_rad: 0.39793506945470714 + } + gain_value { + gain_db: -2.4719 + phi_rad: 4.71238898038469 + theta_rad: 0.3988077340807043 + } + gain_value { + gain_db: -2.7422 + phi_rad: 4.71238898038469 + theta_rad: 0.39968039870670147 + } + gain_value { + gain_db: -2.9817 + phi_rad: 4.71238898038469 + theta_rad: 0.40055306333269863 + } + gain_value { + gain_db: -3.1149 + phi_rad: 4.71238898038469 + theta_rad: 0.4014257279586958 + } + gain_value { + gain_db: -3.1858 + phi_rad: 4.71238898038469 + theta_rad: 0.40229839258469297 + } + gain_value { + gain_db: -3.2386 + phi_rad: 4.71238898038469 + theta_rad: 0.40317105721069013 + } + gain_value { + gain_db: -3.1698 + phi_rad: 4.71238898038469 + theta_rad: 0.40404372183668724 + } + gain_value { + gain_db: -3.108 + phi_rad: 4.71238898038469 + theta_rad: 0.40491638646268446 + } + gain_value { + gain_db: -3.145 + phi_rad: 4.71238898038469 + theta_rad: 0.40578905108868163 + } + gain_value { + gain_db: -3.312 + phi_rad: 4.71238898038469 + theta_rad: 0.4066617157146788 + } + gain_value { + gain_db: -3.6735 + phi_rad: 4.71238898038469 + theta_rad: 0.40753438034067596 + } + gain_value { + gain_db: -4.128 + phi_rad: 4.71238898038469 + theta_rad: 0.40840704496667307 + } + gain_value { + gain_db: -4.6179 + phi_rad: 4.71238898038469 + theta_rad: 0.4092797095926703 + } + gain_value { + gain_db: -5.0248 + phi_rad: 4.71238898038469 + theta_rad: 0.41015237421866746 + } + gain_value { + gain_db: -5.2967 + phi_rad: 4.71238898038469 + theta_rad: 0.4110250388446646 + } + gain_value { + gain_db: -5.501 + phi_rad: 4.71238898038469 + theta_rad: 0.4118977034706618 + } + gain_value { + gain_db: -5.7722 + phi_rad: 4.71238898038469 + theta_rad: 0.4127703680966589 + } + gain_value { + gain_db: -5.9793 + phi_rad: 4.71238898038469 + theta_rad: 0.41364303272265607 + } + gain_value { + gain_db: -6.2701 + phi_rad: 4.71238898038469 + theta_rad: 0.4145156973486533 + } + gain_value { + gain_db: -6.5442 + phi_rad: 4.71238898038469 + theta_rad: 0.41538836197465046 + } + gain_value { + gain_db: -7.0165 + phi_rad: 4.71238898038469 + theta_rad: 0.4162610266006476 + } + gain_value { + gain_db: -7.4611 + phi_rad: 4.71238898038469 + theta_rad: 0.41713369122664473 + } + gain_value { + gain_db: -7.9719 + phi_rad: 4.71238898038469 + theta_rad: 0.4180063558526419 + } + gain_value { + gain_db: -8.2954 + phi_rad: 4.71238898038469 + theta_rad: 0.4188790204786391 + } + gain_value { + gain_db: -8.371 + phi_rad: 4.71238898038469 + theta_rad: 0.4197516851046363 + } + gain_value { + gain_db: -8.3758 + phi_rad: 4.71238898038469 + theta_rad: 0.42062434973063345 + } + gain_value { + gain_db: -8.3594 + phi_rad: 4.71238898038469 + theta_rad: 0.42149701435663056 + } + gain_value { + gain_db: -8.1891 + phi_rad: 4.71238898038469 + theta_rad: 0.4223696789826277 + } + gain_value { + gain_db: -8.2085 + phi_rad: 4.71238898038469 + theta_rad: 0.4232423436086249 + } + gain_value { + gain_db: -8.3148 + phi_rad: 4.71238898038469 + theta_rad: 0.4241150082346221 + } + gain_value { + gain_db: -8.6619 + phi_rad: 4.71238898038469 + theta_rad: 0.4249876728606193 + } + gain_value { + gain_db: -9.2228 + phi_rad: 4.71238898038469 + theta_rad: 0.4258603374866164 + } + gain_value { + gain_db: -9.6303 + phi_rad: 4.71238898038469 + theta_rad: 0.42673300211261356 + } + gain_value { + gain_db: -10.099 + phi_rad: 4.71238898038469 + theta_rad: 0.4276056667386107 + } + gain_value { + gain_db: -10.411 + phi_rad: 4.71238898038469 + theta_rad: 0.4284783313646079 + } + gain_value { + gain_db: -10.655 + phi_rad: 4.71238898038469 + theta_rad: 0.4293509959906051 + } + gain_value { + gain_db: -10.698 + phi_rad: 4.71238898038469 + theta_rad: 0.4302236606166022 + } + gain_value { + gain_db: -10.785 + phi_rad: 4.71238898038469 + theta_rad: 0.4310963252425994 + } + gain_value { + gain_db: -10.887 + phi_rad: 4.71238898038469 + theta_rad: 0.43196898986859655 + } + gain_value { + gain_db: -10.983 + phi_rad: 4.71238898038469 + theta_rad: 0.4328416544945937 + } + gain_value { + gain_db: -11.035 + phi_rad: 4.71238898038469 + theta_rad: 0.43371431912059094 + } + gain_value { + gain_db: -11.145 + phi_rad: 4.71238898038469 + theta_rad: 0.43458698374658805 + } + gain_value { + gain_db: -11.215 + phi_rad: 4.71238898038469 + theta_rad: 0.4354596483725852 + } + gain_value { + gain_db: -11.482 + phi_rad: 4.71238898038469 + theta_rad: 0.4363323129985824 + } + gain_value { + gain_db: -11.762 + phi_rad: 4.71238898038469 + theta_rad: 0.43720497762457955 + } + gain_value { + gain_db: -11.989 + phi_rad: 4.71238898038469 + theta_rad: 0.4380776422505767 + } + gain_value { + gain_db: -12.276 + phi_rad: 4.71238898038469 + theta_rad: 0.4389503068765739 + } + gain_value { + gain_db: -12.311 + phi_rad: 4.71238898038469 + theta_rad: 0.43982297150257105 + } + gain_value { + gain_db: -12.605 + phi_rad: 4.71238898038469 + theta_rad: 0.4406956361285682 + } + gain_value { + gain_db: -12.872 + phi_rad: 4.71238898038469 + theta_rad: 0.4415683007545654 + } + gain_value { + gain_db: -13.561 + phi_rad: 4.71238898038469 + theta_rad: 0.44244096538056255 + } + gain_value { + gain_db: -14.291 + phi_rad: 4.71238898038469 + theta_rad: 0.44331363000655966 + } + gain_value { + gain_db: -14.401 + phi_rad: 4.71238898038469 + theta_rad: 0.4441862946325569 + } + gain_value { + gain_db: -13.921 + phi_rad: 4.71238898038469 + theta_rad: 0.44505895925855404 + } + gain_value { + gain_db: -13.447 + phi_rad: 4.71238898038469 + theta_rad: 0.4459316238845512 + } + gain_value { + gain_db: -13.049 + phi_rad: 4.71238898038469 + theta_rad: 0.4468042885105484 + } + gain_value { + gain_db: -12.567 + phi_rad: 4.71238898038469 + theta_rad: 0.4476769531365455 + } + gain_value { + gain_db: -12.204 + phi_rad: 4.71238898038469 + theta_rad: 0.4485496177625427 + } + gain_value { + gain_db: -11.889 + phi_rad: 4.71238898038469 + theta_rad: 0.4494222823885399 + } + gain_value { + gain_db: -11.7 + phi_rad: 4.71238898038469 + theta_rad: 0.45029494701453704 + } + gain_value { + gain_db: -11.587 + phi_rad: 4.71238898038469 + theta_rad: 0.4511676116405342 + } + gain_value { + gain_db: -11.58 + phi_rad: 4.71238898038469 + theta_rad: 0.4520402762665313 + } + gain_value { + gain_db: -12.089 + phi_rad: 4.71238898038469 + theta_rad: 0.4529129408925285 + } + gain_value { + gain_db: -12.796 + phi_rad: 4.71238898038469 + theta_rad: 0.4537856055185257 + } + gain_value { + gain_db: -13.751 + phi_rad: 4.71238898038469 + theta_rad: 0.45465827014452287 + } + gain_value { + gain_db: -14.266 + phi_rad: 4.71238898038469 + theta_rad: 0.45553093477052004 + } + gain_value { + gain_db: -14.065 + phi_rad: 4.71238898038469 + theta_rad: 0.45640359939651715 + } + gain_value { + gain_db: -12.978 + phi_rad: 4.71238898038469 + theta_rad: 0.4572762640225143 + } + gain_value { + gain_db: -11.481 + phi_rad: 4.71238898038469 + theta_rad: 0.45814892864851153 + } + gain_value { + gain_db: -10.061 + phi_rad: 4.71238898038469 + theta_rad: 0.4590215932745087 + } + gain_value { + gain_db: -9.0621 + phi_rad: 4.71238898038469 + theta_rad: 0.45989425790050587 + } + gain_value { + gain_db: -8.3795 + phi_rad: 4.71238898038469 + theta_rad: 0.460766922526503 + } + gain_value { + gain_db: -7.8283 + phi_rad: 4.71238898038469 + theta_rad: 0.46163958715250014 + } + gain_value { + gain_db: -7.6397 + phi_rad: 4.71238898038469 + theta_rad: 0.4625122517784973 + } + gain_value { + gain_db: -7.3505 + phi_rad: 4.71238898038469 + theta_rad: 0.46338491640449453 + } + gain_value { + gain_db: -7.1439 + phi_rad: 4.71238898038469 + theta_rad: 0.4642575810304917 + } + gain_value { + gain_db: -6.7871 + phi_rad: 4.71238898038469 + theta_rad: 0.4651302456564888 + } + gain_value { + gain_db: -6.2071 + phi_rad: 4.71238898038469 + theta_rad: 0.46600291028248597 + } + gain_value { + gain_db: -5.5124 + phi_rad: 4.71238898038469 + theta_rad: 0.46687557490848314 + } + gain_value { + gain_db: -4.798 + phi_rad: 4.71238898038469 + theta_rad: 0.4677482395344803 + } + gain_value { + gain_db: -3.9963 + phi_rad: 4.71238898038469 + theta_rad: 0.4686209041604775 + } + gain_value { + gain_db: -3.3771 + phi_rad: 4.71238898038469 + theta_rad: 0.46949356878647464 + } + gain_value { + gain_db: -2.7347 + phi_rad: 4.71238898038469 + theta_rad: 0.4703662334124718 + } + gain_value { + gain_db: -2.2449 + phi_rad: 4.71238898038469 + theta_rad: 0.47123889803846897 + } + gain_value { + gain_db: -1.8092 + phi_rad: 4.71238898038469 + theta_rad: 0.47211156266446613 + } + gain_value { + gain_db: -1.4276 + phi_rad: 4.71238898038469 + theta_rad: 0.47298422729046335 + } + gain_value { + gain_db: -1.2594 + phi_rad: 4.71238898038469 + theta_rad: 0.47385689191646047 + } + gain_value { + gain_db: -1.129 + phi_rad: 4.71238898038469 + theta_rad: 0.47472955654245763 + } + gain_value { + gain_db: -0.9575 + phi_rad: 4.71238898038469 + theta_rad: 0.4756022211684548 + } + gain_value { + gain_db: -0.8094 + phi_rad: 4.71238898038469 + theta_rad: 0.47647488579445196 + } + gain_value { + gain_db: -0.6485 + phi_rad: 4.71238898038469 + theta_rad: 0.47734755042044913 + } + gain_value { + gain_db: -0.41623 + phi_rad: 4.71238898038469 + theta_rad: 0.4782202150464463 + } + gain_value { + gain_db: -0.1259 + phi_rad: 4.71238898038469 + theta_rad: 0.47909287967244346 + } + gain_value { + gain_db: 0.16057 + phi_rad: 4.71238898038469 + theta_rad: 0.4799655442984406 + } + gain_value { + gain_db: 0.48897 + phi_rad: 4.71238898038469 + theta_rad: 0.4808382089244378 + } + gain_value { + gain_db: 0.78817 + phi_rad: 4.71238898038469 + theta_rad: 0.48171087355043496 + } + gain_value { + gain_db: 1.0225 + phi_rad: 4.71238898038469 + theta_rad: 0.48258353817643207 + } + gain_value { + gain_db: 1.1478 + phi_rad: 4.71238898038469 + theta_rad: 0.4834562028024293 + } + gain_value { + gain_db: 1.2007 + phi_rad: 4.71238898038469 + theta_rad: 0.48432886742842646 + } + gain_value { + gain_db: 1.2096 + phi_rad: 4.71238898038469 + theta_rad: 0.4852015320544236 + } + gain_value { + gain_db: 1.2237 + phi_rad: 4.71238898038469 + theta_rad: 0.4860741966804208 + } + gain_value { + gain_db: 1.1808 + phi_rad: 4.71238898038469 + theta_rad: 0.4869468613064179 + } + gain_value { + gain_db: 1.1436 + phi_rad: 4.71238898038469 + theta_rad: 0.4878195259324151 + } + gain_value { + gain_db: 1.1118 + phi_rad: 4.71238898038469 + theta_rad: 0.4886921905584123 + } + gain_value { + gain_db: 1.0758 + phi_rad: 4.71238898038469 + theta_rad: 0.48956485518440945 + } + gain_value { + gain_db: 1.0971 + phi_rad: 4.71238898038469 + theta_rad: 0.4904375198104066 + } + gain_value { + gain_db: 1.14 + phi_rad: 4.71238898038469 + theta_rad: 0.49131018443640373 + } + gain_value { + gain_db: 1.2318 + phi_rad: 4.71238898038469 + theta_rad: 0.4921828490624009 + } + gain_value { + gain_db: 1.3135 + phi_rad: 4.71238898038469 + theta_rad: 0.4930555136883981 + } + gain_value { + gain_db: 1.3123 + phi_rad: 4.71238898038469 + theta_rad: 0.4939281783143953 + } + gain_value { + gain_db: 1.3004 + phi_rad: 4.71238898038469 + theta_rad: 0.49480084294039245 + } + gain_value { + gain_db: 1.2445 + phi_rad: 4.71238898038469 + theta_rad: 0.49567350756638956 + } + gain_value { + gain_db: 1.1475 + phi_rad: 4.71238898038469 + theta_rad: 0.4965461721923867 + } + gain_value { + gain_db: 1.0369 + phi_rad: 4.71238898038469 + theta_rad: 0.49741883681838395 + } + gain_value { + gain_db: 0.86173 + phi_rad: 4.71238898038469 + theta_rad: 0.4982915014443811 + } + gain_value { + gain_db: 0.68553 + phi_rad: 4.71238898038469 + theta_rad: 0.4991641660703783 + } + gain_value { + gain_db: 0.55343 + phi_rad: 4.71238898038469 + theta_rad: 0.5000368306963754 + } + gain_value { + gain_db: 0.44543 + phi_rad: 4.71238898038469 + theta_rad: 0.5009094953223726 + } + gain_value { + gain_db: 0.30197 + phi_rad: 4.71238898038469 + theta_rad: 0.5017821599483697 + } + gain_value { + gain_db: 0.1051 + phi_rad: 4.71238898038469 + theta_rad: 0.5026548245743669 + } + gain_value { + gain_db: -0.092233 + phi_rad: 4.71238898038469 + theta_rad: 0.503527489200364 + } + gain_value { + gain_db: -0.26173 + phi_rad: 4.71238898038469 + theta_rad: 0.5044001538263612 + } + gain_value { + gain_db: -0.4764 + phi_rad: 4.71238898038469 + theta_rad: 0.5052728184523584 + } + gain_value { + gain_db: -0.662 + phi_rad: 4.71238898038469 + theta_rad: 0.5061454830783556 + } + gain_value { + gain_db: -0.8392 + phi_rad: 4.71238898038469 + theta_rad: 0.5070181477043527 + } + gain_value { + gain_db: -1.0336 + phi_rad: 4.71238898038469 + theta_rad: 0.5078908123303499 + } + gain_value { + gain_db: -1.199 + phi_rad: 4.71238898038469 + theta_rad: 0.508763476956347 + } + gain_value { + gain_db: -1.279 + phi_rad: 4.71238898038469 + theta_rad: 0.5096361415823443 + } + gain_value { + gain_db: -1.4437 + phi_rad: 4.71238898038469 + theta_rad: 0.5105088062083414 + } + gain_value { + gain_db: -1.6056 + phi_rad: 4.71238898038469 + theta_rad: 0.5113814708343386 + } + gain_value { + gain_db: -1.7812 + phi_rad: 4.71238898038469 + theta_rad: 0.5122541354603357 + } + gain_value { + gain_db: -2.0041 + phi_rad: 4.71238898038469 + theta_rad: 0.5131268000863328 + } + gain_value { + gain_db: -2.3068 + phi_rad: 4.71238898038469 + theta_rad: 0.51399946471233 + } + gain_value { + gain_db: -2.5443 + phi_rad: 4.71238898038469 + theta_rad: 0.5148721293383273 + } + gain_value { + gain_db: -2.8382 + phi_rad: 4.71238898038469 + theta_rad: 0.5157447939643244 + } + gain_value { + gain_db: -3.1765 + phi_rad: 4.71238898038469 + theta_rad: 0.5166174585903216 + } + gain_value { + gain_db: -3.4335 + phi_rad: 4.71238898038469 + theta_rad: 0.5174901232163187 + } + gain_value { + gain_db: -3.7029 + phi_rad: 4.71238898038469 + theta_rad: 0.5183627878423158 + } + gain_value { + gain_db: -3.9106 + phi_rad: 4.71238898038469 + theta_rad: 0.519235452468313 + } + gain_value { + gain_db: -4.114 + phi_rad: 4.71238898038469 + theta_rad: 0.5201081170943103 + } + gain_value { + gain_db: -4.2788 + phi_rad: 4.71238898038469 + theta_rad: 0.5209807817203074 + } + gain_value { + gain_db: -4.4642 + phi_rad: 4.71238898038469 + theta_rad: 0.5218534463463045 + } + gain_value { + gain_db: -4.6965 + phi_rad: 4.71238898038469 + theta_rad: 0.5227261109723017 + } + gain_value { + gain_db: -4.9961 + phi_rad: 4.71238898038469 + theta_rad: 0.5235987755982988 + } + gain_value { + gain_db: -5.2514 + phi_rad: 4.71238898038469 + theta_rad: 0.524471440224296 + } + gain_value { + gain_db: -5.5628 + phi_rad: 4.71238898038469 + theta_rad: 0.5253441048502933 + } + gain_value { + gain_db: -5.8634 + phi_rad: 4.71238898038469 + theta_rad: 0.5262167694762904 + } + gain_value { + gain_db: -6.1282 + phi_rad: 4.71238898038469 + theta_rad: 0.5270894341022875 + } + gain_value { + gain_db: -6.2831 + phi_rad: 4.71238898038469 + theta_rad: 0.5279620987282847 + } + gain_value { + gain_db: -6.427 + phi_rad: 4.71238898038469 + theta_rad: 0.5288347633542818 + } + gain_value { + gain_db: -6.4527 + phi_rad: 4.71238898038469 + theta_rad: 0.529707427980279 + } + gain_value { + gain_db: -6.4288 + phi_rad: 4.71238898038469 + theta_rad: 0.5305800926062761 + } + gain_value { + gain_db: -6.3608 + phi_rad: 4.71238898038469 + theta_rad: 0.5314527572322734 + } + gain_value { + gain_db: -6.5076 + phi_rad: 4.71238898038469 + theta_rad: 0.5323254218582705 + } + gain_value { + gain_db: -6.7393 + phi_rad: 4.71238898038469 + theta_rad: 0.5331980864842677 + } + gain_value { + gain_db: -7.2189 + phi_rad: 4.71238898038469 + theta_rad: 0.5340707511102649 + } + gain_value { + gain_db: -7.8033 + phi_rad: 4.71238898038469 + theta_rad: 0.534943415736262 + } + gain_value { + gain_db: -8.4676 + phi_rad: 4.71238898038469 + theta_rad: 0.5358160803622591 + } + gain_value { + gain_db: -9.1345 + phi_rad: 4.71238898038469 + theta_rad: 0.5366887449882564 + } + gain_value { + gain_db: -9.8418 + phi_rad: 4.71238898038469 + theta_rad: 0.5375614096142535 + } + gain_value { + gain_db: -10.287 + phi_rad: 4.71238898038469 + theta_rad: 0.5384340742402507 + } + gain_value { + gain_db: -10.449 + phi_rad: 4.71238898038469 + theta_rad: 0.5393067388662478 + } + gain_value { + gain_db: -10.543 + phi_rad: 4.71238898038469 + theta_rad: 0.540179403492245 + } + gain_value { + gain_db: -10.073 + phi_rad: 4.71238898038469 + theta_rad: 0.5410520681182421 + } + gain_value { + gain_db: -9.5789 + phi_rad: 4.71238898038469 + theta_rad: 0.5419247327442394 + } + gain_value { + gain_db: -9.2502 + phi_rad: 4.71238898038469 + theta_rad: 0.5427973973702365 + } + gain_value { + gain_db: -9.2159 + phi_rad: 4.71238898038469 + theta_rad: 0.5436700619962336 + } + gain_value { + gain_db: -9.4263 + phi_rad: 4.71238898038469 + theta_rad: 0.5445427266222308 + } + gain_value { + gain_db: -9.7874 + phi_rad: 4.71238898038469 + theta_rad: 0.545415391248228 + } + gain_value { + gain_db: -10.291 + phi_rad: 4.71238898038469 + theta_rad: 0.5462880558742251 + } + gain_value { + gain_db: -10.719 + phi_rad: 4.71238898038469 + theta_rad: 0.5471607205002224 + } + gain_value { + gain_db: -10.966 + phi_rad: 4.71238898038469 + theta_rad: 0.5480333851262195 + } + gain_value { + gain_db: -11.412 + phi_rad: 4.71238898038469 + theta_rad: 0.5489060497522167 + } + gain_value { + gain_db: -11.955 + phi_rad: 4.71238898038469 + theta_rad: 0.5497787143782138 + } + gain_value { + gain_db: -12.326 + phi_rad: 4.71238898038469 + theta_rad: 0.550651379004211 + } + gain_value { + gain_db: -12.154 + phi_rad: 4.71238898038469 + theta_rad: 0.5515240436302081 + } + gain_value { + gain_db: -11.621 + phi_rad: 4.71238898038469 + theta_rad: 0.5523967082562052 + } + gain_value { + gain_db: -11.028 + phi_rad: 4.71238898038469 + theta_rad: 0.5532693728822025 + } + gain_value { + gain_db: -10.547 + phi_rad: 4.71238898038469 + theta_rad: 0.5541420375081997 + } + gain_value { + gain_db: -10.306 + phi_rad: 4.71238898038469 + theta_rad: 0.5550147021341968 + } + gain_value { + gain_db: -10.045 + phi_rad: 4.71238898038469 + theta_rad: 0.555887366760194 + } + gain_value { + gain_db: -9.7567 + phi_rad: 4.71238898038469 + theta_rad: 0.5567600313861911 + } + gain_value { + gain_db: -9.4955 + phi_rad: 4.71238898038469 + theta_rad: 0.5576326960121882 + } + gain_value { + gain_db: -9.3327 + phi_rad: 4.71238898038469 + theta_rad: 0.5585053606381855 + } + gain_value { + gain_db: -9.3111 + phi_rad: 4.71238898038469 + theta_rad: 0.5593780252641826 + } + gain_value { + gain_db: -9.2832 + phi_rad: 4.71238898038469 + theta_rad: 0.5602506898901798 + } + gain_value { + gain_db: -9.1475 + phi_rad: 4.71238898038469 + theta_rad: 0.5611233545161769 + } + gain_value { + gain_db: -8.7534 + phi_rad: 4.71238898038469 + theta_rad: 0.5619960191421741 + } + gain_value { + gain_db: -7.9593 + phi_rad: 4.71238898038469 + theta_rad: 0.5628686837681712 + } + gain_value { + gain_db: -7.337 + phi_rad: 4.71238898038469 + theta_rad: 0.5637413483941683 + } + gain_value { + gain_db: -6.7002 + phi_rad: 4.71238898038469 + theta_rad: 0.5646140130201657 + } + gain_value { + gain_db: -6.2909 + phi_rad: 4.71238898038469 + theta_rad: 0.5654866776461628 + } + gain_value { + gain_db: -6.1039 + phi_rad: 4.71238898038469 + theta_rad: 0.56635934227216 + } + gain_value { + gain_db: -6.0544 + phi_rad: 4.71238898038469 + theta_rad: 0.5672320068981571 + } + gain_value { + gain_db: -6.1511 + phi_rad: 4.71238898038469 + theta_rad: 0.5681046715241542 + } + gain_value { + gain_db: -6.2229 + phi_rad: 4.71238898038469 + theta_rad: 0.5689773361501514 + } + gain_value { + gain_db: -6.1324 + phi_rad: 4.71238898038469 + theta_rad: 0.5698500007761486 + } + gain_value { + gain_db: -5.7919 + phi_rad: 4.71238898038469 + theta_rad: 0.5707226654021458 + } + gain_value { + gain_db: -5.2973 + phi_rad: 4.71238898038469 + theta_rad: 0.5715953300281429 + } + gain_value { + gain_db: -4.8594 + phi_rad: 4.71238898038469 + theta_rad: 0.57246799465414 + } + gain_value { + gain_db: -4.4918 + phi_rad: 4.71238898038469 + theta_rad: 0.5733406592801373 + } + gain_value { + gain_db: -4.1623 + phi_rad: 4.71238898038469 + theta_rad: 0.5742133239061344 + } + gain_value { + gain_db: -3.9424 + phi_rad: 4.71238898038469 + theta_rad: 0.5750859885321317 + } + gain_value { + gain_db: -3.783 + phi_rad: 4.71238898038469 + theta_rad: 0.5759586531581288 + } + gain_value { + gain_db: -3.773 + phi_rad: 4.71238898038469 + theta_rad: 0.5768313177841259 + } + gain_value { + gain_db: -3.904 + phi_rad: 4.71238898038469 + theta_rad: 0.5777039824101231 + } + gain_value { + gain_db: -4.1052 + phi_rad: 4.71238898038469 + theta_rad: 0.5785766470361202 + } + gain_value { + gain_db: -4.4031 + phi_rad: 4.71238898038469 + theta_rad: 0.5794493116621174 + } + gain_value { + gain_db: -4.6739 + phi_rad: 4.71238898038469 + theta_rad: 0.5803219762881145 + } + gain_value { + gain_db: -4.7053 + phi_rad: 4.71238898038469 + theta_rad: 0.5811946409141117 + } + gain_value { + gain_db: -4.5869 + phi_rad: 4.71238898038469 + theta_rad: 0.5820673055401089 + } + gain_value { + gain_db: -4.2577 + phi_rad: 4.71238898038469 + theta_rad: 0.582939970166106 + } + gain_value { + gain_db: -3.9431 + phi_rad: 4.71238898038469 + theta_rad: 0.5838126347921033 + } + gain_value { + gain_db: -3.6712 + phi_rad: 4.71238898038469 + theta_rad: 0.5846852994181004 + } + gain_value { + gain_db: -3.3839 + phi_rad: 4.71238898038469 + theta_rad: 0.5855579640440975 + } + gain_value { + gain_db: -3.2194 + phi_rad: 4.71238898038469 + theta_rad: 0.5864306286700948 + } + gain_value { + gain_db: -3.0752 + phi_rad: 4.71238898038469 + theta_rad: 0.5873032932960919 + } + gain_value { + gain_db: -3.1058 + phi_rad: 4.71238898038469 + theta_rad: 0.5881759579220891 + } + gain_value { + gain_db: -3.2144 + phi_rad: 4.71238898038469 + theta_rad: 0.5890486225480862 + } + gain_value { + gain_db: -3.3402 + phi_rad: 4.71238898038469 + theta_rad: 0.5899212871740833 + } + gain_value { + gain_db: -3.283 + phi_rad: 4.71238898038469 + theta_rad: 0.5907939518000805 + } + gain_value { + gain_db: -2.9292 + phi_rad: 4.71238898038469 + theta_rad: 0.5916666164260777 + } + gain_value { + gain_db: -2.4721 + phi_rad: 4.71238898038469 + theta_rad: 0.592539281052075 + } + gain_value { + gain_db: -2.0842 + phi_rad: 4.71238898038469 + theta_rad: 0.5934119456780721 + } + gain_value { + gain_db: -1.7496 + phi_rad: 4.71238898038469 + theta_rad: 0.5942846103040692 + } + gain_value { + gain_db: -1.5808 + phi_rad: 4.71238898038469 + theta_rad: 0.5951572749300664 + } + gain_value { + gain_db: -1.538 + phi_rad: 4.71238898038469 + theta_rad: 0.5960299395560635 + } + gain_value { + gain_db: -1.5539 + phi_rad: 4.71238898038469 + theta_rad: 0.5969026041820608 + } + gain_value { + gain_db: -1.5838 + phi_rad: 4.71238898038469 + theta_rad: 0.5977752688080579 + } + gain_value { + gain_db: -1.7059 + phi_rad: 4.71238898038469 + theta_rad: 0.598647933434055 + } + gain_value { + gain_db: -1.779 + phi_rad: 4.71238898038469 + theta_rad: 0.5995205980600522 + } + gain_value { + gain_db: -1.7326 + phi_rad: 4.71238898038469 + theta_rad: 0.6003932626860493 + } + gain_value { + gain_db: -1.7169 + phi_rad: 4.71238898038469 + theta_rad: 0.6012659273120465 + } + gain_value { + gain_db: -1.7452 + phi_rad: 4.71238898038469 + theta_rad: 0.6021385919380436 + } + gain_value { + gain_db: -1.7659 + phi_rad: 4.71238898038469 + theta_rad: 0.6030112565640408 + } + gain_value { + gain_db: -1.8111 + phi_rad: 4.71238898038469 + theta_rad: 0.6038839211900381 + } + gain_value { + gain_db: -1.9211 + phi_rad: 4.71238898038469 + theta_rad: 0.6047565858160352 + } + gain_value { + gain_db: -2.0592 + phi_rad: 4.71238898038469 + theta_rad: 0.6056292504420324 + } + gain_value { + gain_db: -2.2747 + phi_rad: 4.71238898038469 + theta_rad: 0.6065019150680295 + } + gain_value { + gain_db: -2.4693 + phi_rad: 4.71238898038469 + theta_rad: 0.6073745796940266 + } + gain_value { + gain_db: -2.7142 + phi_rad: 4.71238898038469 + theta_rad: 0.6082472443200239 + } + gain_value { + gain_db: -3.0418 + phi_rad: 4.71238898038469 + theta_rad: 0.609119908946021 + } + gain_value { + gain_db: -3.2777 + phi_rad: 4.71238898038469 + theta_rad: 0.6099925735720182 + } + gain_value { + gain_db: -3.4528 + phi_rad: 4.71238898038469 + theta_rad: 0.6108652381980153 + } + gain_value { + gain_db: -3.7693 + phi_rad: 4.71238898038469 + theta_rad: 0.6117379028240124 + } + gain_value { + gain_db: -4.0227 + phi_rad: 4.71238898038469 + theta_rad: 0.6126105674500097 + } + gain_value { + gain_db: -4.2429 + phi_rad: 4.71238898038469 + theta_rad: 0.6134832320760069 + } + gain_value { + gain_db: -4.4129 + phi_rad: 4.71238898038469 + theta_rad: 0.6143558967020041 + } + gain_value { + gain_db: -4.5043 + phi_rad: 4.71238898038469 + theta_rad: 0.6152285613280012 + } + gain_value { + gain_db: -4.5439 + phi_rad: 4.71238898038469 + theta_rad: 0.6161012259539983 + } + gain_value { + gain_db: -4.4007 + phi_rad: 4.71238898038469 + theta_rad: 0.6169738905799955 + } + gain_value { + gain_db: -4.4358 + phi_rad: 4.71238898038469 + theta_rad: 0.6178465552059926 + } + gain_value { + gain_db: -4.3815 + phi_rad: 4.71238898038469 + theta_rad: 0.6187192198319899 + } + gain_value { + gain_db: -4.3986 + phi_rad: 4.71238898038469 + theta_rad: 0.619591884457987 + } + gain_value { + gain_db: -4.5835 + phi_rad: 4.71238898038469 + theta_rad: 0.6204645490839841 + } + gain_value { + gain_db: -4.8585 + phi_rad: 4.71238898038469 + theta_rad: 0.6213372137099813 + } + gain_value { + gain_db: -5.0114 + phi_rad: 4.71238898038469 + theta_rad: 0.6222098783359784 + } + gain_value { + gain_db: -5.1303 + phi_rad: 4.71238898038469 + theta_rad: 0.6230825429619757 + } + gain_value { + gain_db: -5.068 + phi_rad: 4.71238898038469 + theta_rad: 0.6239552075879728 + } + gain_value { + gain_db: -4.9195 + phi_rad: 4.71238898038469 + theta_rad: 0.62482787221397 + } + gain_value { + gain_db: -4.657 + phi_rad: 4.71238898038469 + theta_rad: 0.6257005368399672 + } + gain_value { + gain_db: -4.4203 + phi_rad: 4.71238898038469 + theta_rad: 0.6265732014659643 + } + gain_value { + gain_db: -4.3292 + phi_rad: 4.71238898038469 + theta_rad: 0.6274458660919615 + } + gain_value { + gain_db: -4.3251 + phi_rad: 4.71238898038469 + theta_rad: 0.6283185307179586 + } + gain_value { + gain_db: -4.4418 + phi_rad: 4.71238898038469 + theta_rad: 0.6291911953439557 + } + gain_value { + gain_db: -4.654 + phi_rad: 4.71238898038469 + theta_rad: 0.630063859969953 + } + gain_value { + gain_db: -4.8515 + phi_rad: 4.71238898038469 + theta_rad: 0.6309365245959501 + } + gain_value { + gain_db: -4.896 + phi_rad: 4.71238898038469 + theta_rad: 0.6318091892219474 + } + gain_value { + gain_db: -4.9148 + phi_rad: 4.71238898038469 + theta_rad: 0.6326818538479445 + } + gain_value { + gain_db: -4.8152 + phi_rad: 4.71238898038469 + theta_rad: 0.6335545184739416 + } + gain_value { + gain_db: -4.7286 + phi_rad: 4.71238898038469 + theta_rad: 0.6344271830999388 + } + gain_value { + gain_db: -4.6258 + phi_rad: 4.71238898038469 + theta_rad: 0.635299847725936 + } + gain_value { + gain_db: -4.5705 + phi_rad: 4.71238898038469 + theta_rad: 0.6361725123519332 + } + gain_value { + gain_db: -4.5363 + phi_rad: 4.71238898038469 + theta_rad: 0.6370451769779303 + } + gain_value { + gain_db: -4.4406 + phi_rad: 4.71238898038469 + theta_rad: 0.6379178416039274 + } + gain_value { + gain_db: -4.4067 + phi_rad: 4.71238898038469 + theta_rad: 0.6387905062299246 + } + gain_value { + gain_db: -4.3852 + phi_rad: 4.71238898038469 + theta_rad: 0.6396631708559217 + } + gain_value { + gain_db: -4.3404 + phi_rad: 4.71238898038469 + theta_rad: 0.640535835481919 + } + gain_value { + gain_db: -4.3352 + phi_rad: 4.71238898038469 + theta_rad: 0.6414085001079161 + } + gain_value { + gain_db: -4.3592 + phi_rad: 4.71238898038469 + theta_rad: 0.6422811647339133 + } + gain_value { + gain_db: -4.382 + phi_rad: 4.71238898038469 + theta_rad: 0.6431538293599105 + } + gain_value { + gain_db: -4.4163 + phi_rad: 4.71238898038469 + theta_rad: 0.6440264939859076 + } + gain_value { + gain_db: -4.5682 + phi_rad: 4.71238898038469 + theta_rad: 0.6448991586119048 + } + gain_value { + gain_db: -4.547 + phi_rad: 4.71238898038469 + theta_rad: 0.6457718232379019 + } + gain_value { + gain_db: -4.5265 + phi_rad: 4.71238898038469 + theta_rad: 0.646644487863899 + } + gain_value { + gain_db: -4.538 + phi_rad: 4.71238898038469 + theta_rad: 0.6475171524898963 + } + gain_value { + gain_db: -4.485 + phi_rad: 4.71238898038469 + theta_rad: 0.6483898171158934 + } + gain_value { + gain_db: -4.442 + phi_rad: 4.71238898038469 + theta_rad: 0.6492624817418906 + } + gain_value { + gain_db: -4.5996 + phi_rad: 4.71238898038469 + theta_rad: 0.6501351463678877 + } + gain_value { + gain_db: -4.8011 + phi_rad: 4.71238898038469 + theta_rad: 0.6510078109938848 + } + gain_value { + gain_db: -4.9336 + phi_rad: 4.71238898038469 + theta_rad: 0.6518804756198822 + } + gain_value { + gain_db: -5.0536 + phi_rad: 4.71238898038469 + theta_rad: 0.6527531402458793 + } + gain_value { + gain_db: -5.2263 + phi_rad: 4.71238898038469 + theta_rad: 0.6536258048718765 + } + gain_value { + gain_db: -5.0992 + phi_rad: 4.71238898038469 + theta_rad: 0.6544984694978736 + } + gain_value { + gain_db: -5.0447 + phi_rad: 4.71238898038469 + theta_rad: 0.6553711341238707 + } + gain_value { + gain_db: -4.9615 + phi_rad: 4.71238898038469 + theta_rad: 0.6562437987498679 + } + gain_value { + gain_db: -4.8965 + phi_rad: 4.71238898038469 + theta_rad: 0.657116463375865 + } + gain_value { + gain_db: -4.8545 + phi_rad: 4.71238898038469 + theta_rad: 0.6579891280018623 + } + gain_value { + gain_db: -4.7203 + phi_rad: 4.71238898038469 + theta_rad: 0.6588617926278594 + } + gain_value { + gain_db: -4.6657 + phi_rad: 4.71238898038469 + theta_rad: 0.6597344572538565 + } + gain_value { + gain_db: -4.4264 + phi_rad: 4.71238898038469 + theta_rad: 0.6606071218798537 + } + gain_value { + gain_db: -4.0429 + phi_rad: 4.71238898038469 + theta_rad: 0.6614797865058508 + } + gain_value { + gain_db: -3.6418 + phi_rad: 4.71238898038469 + theta_rad: 0.6623524511318482 + } + gain_value { + gain_db: -3.4467 + phi_rad: 4.71238898038469 + theta_rad: 0.6632251157578453 + } + gain_value { + gain_db: -3.07 + phi_rad: 4.71238898038469 + theta_rad: 0.6640977803838424 + } + gain_value { + gain_db: -2.772 + phi_rad: 4.71238898038469 + theta_rad: 0.6649704450098396 + } + gain_value { + gain_db: -2.4208 + phi_rad: 4.71238898038469 + theta_rad: 0.6658431096358367 + } + gain_value { + gain_db: -2.1208 + phi_rad: 4.71238898038469 + theta_rad: 0.6667157742618339 + } + gain_value { + gain_db: -1.7389 + phi_rad: 4.71238898038469 + theta_rad: 0.667588438887831 + } + gain_value { + gain_db: -1.4221 + phi_rad: 4.71238898038469 + theta_rad: 0.6684611035138281 + } + gain_value { + gain_db: -1.1979 + phi_rad: 4.71238898038469 + theta_rad: 0.6693337681398254 + } + gain_value { + gain_db: -1.054 + phi_rad: 4.71238898038469 + theta_rad: 0.6702064327658225 + } + gain_value { + gain_db: -0.88793 + phi_rad: 4.71238898038469 + theta_rad: 0.6710790973918198 + } + gain_value { + gain_db: -0.74037 + phi_rad: 4.71238898038469 + theta_rad: 0.6719517620178169 + } + gain_value { + gain_db: -0.58213 + phi_rad: 4.71238898038469 + theta_rad: 0.672824426643814 + } + gain_value { + gain_db: -0.39387 + phi_rad: 4.71238898038469 + theta_rad: 0.6736970912698113 + } + gain_value { + gain_db: -0.2622 + phi_rad: 4.71238898038469 + theta_rad: 0.6745697558958084 + } + gain_value { + gain_db: -0.14397 + phi_rad: 4.71238898038469 + theta_rad: 0.6754424205218056 + } + gain_value { + gain_db: 0.018033 + phi_rad: 4.71238898038469 + theta_rad: 0.6763150851478027 + } + gain_value { + gain_db: 0.1065 + phi_rad: 4.71238898038469 + theta_rad: 0.6771877497737998 + } + gain_value { + gain_db: 0.15703 + phi_rad: 4.71238898038469 + theta_rad: 0.678060414399797 + } + gain_value { + gain_db: 0.12743 + phi_rad: 4.71238898038469 + theta_rad: 0.6789330790257941 + } + gain_value { + gain_db: 0.0614 + phi_rad: 4.71238898038469 + theta_rad: 0.6798057436517914 + } + gain_value { + gain_db: 0.013667 + phi_rad: 4.71238898038469 + theta_rad: 0.6806784082777885 + } + gain_value { + gain_db: -0.038267 + phi_rad: 4.71238898038469 + theta_rad: 0.6815510729037857 + } + gain_value { + gain_db: -0.1148 + phi_rad: 4.71238898038469 + theta_rad: 0.6824237375297829 + } + gain_value { + gain_db: -0.17537 + phi_rad: 4.71238898038469 + theta_rad: 0.68329640215578 + } + gain_value { + gain_db: -0.20307 + phi_rad: 4.71238898038469 + theta_rad: 0.6841690667817772 + } + gain_value { + gain_db: -0.26477 + phi_rad: 4.71238898038469 + theta_rad: 0.6850417314077744 + } + gain_value { + gain_db: -0.38003 + phi_rad: 4.71238898038469 + theta_rad: 0.6859143960337715 + } + gain_value { + gain_db: -0.47817 + phi_rad: 4.71238898038469 + theta_rad: 0.6867870606597687 + } + gain_value { + gain_db: -0.52227 + phi_rad: 4.71238898038469 + theta_rad: 0.6876597252857658 + } + gain_value { + gain_db: -0.54603 + phi_rad: 4.71238898038469 + theta_rad: 0.688532389911763 + } + gain_value { + gain_db: -0.591 + phi_rad: 4.71238898038469 + theta_rad: 0.6894050545377601 + } + gain_value { + gain_db: -0.65983 + phi_rad: 4.71238898038469 + theta_rad: 0.6902777191637572 + } + gain_value { + gain_db: -0.81127 + phi_rad: 4.71238898038469 + theta_rad: 0.6911503837897546 + } + gain_value { + gain_db: -1.01 + phi_rad: 4.71238898038469 + theta_rad: 0.6920230484157517 + } + gain_value { + gain_db: -1.2308 + phi_rad: 4.71238898038469 + theta_rad: 0.6928957130417489 + } + gain_value { + gain_db: -1.4321 + phi_rad: 4.71238898038469 + theta_rad: 0.693768377667746 + } + gain_value { + gain_db: -1.6029 + phi_rad: 4.71238898038469 + theta_rad: 0.6946410422937431 + } + gain_value { + gain_db: -1.7443 + phi_rad: 4.71238898038469 + theta_rad: 0.6955137069197403 + } + gain_value { + gain_db: -1.8915 + phi_rad: 4.71238898038469 + theta_rad: 0.6963863715457375 + } + gain_value { + gain_db: -2.0019 + phi_rad: 4.71238898038469 + theta_rad: 0.6972590361717347 + } + gain_value { + gain_db: -2.0467 + phi_rad: 4.71238898038469 + theta_rad: 0.6981317007977318 + } + gain_value { + gain_db: -2.1128 + phi_rad: 4.71238898038469 + theta_rad: 0.6990043654237289 + } + gain_value { + gain_db: -2.2412 + phi_rad: 4.71238898038469 + theta_rad: 0.6998770300497261 + } + gain_value { + gain_db: -2.3609 + phi_rad: 4.71238898038469 + theta_rad: 0.7007496946757232 + } + gain_value { + gain_db: -2.5051 + phi_rad: 4.71238898038469 + theta_rad: 0.7016223593017206 + } + gain_value { + gain_db: -2.7386 + phi_rad: 4.71238898038469 + theta_rad: 0.7024950239277177 + } + gain_value { + gain_db: -2.7539 + phi_rad: 4.71238898038469 + theta_rad: 0.7033676885537148 + } + gain_value { + gain_db: -2.792 + phi_rad: 4.71238898038469 + theta_rad: 0.704240353179712 + } + gain_value { + gain_db: -2.7473 + phi_rad: 4.71238898038469 + theta_rad: 0.7051130178057091 + } + gain_value { + gain_db: -2.7315 + phi_rad: 4.71238898038469 + theta_rad: 0.7059856824317063 + } + gain_value { + gain_db: -2.7158 + phi_rad: 4.71238898038469 + theta_rad: 0.7068583470577035 + } + gain_value { + gain_db: -2.6681 + phi_rad: 4.71238898038469 + theta_rad: 0.7077310116837006 + } + gain_value { + gain_db: -2.645 + phi_rad: 4.71238898038469 + theta_rad: 0.7086036763096978 + } + gain_value { + gain_db: -2.5822 + phi_rad: 4.71238898038469 + theta_rad: 0.7094763409356949 + } + gain_value { + gain_db: -2.5139 + phi_rad: 4.71238898038469 + theta_rad: 0.7103490055616922 + } + gain_value { + gain_db: -2.486 + phi_rad: 4.71238898038469 + theta_rad: 0.7112216701876893 + } + gain_value { + gain_db: -2.4894 + phi_rad: 4.71238898038469 + theta_rad: 0.7120943348136864 + } + gain_value { + gain_db: -2.5276 + phi_rad: 4.71238898038469 + theta_rad: 0.7129669994396837 + } + gain_value { + gain_db: -2.5868 + phi_rad: 4.71238898038469 + theta_rad: 0.7138396640656808 + } + gain_value { + gain_db: -2.5645 + phi_rad: 4.71238898038469 + theta_rad: 0.714712328691678 + } + gain_value { + gain_db: -2.513 + phi_rad: 4.71238898038469 + theta_rad: 0.7155849933176751 + } + gain_value { + gain_db: -2.436 + phi_rad: 4.71238898038469 + theta_rad: 0.7164576579436722 + } + gain_value { + gain_db: -2.4078 + phi_rad: 4.71238898038469 + theta_rad: 0.7173303225696694 + } + gain_value { + gain_db: -2.3326 + phi_rad: 4.71238898038469 + theta_rad: 0.7182029871956666 + } + gain_value { + gain_db: -2.3396 + phi_rad: 4.71238898038469 + theta_rad: 0.7190756518216638 + } + gain_value { + gain_db: -2.3404 + phi_rad: 4.71238898038469 + theta_rad: 0.7199483164476609 + } + gain_value { + gain_db: -2.2801 + phi_rad: 4.71238898038469 + theta_rad: 0.7208209810736581 + } + gain_value { + gain_db: -2.2278 + phi_rad: 4.71238898038469 + theta_rad: 0.7216936456996553 + } + gain_value { + gain_db: -2.2272 + phi_rad: 4.71238898038469 + theta_rad: 0.7225663103256524 + } + gain_value { + gain_db: -2.2983 + phi_rad: 4.71238898038469 + theta_rad: 0.7234389749516497 + } + gain_value { + gain_db: -2.3644 + phi_rad: 4.71238898038469 + theta_rad: 0.7243116395776468 + } + gain_value { + gain_db: -2.5794 + phi_rad: 4.71238898038469 + theta_rad: 0.7251843042036439 + } + gain_value { + gain_db: -2.6793 + phi_rad: 4.71238898038469 + theta_rad: 0.7260569688296411 + } + gain_value { + gain_db: -2.8228 + phi_rad: 4.71238898038469 + theta_rad: 0.7269296334556382 + } + gain_value { + gain_db: -2.9476 + phi_rad: 4.71238898038469 + theta_rad: 0.7278022980816354 + } + gain_value { + gain_db: -3.0205 + phi_rad: 4.71238898038469 + theta_rad: 0.7286749627076325 + } + gain_value { + gain_db: -3.0712 + phi_rad: 4.71238898038469 + theta_rad: 0.7295476273336297 + } + gain_value { + gain_db: -3.1462 + phi_rad: 4.71238898038469 + theta_rad: 0.730420291959627 + } + gain_value { + gain_db: -3.0884 + phi_rad: 4.71238898038469 + theta_rad: 0.7312929565856241 + } + gain_value { + gain_db: -3.1062 + phi_rad: 4.71238898038469 + theta_rad: 0.7321656212116213 + } + gain_value { + gain_db: -3.15 + phi_rad: 4.71238898038469 + theta_rad: 0.7330382858376184 + } + gain_value { + gain_db: -3.1225 + phi_rad: 4.71238898038469 + theta_rad: 0.7339109504636155 + } + gain_value { + gain_db: -3.233 + phi_rad: 4.71238898038469 + theta_rad: 0.7347836150896128 + } + gain_value { + gain_db: -3.3349 + phi_rad: 4.71238898038469 + theta_rad: 0.7356562797156099 + } + gain_value { + gain_db: -3.5228 + phi_rad: 4.71238898038469 + theta_rad: 0.7365289443416071 + } + gain_value { + gain_db: -3.5293 + phi_rad: 4.71238898038469 + theta_rad: 0.7374016089676042 + } + gain_value { + gain_db: -3.3577 + phi_rad: 4.71238898038469 + theta_rad: 0.7382742735936013 + } + gain_value { + gain_db: -3.1023 + phi_rad: 4.71238898038469 + theta_rad: 0.7391469382195985 + } + gain_value { + gain_db: -2.8704 + phi_rad: 4.71238898038469 + theta_rad: 0.7400196028455958 + } + gain_value { + gain_db: -2.6362 + phi_rad: 4.71238898038469 + theta_rad: 0.740892267471593 + } + gain_value { + gain_db: -2.5434 + phi_rad: 4.71238898038469 + theta_rad: 0.7417649320975901 + } + gain_value { + gain_db: -2.5456 + phi_rad: 4.71238898038469 + theta_rad: 0.7426375967235872 + } + gain_value { + gain_db: -2.499 + phi_rad: 4.71238898038469 + theta_rad: 0.7435102613495844 + } + gain_value { + gain_db: -2.4593 + phi_rad: 4.71238898038469 + theta_rad: 0.7443829259755815 + } + gain_value { + gain_db: -2.5564 + phi_rad: 4.71238898038469 + theta_rad: 0.7452555906015788 + } + gain_value { + gain_db: -2.6714 + phi_rad: 4.71238898038469 + theta_rad: 0.7461282552275759 + } + gain_value { + gain_db: -2.8231 + phi_rad: 4.71238898038469 + theta_rad: 0.747000919853573 + } + gain_value { + gain_db: -2.9625 + phi_rad: 4.71238898038469 + theta_rad: 0.7478735844795702 + } + gain_value { + gain_db: -3.1057 + phi_rad: 4.71238898038469 + theta_rad: 0.7487462491055673 + } + gain_value { + gain_db: -3.1008 + phi_rad: 4.71238898038469 + theta_rad: 0.7496189137315646 + } + gain_value { + gain_db: -3.0503 + phi_rad: 4.71238898038469 + theta_rad: 0.7504915783575618 + } + gain_value { + gain_db: -2.8857 + phi_rad: 4.71238898038469 + theta_rad: 0.7513642429835589 + } + gain_value { + gain_db: -2.7294 + phi_rad: 4.71238898038469 + theta_rad: 0.7522369076095561 + } + gain_value { + gain_db: -2.6296 + phi_rad: 4.71238898038469 + theta_rad: 0.7531095722355532 + } + gain_value { + gain_db: -2.6604 + phi_rad: 4.71238898038469 + theta_rad: 0.7539822368615504 + } + gain_value { + gain_db: -2.7858 + phi_rad: 4.71238898038469 + theta_rad: 0.7548549014875475 + } + gain_value { + gain_db: -2.9376 + phi_rad: 4.71238898038469 + theta_rad: 0.7557275661135446 + } + gain_value { + gain_db: -3.1296 + phi_rad: 4.71238898038469 + theta_rad: 0.7566002307395419 + } + gain_value { + gain_db: -3.3447 + phi_rad: 4.71238898038469 + theta_rad: 0.757472895365539 + } + gain_value { + gain_db: -3.5182 + phi_rad: 4.71238898038469 + theta_rad: 0.7583455599915362 + } + gain_value { + gain_db: -3.5835 + phi_rad: 4.71238898038469 + theta_rad: 0.7592182246175333 + } + gain_value { + gain_db: -3.5252 + phi_rad: 4.71238898038469 + theta_rad: 0.7600908892435305 + } + gain_value { + gain_db: -3.3745 + phi_rad: 4.71238898038469 + theta_rad: 0.7609635538695277 + } + gain_value { + gain_db: -3.2202 + phi_rad: 4.71238898038469 + theta_rad: 0.7618362184955249 + } + gain_value { + gain_db: -3.1241 + phi_rad: 4.71238898038469 + theta_rad: 0.7627088831215221 + } + gain_value { + gain_db: -3.1298 + phi_rad: 4.71238898038469 + theta_rad: 0.7635815477475192 + } + gain_value { + gain_db: -3.2065 + phi_rad: 4.71238898038469 + theta_rad: 0.7644542123735163 + } + gain_value { + gain_db: -3.3574 + phi_rad: 4.71238898038469 + theta_rad: 0.7653268769995135 + } + gain_value { + gain_db: -3.5585 + phi_rad: 4.71238898038469 + theta_rad: 0.7661995416255106 + } + gain_value { + gain_db: -3.7641 + phi_rad: 4.71238898038469 + theta_rad: 0.7670722062515078 + } + gain_value { + gain_db: -3.9498 + phi_rad: 4.71238898038469 + theta_rad: 0.767944870877505 + } + gain_value { + gain_db: -4.0788 + phi_rad: 4.71238898038469 + theta_rad: 0.7688175355035021 + } + gain_value { + gain_db: -4.1808 + phi_rad: 4.71238898038469 + theta_rad: 0.7696902001294994 + } + gain_value { + gain_db: -4.3851 + phi_rad: 4.71238898038469 + theta_rad: 0.7705628647554965 + } + gain_value { + gain_db: -4.471 + phi_rad: 4.71238898038469 + theta_rad: 0.7714355293814937 + } + gain_value { + gain_db: -4.6042 + phi_rad: 4.71238898038469 + theta_rad: 0.7723081940074908 + } + gain_value { + gain_db: -4.6915 + phi_rad: 4.71238898038469 + theta_rad: 0.773180858633488 + } + gain_value { + gain_db: -4.6916 + phi_rad: 4.71238898038469 + theta_rad: 0.7740535232594852 + } + gain_value { + gain_db: -4.775 + phi_rad: 4.71238898038469 + theta_rad: 0.7749261878854823 + } + gain_value { + gain_db: -4.967 + phi_rad: 4.71238898038469 + theta_rad: 0.7757988525114795 + } + gain_value { + gain_db: -5.0266 + phi_rad: 4.71238898038469 + theta_rad: 0.7766715171374766 + } + gain_value { + gain_db: -4.9852 + phi_rad: 4.71238898038469 + theta_rad: 0.7775441817634737 + } + gain_value { + gain_db: -4.8977 + phi_rad: 4.71238898038469 + theta_rad: 0.778416846389471 + } + gain_value { + gain_db: -4.8152 + phi_rad: 4.71238898038469 + theta_rad: 0.7792895110154682 + } + gain_value { + gain_db: -4.7942 + phi_rad: 4.71238898038469 + theta_rad: 0.7801621756414654 + } + gain_value { + gain_db: -4.95 + phi_rad: 4.71238898038469 + theta_rad: 0.7810348402674625 + } + gain_value { + gain_db: -5.2406 + phi_rad: 4.71238898038469 + theta_rad: 0.7819075048934596 + } + gain_value { + gain_db: -5.6635 + phi_rad: 4.71238898038469 + theta_rad: 0.7827801695194568 + } + gain_value { + gain_db: -6.175 + phi_rad: 4.71238898038469 + theta_rad: 0.783652834145454 + } + gain_value { + gain_db: -6.7167 + phi_rad: 4.71238898038469 + theta_rad: 0.7845254987714512 + } + gain_value { + gain_db: -6.9227 + phi_rad: 4.71238898038469 + theta_rad: 0.7853981633974483 + } + gain_value { + gain_db: -6.9531 + phi_rad: 4.71238898038469 + theta_rad: 0.7862708280234454 + } + gain_value { + gain_db: -6.51 + phi_rad: 4.71238898038469 + theta_rad: 0.7871434926494426 + } + gain_value { + gain_db: -6.0006 + phi_rad: 4.71238898038469 + theta_rad: 0.7880161572754397 + } + gain_value { + gain_db: -5.5807 + phi_rad: 4.71238898038469 + theta_rad: 0.788888821901437 + } + gain_value { + gain_db: -5.2249 + phi_rad: 4.71238898038469 + theta_rad: 0.7897614865274342 + } + gain_value { + gain_db: -4.9502 + phi_rad: 4.71238898038469 + theta_rad: 0.7906341511534313 + } + gain_value { + gain_db: -4.8996 + phi_rad: 4.71238898038469 + theta_rad: 0.7915068157794285 + } + gain_value { + gain_db: -4.9767 + phi_rad: 4.71238898038469 + theta_rad: 0.7923794804054256 + } + gain_value { + gain_db: -5.3113 + phi_rad: 4.71238898038469 + theta_rad: 0.7932521450314228 + } + gain_value { + gain_db: -5.7076 + phi_rad: 4.71238898038469 + theta_rad: 0.7941248096574199 + } + gain_value { + gain_db: -6.299 + phi_rad: 4.71238898038469 + theta_rad: 0.794997474283417 + } + gain_value { + gain_db: -6.82 + phi_rad: 4.71238898038469 + theta_rad: 0.7958701389094143 + } + gain_value { + gain_db: -7.2063 + phi_rad: 4.71238898038469 + theta_rad: 0.7967428035354114 + } + gain_value { + gain_db: -7.0775 + phi_rad: 4.71238898038469 + theta_rad: 0.7976154681614086 + } + gain_value { + gain_db: -6.4124 + phi_rad: 4.71238898038469 + theta_rad: 0.7984881327874057 + } + gain_value { + gain_db: -5.7244 + phi_rad: 4.71238898038469 + theta_rad: 0.7993607974134029 + } + gain_value { + gain_db: -5.0768 + phi_rad: 4.71238898038469 + theta_rad: 0.8002334620394002 + } + gain_value { + gain_db: -4.6362 + phi_rad: 4.71238898038469 + theta_rad: 0.8011061266653973 + } + gain_value { + gain_db: -4.4499 + phi_rad: 4.71238898038469 + theta_rad: 0.8019787912913945 + } + gain_value { + gain_db: -4.4328 + phi_rad: 4.71238898038469 + theta_rad: 0.8028514559173916 + } + gain_value { + gain_db: -4.4043 + phi_rad: 4.71238898038469 + theta_rad: 0.8037241205433887 + } + gain_value { + gain_db: -4.5009 + phi_rad: 4.71238898038469 + theta_rad: 0.8045967851693859 + } + gain_value { + gain_db: -4.6127 + phi_rad: 4.71238898038469 + theta_rad: 0.805469449795383 + } + gain_value { + gain_db: -4.7608 + phi_rad: 4.71238898038469 + theta_rad: 0.8063421144213803 + } + gain_value { + gain_db: -4.9759 + phi_rad: 4.71238898038469 + theta_rad: 0.8072147790473774 + } + gain_value { + gain_db: -5.0647 + phi_rad: 4.71238898038469 + theta_rad: 0.8080874436733745 + } + gain_value { + gain_db: -5.049 + phi_rad: 4.71238898038469 + theta_rad: 0.8089601082993718 + } + gain_value { + gain_db: -4.914 + phi_rad: 4.71238898038469 + theta_rad: 0.8098327729253689 + } + gain_value { + gain_db: -4.7883 + phi_rad: 4.71238898038469 + theta_rad: 0.8107054375513661 + } + gain_value { + gain_db: -4.6135 + phi_rad: 4.71238898038469 + theta_rad: 0.8115781021773633 + } + gain_value { + gain_db: -4.5235 + phi_rad: 4.71238898038469 + theta_rad: 0.8124507668033604 + } + gain_value { + gain_db: -4.4481 + phi_rad: 4.71238898038469 + theta_rad: 0.8133234314293576 + } + gain_value { + gain_db: -4.3556 + phi_rad: 4.71238898038469 + theta_rad: 0.8141960960553547 + } + gain_value { + gain_db: -4.376 + phi_rad: 4.71238898038469 + theta_rad: 0.8150687606813519 + } + gain_value { + gain_db: -4.4912 + phi_rad: 4.71238898038469 + theta_rad: 0.815941425307349 + } + gain_value { + gain_db: -4.7059 + phi_rad: 4.71238898038469 + theta_rad: 0.8168140899333461 + } + gain_value { + gain_db: -4.951 + phi_rad: 4.71238898038469 + theta_rad: 0.8176867545593434 + } + gain_value { + gain_db: -5.1249 + phi_rad: 4.71238898038469 + theta_rad: 0.8185594191853406 + } + gain_value { + gain_db: -5.2689 + phi_rad: 4.71238898038469 + theta_rad: 0.8194320838113378 + } + gain_value { + gain_db: -5.1159 + phi_rad: 4.71238898038469 + theta_rad: 0.8203047484373349 + } + gain_value { + gain_db: -4.8862 + phi_rad: 4.71238898038469 + theta_rad: 0.821177413063332 + } + gain_value { + gain_db: -4.6761 + phi_rad: 4.71238898038469 + theta_rad: 0.8220500776893293 + } + gain_value { + gain_db: -4.5417 + phi_rad: 4.71238898038469 + theta_rad: 0.8229227423153264 + } + gain_value { + gain_db: -4.6719 + phi_rad: 4.71238898038469 + theta_rad: 0.8237954069413236 + } + gain_value { + gain_db: -4.9669 + phi_rad: 4.71238898038469 + theta_rad: 0.8246680715673207 + } + gain_value { + gain_db: -5.5039 + phi_rad: 4.71238898038469 + theta_rad: 0.8255407361933178 + } + gain_value { + gain_db: -6.0924 + phi_rad: 4.71238898038469 + theta_rad: 0.826413400819315 + } + gain_value { + gain_db: -6.6515 + phi_rad: 4.71238898038469 + theta_rad: 0.8272860654453121 + } + gain_value { + gain_db: -7.0924 + phi_rad: 4.71238898038469 + theta_rad: 0.8281587300713095 + } + gain_value { + gain_db: -7.1288 + phi_rad: 4.71238898038469 + theta_rad: 0.8290313946973066 + } + gain_value { + gain_db: -6.8818 + phi_rad: 4.71238898038469 + theta_rad: 0.8299040593233037 + } + gain_value { + gain_db: -6.5942 + phi_rad: 4.71238898038469 + theta_rad: 0.8307767239493009 + } + gain_value { + gain_db: -6.2885 + phi_rad: 4.71238898038469 + theta_rad: 0.831649388575298 + } + gain_value { + gain_db: -6.2454 + phi_rad: 4.71238898038469 + theta_rad: 0.8325220532012952 + } + gain_value { + gain_db: -6.3619 + phi_rad: 4.71238898038469 + theta_rad: 0.8333947178272924 + } + gain_value { + gain_db: -6.7459 + phi_rad: 4.71238898038469 + theta_rad: 0.8342673824532895 + } + gain_value { + gain_db: -7.0916 + phi_rad: 4.71238898038469 + theta_rad: 0.8351400470792867 + } + gain_value { + gain_db: -7.44 + phi_rad: 4.71238898038469 + theta_rad: 0.8360127117052838 + } + gain_value { + gain_db: -7.5978 + phi_rad: 4.71238898038469 + theta_rad: 0.836885376331281 + } + gain_value { + gain_db: -7.4018 + phi_rad: 4.71238898038469 + theta_rad: 0.8377580409572782 + } + gain_value { + gain_db: -7.2108 + phi_rad: 4.71238898038469 + theta_rad: 0.8386307055832753 + } + gain_value { + gain_db: -6.8891 + phi_rad: 4.71238898038469 + theta_rad: 0.8395033702092726 + } + gain_value { + gain_db: -6.4427 + phi_rad: 4.71238898038469 + theta_rad: 0.8403760348352697 + } + gain_value { + gain_db: -6.2817 + phi_rad: 4.71238898038469 + theta_rad: 0.8412486994612669 + } + gain_value { + gain_db: -6.0982 + phi_rad: 4.71238898038469 + theta_rad: 0.842121364087264 + } + gain_value { + gain_db: -5.8807 + phi_rad: 4.71238898038469 + theta_rad: 0.8429940287132611 + } + gain_value { + gain_db: -5.8465 + phi_rad: 4.71238898038469 + theta_rad: 0.8438666933392583 + } + gain_value { + gain_db: -5.8423 + phi_rad: 4.71238898038469 + theta_rad: 0.8447393579652555 + } + gain_value { + gain_db: -5.925 + phi_rad: 4.71238898038469 + theta_rad: 0.8456120225912527 + } + gain_value { + gain_db: -6.1953 + phi_rad: 4.71238898038469 + theta_rad: 0.8464846872172498 + } + gain_value { + gain_db: -6.5457 + phi_rad: 4.71238898038469 + theta_rad: 0.8473573518432469 + } + gain_value { + gain_db: -6.8108 + phi_rad: 4.71238898038469 + theta_rad: 0.8482300164692442 + } + gain_value { + gain_db: -7.1676 + phi_rad: 4.71238898038469 + theta_rad: 0.8491026810952413 + } + gain_value { + gain_db: -7.2247 + phi_rad: 4.71238898038469 + theta_rad: 0.8499753457212386 + } + gain_value { + gain_db: -7.0338 + phi_rad: 4.71238898038469 + theta_rad: 0.8508480103472357 + } + gain_value { + gain_db: -6.7438 + phi_rad: 4.71238898038469 + theta_rad: 0.8517206749732328 + } + gain_value { + gain_db: -6.4796 + phi_rad: 4.71238898038469 + theta_rad: 0.85259333959923 + } + gain_value { + gain_db: -6.2918 + phi_rad: 4.71238898038469 + theta_rad: 0.8534660042252271 + } + gain_value { + gain_db: -6.1647 + phi_rad: 4.71238898038469 + theta_rad: 0.8543386688512243 + } + gain_value { + gain_db: -6.0979 + phi_rad: 4.71238898038469 + theta_rad: 0.8552113334772214 + } + gain_value { + gain_db: -6.1712 + phi_rad: 4.71238898038469 + theta_rad: 0.8560839981032186 + } + gain_value { + gain_db: -6.3628 + phi_rad: 4.71238898038469 + theta_rad: 0.8569566627292158 + } + gain_value { + gain_db: -6.6798 + phi_rad: 4.71238898038469 + theta_rad: 0.857829327355213 + } + gain_value { + gain_db: -7.1333 + phi_rad: 4.71238898038469 + theta_rad: 0.8587019919812102 + } + gain_value { + gain_db: -7.6608 + phi_rad: 4.71238898038469 + theta_rad: 0.8595746566072073 + } + gain_value { + gain_db: -8.1675 + phi_rad: 4.71238898038469 + theta_rad: 0.8604473212332044 + } + gain_value { + gain_db: -8.5815 + phi_rad: 4.71238898038469 + theta_rad: 0.8613199858592017 + } + gain_value { + gain_db: -8.8551 + phi_rad: 4.71238898038469 + theta_rad: 0.8621926504851988 + } + gain_value { + gain_db: -8.7879 + phi_rad: 4.71238898038469 + theta_rad: 0.863065315111196 + } + gain_value { + gain_db: -8.3143 + phi_rad: 4.71238898038469 + theta_rad: 0.8639379797371931 + } + gain_value { + gain_db: -7.6085 + phi_rad: 4.71238898038469 + theta_rad: 0.8648106443631902 + } + gain_value { + gain_db: -7.0169 + phi_rad: 4.71238898038469 + theta_rad: 0.8656833089891874 + } + gain_value { + gain_db: -6.557 + phi_rad: 4.71238898038469 + theta_rad: 0.8665559736151845 + } + gain_value { + gain_db: -6.2594 + phi_rad: 4.71238898038469 + theta_rad: 0.8674286382411819 + } + gain_value { + gain_db: -6.3176 + phi_rad: 4.71238898038469 + theta_rad: 0.868301302867179 + } + gain_value { + gain_db: -6.375 + phi_rad: 4.71238898038469 + theta_rad: 0.8691739674931761 + } + gain_value { + gain_db: -6.637 + phi_rad: 4.71238898038469 + theta_rad: 0.8700466321191733 + } + gain_value { + gain_db: -7.0183 + phi_rad: 4.71238898038469 + theta_rad: 0.8709192967451704 + } + gain_value { + gain_db: -7.5694 + phi_rad: 4.71238898038469 + theta_rad: 0.8717919613711677 + } + gain_value { + gain_db: -8.0731 + phi_rad: 4.71238898038469 + theta_rad: 0.8726646259971648 + } + gain_value { + gain_db: -8.8419 + phi_rad: 4.71238898038469 + theta_rad: 0.8735372906231619 + } + gain_value { + gain_db: -9.5079 + phi_rad: 4.71238898038469 + theta_rad: 0.8744099552491591 + } + gain_value { + gain_db: -10.332 + phi_rad: 4.71238898038469 + theta_rad: 0.8752826198751562 + } + gain_value { + gain_db: -10.921 + phi_rad: 4.71238898038469 + theta_rad: 0.8761552845011534 + } + gain_value { + gain_db: -9.6472 + phi_rad: 4.71238898038469 + theta_rad: 0.8770279491271507 + } + gain_value { + gain_db: -8.1711 + phi_rad: 4.71238898038469 + theta_rad: 0.8779006137531478 + } + gain_value { + gain_db: -7.1714 + phi_rad: 4.71238898038469 + theta_rad: 0.878773278379145 + } + gain_value { + gain_db: -6.5169 + phi_rad: 4.71238898038469 + theta_rad: 0.8796459430051421 + } + gain_value { + gain_db: -6.1893 + phi_rad: 4.71238898038469 + theta_rad: 0.8805186076311393 + } + gain_value { + gain_db: -6.1584 + phi_rad: 4.71238898038469 + theta_rad: 0.8813912722571364 + } + gain_value { + gain_db: -6.4318 + phi_rad: 4.71238898038469 + theta_rad: 0.8822639368831335 + } + gain_value { + gain_db: -6.8943 + phi_rad: 4.71238898038469 + theta_rad: 0.8831366015091308 + } + gain_value { + gain_db: -7.4745 + phi_rad: 4.71238898038469 + theta_rad: 0.8840092661351279 + } + gain_value { + gain_db: -8.1915 + phi_rad: 4.71238898038469 + theta_rad: 0.8848819307611251 + } + gain_value { + gain_db: -8.9898 + phi_rad: 4.71238898038469 + theta_rad: 0.8857545953871222 + } + gain_value { + gain_db: -9.7539 + phi_rad: 4.71238898038469 + theta_rad: 0.8866272600131193 + } + gain_value { + gain_db: -10.549 + phi_rad: 4.71238898038469 + theta_rad: 0.8874999246391166 + } + gain_value { + gain_db: -10.971 + phi_rad: 4.71238898038469 + theta_rad: 0.8883725892651138 + } + gain_value { + gain_db: -10.198 + phi_rad: 4.71238898038469 + theta_rad: 0.889245253891111 + } + gain_value { + gain_db: -9.142 + phi_rad: 4.71238898038469 + theta_rad: 0.8901179185171081 + } + gain_value { + gain_db: -8.3929 + phi_rad: 4.71238898038469 + theta_rad: 0.8909905831431052 + } + gain_value { + gain_db: -7.9328 + phi_rad: 4.71238898038469 + theta_rad: 0.8918632477691024 + } + gain_value { + gain_db: -7.8592 + phi_rad: 4.71238898038469 + theta_rad: 0.8927359123950995 + } + gain_value { + gain_db: -8.0317 + phi_rad: 4.71238898038469 + theta_rad: 0.8936085770210968 + } + gain_value { + gain_db: -8.3954 + phi_rad: 4.71238898038469 + theta_rad: 0.8944812416470939 + } + gain_value { + gain_db: -8.9021 + phi_rad: 4.71238898038469 + theta_rad: 0.895353906273091 + } + gain_value { + gain_db: -9.6004 + phi_rad: 4.71238898038469 + theta_rad: 0.8962265708990882 + } + gain_value { + gain_db: -10.139 + phi_rad: 4.71238898038469 + theta_rad: 0.8970992355250854 + } + gain_value { + gain_db: -10.554 + phi_rad: 4.71238898038469 + theta_rad: 0.8979719001510826 + } + gain_value { + gain_db: -10.53 + phi_rad: 4.71238898038469 + theta_rad: 0.8988445647770797 + } + gain_value { + gain_db: -10.06 + phi_rad: 4.71238898038469 + theta_rad: 0.8997172294030769 + } + gain_value { + gain_db: -9.3583 + phi_rad: 4.71238898038469 + theta_rad: 0.9005898940290741 + } + gain_value { + gain_db: -8.8977 + phi_rad: 4.71238898038469 + theta_rad: 0.9014625586550712 + } + gain_value { + gain_db: -8.5248 + phi_rad: 4.71238898038469 + theta_rad: 0.9023352232810684 + } + gain_value { + gain_db: -8.2848 + phi_rad: 4.71238898038469 + theta_rad: 0.9032078879070655 + } + gain_value { + gain_db: -8.2202 + phi_rad: 4.71238898038469 + theta_rad: 0.9040805525330626 + } + gain_value { + gain_db: -8.2813 + phi_rad: 4.71238898038469 + theta_rad: 0.9049532171590599 + } + gain_value { + gain_db: -8.4949 + phi_rad: 4.71238898038469 + theta_rad: 0.905825881785057 + } + gain_value { + gain_db: -8.7844 + phi_rad: 4.71238898038469 + theta_rad: 0.9066985464110543 + } + gain_value { + gain_db: -9.0715 + phi_rad: 4.71238898038469 + theta_rad: 0.9075712110370514 + } + gain_value { + gain_db: -9.4192 + phi_rad: 4.71238898038469 + theta_rad: 0.9084438756630485 + } + gain_value { + gain_db: -9.5743 + phi_rad: 4.71238898038469 + theta_rad: 0.9093165402890457 + } + gain_value { + gain_db: -9.7644 + phi_rad: 4.71238898038469 + theta_rad: 0.9101892049150428 + } + gain_value { + gain_db: -9.9624 + phi_rad: 4.71238898038469 + theta_rad: 0.9110618695410401 + } + gain_value { + gain_db: -10.06 + phi_rad: 4.71238898038469 + theta_rad: 0.9119345341670372 + } + gain_value { + gain_db: -9.983 + phi_rad: 4.71238898038469 + theta_rad: 0.9128071987930343 + } + gain_value { + gain_db: -9.8319 + phi_rad: 4.71238898038469 + theta_rad: 0.9136798634190315 + } + gain_value { + gain_db: -9.7009 + phi_rad: 4.71238898038469 + theta_rad: 0.9145525280450286 + } + gain_value { + gain_db: -9.6486 + phi_rad: 4.71238898038469 + theta_rad: 0.9154251926710258 + } + gain_value { + gain_db: -9.4835 + phi_rad: 4.71238898038469 + theta_rad: 0.9162978572970231 + } + gain_value { + gain_db: -9.0966 + phi_rad: 4.71238898038469 + theta_rad: 0.9171705219230202 + } + gain_value { + gain_db: -8.8163 + phi_rad: 4.71238898038469 + theta_rad: 0.9180431865490174 + } + gain_value { + gain_db: -8.6939 + phi_rad: 4.71238898038469 + theta_rad: 0.9189158511750145 + } + gain_value { + gain_db: -8.8511 + phi_rad: 4.71238898038469 + theta_rad: 0.9197885158010117 + } + gain_value { + gain_db: -9.1252 + phi_rad: 4.71238898038469 + theta_rad: 0.9206611804270088 + } + gain_value { + gain_db: -9.3312 + phi_rad: 4.71238898038469 + theta_rad: 0.921533845053006 + } + gain_value { + gain_db: -9.5011 + phi_rad: 4.71238898038469 + theta_rad: 0.9224065096790032 + } + gain_value { + gain_db: -9.6498 + phi_rad: 4.71238898038469 + theta_rad: 0.9232791743050003 + } + gain_value { + gain_db: -9.8583 + phi_rad: 4.71238898038469 + theta_rad: 0.9241518389309975 + } + gain_value { + gain_db: -9.9387 + phi_rad: 4.71238898038469 + theta_rad: 0.9250245035569946 + } + gain_value { + gain_db: -10.015 + phi_rad: 4.71238898038469 + theta_rad: 0.9258971681829917 + } + gain_value { + gain_db: -10.142 + phi_rad: 4.71238898038469 + theta_rad: 0.9267698328089891 + } + gain_value { + gain_db: -10.318 + phi_rad: 4.71238898038469 + theta_rad: 0.9276424974349862 + } + gain_value { + gain_db: -10.685 + phi_rad: 4.71238898038469 + theta_rad: 0.9285151620609834 + } + gain_value { + gain_db: -10.811 + phi_rad: 4.71238898038469 + theta_rad: 0.9293878266869805 + } + gain_value { + gain_db: -10.305 + phi_rad: 4.71238898038469 + theta_rad: 0.9302604913129776 + } + gain_value { + gain_db: -9.3806 + phi_rad: 4.71238898038469 + theta_rad: 0.9311331559389748 + } + gain_value { + gain_db: -8.5397 + phi_rad: 4.71238898038469 + theta_rad: 0.9320058205649719 + } + gain_value { + gain_db: -7.9191 + phi_rad: 4.71238898038469 + theta_rad: 0.9328784851909692 + } + gain_value { + gain_db: -7.5846 + phi_rad: 4.71238898038469 + theta_rad: 0.9337511498169663 + } + gain_value { + gain_db: -7.3926 + phi_rad: 4.71238898038469 + theta_rad: 0.9346238144429634 + } + gain_value { + gain_db: -7.4692 + phi_rad: 4.71238898038469 + theta_rad: 0.9354964790689606 + } + gain_value { + gain_db: -7.6497 + phi_rad: 4.71238898038469 + theta_rad: 0.9363691436949578 + } + gain_value { + gain_db: -7.7269 + phi_rad: 4.71238898038469 + theta_rad: 0.937241808320955 + } + gain_value { + gain_db: -7.9347 + phi_rad: 4.71238898038469 + theta_rad: 0.9381144729469522 + } + gain_value { + gain_db: -8.1071 + phi_rad: 4.71238898038469 + theta_rad: 0.9389871375729493 + } + gain_value { + gain_db: -8.3337 + phi_rad: 4.71238898038469 + theta_rad: 0.9398598021989465 + } + gain_value { + gain_db: -8.3882 + phi_rad: 4.71238898038469 + theta_rad: 0.9407324668249436 + } + gain_value { + gain_db: -8.1823 + phi_rad: 4.71238898038469 + theta_rad: 0.9416051314509408 + } + gain_value { + gain_db: -7.7457 + phi_rad: 4.71238898038469 + theta_rad: 0.9424777960769379 + } + gain_value { + gain_db: -7.1551 + phi_rad: 4.71238898038469 + theta_rad: 0.943350460702935 + } + gain_value { + gain_db: -6.9207 + phi_rad: 4.71238898038469 + theta_rad: 0.9442231253289323 + } + gain_value { + gain_db: -6.717 + phi_rad: 4.71238898038469 + theta_rad: 0.9450957899549294 + } + gain_value { + gain_db: -6.6304 + phi_rad: 4.71238898038469 + theta_rad: 0.9459684545809267 + } + gain_value { + gain_db: -6.7872 + phi_rad: 4.71238898038469 + theta_rad: 0.9468411192069238 + } + gain_value { + gain_db: -7.0344 + phi_rad: 4.71238898038469 + theta_rad: 0.9477137838329209 + } + gain_value { + gain_db: -7.3388 + phi_rad: 4.71238898038469 + theta_rad: 0.9485864484589182 + } + gain_value { + gain_db: -7.6369 + phi_rad: 4.71238898038469 + theta_rad: 0.9494591130849153 + } + gain_value { + gain_db: -7.9284 + phi_rad: 4.71238898038469 + theta_rad: 0.9503317777109125 + } + gain_value { + gain_db: -8.2398 + phi_rad: 4.71238898038469 + theta_rad: 0.9512044423369096 + } + gain_value { + gain_db: -8.394 + phi_rad: 4.71238898038469 + theta_rad: 0.9520771069629067 + } + gain_value { + gain_db: -8.0908 + phi_rad: 4.71238898038469 + theta_rad: 0.9529497715889039 + } + gain_value { + gain_db: -7.7627 + phi_rad: 4.71238898038469 + theta_rad: 0.953822436214901 + } + gain_value { + gain_db: -7.4598 + phi_rad: 4.71238898038469 + theta_rad: 0.9546951008408983 + } + gain_value { + gain_db: -7.2739 + phi_rad: 4.71238898038469 + theta_rad: 0.9555677654668955 + } + gain_value { + gain_db: -7.1685 + phi_rad: 4.71238898038469 + theta_rad: 0.9564404300928926 + } + gain_value { + gain_db: -7.0217 + phi_rad: 4.71238898038469 + theta_rad: 0.9573130947188898 + } + gain_value { + gain_db: -7.1128 + phi_rad: 4.71238898038469 + theta_rad: 0.9581857593448869 + } + gain_value { + gain_db: -7.3217 + phi_rad: 4.71238898038469 + theta_rad: 0.9590584239708841 + } + gain_value { + gain_db: -7.6047 + phi_rad: 4.71238898038469 + theta_rad: 0.9599310885968813 + } + gain_value { + gain_db: -7.7538 + phi_rad: 4.71238898038469 + theta_rad: 0.9608037532228784 + } + gain_value { + gain_db: -7.9371 + phi_rad: 4.71238898038469 + theta_rad: 0.9616764178488756 + } + gain_value { + gain_db: -8.2226 + phi_rad: 4.71238898038469 + theta_rad: 0.9625490824748727 + } + gain_value { + gain_db: -8.6419 + phi_rad: 4.71238898038469 + theta_rad: 0.9634217471008699 + } + gain_value { + gain_db: -9.1256 + phi_rad: 4.71238898038469 + theta_rad: 0.964294411726867 + } + gain_value { + gain_db: -9.3132 + phi_rad: 4.71238898038469 + theta_rad: 0.9651670763528641 + } + gain_value { + gain_db: -9.2371 + phi_rad: 4.71238898038469 + theta_rad: 0.9660397409788615 + } + gain_value { + gain_db: -8.9556 + phi_rad: 4.71238898038469 + theta_rad: 0.9669124056048586 + } + gain_value { + gain_db: -8.8785 + phi_rad: 4.71238898038469 + theta_rad: 0.9677850702308558 + } + gain_value { + gain_db: -8.5906 + phi_rad: 4.71238898038469 + theta_rad: 0.9686577348568529 + } + gain_value { + gain_db: -8.4778 + phi_rad: 4.71238898038469 + theta_rad: 0.96953039948285 + } + gain_value { + gain_db: -8.5718 + phi_rad: 4.71238898038469 + theta_rad: 0.9704030641088472 + } + gain_value { + gain_db: -8.8357 + phi_rad: 4.71238898038469 + theta_rad: 0.9712757287348444 + } + gain_value { + gain_db: -8.9461 + phi_rad: 4.71238898038469 + theta_rad: 0.9721483933608416 + } + gain_value { + gain_db: -8.9762 + phi_rad: 4.71238898038469 + theta_rad: 0.9730210579868387 + } + gain_value { + gain_db: -8.8829 + phi_rad: 4.71238898038469 + theta_rad: 0.9738937226128358 + } + gain_value { + gain_db: -8.8564 + phi_rad: 4.71238898038469 + theta_rad: 0.9747663872388331 + } + gain_value { + gain_db: -8.9876 + phi_rad: 4.71238898038469 + theta_rad: 0.9756390518648302 + } + gain_value { + gain_db: -9.0777 + phi_rad: 4.71238898038469 + theta_rad: 0.9765117164908275 + } + gain_value { + gain_db: -9.1638 + phi_rad: 4.71238898038469 + theta_rad: 0.9773843811168246 + } + gain_value { + gain_db: -9.285 + phi_rad: 4.71238898038469 + theta_rad: 0.9782570457428217 + } + gain_value { + gain_db: -9.4668 + phi_rad: 4.71238898038469 + theta_rad: 0.9791297103688189 + } + gain_value { + gain_db: -9.6869 + phi_rad: 4.71238898038469 + theta_rad: 0.980002374994816 + } + gain_value { + gain_db: -9.751 + phi_rad: 4.71238898038469 + theta_rad: 0.9808750396208132 + } + gain_value { + gain_db: -9.7791 + phi_rad: 4.71238898038469 + theta_rad: 0.9817477042468103 + } + gain_value { + gain_db: -9.6264 + phi_rad: 4.71238898038469 + theta_rad: 0.9826203688728075 + } + gain_value { + gain_db: -9.3183 + phi_rad: 4.71238898038469 + theta_rad: 0.9834930334988047 + } + gain_value { + gain_db: -8.833 + phi_rad: 4.71238898038469 + theta_rad: 0.9843656981248018 + } + gain_value { + gain_db: -8.4651 + phi_rad: 4.71238898038469 + theta_rad: 0.9852383627507991 + } + gain_value { + gain_db: -8.3317 + phi_rad: 4.71238898038469 + theta_rad: 0.9861110273767962 + } + gain_value { + gain_db: -8.4291 + phi_rad: 4.71238898038469 + theta_rad: 0.9869836920027933 + } + gain_value { + gain_db: -8.6962 + phi_rad: 4.71238898038469 + theta_rad: 0.9878563566287906 + } + gain_value { + gain_db: -8.9009 + phi_rad: 4.71238898038469 + theta_rad: 0.9887290212547877 + } + gain_value { + gain_db: -9.1171 + phi_rad: 4.71238898038469 + theta_rad: 0.9896016858807849 + } + gain_value { + gain_db: -9.2595 + phi_rad: 4.71238898038469 + theta_rad: 0.990474350506782 + } + gain_value { + gain_db: -9.2382 + phi_rad: 4.71238898038469 + theta_rad: 0.9913470151327791 + } + gain_value { + gain_db: -9.013 + phi_rad: 4.71238898038469 + theta_rad: 0.9922196797587763 + } + gain_value { + gain_db: -8.5518 + phi_rad: 4.71238898038469 + theta_rad: 0.9930923443847735 + } + gain_value { + gain_db: -8.1231 + phi_rad: 4.71238898038469 + theta_rad: 0.9939650090107707 + } + gain_value { + gain_db: -7.7421 + phi_rad: 4.71238898038469 + theta_rad: 0.9948376736367679 + } + gain_value { + gain_db: -7.271 + phi_rad: 4.71238898038469 + theta_rad: 0.995710338262765 + } + gain_value { + gain_db: -6.8776 + phi_rad: 4.71238898038469 + theta_rad: 0.9965830028887622 + } + gain_value { + gain_db: -6.5112 + phi_rad: 4.71238898038469 + theta_rad: 0.9974556675147593 + } + gain_value { + gain_db: -6.2546 + phi_rad: 4.71238898038469 + theta_rad: 0.9983283321407566 + } + gain_value { + gain_db: -6.2377 + phi_rad: 4.71238898038469 + theta_rad: 0.9992009967667537 + } + gain_value { + gain_db: -6.4501 + phi_rad: 4.71238898038469 + theta_rad: 1.0000736613927508 + } + gain_value { + gain_db: -6.8625 + phi_rad: 4.71238898038469 + theta_rad: 1.0009463260187481 + } + gain_value { + gain_db: -7.6395 + phi_rad: 4.71238898038469 + theta_rad: 1.0018189906447452 + } + gain_value { + gain_db: -8.3805 + phi_rad: 4.71238898038469 + theta_rad: 1.0026916552707423 + } + gain_value { + gain_db: -9.0843 + phi_rad: 4.71238898038469 + theta_rad: 1.0035643198967394 + } + gain_value { + gain_db: -9.419 + phi_rad: 4.71238898038469 + theta_rad: 1.0044369845227366 + } + gain_value { + gain_db: -8.9 + phi_rad: 4.71238898038469 + theta_rad: 1.0053096491487339 + } + gain_value { + gain_db: -8.0861 + phi_rad: 4.71238898038469 + theta_rad: 1.006182313774731 + } + gain_value { + gain_db: -7.5217 + phi_rad: 4.71238898038469 + theta_rad: 1.007054978400728 + } + gain_value { + gain_db: -7.0983 + phi_rad: 4.71238898038469 + theta_rad: 1.0079276430267252 + } + gain_value { + gain_db: -6.6734 + phi_rad: 4.71238898038469 + theta_rad: 1.0088003076527223 + } + gain_value { + gain_db: -6.3214 + phi_rad: 4.71238898038469 + theta_rad: 1.0096729722787197 + } + gain_value { + gain_db: -6.2128 + phi_rad: 4.71238898038469 + theta_rad: 1.0105456369047168 + } + gain_value { + gain_db: -6.2627 + phi_rad: 4.71238898038469 + theta_rad: 1.011418301530714 + } + gain_value { + gain_db: -6.5949 + phi_rad: 4.71238898038469 + theta_rad: 1.0122909661567112 + } + gain_value { + gain_db: -7.1933 + phi_rad: 4.71238898038469 + theta_rad: 1.0131636307827083 + } + gain_value { + gain_db: -7.9636 + phi_rad: 4.71238898038469 + theta_rad: 1.0140362954087054 + } + gain_value { + gain_db: -9.3962 + phi_rad: 4.71238898038469 + theta_rad: 1.0149089600347025 + } + gain_value { + gain_db: -11.361 + phi_rad: 4.71238898038469 + theta_rad: 1.0157816246606999 + } + gain_value { + gain_db: -12.328 + phi_rad: 4.71238898038469 + theta_rad: 1.016654289286697 + } + gain_value { + gain_db: -10.743 + phi_rad: 4.71238898038469 + theta_rad: 1.017526953912694 + } + gain_value { + gain_db: -9.9616 + phi_rad: 4.71238898038469 + theta_rad: 1.0183996185386912 + } + gain_value { + gain_db: -9.393 + phi_rad: 4.71238898038469 + theta_rad: 1.0192722831646885 + } + gain_value { + gain_db: -9.0306 + phi_rad: 4.71238898038469 + theta_rad: 1.0201449477906857 + } + gain_value { + gain_db: -8.7479 + phi_rad: 4.71238898038469 + theta_rad: 1.0210176124166828 + } + gain_value { + gain_db: -8.5751 + phi_rad: 4.71238898038469 + theta_rad: 1.0218902770426799 + } + gain_value { + gain_db: -8.5228 + phi_rad: 4.71238898038469 + theta_rad: 1.0227629416686772 + } + gain_value { + gain_db: -8.6179 + phi_rad: 4.71238898038469 + theta_rad: 1.0236356062946743 + } + gain_value { + gain_db: -8.8111 + phi_rad: 4.71238898038469 + theta_rad: 1.0245082709206714 + } + gain_value { + gain_db: -9.1932 + phi_rad: 4.71238898038469 + theta_rad: 1.0253809355466685 + } + gain_value { + gain_db: -9.6505 + phi_rad: 4.71238898038469 + theta_rad: 1.0262536001726656 + } + gain_value { + gain_db: -10.696 + phi_rad: 4.71238898038469 + theta_rad: 1.027126264798663 + } + gain_value { + gain_db: -11.455 + phi_rad: 4.71238898038469 + theta_rad: 1.02799892942466 + } + gain_value { + gain_db: -11.393 + phi_rad: 4.71238898038469 + theta_rad: 1.0288715940506574 + } + gain_value { + gain_db: -10.918 + phi_rad: 4.71238898038469 + theta_rad: 1.0297442586766545 + } + gain_value { + gain_db: -10.589 + phi_rad: 4.71238898038469 + theta_rad: 1.0306169233026516 + } + gain_value { + gain_db: -10.319 + phi_rad: 4.71238898038469 + theta_rad: 1.0314895879286488 + } + gain_value { + gain_db: -9.9081 + phi_rad: 4.71238898038469 + theta_rad: 1.0323622525546459 + } + gain_value { + gain_db: -9.6744 + phi_rad: 4.71238898038469 + theta_rad: 1.0332349171806432 + } + gain_value { + gain_db: -9.5012 + phi_rad: 4.71238898038469 + theta_rad: 1.0341075818066403 + } + gain_value { + gain_db: -9.5357 + phi_rad: 4.71238898038469 + theta_rad: 1.0349802464326374 + } + gain_value { + gain_db: -9.6254 + phi_rad: 4.71238898038469 + theta_rad: 1.0358529110586345 + } + gain_value { + gain_db: -9.7057 + phi_rad: 4.71238898038469 + theta_rad: 1.0367255756846316 + } + gain_value { + gain_db: -9.7463 + phi_rad: 4.71238898038469 + theta_rad: 1.037598240310629 + } + gain_value { + gain_db: -10.133 + phi_rad: 4.71238898038469 + theta_rad: 1.038470904936626 + } + gain_value { + gain_db: -10.784 + phi_rad: 4.71238898038469 + theta_rad: 1.0393435695626232 + } + gain_value { + gain_db: -11.791 + phi_rad: 4.71238898038469 + theta_rad: 1.0402162341886205 + } + gain_value { + gain_db: -13.268 + phi_rad: 4.71238898038469 + theta_rad: 1.0410888988146176 + } + gain_value { + gain_db: -15.165 + phi_rad: 4.71238898038469 + theta_rad: 1.0419615634406147 + } + gain_value { + gain_db: -14.721 + phi_rad: 4.71238898038469 + theta_rad: 1.0428342280666119 + } + gain_value { + gain_db: -13.666 + phi_rad: 4.71238898038469 + theta_rad: 1.043706892692609 + } + gain_value { + gain_db: -12.83 + phi_rad: 4.71238898038469 + theta_rad: 1.0445795573186063 + } + gain_value { + gain_db: -12.622 + phi_rad: 4.71238898038469 + theta_rad: 1.0454522219446034 + } + gain_value { + gain_db: -12.577 + phi_rad: 4.71238898038469 + theta_rad: 1.0463248865706005 + } + gain_value { + gain_db: -12.879 + phi_rad: 4.71238898038469 + theta_rad: 1.0471975511965976 + } + gain_value { + gain_db: -13.372 + phi_rad: 4.71238898038469 + theta_rad: 1.0480702158225947 + } + gain_value { + gain_db: -13.47 + phi_rad: 4.71238898038469 + theta_rad: 1.048942880448592 + } + gain_value { + gain_db: -13.47 + phi_rad: 4.71238898038469 + theta_rad: 1.0498155450745892 + } + gain_value { + gain_db: -13.357 + phi_rad: 4.71238898038469 + theta_rad: 1.0506882097005865 + } + gain_value { + gain_db: -13.404 + phi_rad: 4.71238898038469 + theta_rad: 1.0515608743265836 + } + gain_value { + gain_db: -13.722 + phi_rad: 4.71238898038469 + theta_rad: 1.0524335389525807 + } + gain_value { + gain_db: -14.318 + phi_rad: 4.71238898038469 + theta_rad: 1.0533062035785778 + } + gain_value { + gain_db: -15.138 + phi_rad: 4.71238898038469 + theta_rad: 1.054178868204575 + } + gain_value { + gain_db: -15.832 + phi_rad: 4.71238898038469 + theta_rad: 1.0550515328305723 + } + gain_value { + gain_db: -16.937 + phi_rad: 4.71238898038469 + theta_rad: 1.0559241974565694 + } + gain_value { + gain_db: -17.833 + phi_rad: 4.71238898038469 + theta_rad: 1.0567968620825665 + } + gain_value { + gain_db: -18.549 + phi_rad: 4.71238898038469 + theta_rad: 1.0576695267085636 + } + gain_value { + gain_db: -19.574 + phi_rad: 4.71238898038469 + theta_rad: 1.058542191334561 + } + gain_value { + gain_db: -20.312 + phi_rad: 4.71238898038469 + theta_rad: 1.059414855960558 + } + gain_value { + gain_db: -19.813 + phi_rad: 4.71238898038469 + theta_rad: 1.0602875205865552 + } + gain_value { + gain_db: -17.558 + phi_rad: 4.71238898038469 + theta_rad: 1.0611601852125523 + } + gain_value { + gain_db: -16.069 + phi_rad: 4.71238898038469 + theta_rad: 1.0620328498385496 + } + gain_value { + gain_db: -15.273 + phi_rad: 4.71238898038469 + theta_rad: 1.0629055144645467 + } + gain_value { + gain_db: -15.054 + phi_rad: 4.71238898038469 + theta_rad: 1.0637781790905438 + } + gain_value { + gain_db: -15.291 + phi_rad: 4.71238898038469 + theta_rad: 1.064650843716541 + } + gain_value { + gain_db: -15.428 + phi_rad: 4.71238898038469 + theta_rad: 1.065523508342538 + } + gain_value { + gain_db: -15.249 + phi_rad: 4.71238898038469 + theta_rad: 1.0663961729685354 + } + gain_value { + gain_db: -15.106 + phi_rad: 4.71238898038469 + theta_rad: 1.0672688375945325 + } + gain_value { + gain_db: -14.354 + phi_rad: 4.71238898038469 + theta_rad: 1.0681415022205298 + } + gain_value { + gain_db: -13.486 + phi_rad: 4.71238898038469 + theta_rad: 1.069014166846527 + } + gain_value { + gain_db: -12.822 + phi_rad: 4.71238898038469 + theta_rad: 1.069886831472524 + } + gain_value { + gain_db: -12.671 + phi_rad: 4.71238898038469 + theta_rad: 1.0707594960985212 + } + gain_value { + gain_db: -12.547 + phi_rad: 4.71238898038469 + theta_rad: 1.0716321607245183 + } + gain_value { + gain_db: -12.599 + phi_rad: 4.71238898038469 + theta_rad: 1.0725048253505156 + } + gain_value { + gain_db: -12.483 + phi_rad: 4.71238898038469 + theta_rad: 1.0733774899765127 + } + gain_value { + gain_db: -12.194 + phi_rad: 4.71238898038469 + theta_rad: 1.0742501546025098 + } + gain_value { + gain_db: -12.085 + phi_rad: 4.71238898038469 + theta_rad: 1.075122819228507 + } + gain_value { + gain_db: -12.144 + phi_rad: 4.71238898038469 + theta_rad: 1.075995483854504 + } + gain_value { + gain_db: -12.111 + phi_rad: 4.71238898038469 + theta_rad: 1.0768681484805014 + } + gain_value { + gain_db: -11.69 + phi_rad: 4.71238898038469 + theta_rad: 1.0777408131064985 + } + gain_value { + gain_db: -11.208 + phi_rad: 4.71238898038469 + theta_rad: 1.0786134777324956 + } + gain_value { + gain_db: -10.545 + phi_rad: 4.71238898038469 + theta_rad: 1.079486142358493 + } + gain_value { + gain_db: -9.9421 + phi_rad: 4.71238898038469 + theta_rad: 1.08035880698449 + } + gain_value { + gain_db: -9.5761 + phi_rad: 4.71238898038469 + theta_rad: 1.0812314716104872 + } + gain_value { + gain_db: -9.3488 + phi_rad: 4.71238898038469 + theta_rad: 1.0821041362364843 + } + gain_value { + gain_db: -9.4101 + phi_rad: 4.71238898038469 + theta_rad: 1.0829768008624814 + } + gain_value { + gain_db: -9.7648 + phi_rad: 4.71238898038469 + theta_rad: 1.0838494654884787 + } + gain_value { + gain_db: -10.16 + phi_rad: 4.71238898038469 + theta_rad: 1.0847221301144758 + } + gain_value { + gain_db: -10.422 + phi_rad: 4.71238898038469 + theta_rad: 1.085594794740473 + } + gain_value { + gain_db: -10.702 + phi_rad: 4.71238898038469 + theta_rad: 1.08646745936647 + } + gain_value { + gain_db: -10.977 + phi_rad: 4.71238898038469 + theta_rad: 1.0873401239924672 + } + gain_value { + gain_db: -11.341 + phi_rad: 4.71238898038469 + theta_rad: 1.0882127886184645 + } + gain_value { + gain_db: -11.617 + phi_rad: 4.71238898038469 + theta_rad: 1.0890854532444616 + } + gain_value { + gain_db: -12.023 + phi_rad: 4.71238898038469 + theta_rad: 1.089958117870459 + } + gain_value { + gain_db: -12.507 + phi_rad: 4.71238898038469 + theta_rad: 1.090830782496456 + } + gain_value { + gain_db: -13.078 + phi_rad: 4.71238898038469 + theta_rad: 1.0917034471224532 + } + gain_value { + gain_db: -13.849 + phi_rad: 4.71238898038469 + theta_rad: 1.0925761117484503 + } + gain_value { + gain_db: -14.476 + phi_rad: 4.71238898038469 + theta_rad: 1.0934487763744474 + } + gain_value { + gain_db: -15.25 + phi_rad: 4.71238898038469 + theta_rad: 1.0943214410004447 + } + gain_value { + gain_db: -16.115 + phi_rad: 4.71238898038469 + theta_rad: 1.0951941056264418 + } + gain_value { + gain_db: -16.51 + phi_rad: 4.71238898038469 + theta_rad: 1.096066770252439 + } + gain_value { + gain_db: -16.971 + phi_rad: 4.71238898038469 + theta_rad: 1.096939434878436 + } + gain_value { + gain_db: -17.336 + phi_rad: 4.71238898038469 + theta_rad: 1.0978120995044334 + } + gain_value { + gain_db: -18.041 + phi_rad: 4.71238898038469 + theta_rad: 1.0986847641304305 + } + gain_value { + gain_db: -18.699 + phi_rad: 4.71238898038469 + theta_rad: 1.0995574287564276 + } + gain_value { + gain_db: -19.927 + phi_rad: 4.71238898038469 + theta_rad: 1.1004300933824247 + } + gain_value { + gain_db: -20.773 + phi_rad: 4.71238898038469 + theta_rad: 1.101302758008422 + } + gain_value { + gain_db: -21.947 + phi_rad: 4.71238898038469 + theta_rad: 1.1021754226344191 + } + gain_value { + gain_db: -21.747 + phi_rad: 4.71238898038469 + theta_rad: 1.1030480872604163 + } + gain_value { + gain_db: -20.302 + phi_rad: 4.71238898038469 + theta_rad: 1.1039207518864134 + } + gain_value { + gain_db: -19.062 + phi_rad: 4.71238898038469 + theta_rad: 1.1047934165124105 + } + gain_value { + gain_db: -17.99 + phi_rad: 4.71238898038469 + theta_rad: 1.1056660811384078 + } + gain_value { + gain_db: -17.246 + phi_rad: 4.71238898038469 + theta_rad: 1.106538745764405 + } + gain_value { + gain_db: -16.755 + phi_rad: 4.71238898038469 + theta_rad: 1.1074114103904023 + } + gain_value { + gain_db: -16.533 + phi_rad: 4.71238898038469 + theta_rad: 1.1082840750163994 + } + gain_value { + gain_db: -16.36 + phi_rad: 4.71238898038469 + theta_rad: 1.1091567396423965 + } + gain_value { + gain_db: -16.578 + phi_rad: 4.71238898038469 + theta_rad: 1.1100294042683936 + } + gain_value { + gain_db: -17.039 + phi_rad: 4.71238898038469 + theta_rad: 1.1109020688943907 + } + gain_value { + gain_db: -18.07 + phi_rad: 4.71238898038469 + theta_rad: 1.111774733520388 + } + gain_value { + gain_db: -19.302 + phi_rad: 4.71238898038469 + theta_rad: 1.1126473981463851 + } + gain_value { + gain_db: -20.072 + phi_rad: 4.71238898038469 + theta_rad: 1.1135200627723822 + } + gain_value { + gain_db: -19.572 + phi_rad: 4.71238898038469 + theta_rad: 1.1143927273983794 + } + gain_value { + gain_db: -18.501 + phi_rad: 4.71238898038469 + theta_rad: 1.1152653920243765 + } + gain_value { + gain_db: -17.507 + phi_rad: 4.71238898038469 + theta_rad: 1.1161380566503738 + } + gain_value { + gain_db: -17.032 + phi_rad: 4.71238898038469 + theta_rad: 1.117010721276371 + } + gain_value { + gain_db: -16.7 + phi_rad: 4.71238898038469 + theta_rad: 1.117883385902368 + } + gain_value { + gain_db: -17.147 + phi_rad: 4.71238898038469 + theta_rad: 1.1187560505283651 + } + gain_value { + gain_db: -18.315 + phi_rad: 4.71238898038469 + theta_rad: 1.1196287151543625 + } + gain_value { + gain_db: -21.813 + phi_rad: 4.71238898038469 + theta_rad: 1.1205013797803596 + } + gain_value { + gain_db: -30.541 + phi_rad: 4.71238898038469 + theta_rad: 1.1213740444063567 + } + gain_value { + gain_db: -22.528 + phi_rad: 4.71238898038469 + theta_rad: 1.1222467090323538 + } + gain_value { + gain_db: -18.589 + phi_rad: 4.71238898038469 + theta_rad: 1.123119373658351 + } + gain_value { + gain_db: -16.442 + phi_rad: 4.71238898038469 + theta_rad: 1.1239920382843482 + } + gain_value { + gain_db: -14.846 + phi_rad: 4.71238898038469 + theta_rad: 1.1248647029103453 + } + gain_value { + gain_db: -13.449 + phi_rad: 4.71238898038469 + theta_rad: 1.1257373675363425 + } + gain_value { + gain_db: -12.335 + phi_rad: 4.71238898038469 + theta_rad: 1.1266100321623396 + } + gain_value { + gain_db: -11.399 + phi_rad: 4.71238898038469 + theta_rad: 1.1274826967883367 + } + gain_value { + gain_db: -10.834 + phi_rad: 4.71238898038469 + theta_rad: 1.1283553614143342 + } + gain_value { + gain_db: -10.669 + phi_rad: 4.71238898038469 + theta_rad: 1.1292280260403313 + } + gain_value { + gain_db: -11.034 + phi_rad: 4.71238898038469 + theta_rad: 1.1301006906663285 + } + gain_value { + gain_db: -11.884 + phi_rad: 4.71238898038469 + theta_rad: 1.1309733552923256 + } + gain_value { + gain_db: -13.091 + phi_rad: 4.71238898038469 + theta_rad: 1.1318460199183227 + } + gain_value { + gain_db: -14.686 + phi_rad: 4.71238898038469 + theta_rad: 1.13271868454432 + } + gain_value { + gain_db: -16.103 + phi_rad: 4.71238898038469 + theta_rad: 1.1335913491703171 + } + gain_value { + gain_db: -16.886 + phi_rad: 4.71238898038469 + theta_rad: 1.1344640137963142 + } + gain_value { + gain_db: -16.575 + phi_rad: 4.71238898038469 + theta_rad: 1.1353366784223113 + } + gain_value { + gain_db: -14.873 + phi_rad: 4.71238898038469 + theta_rad: 1.1362093430483085 + } + gain_value { + gain_db: -13.01 + phi_rad: 4.71238898038469 + theta_rad: 1.1370820076743058 + } + gain_value { + gain_db: -11.473 + phi_rad: 4.71238898038469 + theta_rad: 1.137954672300303 + } + gain_value { + gain_db: -10.763 + phi_rad: 4.71238898038469 + theta_rad: 1.1388273369263 + } + gain_value { + gain_db: -10.494 + phi_rad: 4.71238898038469 + theta_rad: 1.1397000015522971 + } + gain_value { + gain_db: -10.681 + phi_rad: 4.71238898038469 + theta_rad: 1.1405726661782942 + } + gain_value { + gain_db: -11.059 + phi_rad: 4.71238898038469 + theta_rad: 1.1414453308042916 + } + gain_value { + gain_db: -12.057 + phi_rad: 4.71238898038469 + theta_rad: 1.1423179954302887 + } + gain_value { + gain_db: -13.332 + phi_rad: 4.71238898038469 + theta_rad: 1.1431906600562858 + } + gain_value { + gain_db: -15.903 + phi_rad: 4.71238898038469 + theta_rad: 1.144063324682283 + } + gain_value { + gain_db: -20.483 + phi_rad: 4.71238898038469 + theta_rad: 1.14493598930828 + } + gain_value { + gain_db: -16.274 + phi_rad: 4.71238898038469 + theta_rad: 1.1458086539342776 + } + gain_value { + gain_db: -14.52 + phi_rad: 4.71238898038469 + theta_rad: 1.1466813185602747 + } + gain_value { + gain_db: -13.461 + phi_rad: 4.71238898038469 + theta_rad: 1.1475539831862718 + } + gain_value { + gain_db: -12.479 + phi_rad: 4.71238898038469 + theta_rad: 1.1484266478122689 + } + gain_value { + gain_db: -11.82 + phi_rad: 4.71238898038469 + theta_rad: 1.149299312438266 + } + gain_value { + gain_db: -11.372 + phi_rad: 4.71238898038469 + theta_rad: 1.1501719770642633 + } + gain_value { + gain_db: -10.988 + phi_rad: 4.71238898038469 + theta_rad: 1.1510446416902604 + } + gain_value { + gain_db: -10.938 + phi_rad: 4.71238898038469 + theta_rad: 1.1519173063162575 + } + gain_value { + gain_db: -11.339 + phi_rad: 4.71238898038469 + theta_rad: 1.1527899709422547 + } + gain_value { + gain_db: -11.743 + phi_rad: 4.71238898038469 + theta_rad: 1.1536626355682518 + } + gain_value { + gain_db: -12.44 + phi_rad: 4.71238898038469 + theta_rad: 1.154535300194249 + } + gain_value { + gain_db: -12.702 + phi_rad: 4.71238898038469 + theta_rad: 1.1554079648202462 + } + gain_value { + gain_db: -12.773 + phi_rad: 4.71238898038469 + theta_rad: 1.1562806294462433 + } + gain_value { + gain_db: -12.994 + phi_rad: 4.71238898038469 + theta_rad: 1.1571532940722404 + } + gain_value { + gain_db: -13.238 + phi_rad: 4.71238898038469 + theta_rad: 1.1580259586982375 + } + gain_value { + gain_db: -13.726 + phi_rad: 4.71238898038469 + theta_rad: 1.1588986233242349 + } + gain_value { + gain_db: -14.648 + phi_rad: 4.71238898038469 + theta_rad: 1.159771287950232 + } + gain_value { + gain_db: -14.579 + phi_rad: 4.71238898038469 + theta_rad: 1.160643952576229 + } + gain_value { + gain_db: -13.561 + phi_rad: 4.71238898038469 + theta_rad: 1.1615166172022262 + } + gain_value { + gain_db: -12.924 + phi_rad: 4.71238898038469 + theta_rad: 1.1623892818282233 + } + gain_value { + gain_db: -12.314 + phi_rad: 4.71238898038469 + theta_rad: 1.1632619464542207 + } + gain_value { + gain_db: -12.082 + phi_rad: 4.71238898038469 + theta_rad: 1.1641346110802178 + } + gain_value { + gain_db: -12.357 + phi_rad: 4.71238898038469 + theta_rad: 1.1650072757062149 + } + gain_value { + gain_db: -12.819 + phi_rad: 4.71238898038469 + theta_rad: 1.165879940332212 + } + gain_value { + gain_db: -14.363 + phi_rad: 4.71238898038469 + theta_rad: 1.166752604958209 + } + gain_value { + gain_db: -18.871 + phi_rad: 4.71238898038469 + theta_rad: 1.1676252695842066 + } + gain_value { + gain_db: -15.63 + phi_rad: 4.71238898038469 + theta_rad: 1.1684979342102038 + } + gain_value { + gain_db: -13.642 + phi_rad: 4.71238898038469 + theta_rad: 1.1693705988362009 + } + gain_value { + gain_db: -12.916 + phi_rad: 4.71238898038469 + theta_rad: 1.170243263462198 + } + gain_value { + gain_db: -12.867 + phi_rad: 4.71238898038469 + theta_rad: 1.171115928088195 + } + gain_value { + gain_db: -13.086 + phi_rad: 4.71238898038469 + theta_rad: 1.1719885927141924 + } + gain_value { + gain_db: -13.663 + phi_rad: 4.71238898038469 + theta_rad: 1.1728612573401895 + } + gain_value { + gain_db: -14.657 + phi_rad: 4.71238898038469 + theta_rad: 1.1737339219661866 + } + gain_value { + gain_db: -15.561 + phi_rad: 4.71238898038469 + theta_rad: 1.1746065865921838 + } + gain_value { + gain_db: -17.369 + phi_rad: 4.71238898038469 + theta_rad: 1.1754792512181809 + } + gain_value { + gain_db: -20.011 + phi_rad: 4.71238898038469 + theta_rad: 1.1763519158441782 + } + gain_value { + gain_db: -22.053 + phi_rad: 4.71238898038469 + theta_rad: 1.1772245804701753 + } + gain_value { + gain_db: -20.149 + phi_rad: 4.71238898038469 + theta_rad: 1.1780972450961724 + } + gain_value { + gain_db: -18.968 + phi_rad: 4.71238898038469 + theta_rad: 1.1789699097221695 + } + gain_value { + gain_db: -18.504 + phi_rad: 4.71238898038469 + theta_rad: 1.1798425743481666 + } + gain_value { + gain_db: -18.341 + phi_rad: 4.71238898038469 + theta_rad: 1.180715238974164 + } + gain_value { + gain_db: -19.018 + phi_rad: 4.71238898038469 + theta_rad: 1.181587903600161 + } + gain_value { + gain_db: -20.686 + phi_rad: 4.71238898038469 + theta_rad: 1.1824605682261582 + } + gain_value { + gain_db: -22.889 + phi_rad: 4.71238898038469 + theta_rad: 1.1833332328521553 + } + gain_value { + gain_db: -19.796 + phi_rad: 4.71238898038469 + theta_rad: 1.1842058974781524 + } + gain_value { + gain_db: -17.783 + phi_rad: 4.71238898038469 + theta_rad: 1.18507856210415 + } + gain_value { + gain_db: -17.004 + phi_rad: 4.71238898038469 + theta_rad: 1.185951226730147 + } + gain_value { + gain_db: -16.06 + phi_rad: 4.71238898038469 + theta_rad: 1.1868238913561442 + } + gain_value { + gain_db: -15.468 + phi_rad: 4.71238898038469 + theta_rad: 1.1876965559821413 + } + gain_value { + gain_db: -15.277 + phi_rad: 4.71238898038469 + theta_rad: 1.1885692206081384 + } + gain_value { + gain_db: -14.869 + phi_rad: 4.71238898038469 + theta_rad: 1.1894418852341357 + } + gain_value { + gain_db: -14.427 + phi_rad: 4.71238898038469 + theta_rad: 1.1903145498601329 + } + gain_value { + gain_db: -13.865 + phi_rad: 4.71238898038469 + theta_rad: 1.19118721448613 + } + gain_value { + gain_db: -13.35 + phi_rad: 4.71238898038469 + theta_rad: 1.192059879112127 + } + gain_value { + gain_db: -12.722 + phi_rad: 4.71238898038469 + theta_rad: 1.1929325437381242 + } + gain_value { + gain_db: -12.325 + phi_rad: 4.71238898038469 + theta_rad: 1.1938052083641215 + } + gain_value { + gain_db: -11.785 + phi_rad: 4.71238898038469 + theta_rad: 1.1946778729901186 + } + gain_value { + gain_db: -11.439 + phi_rad: 4.71238898038469 + theta_rad: 1.1955505376161157 + } + gain_value { + gain_db: -11.314 + phi_rad: 4.71238898038469 + theta_rad: 1.1964232022421128 + } + gain_value { + gain_db: -11.462 + phi_rad: 4.71238898038469 + theta_rad: 1.19729586686811 + } + gain_value { + gain_db: -12.164 + phi_rad: 4.71238898038469 + theta_rad: 1.1981685314941073 + } + gain_value { + gain_db: -12.909 + phi_rad: 4.71238898038469 + theta_rad: 1.1990411961201044 + } + gain_value { + gain_db: -14.014 + phi_rad: 4.71238898038469 + theta_rad: 1.1999138607461015 + } + gain_value { + gain_db: -15.746 + phi_rad: 4.71238898038469 + theta_rad: 1.2007865253720986 + } + gain_value { + gain_db: -17.786 + phi_rad: 4.71238898038469 + theta_rad: 1.2016591899980957 + } + gain_value { + gain_db: -19.815 + phi_rad: 4.71238898038469 + theta_rad: 1.202531854624093 + } + gain_value { + gain_db: -18.867 + phi_rad: 4.71238898038469 + theta_rad: 1.2034045192500902 + } + gain_value { + gain_db: -16.104 + phi_rad: 4.71238898038469 + theta_rad: 1.2042771838760873 + } + gain_value { + gain_db: -14.042 + phi_rad: 4.71238898038469 + theta_rad: 1.2051498485020844 + } + gain_value { + gain_db: -12.685 + phi_rad: 4.71238898038469 + theta_rad: 1.2060225131280815 + } + gain_value { + gain_db: -12.003 + phi_rad: 4.71238898038469 + theta_rad: 1.206895177754079 + } + gain_value { + gain_db: -11.709 + phi_rad: 4.71238898038469 + theta_rad: 1.2077678423800762 + } + gain_value { + gain_db: -11.881 + phi_rad: 4.71238898038469 + theta_rad: 1.2086405070060733 + } + gain_value { + gain_db: -12.557 + phi_rad: 4.71238898038469 + theta_rad: 1.2095131716320704 + } + gain_value { + gain_db: -13.733 + phi_rad: 4.71238898038469 + theta_rad: 1.2103858362580675 + } + gain_value { + gain_db: -16.154 + phi_rad: 4.71238898038469 + theta_rad: 1.2112585008840648 + } + gain_value { + gain_db: -22.126 + phi_rad: 4.71238898038469 + theta_rad: 1.212131165510062 + } + gain_value { + gain_db: -17.552 + phi_rad: 4.71238898038469 + theta_rad: 1.213003830136059 + } + gain_value { + gain_db: -17.486 + phi_rad: 4.71238898038469 + theta_rad: 1.2138764947620562 + } + gain_value { + gain_db: -17.804 + phi_rad: 4.71238898038469 + theta_rad: 1.2147491593880533 + } + gain_value { + gain_db: -15.345 + phi_rad: 4.71238898038469 + theta_rad: 1.2156218240140506 + } + gain_value { + gain_db: -13.732 + phi_rad: 4.71238898038469 + theta_rad: 1.2164944886400477 + } + gain_value { + gain_db: -12.883 + phi_rad: 4.71238898038469 + theta_rad: 1.2173671532660448 + } + gain_value { + gain_db: -13.072 + phi_rad: 4.71238898038469 + theta_rad: 1.218239817892042 + } + gain_value { + gain_db: -14.161 + phi_rad: 4.71238898038469 + theta_rad: 1.219112482518039 + } + gain_value { + gain_db: -15.554 + phi_rad: 4.71238898038469 + theta_rad: 1.2199851471440364 + } + gain_value { + gain_db: -14.237 + phi_rad: 4.71238898038469 + theta_rad: 1.2208578117700335 + } + gain_value { + gain_db: -13.031 + phi_rad: 4.71238898038469 + theta_rad: 1.2217304763960306 + } + gain_value { + gain_db: -12.937 + phi_rad: 4.71238898038469 + theta_rad: 1.2226031410220277 + } + gain_value { + gain_db: -13.549 + phi_rad: 4.71238898038469 + theta_rad: 1.2234758056480248 + } + gain_value { + gain_db: -15.155 + phi_rad: 4.71238898038469 + theta_rad: 1.2243484702740224 + } + gain_value { + gain_db: -19.863 + phi_rad: 4.71238898038469 + theta_rad: 1.2252211349000195 + } + gain_value { + gain_db: -14.406 + phi_rad: 4.71238898038469 + theta_rad: 1.2260937995260166 + } + gain_value { + gain_db: -13.192 + phi_rad: 4.71238898038469 + theta_rad: 1.2269664641520137 + } + gain_value { + gain_db: -12.786 + phi_rad: 4.71238898038469 + theta_rad: 1.2278391287780108 + } + gain_value { + gain_db: -13.347 + phi_rad: 4.71238898038469 + theta_rad: 1.2287117934040082 + } + gain_value { + gain_db: -14.331 + phi_rad: 4.71238898038469 + theta_rad: 1.2295844580300053 + } + gain_value { + gain_db: -16.141 + phi_rad: 4.71238898038469 + theta_rad: 1.2304571226560024 + } + gain_value { + gain_db: -17.888 + phi_rad: 4.71238898038469 + theta_rad: 1.2313297872819995 + } + gain_value { + gain_db: -17.722 + phi_rad: 4.71238898038469 + theta_rad: 1.2322024519079966 + } + gain_value { + gain_db: -16.34 + phi_rad: 4.71238898038469 + theta_rad: 1.233075116533994 + } + gain_value { + gain_db: -15.168 + phi_rad: 4.71238898038469 + theta_rad: 1.233947781159991 + } + gain_value { + gain_db: -14.351 + phi_rad: 4.71238898038469 + theta_rad: 1.2348204457859882 + } + gain_value { + gain_db: -14.157 + phi_rad: 4.71238898038469 + theta_rad: 1.2356931104119853 + } + gain_value { + gain_db: -13.823 + phi_rad: 4.71238898038469 + theta_rad: 1.2365657750379824 + } + gain_value { + gain_db: -13.08 + phi_rad: 4.71238898038469 + theta_rad: 1.2374384396639797 + } + gain_value { + gain_db: -12.281 + phi_rad: 4.71238898038469 + theta_rad: 1.2383111042899768 + } + gain_value { + gain_db: -11.687 + phi_rad: 4.71238898038469 + theta_rad: 1.239183768915974 + } + gain_value { + gain_db: -11.742 + phi_rad: 4.71238898038469 + theta_rad: 1.240056433541971 + } + gain_value { + gain_db: -11.972 + phi_rad: 4.71238898038469 + theta_rad: 1.2409290981679681 + } + gain_value { + gain_db: -12.668 + phi_rad: 4.71238898038469 + theta_rad: 1.2418017627939655 + } + gain_value { + gain_db: -13.846 + phi_rad: 4.71238898038469 + theta_rad: 1.2426744274199626 + } + gain_value { + gain_db: -15.267 + phi_rad: 4.71238898038469 + theta_rad: 1.2435470920459597 + } + gain_value { + gain_db: -16.979 + phi_rad: 4.71238898038469 + theta_rad: 1.2444197566719568 + } + gain_value { + gain_db: -17.887 + phi_rad: 4.71238898038469 + theta_rad: 1.245292421297954 + } + gain_value { + gain_db: -17.23 + phi_rad: 4.71238898038469 + theta_rad: 1.2461650859239515 + } + gain_value { + gain_db: -16.015 + phi_rad: 4.71238898038469 + theta_rad: 1.2470377505499486 + } + gain_value { + gain_db: -15.436 + phi_rad: 4.71238898038469 + theta_rad: 1.2479104151759457 + } + gain_value { + gain_db: -15.506 + phi_rad: 4.71238898038469 + theta_rad: 1.2487830798019428 + } + gain_value { + gain_db: -16.225 + phi_rad: 4.71238898038469 + theta_rad: 1.24965574442794 + } + gain_value { + gain_db: -19.265 + phi_rad: 4.71238898038469 + theta_rad: 1.2505284090539373 + } + gain_value { + gain_db: -17.627 + phi_rad: 4.71238898038469 + theta_rad: 1.2514010736799344 + } + gain_value { + gain_db: -14.887 + phi_rad: 4.71238898038469 + theta_rad: 1.2522737383059315 + } + gain_value { + gain_db: -13.294 + phi_rad: 4.71238898038469 + theta_rad: 1.2531464029319286 + } + gain_value { + gain_db: -12.542 + phi_rad: 4.71238898038469 + theta_rad: 1.2540190675579257 + } + gain_value { + gain_db: -11.958 + phi_rad: 4.71238898038469 + theta_rad: 1.254891732183923 + } + gain_value { + gain_db: -11.661 + phi_rad: 4.71238898038469 + theta_rad: 1.2557643968099201 + } + gain_value { + gain_db: -11.721 + phi_rad: 4.71238898038469 + theta_rad: 1.2566370614359172 + } + gain_value { + gain_db: -12.428 + phi_rad: 4.71238898038469 + theta_rad: 1.2575097260619144 + } + gain_value { + gain_db: -13.266 + phi_rad: 4.71238898038469 + theta_rad: 1.2583823906879115 + } + gain_value { + gain_db: -13.981 + phi_rad: 4.71238898038469 + theta_rad: 1.2592550553139088 + } + gain_value { + gain_db: -13.972 + phi_rad: 4.71238898038469 + theta_rad: 1.260127719939906 + } + gain_value { + gain_db: -13.867 + phi_rad: 4.71238898038469 + theta_rad: 1.261000384565903 + } + gain_value { + gain_db: -14.905 + phi_rad: 4.71238898038469 + theta_rad: 1.2618730491919001 + } + gain_value { + gain_db: -19.657 + phi_rad: 4.71238898038469 + theta_rad: 1.2627457138178972 + } + gain_value { + gain_db: -15.099 + phi_rad: 4.71238898038469 + theta_rad: 1.2636183784438948 + } + gain_value { + gain_db: -12.828 + phi_rad: 4.71238898038469 + theta_rad: 1.264491043069892 + } + gain_value { + gain_db: -11.796 + phi_rad: 4.71238898038469 + theta_rad: 1.265363707695889 + } + gain_value { + gain_db: -11.192 + phi_rad: 4.71238898038469 + theta_rad: 1.2662363723218861 + } + gain_value { + gain_db: -11.008 + phi_rad: 4.71238898038469 + theta_rad: 1.2671090369478832 + } + gain_value { + gain_db: -10.959 + phi_rad: 4.71238898038469 + theta_rad: 1.2679817015738806 + } + gain_value { + gain_db: -10.888 + phi_rad: 4.71238898038469 + theta_rad: 1.2688543661998777 + } + gain_value { + gain_db: -10.283 + phi_rad: 4.71238898038469 + theta_rad: 1.2697270308258748 + } + gain_value { + gain_db: -9.6435 + phi_rad: 4.71238898038469 + theta_rad: 1.270599695451872 + } + gain_value { + gain_db: -8.9951 + phi_rad: 4.71238898038469 + theta_rad: 1.271472360077869 + } + gain_value { + gain_db: -8.6158 + phi_rad: 4.71238898038469 + theta_rad: 1.2723450247038663 + } + gain_value { + gain_db: -8.5508 + phi_rad: 4.71238898038469 + theta_rad: 1.2732176893298635 + } + gain_value { + gain_db: -8.7877 + phi_rad: 4.71238898038469 + theta_rad: 1.2740903539558606 + } + gain_value { + gain_db: -9.3833 + phi_rad: 4.71238898038469 + theta_rad: 1.2749630185818577 + } + gain_value { + gain_db: -10.473 + phi_rad: 4.71238898038469 + theta_rad: 1.2758356832078548 + } + gain_value { + gain_db: -12.191 + phi_rad: 4.71238898038469 + theta_rad: 1.2767083478338521 + } + gain_value { + gain_db: -14.271 + phi_rad: 4.71238898038469 + theta_rad: 1.2775810124598492 + } + gain_value { + gain_db: -17.333 + phi_rad: 4.71238898038469 + theta_rad: 1.2784536770858463 + } + gain_value { + gain_db: -16.961 + phi_rad: 4.71238898038469 + theta_rad: 1.2793263417118435 + } + gain_value { + gain_db: -17.384 + phi_rad: 4.71238898038469 + theta_rad: 1.2801990063378406 + } + gain_value { + gain_db: -17.551 + phi_rad: 4.71238898038469 + theta_rad: 1.281071670963838 + } + gain_value { + gain_db: -13.021 + phi_rad: 4.71238898038469 + theta_rad: 1.281944335589835 + } + gain_value { + gain_db: -10.581 + phi_rad: 4.71238898038469 + theta_rad: 1.2828170002158321 + } + gain_value { + gain_db: -9.3134 + phi_rad: 4.71238898038469 + theta_rad: 1.2836896648418292 + } + gain_value { + gain_db: -8.3481 + phi_rad: 4.71238898038469 + theta_rad: 1.2845623294678266 + } + gain_value { + gain_db: -7.9509 + phi_rad: 4.71238898038469 + theta_rad: 1.285434994093824 + } + gain_value { + gain_db: -8.0469 + phi_rad: 4.71238898038469 + theta_rad: 1.286307658719821 + } + gain_value { + gain_db: -8.4952 + phi_rad: 4.71238898038469 + theta_rad: 1.2871803233458181 + } + gain_value { + gain_db: -9.4079 + phi_rad: 4.71238898038469 + theta_rad: 1.2880529879718152 + } + gain_value { + gain_db: -10.755 + phi_rad: 4.71238898038469 + theta_rad: 1.2889256525978123 + } + gain_value { + gain_db: -12.635 + phi_rad: 4.71238898038469 + theta_rad: 1.2897983172238097 + } + gain_value { + gain_db: -15.539 + phi_rad: 4.71238898038469 + theta_rad: 1.2906709818498068 + } + gain_value { + gain_db: -16.414 + phi_rad: 4.71238898038469 + theta_rad: 1.2915436464758039 + } + gain_value { + gain_db: -14.888 + phi_rad: 4.71238898038469 + theta_rad: 1.292416311101801 + } + gain_value { + gain_db: -14.332 + phi_rad: 4.71238898038469 + theta_rad: 1.293288975727798 + } + gain_value { + gain_db: -14.482 + phi_rad: 4.71238898038469 + theta_rad: 1.2941616403537954 + } + gain_value { + gain_db: -14.801 + phi_rad: 4.71238898038469 + theta_rad: 1.2950343049797925 + } + gain_value { + gain_db: -14.387 + phi_rad: 4.71238898038469 + theta_rad: 1.2959069696057897 + } + gain_value { + gain_db: -13.607 + phi_rad: 4.71238898038469 + theta_rad: 1.2967796342317868 + } + gain_value { + gain_db: -12.525 + phi_rad: 4.71238898038469 + theta_rad: 1.2976522988577839 + } + gain_value { + gain_db: -12.104 + phi_rad: 4.71238898038469 + theta_rad: 1.2985249634837812 + } + gain_value { + gain_db: -12.307 + phi_rad: 4.71238898038469 + theta_rad: 1.2993976281097783 + } + gain_value { + gain_db: -12.804 + phi_rad: 4.71238898038469 + theta_rad: 1.3002702927357754 + } + gain_value { + gain_db: -13.332 + phi_rad: 4.71238898038469 + theta_rad: 1.3011429573617725 + } + gain_value { + gain_db: -14.369 + phi_rad: 4.71238898038469 + theta_rad: 1.3020156219877697 + } + gain_value { + gain_db: -15.184 + phi_rad: 4.71238898038469 + theta_rad: 1.3028882866137672 + } + gain_value { + gain_db: -14.484 + phi_rad: 4.71238898038469 + theta_rad: 1.3037609512397643 + } + gain_value { + gain_db: -13.472 + phi_rad: 4.71238898038469 + theta_rad: 1.3046336158657614 + } + gain_value { + gain_db: -12.844 + phi_rad: 4.71238898038469 + theta_rad: 1.3055062804917585 + } + gain_value { + gain_db: -12.58 + phi_rad: 4.71238898038469 + theta_rad: 1.3063789451177557 + } + gain_value { + gain_db: -12.585 + phi_rad: 4.71238898038469 + theta_rad: 1.307251609743753 + } + gain_value { + gain_db: -12.716 + phi_rad: 4.71238898038469 + theta_rad: 1.30812427436975 + } + gain_value { + gain_db: -13.388 + phi_rad: 4.71238898038469 + theta_rad: 1.3089969389957472 + } + gain_value { + gain_db: -15.052 + phi_rad: 4.71238898038469 + theta_rad: 1.3098696036217443 + } + gain_value { + gain_db: -21.153 + phi_rad: 4.71238898038469 + theta_rad: 1.3107422682477414 + } + gain_value { + gain_db: -16.503 + phi_rad: 4.71238898038469 + theta_rad: 1.3116149328737388 + } + gain_value { + gain_db: -15.305 + phi_rad: 4.71238898038469 + theta_rad: 1.3124875974997359 + } + gain_value { + gain_db: -14.833 + phi_rad: 4.71238898038469 + theta_rad: 1.313360262125733 + } + gain_value { + gain_db: -14.738 + phi_rad: 4.71238898038469 + theta_rad: 1.31423292675173 + } + gain_value { + gain_db: -14.091 + phi_rad: 4.71238898038469 + theta_rad: 1.3151055913777272 + } + gain_value { + gain_db: -13.573 + phi_rad: 4.71238898038469 + theta_rad: 1.3159782560037245 + } + gain_value { + gain_db: -13.009 + phi_rad: 4.71238898038469 + theta_rad: 1.3168509206297216 + } + gain_value { + gain_db: -13.226 + phi_rad: 4.71238898038469 + theta_rad: 1.3177235852557188 + } + gain_value { + gain_db: -13.553 + phi_rad: 4.71238898038469 + theta_rad: 1.3185962498817159 + } + gain_value { + gain_db: -14.364 + phi_rad: 4.71238898038469 + theta_rad: 1.319468914507713 + } + gain_value { + gain_db: -14.454 + phi_rad: 4.71238898038469 + theta_rad: 1.3203415791337103 + } + gain_value { + gain_db: -14.596 + phi_rad: 4.71238898038469 + theta_rad: 1.3212142437597074 + } + gain_value { + gain_db: -14.483 + phi_rad: 4.71238898038469 + theta_rad: 1.3220869083857045 + } + gain_value { + gain_db: -14.326 + phi_rad: 4.71238898038469 + theta_rad: 1.3229595730117016 + } + gain_value { + gain_db: -14.297 + phi_rad: 4.71238898038469 + theta_rad: 1.323832237637699 + } + gain_value { + gain_db: -14.593 + phi_rad: 4.71238898038469 + theta_rad: 1.3247049022636963 + } + gain_value { + gain_db: -15.802 + phi_rad: 4.71238898038469 + theta_rad: 1.3255775668896934 + } + gain_value { + gain_db: -20.935 + phi_rad: 4.71238898038469 + theta_rad: 1.3264502315156905 + } + gain_value { + gain_db: -15.922 + phi_rad: 4.71238898038469 + theta_rad: 1.3273228961416876 + } + gain_value { + gain_db: -13.835 + phi_rad: 4.71238898038469 + theta_rad: 1.3281955607676847 + } + gain_value { + gain_db: -12.554 + phi_rad: 4.71238898038469 + theta_rad: 1.329068225393682 + } + gain_value { + gain_db: -11.996 + phi_rad: 4.71238898038469 + theta_rad: 1.3299408900196792 + } + gain_value { + gain_db: -11.845 + phi_rad: 4.71238898038469 + theta_rad: 1.3308135546456763 + } + gain_value { + gain_db: -12.003 + phi_rad: 4.71238898038469 + theta_rad: 1.3316862192716734 + } + gain_value { + gain_db: -12.652 + phi_rad: 4.71238898038469 + theta_rad: 1.3325588838976705 + } + gain_value { + gain_db: -13.804 + phi_rad: 4.71238898038469 + theta_rad: 1.3334315485236679 + } + gain_value { + gain_db: -15.58 + phi_rad: 4.71238898038469 + theta_rad: 1.334304213149665 + } + gain_value { + gain_db: -15.826 + phi_rad: 4.71238898038469 + theta_rad: 1.335176877775662 + } + gain_value { + gain_db: -13.671 + phi_rad: 4.71238898038469 + theta_rad: 1.3360495424016592 + } + gain_value { + gain_db: -12.681 + phi_rad: 4.71238898038469 + theta_rad: 1.3369222070276563 + } + gain_value { + gain_db: -12.628 + phi_rad: 4.71238898038469 + theta_rad: 1.3377948716536536 + } + gain_value { + gain_db: -12.886 + phi_rad: 4.71238898038469 + theta_rad: 1.3386675362796507 + } + gain_value { + gain_db: -12.362 + phi_rad: 4.71238898038469 + theta_rad: 1.3395402009056478 + } + gain_value { + gain_db: -11.691 + phi_rad: 4.71238898038469 + theta_rad: 1.340412865531645 + } + gain_value { + gain_db: -11.472 + phi_rad: 4.71238898038469 + theta_rad: 1.341285530157642 + } + gain_value { + gain_db: -11.497 + phi_rad: 4.71238898038469 + theta_rad: 1.3421581947836396 + } + gain_value { + gain_db: -11.206 + phi_rad: 4.71238898038469 + theta_rad: 1.3430308594096367 + } + gain_value { + gain_db: -10.807 + phi_rad: 4.71238898038469 + theta_rad: 1.3439035240356338 + } + gain_value { + gain_db: -10.536 + phi_rad: 4.71238898038469 + theta_rad: 1.344776188661631 + } + gain_value { + gain_db: -10.271 + phi_rad: 4.71238898038469 + theta_rad: 1.345648853287628 + } + gain_value { + gain_db: -10.306 + phi_rad: 4.71238898038469 + theta_rad: 1.3465215179136254 + } + gain_value { + gain_db: -10.72 + phi_rad: 4.71238898038469 + theta_rad: 1.3473941825396225 + } + gain_value { + gain_db: -10.972 + phi_rad: 4.71238898038469 + theta_rad: 1.3482668471656196 + } + gain_value { + gain_db: -11.203 + phi_rad: 4.71238898038469 + theta_rad: 1.3491395117916167 + } + gain_value { + gain_db: -11.226 + phi_rad: 4.71238898038469 + theta_rad: 1.3500121764176138 + } + gain_value { + gain_db: -11.068 + phi_rad: 4.71238898038469 + theta_rad: 1.3508848410436112 + } + gain_value { + gain_db: -10.953 + phi_rad: 4.71238898038469 + theta_rad: 1.3517575056696083 + } + gain_value { + gain_db: -11.206 + phi_rad: 4.71238898038469 + theta_rad: 1.3526301702956054 + } + gain_value { + gain_db: -11.723 + phi_rad: 4.71238898038469 + theta_rad: 1.3535028349216025 + } + gain_value { + gain_db: -12.608 + phi_rad: 4.71238898038469 + theta_rad: 1.3543754995475996 + } + gain_value { + gain_db: -13.23 + phi_rad: 4.71238898038469 + theta_rad: 1.355248164173597 + } + gain_value { + gain_db: -14.124 + phi_rad: 4.71238898038469 + theta_rad: 1.356120828799594 + } + gain_value { + gain_db: -15.866 + phi_rad: 4.71238898038469 + theta_rad: 1.3569934934255912 + } + gain_value { + gain_db: -15.606 + phi_rad: 4.71238898038469 + theta_rad: 1.3578661580515883 + } + gain_value { + gain_db: -13.343 + phi_rad: 4.71238898038469 + theta_rad: 1.3587388226775854 + } + gain_value { + gain_db: -11.976 + phi_rad: 4.71238898038469 + theta_rad: 1.3596114873035827 + } + gain_value { + gain_db: -11.432 + phi_rad: 4.71238898038469 + theta_rad: 1.3604841519295798 + } + gain_value { + gain_db: -11.623 + phi_rad: 4.71238898038469 + theta_rad: 1.361356816555577 + } + gain_value { + gain_db: -12.146 + phi_rad: 4.71238898038469 + theta_rad: 1.362229481181574 + } + gain_value { + gain_db: -13.249 + phi_rad: 4.71238898038469 + theta_rad: 1.3631021458075714 + } + gain_value { + gain_db: -14.455 + phi_rad: 4.71238898038469 + theta_rad: 1.3639748104335687 + } + gain_value { + gain_db: -15.47 + phi_rad: 4.71238898038469 + theta_rad: 1.3648474750595658 + } + gain_value { + gain_db: -16.356 + phi_rad: 4.71238898038469 + theta_rad: 1.365720139685563 + } + gain_value { + gain_db: -17.172 + phi_rad: 4.71238898038469 + theta_rad: 1.36659280431156 + } + gain_value { + gain_db: -17.07 + phi_rad: 4.71238898038469 + theta_rad: 1.3674654689375572 + } + gain_value { + gain_db: -16.22 + phi_rad: 4.71238898038469 + theta_rad: 1.3683381335635545 + } + gain_value { + gain_db: -15.182 + phi_rad: 4.71238898038469 + theta_rad: 1.3692107981895516 + } + gain_value { + gain_db: -14.604 + phi_rad: 4.71238898038469 + theta_rad: 1.3700834628155487 + } + gain_value { + gain_db: -14.829 + phi_rad: 4.71238898038469 + theta_rad: 1.3709561274415458 + } + gain_value { + gain_db: -15.88 + phi_rad: 4.71238898038469 + theta_rad: 1.371828792067543 + } + gain_value { + gain_db: -17.462 + phi_rad: 4.71238898038469 + theta_rad: 1.3727014566935403 + } + gain_value { + gain_db: -14.751 + phi_rad: 4.71238898038469 + theta_rad: 1.3735741213195374 + } + gain_value { + gain_db: -13.319 + phi_rad: 4.71238898038469 + theta_rad: 1.3744467859455345 + } + gain_value { + gain_db: -13.003 + phi_rad: 4.71238898038469 + theta_rad: 1.3753194505715316 + } + gain_value { + gain_db: -13.688 + phi_rad: 4.71238898038469 + theta_rad: 1.3761921151975287 + } + gain_value { + gain_db: -14.942 + phi_rad: 4.71238898038469 + theta_rad: 1.377064779823526 + } + gain_value { + gain_db: -16.865 + phi_rad: 4.71238898038469 + theta_rad: 1.3779374444495232 + } + gain_value { + gain_db: -17.644 + phi_rad: 4.71238898038469 + theta_rad: 1.3788101090755203 + } + gain_value { + gain_db: -16.714 + phi_rad: 4.71238898038469 + theta_rad: 1.3796827737015174 + } + gain_value { + gain_db: -15.177 + phi_rad: 4.71238898038469 + theta_rad: 1.3805554383275145 + } + gain_value { + gain_db: -14.429 + phi_rad: 4.71238898038469 + theta_rad: 1.381428102953512 + } + gain_value { + gain_db: -14.563 + phi_rad: 4.71238898038469 + theta_rad: 1.3823007675795091 + } + gain_value { + gain_db: -16.141 + phi_rad: 4.71238898038469 + theta_rad: 1.3831734322055063 + } + gain_value { + gain_db: -22.148 + phi_rad: 4.71238898038469 + theta_rad: 1.3840460968315034 + } + gain_value { + gain_db: -15.568 + phi_rad: 4.71238898038469 + theta_rad: 1.3849187614575005 + } + gain_value { + gain_db: -14.517 + phi_rad: 4.71238898038469 + theta_rad: 1.3857914260834978 + } + gain_value { + gain_db: -14.327 + phi_rad: 4.71238898038469 + theta_rad: 1.386664090709495 + } + gain_value { + gain_db: -15.472 + phi_rad: 4.71238898038469 + theta_rad: 1.387536755335492 + } + gain_value { + gain_db: -17.637 + phi_rad: 4.71238898038469 + theta_rad: 1.3884094199614891 + } + gain_value { + gain_db: -18.616 + phi_rad: 4.71238898038469 + theta_rad: 1.3892820845874863 + } + gain_value { + gain_db: -16.854 + phi_rad: 4.71238898038469 + theta_rad: 1.3901547492134836 + } + gain_value { + gain_db: -16.428 + phi_rad: 4.71238898038469 + theta_rad: 1.3910274138394807 + } + gain_value { + gain_db: -17.809 + phi_rad: 4.71238898038469 + theta_rad: 1.3919000784654778 + } + gain_value { + gain_db: -19.877 + phi_rad: 4.71238898038469 + theta_rad: 1.392772743091475 + } + gain_value { + gain_db: -16.702 + phi_rad: 4.71238898038469 + theta_rad: 1.393645407717472 + } + gain_value { + gain_db: -15.415 + phi_rad: 4.71238898038469 + theta_rad: 1.3945180723434694 + } + gain_value { + gain_db: -15.032 + phi_rad: 4.71238898038469 + theta_rad: 1.3953907369694665 + } + gain_value { + gain_db: -15.195 + phi_rad: 4.71238898038469 + theta_rad: 1.3962634015954636 + } + gain_value { + gain_db: -16.383 + phi_rad: 4.71238898038469 + theta_rad: 1.3971360662214607 + } + gain_value { + gain_db: -17.037 + phi_rad: 4.71238898038469 + theta_rad: 1.3980087308474578 + } + gain_value { + gain_db: -15.431 + phi_rad: 4.71238898038469 + theta_rad: 1.3988813954734551 + } + gain_value { + gain_db: -14.331 + phi_rad: 4.71238898038469 + theta_rad: 1.3997540600994522 + } + gain_value { + gain_db: -13.55 + phi_rad: 4.71238898038469 + theta_rad: 1.4006267247254494 + } + gain_value { + gain_db: -12.949 + phi_rad: 4.71238898038469 + theta_rad: 1.4014993893514465 + } + gain_value { + gain_db: -12.712 + phi_rad: 4.71238898038469 + theta_rad: 1.4023720539774438 + } + gain_value { + gain_db: -13.38 + phi_rad: 4.71238898038469 + theta_rad: 1.4032447186034411 + } + gain_value { + gain_db: -15.241 + phi_rad: 4.71238898038469 + theta_rad: 1.4041173832294382 + } + gain_value { + gain_db: -22.308 + phi_rad: 4.71238898038469 + theta_rad: 1.4049900478554354 + } + gain_value { + gain_db: -17.794 + phi_rad: 4.71238898038469 + theta_rad: 1.4058627124814325 + } + gain_value { + gain_db: -18.072 + phi_rad: 4.71238898038469 + theta_rad: 1.4067353771074296 + } + gain_value { + gain_db: -21.645 + phi_rad: 4.71238898038469 + theta_rad: 1.407608041733427 + } + gain_value { + gain_db: -23.818 + phi_rad: 4.71238898038469 + theta_rad: 1.408480706359424 + } + gain_value { + gain_db: -17.517 + phi_rad: 4.71238898038469 + theta_rad: 1.4093533709854211 + } + gain_value { + gain_db: -14.601 + phi_rad: 4.71238898038469 + theta_rad: 1.4102260356114182 + } + gain_value { + gain_db: -13.008 + phi_rad: 4.71238898038469 + theta_rad: 1.4110987002374153 + } + gain_value { + gain_db: -12.477 + phi_rad: 4.71238898038469 + theta_rad: 1.4119713648634127 + } + gain_value { + gain_db: -12.349 + phi_rad: 4.71238898038469 + theta_rad: 1.4128440294894098 + } + gain_value { + gain_db: -12.223 + phi_rad: 4.71238898038469 + theta_rad: 1.413716694115407 + } + gain_value { + gain_db: -11.83 + phi_rad: 4.71238898038469 + theta_rad: 1.414589358741404 + } + gain_value { + gain_db: -11.248 + phi_rad: 4.71238898038469 + theta_rad: 1.4154620233674011 + } + gain_value { + gain_db: -11.277 + phi_rad: 4.71238898038469 + theta_rad: 1.4163346879933985 + } + gain_value { + gain_db: -11.863 + phi_rad: 4.71238898038469 + theta_rad: 1.4172073526193956 + } + gain_value { + gain_db: -13.079 + phi_rad: 4.71238898038469 + theta_rad: 1.4180800172453927 + } + gain_value { + gain_db: -14.83 + phi_rad: 4.71238898038469 + theta_rad: 1.4189526818713898 + } + gain_value { + gain_db: -17.897 + phi_rad: 4.71238898038469 + theta_rad: 1.419825346497387 + } + gain_value { + gain_db: -19.552 + phi_rad: 4.71238898038469 + theta_rad: 1.4206980111233845 + } + gain_value { + gain_db: -17.837 + phi_rad: 4.71238898038469 + theta_rad: 1.4215706757493816 + } + gain_value { + gain_db: -15.01 + phi_rad: 4.71238898038469 + theta_rad: 1.4224433403753787 + } + gain_value { + gain_db: -13.428 + phi_rad: 4.71238898038469 + theta_rad: 1.4233160050013758 + } + gain_value { + gain_db: -12.915 + phi_rad: 4.71238898038469 + theta_rad: 1.424188669627373 + } + gain_value { + gain_db: -12.952 + phi_rad: 4.71238898038469 + theta_rad: 1.4250613342533702 + } + gain_value { + gain_db: -14.295 + phi_rad: 4.71238898038469 + theta_rad: 1.4259339988793673 + } + gain_value { + gain_db: -15.256 + phi_rad: 4.71238898038469 + theta_rad: 1.4268066635053644 + } + gain_value { + gain_db: -13.63 + phi_rad: 4.71238898038469 + theta_rad: 1.4276793281313616 + } + gain_value { + gain_db: -13.442 + phi_rad: 4.71238898038469 + theta_rad: 1.4285519927573587 + } + gain_value { + gain_db: -14.136 + phi_rad: 4.71238898038469 + theta_rad: 1.429424657383356 + } + gain_value { + gain_db: -14.985 + phi_rad: 4.71238898038469 + theta_rad: 1.4302973220093531 + } + gain_value { + gain_db: -15.432 + phi_rad: 4.71238898038469 + theta_rad: 1.4311699866353502 + } + gain_value { + gain_db: -15.245 + phi_rad: 4.71238898038469 + theta_rad: 1.4320426512613473 + } + gain_value { + gain_db: -13.615 + phi_rad: 4.71238898038469 + theta_rad: 1.4329153158873444 + } + gain_value { + gain_db: -11.813 + phi_rad: 4.71238898038469 + theta_rad: 1.4337879805133418 + } + gain_value { + gain_db: -10.713 + phi_rad: 4.71238898038469 + theta_rad: 1.4346606451393389 + } + gain_value { + gain_db: -10.165 + phi_rad: 4.71238898038469 + theta_rad: 1.435533309765336 + } + gain_value { + gain_db: -10.128 + phi_rad: 4.71238898038469 + theta_rad: 1.436405974391333 + } + gain_value { + gain_db: -10.453 + phi_rad: 4.71238898038469 + theta_rad: 1.4372786390173302 + } + gain_value { + gain_db: -11.408 + phi_rad: 4.71238898038469 + theta_rad: 1.4381513036433275 + } + gain_value { + gain_db: -13.067 + phi_rad: 4.71238898038469 + theta_rad: 1.4390239682693247 + } + gain_value { + gain_db: -16.669 + phi_rad: 4.71238898038469 + theta_rad: 1.4398966328953218 + } + gain_value { + gain_db: -16.877 + phi_rad: 4.71238898038469 + theta_rad: 1.4407692975213189 + } + gain_value { + gain_db: -15.017 + phi_rad: 4.71238898038469 + theta_rad: 1.4416419621473162 + } + gain_value { + gain_db: -14.116 + phi_rad: 4.71238898038469 + theta_rad: 1.4425146267733135 + } + gain_value { + gain_db: -13.228 + phi_rad: 4.71238898038469 + theta_rad: 1.4433872913993107 + } + gain_value { + gain_db: -12.868 + phi_rad: 4.71238898038469 + theta_rad: 1.4442599560253078 + } + gain_value { + gain_db: -12.727 + phi_rad: 4.71238898038469 + theta_rad: 1.4451326206513049 + } + gain_value { + gain_db: -12.508 + phi_rad: 4.71238898038469 + theta_rad: 1.446005285277302 + } + gain_value { + gain_db: -12.224 + phi_rad: 4.71238898038469 + theta_rad: 1.4468779499032993 + } + gain_value { + gain_db: -12.078 + phi_rad: 4.71238898038469 + theta_rad: 1.4477506145292964 + } + gain_value { + gain_db: -12.469 + phi_rad: 4.71238898038469 + theta_rad: 1.4486232791552935 + } + gain_value { + gain_db: -13.322 + phi_rad: 4.71238898038469 + theta_rad: 1.4494959437812907 + } + gain_value { + gain_db: -14.678 + phi_rad: 4.71238898038469 + theta_rad: 1.4503686084072878 + } + gain_value { + gain_db: -16.824 + phi_rad: 4.71238898038469 + theta_rad: 1.451241273033285 + } + gain_value { + gain_db: -20.932 + phi_rad: 4.71238898038469 + theta_rad: 1.4521139376592822 + } + gain_value { + gain_db: -17.244 + phi_rad: 4.71238898038469 + theta_rad: 1.4529866022852793 + } + gain_value { + gain_db: -14.441 + phi_rad: 4.71238898038469 + theta_rad: 1.4538592669112764 + } + gain_value { + gain_db: -12.638 + phi_rad: 4.71238898038469 + theta_rad: 1.4547319315372735 + } + gain_value { + gain_db: -11.425 + phi_rad: 4.71238898038469 + theta_rad: 1.4556045961632709 + } + gain_value { + gain_db: -10.783 + phi_rad: 4.71238898038469 + theta_rad: 1.456477260789268 + } + gain_value { + gain_db: -10.6 + phi_rad: 4.71238898038469 + theta_rad: 1.457349925415265 + } + gain_value { + gain_db: -10.775 + phi_rad: 4.71238898038469 + theta_rad: 1.4582225900412622 + } + gain_value { + gain_db: -11.587 + phi_rad: 4.71238898038469 + theta_rad: 1.4590952546672593 + } + gain_value { + gain_db: -12.41 + phi_rad: 4.71238898038469 + theta_rad: 1.4599679192932569 + } + gain_value { + gain_db: -12.734 + phi_rad: 4.71238898038469 + theta_rad: 1.460840583919254 + } + gain_value { + gain_db: -12.925 + phi_rad: 4.71238898038469 + theta_rad: 1.461713248545251 + } + gain_value { + gain_db: -13.219 + phi_rad: 4.71238898038469 + theta_rad: 1.4625859131712482 + } + gain_value { + gain_db: -13.443 + phi_rad: 4.71238898038469 + theta_rad: 1.4634585777972453 + } + gain_value { + gain_db: -13.598 + phi_rad: 4.71238898038469 + theta_rad: 1.4643312424232426 + } + gain_value { + gain_db: -13.959 + phi_rad: 4.71238898038469 + theta_rad: 1.4652039070492398 + } + gain_value { + gain_db: -14.791 + phi_rad: 4.71238898038469 + theta_rad: 1.4660765716752369 + } + gain_value { + gain_db: -17.301 + phi_rad: 4.71238898038469 + theta_rad: 1.466949236301234 + } + gain_value { + gain_db: -17.842 + phi_rad: 4.71238898038469 + theta_rad: 1.467821900927231 + } + gain_value { + gain_db: -16.214 + phi_rad: 4.71238898038469 + theta_rad: 1.4686945655532284 + } + gain_value { + gain_db: -15.733 + phi_rad: 4.71238898038469 + theta_rad: 1.4695672301792255 + } + gain_value { + gain_db: -15.777 + phi_rad: 4.71238898038469 + theta_rad: 1.4704398948052226 + } + gain_value { + gain_db: -15.603 + phi_rad: 4.71238898038469 + theta_rad: 1.4713125594312197 + } + gain_value { + gain_db: -16.332 + phi_rad: 4.71238898038469 + theta_rad: 1.4721852240572169 + } + gain_value { + gain_db: -18.554 + phi_rad: 4.71238898038469 + theta_rad: 1.4730578886832142 + } + gain_value { + gain_db: -18.987 + phi_rad: 4.71238898038469 + theta_rad: 1.4739305533092113 + } + gain_value { + gain_db: -17.301 + phi_rad: 4.71238898038469 + theta_rad: 1.4748032179352084 + } + gain_value { + gain_db: -16.621 + phi_rad: 4.71238898038469 + theta_rad: 1.4756758825612055 + } + gain_value { + gain_db: -18.064 + phi_rad: 4.71238898038469 + theta_rad: 1.4765485471872026 + } + gain_value { + gain_db: -17.745 + phi_rad: 4.71238898038469 + theta_rad: 1.4774212118132 + } + gain_value { + gain_db: -16.32 + phi_rad: 4.71238898038469 + theta_rad: 1.478293876439197 + } + gain_value { + gain_db: -16.647 + phi_rad: 4.71238898038469 + theta_rad: 1.4791665410651942 + } + gain_value { + gain_db: -17.175 + phi_rad: 4.71238898038469 + theta_rad: 1.4800392056911915 + } + gain_value { + gain_db: -18.321 + phi_rad: 4.71238898038469 + theta_rad: 1.4809118703171886 + } + gain_value { + gain_db: -17.471 + phi_rad: 4.71238898038469 + theta_rad: 1.481784534943186 + } + gain_value { + gain_db: -17.294 + phi_rad: 4.71238898038469 + theta_rad: 1.482657199569183 + } + gain_value { + gain_db: -17.069 + phi_rad: 4.71238898038469 + theta_rad: 1.4835298641951802 + } + gain_value { + gain_db: -18.767 + phi_rad: 4.71238898038469 + theta_rad: 1.4844025288211773 + } + gain_value { + gain_db: -23.361 + phi_rad: 4.71238898038469 + theta_rad: 1.4852751934471744 + } + gain_value { + gain_db: -18.829 + phi_rad: 4.71238898038469 + theta_rad: 1.4861478580731717 + } + gain_value { + gain_db: -16.394 + phi_rad: 4.71238898038469 + theta_rad: 1.4870205226991688 + } + gain_value { + gain_db: -15.181 + phi_rad: 4.71238898038469 + theta_rad: 1.487893187325166 + } + gain_value { + gain_db: -14.13 + phi_rad: 4.71238898038469 + theta_rad: 1.488765851951163 + } + gain_value { + gain_db: -13.62 + phi_rad: 4.71238898038469 + theta_rad: 1.4896385165771602 + } + gain_value { + gain_db: -13.744 + phi_rad: 4.71238898038469 + theta_rad: 1.4905111812031575 + } + gain_value { + gain_db: -14.326 + phi_rad: 4.71238898038469 + theta_rad: 1.4913838458291546 + } + gain_value { + gain_db: -14.903 + phi_rad: 4.71238898038469 + theta_rad: 1.4922565104551517 + } + gain_value { + gain_db: -15.16 + phi_rad: 4.71238898038469 + theta_rad: 1.4931291750811488 + } + gain_value { + gain_db: -15.506 + phi_rad: 4.71238898038469 + theta_rad: 1.494001839707146 + } + gain_value { + gain_db: -17.779 + phi_rad: 4.71238898038469 + theta_rad: 1.4948745043331433 + } + gain_value { + gain_db: -19.671 + phi_rad: 4.71238898038469 + theta_rad: 1.4957471689591404 + } + gain_value { + gain_db: -18.567 + phi_rad: 4.71238898038469 + theta_rad: 1.4966198335851375 + } + gain_value { + gain_db: -19.406 + phi_rad: 4.71238898038469 + theta_rad: 1.4974924982111346 + } + gain_value { + gain_db: -20.464 + phi_rad: 4.71238898038469 + theta_rad: 1.4983651628371317 + } + gain_value { + gain_db: -18.447 + phi_rad: 4.71238898038469 + theta_rad: 1.4992378274631293 + } + gain_value { + gain_db: -15.195 + phi_rad: 4.71238898038469 + theta_rad: 1.5001104920891264 + } + gain_value { + gain_db: -12.99 + phi_rad: 4.71238898038469 + theta_rad: 1.5009831567151235 + } + gain_value { + gain_db: -11.453 + phi_rad: 4.71238898038469 + theta_rad: 1.5018558213411206 + } + gain_value { + gain_db: -10.516 + phi_rad: 4.71238898038469 + theta_rad: 1.5027284859671177 + } + gain_value { + gain_db: -9.867 + phi_rad: 4.71238898038469 + theta_rad: 1.503601150593115 + } + gain_value { + gain_db: -9.4462 + phi_rad: 4.71238898038469 + theta_rad: 1.5044738152191122 + } + gain_value { + gain_db: -9.464 + phi_rad: 4.71238898038469 + theta_rad: 1.5053464798451093 + } + gain_value { + gain_db: -9.6794 + phi_rad: 4.71238898038469 + theta_rad: 1.5062191444711064 + } + gain_value { + gain_db: -10.31 + phi_rad: 4.71238898038469 + theta_rad: 1.5070918090971035 + } + gain_value { + gain_db: -11.414 + phi_rad: 4.71238898038469 + theta_rad: 1.5079644737231008 + } + gain_value { + gain_db: -13.356 + phi_rad: 4.71238898038469 + theta_rad: 1.508837138349098 + } + gain_value { + gain_db: -17.015 + phi_rad: 4.71238898038469 + theta_rad: 1.509709802975095 + } + gain_value { + gain_db: -17.695 + phi_rad: 4.71238898038469 + theta_rad: 1.5105824676010922 + } + gain_value { + gain_db: -17.287 + phi_rad: 4.71238898038469 + theta_rad: 1.5114551322270893 + } + gain_value { + gain_db: -17.153 + phi_rad: 4.71238898038469 + theta_rad: 1.5123277968530866 + } + gain_value { + gain_db: -17.063 + phi_rad: 4.71238898038469 + theta_rad: 1.5132004614790837 + } + gain_value { + gain_db: -16.31 + phi_rad: 4.71238898038469 + theta_rad: 1.5140731261050808 + } + gain_value { + gain_db: -15.118 + phi_rad: 4.71238898038469 + theta_rad: 1.514945790731078 + } + gain_value { + gain_db: -13.941 + phi_rad: 4.71238898038469 + theta_rad: 1.515818455357075 + } + gain_value { + gain_db: -12.971 + phi_rad: 4.71238898038469 + theta_rad: 1.5166911199830724 + } + gain_value { + gain_db: -12.74 + phi_rad: 4.71238898038469 + theta_rad: 1.5175637846090695 + } + gain_value { + gain_db: -13.056 + phi_rad: 4.71238898038469 + theta_rad: 1.5184364492350666 + } + gain_value { + gain_db: -14.112 + phi_rad: 4.71238898038469 + theta_rad: 1.519309113861064 + } + gain_value { + gain_db: -16.176 + phi_rad: 4.71238898038469 + theta_rad: 1.520181778487061 + } + gain_value { + gain_db: -19.036 + phi_rad: 4.71238898038469 + theta_rad: 1.5210544431130584 + } + gain_value { + gain_db: -22.422 + phi_rad: 4.71238898038469 + theta_rad: 1.5219271077390555 + } + gain_value { + gain_db: -20.842 + phi_rad: 4.71238898038469 + theta_rad: 1.5227997723650526 + } + gain_value { + gain_db: -18.741 + phi_rad: 4.71238898038469 + theta_rad: 1.5236724369910497 + } + gain_value { + gain_db: -18.051 + phi_rad: 4.71238898038469 + theta_rad: 1.5245451016170468 + } + gain_value { + gain_db: -17.812 + phi_rad: 4.71238898038469 + theta_rad: 1.5254177662430441 + } + gain_value { + gain_db: -17.533 + phi_rad: 4.71238898038469 + theta_rad: 1.5262904308690413 + } + gain_value { + gain_db: -17.799 + phi_rad: 4.71238898038469 + theta_rad: 1.5271630954950384 + } + gain_value { + gain_db: -17.086 + phi_rad: 4.71238898038469 + theta_rad: 1.5280357601210355 + } + gain_value { + gain_db: -15.585 + phi_rad: 4.71238898038469 + theta_rad: 1.5289084247470326 + } + gain_value { + gain_db: -14.427 + phi_rad: 4.71238898038469 + theta_rad: 1.52978108937303 + } + gain_value { + gain_db: -13.819 + phi_rad: 4.71238898038469 + theta_rad: 1.530653753999027 + } + gain_value { + gain_db: -13.711 + phi_rad: 4.71238898038469 + theta_rad: 1.5315264186250241 + } + gain_value { + gain_db: -14.115 + phi_rad: 4.71238898038469 + theta_rad: 1.5323990832510213 + } + gain_value { + gain_db: -14.969 + phi_rad: 4.71238898038469 + theta_rad: 1.5332717478770184 + } + gain_value { + gain_db: -16.891 + phi_rad: 4.71238898038469 + theta_rad: 1.5341444125030157 + } + gain_value { + gain_db: -19.179 + phi_rad: 4.71238898038469 + theta_rad: 1.5350170771290128 + } + gain_value { + gain_db: -21.294 + phi_rad: 4.71238898038469 + theta_rad: 1.53588974175501 + } + gain_value { + gain_db: -21.112 + phi_rad: 4.71238898038469 + theta_rad: 1.536762406381007 + } + gain_value { + gain_db: -20.624 + phi_rad: 4.71238898038469 + theta_rad: 1.5376350710070041 + } + gain_value { + gain_db: -18.24 + phi_rad: 4.71238898038469 + theta_rad: 1.5385077356330017 + } + gain_value { + gain_db: -15.781 + phi_rad: 4.71238898038469 + theta_rad: 1.5393804002589988 + } + gain_value { + gain_db: -14.793 + phi_rad: 4.71238898038469 + theta_rad: 1.540253064884996 + } + gain_value { + gain_db: -14.055 + phi_rad: 4.71238898038469 + theta_rad: 1.541125729510993 + } + gain_value { + gain_db: -13.533 + phi_rad: 4.71238898038469 + theta_rad: 1.5419983941369901 + } + gain_value { + gain_db: -13.693 + phi_rad: 4.71238898038469 + theta_rad: 1.5428710587629875 + } + gain_value { + gain_db: -14.74 + phi_rad: 4.71238898038469 + theta_rad: 1.5437437233889846 + } + gain_value { + gain_db: -16.173 + phi_rad: 4.71238898038469 + theta_rad: 1.5446163880149817 + } + gain_value { + gain_db: -19.738 + phi_rad: 4.71238898038469 + theta_rad: 1.5454890526409788 + } + gain_value { + gain_db: -19.599 + phi_rad: 4.71238898038469 + theta_rad: 1.546361717266976 + } + gain_value { + gain_db: -20.766 + phi_rad: 4.71238898038469 + theta_rad: 1.5472343818929732 + } + gain_value { + gain_db: -20.318 + phi_rad: 4.71238898038469 + theta_rad: 1.5481070465189704 + } + gain_value { + gain_db: -19.884 + phi_rad: 4.71238898038469 + theta_rad: 1.5489797111449675 + } + gain_value { + gain_db: -21.468 + phi_rad: 4.71238898038469 + theta_rad: 1.5498523757709646 + } + gain_value { + gain_db: -23.314 + phi_rad: 4.71238898038469 + theta_rad: 1.5507250403969617 + } + gain_value { + gain_db: -20.346 + phi_rad: 4.71238898038469 + theta_rad: 1.551597705022959 + } + gain_value { + gain_db: -20.036 + phi_rad: 4.71238898038469 + theta_rad: 1.5524703696489561 + } + gain_value { + gain_db: -15.374 + phi_rad: 4.71238898038469 + theta_rad: 1.5533430342749532 + } + gain_value { + gain_db: -12.522 + phi_rad: 4.71238898038469 + theta_rad: 1.5542156989009503 + } + gain_value { + gain_db: -11.16 + phi_rad: 4.71238898038469 + theta_rad: 1.5550883635269475 + } + gain_value { + gain_db: -10.688 + phi_rad: 4.71238898038469 + theta_rad: 1.5559610281529448 + } + gain_value { + gain_db: -10.622 + phi_rad: 4.71238898038469 + theta_rad: 1.556833692778942 + } + gain_value { + gain_db: -11.033 + phi_rad: 4.71238898038469 + theta_rad: 1.557706357404939 + } + gain_value { + gain_db: -12.064 + phi_rad: 4.71238898038469 + theta_rad: 1.5585790220309363 + } + gain_value { + gain_db: -13.277 + phi_rad: 4.71238898038469 + theta_rad: 1.5594516866569335 + } + gain_value { + gain_db: -15.06 + phi_rad: 4.71238898038469 + theta_rad: 1.5603243512829308 + } + gain_value { + gain_db: -17.771 + phi_rad: 4.71238898038469 + theta_rad: 1.561197015908928 + } + gain_value { + gain_db: -20.484 + phi_rad: 4.71238898038469 + theta_rad: 1.562069680534925 + } + gain_value { + gain_db: -23.482 + phi_rad: 4.71238898038469 + theta_rad: 1.5629423451609221 + } + gain_value { + gain_db: -24.178 + phi_rad: 4.71238898038469 + theta_rad: 1.5638150097869192 + } + gain_value { + gain_db: -19.458 + phi_rad: 4.71238898038469 + theta_rad: 1.5646876744129166 + } + gain_value { + gain_db: -18.489 + phi_rad: 4.71238898038469 + theta_rad: 1.5655603390389137 + } + gain_value { + gain_db: -18.008 + phi_rad: 4.71238898038469 + theta_rad: 1.5664330036649108 + } + gain_value { + gain_db: -16.321 + phi_rad: 4.71238898038469 + theta_rad: 1.567305668290908 + } + gain_value { + gain_db: -15.026 + phi_rad: 4.71238898038469 + theta_rad: 1.568178332916905 + } + gain_value { + gain_db: -14.706 + phi_rad: 4.71238898038469 + theta_rad: 1.5690509975429023 + } + gain_value { + gain_db: -14.95 + phi_rad: 4.71238898038469 + theta_rad: 1.5699236621688994 + } + gain_value { + gain_db: -15.418 + phi_rad: 4.71238898038469 + theta_rad: 1.5707963267948966 + } + } + } + near_field_range_m: 2 + } +} diff --git a/entity_samples/build_a_scenario_tutorial/BUILD b/entity_samples/build_a_scenario_tutorial/BUILD new file mode 100644 index 0000000..86f133c --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/BUILD @@ -0,0 +1,20 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "build_a_scenario_tutorial", + srcs = glob(["*.textproto"]), +) diff --git a/entity_samples/build_a_scenario_tutorial/README.md b/entity_samples/build_a_scenario_tutorial/README.md new file mode 100644 index 0000000..fa767e2 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/README.md @@ -0,0 +1,22 @@ +# Sample Entities for Building a Scenario Tutorial + +This directory contains the Spacetime entity definitions used in the [Building a Scenario](https://docs.spacetime.aalyria.com/scenario-building) tutorial. + +## Entities included + +* Antenna Profile + - `antenna_pattern.textproto` +* Band Profile + - `band_profile.textproto` +* Platform Definition + - `satellite_platform_definition.textproto` + - `user_terminal_platform_definition.textproto` +* Network Node + - `gateway_network_node.textproto` + - `satellite_network_node.textproto` + - `user_termina_network_node.textproto` +* Wired Link + - `terrestrial_link_forward.textproto` + - `terrestrial_link_backward.textproto` +* Service Request + - `service_request.textproto` \ No newline at end of file diff --git a/entity_samples/build_a_scenario_tutorial/antenna_pattern.textproto b/entity_samples/build_a_scenario_tutorial/antenna_pattern.textproto new file mode 100644 index 0000000..4331460 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/antenna_pattern.textproto @@ -0,0 +1,27 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-antenna-pattern" + group: { + type: ANTENNA_PATTERN + } + antenna_pattern { + parabolic_pattern: { + diameter_m: 1 + efficiency_percent: 0.9 + backlobe_gain_db: -60 + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/band_profile.textproto b/entity_samples/build_a_scenario_tutorial/band_profile.textproto new file mode 100644 index 0000000..ad5c759 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/band_profile.textproto @@ -0,0 +1,37 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-band-profile" + group: { + type: BAND_PROFILE + } + band_profile { + channel_width_hz: 250000000 + rate_table: { + received_signal_power_steps: { + min_received_signal_power_dbw: -100 + tx_data_rate_bps: 1e8 + } + received_signal_power_steps: { + min_received_signal_power_dbw: -90 + tx_data_rate_bps: 2e8 + } + received_signal_power_steps: { + min_received_signal_power_dbw: -80 + tx_data_rate_bps: 3e8 + } + } + } +} \ No newline at end of file diff --git a/entity_samples/build_a_scenario_tutorial/gateway_network_node.textproto b/entity_samples/build_a_scenario_tutorial/gateway_network_node.textproto new file mode 100644 index 0000000..cab2e38 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/gateway_network_node.textproto @@ -0,0 +1,38 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-gateway-network-node" + group: { + type: NETWORK_NODE + } + network_node { + name: "gateway" + type: "Gateway" + subnet: "211.154.172.215/32" + node_interface: { + interface_id: "wireless" + wireless: { + transceiver_model_id: { + platform_id: "test-gateway-platform-definition" + transceiver_model_id: "transceiver-model" + } + } + } + node_interface: { + interface_id: "wan" + wired: {} + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/gateway_platform_definition.textproto b/entity_samples/build_a_scenario_tutorial/gateway_platform_definition.textproto new file mode 100644 index 0000000..bb36c90 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/gateway_platform_definition.textproto @@ -0,0 +1,89 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-gateway-platform-definition" + group: { + type: PLATFORM_DEFINITION + } + platform { + name: "gateway" + type: "Gateway" + coordinates: { + geodetic_wgs84: { + longitude_deg: -121.1 + latitude_deg: 35.4 + } + } + + transceiver_model: { + id: "transceiver-model" + + transmitter: { + name: "tx" + channel_set: { + key: "test-band-profile" + value: { + channel: { + key: 13000000000 + value: { + max_power_watts: 100 + } + } + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + receiver: { + name: "rx" + channel_set: { + key: "test-band-profile" + value: { + center_frequency_hz: 14000000000 + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + antenna: { + name: "antenna" + antenna_pattern_id: "test-antenna-pattern" + targeting: {} + } + + macs { + type: "DVBS2" + role: "HUB" + max_connections: 1 + } + } + } +} \ No newline at end of file diff --git a/entity_samples/build_a_scenario_tutorial/point_of_presence_network_node.textproto b/entity_samples/build_a_scenario_tutorial/point_of_presence_network_node.textproto new file mode 100644 index 0000000..484c8f4 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/point_of_presence_network_node.textproto @@ -0,0 +1,30 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-pop-network-node" + group: { + type: NETWORK_NODE + } + network_node { + name: "pop" + type: "POP" + category_tag: "PoP" + subnet: "121.201.204.104/32" + node_interface: { + interface_id: "wan" + wired: {} + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/satellite_network_node.textproto b/entity_samples/build_a_scenario_tutorial/satellite_network_node.textproto new file mode 100644 index 0000000..812005b --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/satellite_network_node.textproto @@ -0,0 +1,43 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-satellite-network-node" + group: { + type: NETWORK_NODE + } + network_node { + name: "sat" + type: "GEO" + subnet: "191.165.104.62/32" + node_interface: { + interface_id: "user-link" + wireless: { + transceiver_model_id: { + platform_id: "test-satellite-platform-definition" + transceiver_model_id: "user-link-transceiver-model" + } + } + } + node_interface: { + interface_id: "gateway-link" + wireless: { + transceiver_model_id: { + platform_id: "test-satellite-platform-definition" + transceiver_model_id: "gateway-link-transceiver-model" + } + } + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/satellite_platform_definition.textproto b/entity_samples/build_a_scenario_tutorial/satellite_platform_definition.textproto new file mode 100644 index 0000000..5b702d9 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/satellite_platform_definition.textproto @@ -0,0 +1,149 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-satellite-platform-definition" + group: { + type: PLATFORM_DEFINITION + } + platform { + name: "sat" + type: "GEO" + coordinates: { + geodetic_wgs84: { + longitude_deg: -121.1 + latitude_deg: 37.4 + height_wgs84_m: 36000000 + } + } + + transceiver_model: { + id: "user-link-transceiver-model" + + transmitter: { + name: "tx" + channel_set: { + key: "test-band-profile" + value: { + channel: { + key: 12000000000 + value: { + max_power_watts: 100 + } + } + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + receiver: { + name: "rx" + channel_set: { + key: "test-band-profile" + value: { + center_frequency_hz: 11000000000 + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + antenna: { + name: "antenna" + antenna_pattern_id: "test-antenna-pattern" + targeting: {} + } + + macs { + type: "DVBS2" + role: "REMOTE" + max_connections: 1 + } + } + + transceiver_model: { + id: "gateway-link-transceiver-model" + + transmitter: { + name: "tx" + channel_set: { + key: "test-band-profile" + value: { + channel: { + key: 14000000000 + value: { + max_power_watts: 100 + } + } + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + receiver: { + name: "rx" + channel_set: { + key: "test-band-profile" + value: { + center_frequency_hz: 13000000000 + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + antenna: { + name: "antenna" + antenna_pattern_id: "test-antenna-pattern" + targeting: {} + } + + macs { + type: "DVBS2" + role: "REMOTE" + max_connections: 1 + } + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/service_request.textproto b/entity_samples/build_a_scenario_tutorial/service_request.textproto new file mode 100644 index 0000000..8d4ddf4 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/service_request.textproto @@ -0,0 +1,27 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-pop-to-user-terminal-service-request" + group: { + type: SERVICE_REQUEST + } + service_request { + src_node_id: "test-pop-network-node" + dst_node_id: "test-user-terminal-network-node" + requirements: { + bandwidth_bps_requested: 1e+06 + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/terrestrial_link_backward.textproto b/entity_samples/build_a_scenario_tutorial/terrestrial_link_backward.textproto new file mode 100644 index 0000000..3eb4eee --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/terrestrial_link_backward.textproto @@ -0,0 +1,34 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-gateway-to-pop-interface-link-report" + group: { + type: INTERFACE_LINK_REPORT + } + interface_link_report: { + src: { + node_id: "test-gateway-network-node" + interface_id: "wan" + } + dst: { + node_id: "test-pop-network-node" + interface_id: "wan" + } + access_intervals: { + accessibility: ACCESS_EXISTS + data_rate_bps: 1e+12 + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/terrestrial_link_forward.textproto b/entity_samples/build_a_scenario_tutorial/terrestrial_link_forward.textproto new file mode 100644 index 0000000..c5f6a53 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/terrestrial_link_forward.textproto @@ -0,0 +1,34 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-pop-to-gateway-interface-link-report" + group: { + type: INTERFACE_LINK_REPORT + } + interface_link_report: { + src: { + node_id: "test-pop-network-node" + interface_id: "wan" + } + dst: { + node_id: "test-gateway-network-node" + interface_id: "wan" + } + access_intervals: { + accessibility: ACCESS_EXISTS + data_rate_bps: 1e+12 + } + } +} diff --git a/entity_samples/build_a_scenario_tutorial/user_terminal_network_node.textproto b/entity_samples/build_a_scenario_tutorial/user_terminal_network_node.textproto new file mode 100644 index 0000000..096304a --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/user_terminal_network_node.textproto @@ -0,0 +1,36 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-user-terminal-network-node" + group: { + type: NETWORK_NODE + } + network_node { + name: "user-terminal" + type: "UserTerminal" + category_tag: "User Terminal" + subnet: "107.89.175.219/32" + node_interface: { + interface_id: "wireless" + wireless: { + transceiver_model_id: { + platform_id: "test-user-terminal-platform-definition" + transceiver_model_id: "transceiver-model" + } + } + } + subnet: "fd00:0:0:2a:0:0:0:0/64" + } +} diff --git a/entity_samples/build_a_scenario_tutorial/user_terminal_platform_definition.textproto b/entity_samples/build_a_scenario_tutorial/user_terminal_platform_definition.textproto new file mode 100644 index 0000000..790c9e3 --- /dev/null +++ b/entity_samples/build_a_scenario_tutorial/user_terminal_platform_definition.textproto @@ -0,0 +1,90 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +entity { + id: "test-user-terminal-platform-definition" + group: { + type: PLATFORM_DEFINITION + } + platform { + name: "user-terminal" + type: "UserTerminal" + category_tag: "User Terminal" + coordinates: { + geodetic_wgs84: { + longitude_deg: -121.7 + latitude_deg: 37.7 + } + } + + transceiver_model: { + id: "transceiver-model" + + transmitter: { + name: "tx" + channel_set: { + key: "test-band-profile" + value: { + channel: { + key: 1000000000 + value: { + max_power_watts: 100 + } + } + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + receiver: { + name: "rx" + channel_set: { + key: "test-band-profile" + value: { + center_frequency_hz: 12000000000 + } + } + signal_processing_step: { + amplifier: { + constant_gain: { + gain_db: 10 + noise_factor: 1 + reference_temperature_k: 290 + } + } + } + } + + antenna: { + name: "antenna" + antenna_pattern_id: "test-antenna-pattern" + targeting: {} + } + + macs { + type: "DVBS2" + role: "HUB" + max_connections: 1 + } + } + } +} diff --git a/gen_tagname b/gen_tagname new file mode 100755 index 0000000..2d1ab8a --- /dev/null +++ b/gen_tagname @@ -0,0 +1,39 @@ +#!/bin/bash + +# Copyright (c) Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +# Check no uncommitted changes. +if [ -n "$(git status --porcelain)" ]; then + echo "Error: uncommitted changes in the working tree" + exit 1 +fi + +# Check on release-* branch and extract SEMVER. +current_branch=$(git branch --show-current) +case "${current_branch}" in + release-*) + readonly SEMVER=${current_branch//release-/} + ;; + *) + echo "Error: '${current_branch}' is not a release branch" >&2 + exit 1 +esac + +# Get extra pre-release metadata. +PRE_RELEASE=$(git show -s --format="%ct-%cs-SHA-%h" HEAD) + +echo "${SEMVER}-${PRE_RELEASE}" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aa59b6f --- /dev/null +++ b/go.mod @@ -0,0 +1,53 @@ +module aalyria.com/spacetime + +go 1.23.0 + +require ( + github.com/fullstorydev/grpcurl v1.8.7 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.6.0 + github.com/jhump/protoreflect v1.15.2 + github.com/jonboulle/clockwork v0.4.0 + github.com/prometheus/client_model v0.6.1 + github.com/prometheus/prom2json v1.3.3 + github.com/rs/zerolog v1.33.0 + github.com/urfave/cli/v2 v2.25.7 + github.com/vishvananda/netlink v1.3.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 + golang.org/x/sync v0.8.0 + golang.org/x/sys v0.25.0 + google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed + google.golang.org/grpc v1.66.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + github.com/bufbuild/protocompile v0.6.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/prometheus v0.51.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1b83092 --- /dev/null +++ b/go.sum @@ -0,0 +1,424 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= +github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fullstorydev/grpcurl v1.8.7 h1:xJWosq3BQovQ4QrdPO72OrPiWuGgEsxY8ldYsJbPrqI= +github.com/fullstorydev/grpcurl v1.8.7/go.mod h1:pVtM4qe3CMoLaIzYS8uvTuDj2jVYmXqMUkZeijnXp/E= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= +github.com/jhump/protoreflect v1.15.2 h1:7YppbATX94jEt9KLAc5hICx4h6Yt3SaavhQRsIUEHP0= +github.com/jhump/protoreflect v1.15.2/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= +github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= +github.com/prometheus/prometheus v0.51.2 h1:U0faf1nT4CB9DkBW87XLJCBi2s8nwWXdTbyzRUAkX0w= +github.com/prometheus/prometheus v0.51.2/go.mod h1:yv4MwOn3yHMQ6MZGHPg/U7Fcyqf+rxqiZfSur6myVtc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/java/com/aalyria/spacetime/authentication/BUILD b/java/com/aalyria/spacetime/authentication/BUILD new file mode 100644 index 0000000..5208818 --- /dev/null +++ b/java/com/aalyria/spacetime/authentication/BUILD @@ -0,0 +1,37 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("//bazel/java_format_rules:def.bzl", "java_format_test") +load("//bazel/java_rules:def.bzl", "java_library") + +package(default_visibility = ["//visibility:public"]) + +java_format_test( + name = "java_format_test", + srcs = glob(["*.java"]), +) + +java_library( + name = "Authentication", + srcs = [ + "JwtManager.java", + "SpacetimeCallCredentials.java", + ], + deps = [ + "@maven//:com_google_code_gson_gson", + "@maven//:com_google_guava_guava", + "@maven//:io_grpc_grpc_api", + "@maven//:org_bouncycastle_bcprov_jdk15on", + ], +) diff --git a/java/com/aalyria/spacetime/authentication/JwtManager.java b/java/com/aalyria/spacetime/authentication/JwtManager.java new file mode 100644 index 0000000..afb9d2c --- /dev/null +++ b/java/com/aalyria/spacetime/authentication/JwtManager.java @@ -0,0 +1,259 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aalyria.spacetime.authentication; + +import com.google.gson.Gson; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; + +/** This class handles creating the JWTs that are required to authenticate to Spacetime. */ +public class JwtManager { + private final Duration lifetime; + private long issueTimeEpochSeconds; + private long expirationTimeEpochSeconds; + private final String audience; + private final String issuer; + private final String subject; + private final String targetAudience; + private final String privateKeyId; + // The RSA private key in either PKCS8 or PKCS1, ASN.1 DER form + // (PEM-encoded) used to sign the JWT. + private final String privateKey; + private String jwtValue = ""; + + private static final String PRIVATE_KEY_HEADER = "-----BEGIN RSA PRIVATE KEY-----"; + private static final String PRIVATE_KEY_FOOTER = "-----END RSA PRIVATE KEY-----"; + private static final Gson gson = new Gson(); + private static final Clock clock = Clock.system(ZoneId.systemDefault()); + + public static class Builder { + private Duration lifetime = Duration.ofHours(1); + private String audience = ""; + private String issuer = ""; + private String subject = ""; + private String targetAudience = ""; + private String privateKeyId = ""; + private String privateKey = ""; + + public Builder setIssuer(String issuer) { + this.issuer = Objects.requireNonNull(issuer); + return this; + } + + public Builder setSubject(String subject) { + this.subject = Objects.requireNonNull(subject); + return this; + } + + public Builder setAudience(String audience) { + this.audience = Objects.requireNonNull(audience); + return this; + } + + public Builder setTargetAudience(String targetAudience) { + this.targetAudience = Objects.requireNonNull(targetAudience); + return this; + } + + public Builder setPrivateKeyId(String privateKeyId) { + this.privateKeyId = Objects.requireNonNull(privateKeyId); + return this; + } + + public Builder setPrivateKey(String privateKey) { + this.privateKey = Objects.requireNonNull(privateKey); + return this; + } + + public Builder setLifetime(Duration lifetime) { + this.lifetime = Objects.requireNonNull(lifetime); + return this; + } + + public JwtManager build() { + return new JwtManager( + lifetime, audience, issuer, subject, targetAudience, privateKeyId, privateKey); + } + } + + private JwtManager( + Duration lifetime, + String audience, + String issuer, + String subject, + String targetAudience, + String privateKeyId, + String privateKey) { + this.lifetime = lifetime; + this.audience = audience; + this.issuer = issuer; + this.subject = !subject.isEmpty() ? subject : issuer; + this.targetAudience = targetAudience; + this.privateKeyId = privateKeyId; + this.privateKey = privateKey; + this.jwtValue = generateJwt(); + } + + // If the user passes in a JWT, this string will be used as the token + // and will not be refreshed. + public JwtManager(String jwtValue) { + lifetime = Duration.ofHours(1); + expirationTimeEpochSeconds = Long.MAX_VALUE; + audience = ""; + issuer = ""; + subject = ""; + targetAudience = ""; + privateKeyId = ""; + privateKey = ""; + this.jwtValue = jwtValue; + } + + private Optional parsePrivateKey(byte[] encodedKey, KeyFactory keyFactory) { + try { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey); + return Optional.of(keyFactory.generatePrivate(keySpec)); + } catch (InvalidKeySpecException e) { + return Optional.empty(); + } + } + + private PrivateKey decodePrivateKey() { + String encodedKeyValue = + privateKey + .replace(PRIVATE_KEY_HEADER, /* replacement= */ "") + .replaceAll(System.lineSeparator(), /* replacement= */ "") + .replace(PRIVATE_KEY_FOOTER, /* replacement= */ ""); + byte[] encodedKey = Base64.getDecoder().decode(encodedKeyValue); + + Optional privateKey; + try { + // Assumes the key is in PKCS8 format. + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + privateKey = parsePrivateKey(encodedKey, keyFactory); + if (privateKey.isEmpty()) { + // If the key could not be parsed in PKCS8 format, it should be in PKCS1 + // format. + AlgorithmIdentifier algorithmIdentifier = + new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE); + PrivateKeyInfo privateKeyInfo = + new PrivateKeyInfo(algorithmIdentifier, ASN1Sequence.getInstance(encodedKey)); + privateKey = parsePrivateKey(privateKeyInfo.getEncoded(), keyFactory); + } + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Algorithm for KeyFactory not found.", e); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid private key.", e); + } + return privateKey.orElseThrow( + () -> new IllegalArgumentException("The private key could not be decoded.")); + } + + private byte[] generateHeader() { + Map header = + Map.of( + "alg", "RS256", + "typ", "JWT", + "kid", privateKeyId); + return Base64.getEncoder().encode(gson.toJson(header).getBytes()); + } + + private byte[] generatePayload() { + Instant now = clock.instant(); + issueTimeEpochSeconds = now.getEpochSecond(); + expirationTimeEpochSeconds = now.plusSeconds(lifetime.getSeconds()).getEpochSecond(); + Map payload = + new HashMap<>( + Map.of( + "aud", audience, + "exp", String.valueOf(expirationTimeEpochSeconds), + "iat", String.valueOf(issueTimeEpochSeconds), + "iss", issuer, + "sub", subject)); + if (!targetAudience.isEmpty()) { + payload.put("target_audience", targetAudience); + } + return Base64.getEncoder().encode(gson.toJson(payload).getBytes()); + } + + private byte[] generateSignature(byte[] header, byte[] payload, PrivateKey privateKey) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(header); + outputStream.write((new String(".").getBytes())); + outputStream.write(payload); + + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(privateKey); + signature.update(outputStream.toByteArray()); + return Base64.getEncoder().encode(signature.sign()); + } catch (IOException e) { + throw new RuntimeException("Signature could not be written.", e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Signature algorithm was invalid.", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("Private key used in signature was invalid.", e); + } catch (SignatureException e) { + throw new RuntimeException("Signature could not be created.", e); + } + } + + public String generateJwt() { + PrivateKey privateKey = decodePrivateKey(); + byte[] header = generateHeader(); + byte[] payload = generatePayload(); + byte[] signature = generateSignature(header, payload, privateKey); + + ByteArrayOutputStream jwtOutputStream = new ByteArrayOutputStream(); + byte[] separator = new String(".").getBytes(); + try { + jwtOutputStream.write(header); + jwtOutputStream.write(separator); + jwtOutputStream.write(payload); + jwtOutputStream.write(separator); + jwtOutputStream.write(signature); + } catch (IOException e) { + throw new RuntimeException("Error writing JWT.", e); + } + jwtValue = jwtOutputStream.toString(); + return jwtValue; + } + + // Returns the existing JWT. + public String getJwt() { + return jwtValue; + } +} diff --git a/java/com/aalyria/spacetime/authentication/SpacetimeCallCredentials.java b/java/com/aalyria/spacetime/authentication/SpacetimeCallCredentials.java new file mode 100644 index 0000000..6b47f16 --- /dev/null +++ b/java/com/aalyria/spacetime/authentication/SpacetimeCallCredentials.java @@ -0,0 +1,278 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aalyria.spacetime.authentication; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.grpc.CallCredentials; +import io.grpc.CallCredentials.MetadataApplier; +import io.grpc.CallCredentials.RequestInfo; +import io.grpc.Metadata; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.net.URLEncoder; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * A class that supplies per-RPC credentials, which are based on two signed JWTs, one for + * authenticating to the Spacetime backend and one for authenticating through the secure proxy. + */ +public class SpacetimeCallCredentials extends CallCredentials { + // A manager for the JWT used to authenticate to the Spacetime backend. + private final JwtManager spacetimeAuthJwtManager; + // A manager for the JWT used to authenticate through the secure proxy. + private final JwtManager proxyAuthJwtManager; + // An OpenID Connect token that the proxyAuthJwt's value is exchanged to receive. + private volatile String oidcToken = ""; + private long oidcTokenExpirationTimeEpochSeconds; + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Clock clock; + // Spacetime-specific parameters. + private final String gcpOidcTokenCreationUrl; + private final Duration oidcTokenLifetime; + + // OIDC tokens are provided by Google's OAuth endpoint by default. + private static final String DEFAULT_GCP_OIDC_TOKEN_CREATION_URL = + "https://www.googleapis.com/oauth2/v4/token"; + // The OIDC tokens are valid for 1 hour by default. + private static final Duration DEFAULT_OIDC_TOKEN_LIFETIME_SECONDS = Duration.ofHours(1); + private static final String PROXY_TARGET_AUDIENCE = + "60292403139-me68tjgajl5dcdbpnlm2ek830lvsnslq.apps.googleusercontent.com"; + // If the OIDC token will expire within this margin, it will be re-created. + private static final Duration OIDC_TOKEN_EXPIRATION_TIME_MARGIN = Duration.ofMinutes(5); + + private static final Gson gson = new Gson(); + + public static SpacetimeCallCredentials createFromPrivateKey( + String host, String agentEmail, String privateKeyId, String privateKey) { + JwtManager spacetimeAuthJwtManager = + new JwtManager.Builder() + .setIssuer(agentEmail) + .setSubject(agentEmail) + .setAudience(host) + .setPrivateKeyId(privateKeyId) + .setPrivateKey(privateKey) + .build(); + JwtManager proxyAuthJwtManager = + new JwtManager.Builder() + .setIssuer(agentEmail) + .setSubject(agentEmail) + .setAudience(DEFAULT_GCP_OIDC_TOKEN_CREATION_URL) + .setTargetAudience(PROXY_TARGET_AUDIENCE) + .setPrivateKeyId(privateKeyId) + .setPrivateKey(privateKey) + .build(); + return new SpacetimeCallCredentials( + spacetimeAuthJwtManager, + proxyAuthJwtManager, + DEFAULT_GCP_OIDC_TOKEN_CREATION_URL, + DEFAULT_OIDC_TOKEN_LIFETIME_SECONDS, + Clock.system(ZoneId.systemDefault())); + } + + @VisibleForTesting + protected static SpacetimeCallCredentials createFromPrivateKey( + String host, + String agentEmail, + String privateKeyId, + String privateKey, + String testGcpOidcTokenCreationUrl, + Duration testOidcTokenLifetime, + Clock clock) { + JwtManager spacetimeAuthJwtManager = + new JwtManager.Builder() + .setIssuer(agentEmail) + .setSubject(agentEmail) + .setAudience(host) + .setPrivateKeyId(privateKeyId) + .setPrivateKey(privateKey) + .build(); + JwtManager proxyAuthJwtManager = + new JwtManager.Builder() + .setIssuer(agentEmail) + .setSubject(agentEmail) + .setAudience(testGcpOidcTokenCreationUrl) + .setTargetAudience(PROXY_TARGET_AUDIENCE) + .setPrivateKeyId(privateKeyId) + .setPrivateKey(privateKey) + .build(); + return new SpacetimeCallCredentials( + spacetimeAuthJwtManager, + proxyAuthJwtManager, + testGcpOidcTokenCreationUrl, + testOidcTokenLifetime, + clock); + } + + public static SpacetimeCallCredentials createFromJwt( + String spacetimeAuthJwt, String proxyAuthJwt) { + JwtManager spacetimeAuthJwtManager = new JwtManager(spacetimeAuthJwt); + JwtManager proxyAuthJwtManager = new JwtManager(proxyAuthJwt); + return new SpacetimeCallCredentials( + spacetimeAuthJwtManager, + proxyAuthJwtManager, + DEFAULT_GCP_OIDC_TOKEN_CREATION_URL, + DEFAULT_OIDC_TOKEN_LIFETIME_SECONDS, + Clock.system(ZoneId.systemDefault())); + } + + private SpacetimeCallCredentials( + JwtManager spacetimeAuthJwtManager, + JwtManager proxyAuthJwtManager, + String gcpOidcTokenCreationUrl, + Duration oidcTokenLifetime, + Clock clock) { + this.spacetimeAuthJwtManager = spacetimeAuthJwtManager; + this.proxyAuthJwtManager = proxyAuthJwtManager; + this.gcpOidcTokenCreationUrl = gcpOidcTokenCreationUrl; + this.oidcTokenLifetime = oidcTokenLifetime; + this.clock = clock; + } + + // This method does not block as requested by the gRPC documentation, + // but the RPC will not proceed until the .apply method is called in the + // async execution. + @Override + public void applyRequestMetadata( + RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) { + CompletableFuture.supplyAsync( + () -> { + // If the OIDC token is expired, or within OIDC_TOKEN_EXPIRATION_TIME_MARGIN + // of its expiration time, then the proxyAuthJwt should be regenerated. + boolean isOidcTokenExpired = false; + try { + readWriteLock.readLock().lock(); + isOidcTokenExpired = isOidcTokenExpired(clock.instant()); + } finally { + readWriteLock.readLock().unlock(); + } + if (isOidcTokenExpired) { + try { + readWriteLock.writeLock().lock(); + // Check the validity of the OIDC token again, since another thread may have + // already refreshed it. + Instant now = clock.instant(); + if (isOidcTokenExpired(now)) { + String proxyAuthJwt = proxyAuthJwtManager.generateJwt(); + oidcTokenExpirationTimeEpochSeconds = + now.plusSeconds(oidcTokenLifetime.getSeconds()).getEpochSecond(); + oidcToken = exchangeProxyAuthJwtForOidcToken(proxyAuthJwt); + } + } finally { + readWriteLock.writeLock().unlock(); + } + } + return oidcToken; + }, + executor) + .thenAcceptAsync( + (String oidcToken) -> { + Metadata headers = new Metadata(); + Metadata.Key authorizationHeaderKey = + Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + headers.put( + authorizationHeaderKey, "Bearer " + spacetimeAuthJwtManager.generateJwt()); + Metadata.Key proxyAuthorizationKey = + Metadata.Key.of("Proxy-Authorization", Metadata.ASCII_STRING_MARSHALLER); + headers.put(proxyAuthorizationKey, "Bearer " + oidcToken); + metadataApplier.apply(headers); + }, + executor); + } + + @Override + public void thisUsesUnstableApi() {} + + // Takes a map of key, value pairs representing URL parameters and + // encodes them so they can be sent in the URL of an HTTP request. + private String encodeUrlParams(Map urlParams) { + StringBuilder encodedUrlParams = new StringBuilder(); + for (Map.Entry urlParam : urlParams.entrySet()) { + if (encodedUrlParams.length() != 0) { + encodedUrlParams.append("&"); + } + encodedUrlParams.append(URLEncoder.encode(urlParam.getKey(), UTF_8)); + encodedUrlParams.append("="); + encodedUrlParams.append(URLEncoder.encode(urlParam.getValue(), UTF_8)); + } + return encodedUrlParams.toString(); + } + + // Exchanges the proxyAuthJwt for an OpenID Connect token. + private String exchangeProxyAuthJwtForOidcToken(String proxyAuthJwtValue) { + // Constructs the URL parameters to fetch an OpenID Connect token. + Map urlParams = + Map.of( + "grant_type", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion", + proxyAuthJwtValue); + byte[] postData = {}; + String encodedUrlParams = encodeUrlParams(urlParams); + postData = encodedUrlParams.getBytes(UTF_8); + + String requestUrl = gcpOidcTokenCreationUrl; + StringBuilder response = new StringBuilder(); + try { + // Creates the request. + URL url = new URL(requestUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setRequestProperty("Content-Length", Integer.toString(postData.length)); + connection.getOutputStream().write(postData); + + // Reads the response, which contains the OpenID Connect token. + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String decodedString; + while ((decodedString = in.readLine()) != null) { + response.append(decodedString); + } + in.close(); + } catch (MalformedURLException e) { + throw new RuntimeException("Error creating a URL from " + requestUrl, e); + } catch (ProtocolException e) { + throw new RuntimeException("Error setting the POST method", e); + } catch (IOException e) { + throw new RuntimeException("Error reading or writing from the connection's stream", e); + } + + // Parses the OpenID Connect token. + TypeToken> mapType = new TypeToken>() {}; + return gson.fromJson(response.toString(), mapType).get("id_token"); + } + + private boolean isOidcTokenExpired(Instant now) { + return oidcToken.isEmpty() + || now.plusSeconds(OIDC_TOKEN_EXPIRATION_TIME_MARGIN.getSeconds()).getEpochSecond() + > oidcTokenExpirationTimeEpochSeconds; + } +} diff --git a/java/com/aalyria/spacetime/codesamples/nbi/client/BUILD b/java/com/aalyria/spacetime/codesamples/nbi/client/BUILD new file mode 100644 index 0000000..df2f1b9 --- /dev/null +++ b/java/com/aalyria/spacetime/codesamples/nbi/client/BUILD @@ -0,0 +1,33 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("//bazel/java_format_rules:def.bzl", "java_format_test") +load("//bazel/java_rules:def.bzl", "java_binary") + +java_format_test( + name = "java_format_test", + srcs = glob(["*.java"]), +) + +java_binary( + name = "ListEntities", + srcs = [ + "ListEntities.java", + ], + deps = [ + "//api/nbi/v1alpha:nbi_java_grpc", + "//java/com/aalyria/spacetime/authentication:Authentication", + "@maven//:com_google_code_gson_gson", + ], +) diff --git a/java/com/aalyria/spacetime/codesamples/nbi/client/ListEntities.java b/java/com/aalyria/spacetime/codesamples/nbi/client/ListEntities.java new file mode 100644 index 0000000..c52af40 --- /dev/null +++ b/java/com/aalyria/spacetime/codesamples/nbi/client/ListEntities.java @@ -0,0 +1,81 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aalyria.spacetime.codesamples.nbi.client; + +import static io.grpc.Grpc.newChannelBuilderForAddress; + +import com.aalyria.spacetime.api.nbi.v1alpha.Nbi.EntityType; +import com.aalyria.spacetime.api.nbi.v1alpha.Nbi.ListEntitiesRequest; +import com.aalyria.spacetime.api.nbi.v1alpha.Nbi.ListEntitiesResponse; +import com.aalyria.spacetime.api.nbi.v1alpha.NetOpsGrpc; +import com.aalyria.spacetime.authentication.SpacetimeCallCredentials; +import io.grpc.CompositeChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.TlsChannelCredentials; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * A sample class that sets up the authentication parameters to call Spacetime's Northbound + * Interface (NBI). + */ +public class ListEntities { + private static final int PORT = 443; + + public static void main(String[] args) { + if (args.length < 4) { + System.err.println( + "Error parsing arguments. Provide a value for host, " + + "agent email, agent private key ID, and agent private key file."); + System.exit(-1); + } + final String HOST = args[0]; + final String AGENT_EMAIL = args[1]; + final String AGENT_PRIV_KEY_ID = args[2]; + final String AGENT_PRIV_KEY_FILE = args[3]; + + // The private key should start with "-----BEGIN RSA PRIVATE KEY-----" and + // end with "-----END RSA PRIVATE KEY-----". In between, there should be newline-delimtted + // strings of characters. + String privateKey; + try { + privateKey = new String(Files.readAllBytes(Paths.get(AGENT_PRIV_KEY_FILE))); + } catch (IOException e) { + throw new RuntimeException("Private key could not be read.", e); + } + + // Sets up the channel using the two signed JWTs for RPCs to the NBI. + ManagedChannel channel = + newChannelBuilderForAddress( + HOST, + PORT, + CompositeChannelCredentials.create( + TlsChannelCredentials.create(), + SpacetimeCallCredentials.createFromPrivateKey( + HOST, AGENT_EMAIL, AGENT_PRIV_KEY_ID, privateKey))) + .enableRetry() + .build(); + + // Sets up a stub to invoke RPCs against the NBI's NetOps service. + NetOpsGrpc.NetOpsBlockingStub stub = NetOpsGrpc.newBlockingStub(channel); + + // This stub can now be used to call any method in the NetOps service. + ListEntitiesRequest request = + ListEntitiesRequest.newBuilder().setType(EntityType.PLATFORM_DEFINITION).build(); + ListEntitiesResponse entities = stub.listEntities(request); + System.out.println("ListEntitiesResponse received: \n" + entities.toString()); + } +} diff --git a/java/com/aalyria/spacetime/codesamples/nbi/client/README.md b/java/com/aalyria/spacetime/codesamples/nbi/client/README.md new file mode 100644 index 0000000..69621f3 --- /dev/null +++ b/java/com/aalyria/spacetime/codesamples/nbi/client/README.md @@ -0,0 +1,21 @@ +# Northbound Interface (NBI) Client Code Samples + +These binaries are sample implementations of how to interact with Spacetime's +NBI. + +Start by creating a public and private key pair (see instructions [here](https://docs.spacetime.aalyria.com/authentication)). +Share the public key certificate with your contact on the Aalyria team, and they will provide you with a private key ID for +your application. + +```sh + DOMAIN="${DOMAIN:?should be provided by your Aalyria contact}" + AGENT_EMAIL="${AGENT_EMAIL:?should be provided by your Aalyria contact}" + AGENT_PRIV_KEY_ID="${AGENT_PRIV_KEY_ID:?should be provided by your Aalyria contact}" + # This is the path to your private key. + AGENT_PRIV_KEY_FILE="/path/to/your/private/key/in/PKSC8/format.pem" +``` + +To run the `ListEntities` sample, run: +```sh + bazel run //java/com/aalyria/spacetime/codesamples/nbi/client:ListEntities -- "$DOMAIN" "$AGENT_EMAIL" "$AGENT_PRIV_KEY_ID" "$AGENT_PRIV_KEY_FILE" +``` diff --git a/javatests/com/aalyria/spacetime/authentication/BUILD b/javatests/com/aalyria/spacetime/authentication/BUILD new file mode 100644 index 0000000..2d66577 --- /dev/null +++ b/javatests/com/aalyria/spacetime/authentication/BUILD @@ -0,0 +1,42 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("//bazel/java_format_rules:def.bzl", "java_format_test") +load("//bazel/java_rules:def.bzl", "java_test") + +java_format_test( + name = "java_format_test", + srcs = glob(["*.java"]), +) + +java_test( + name = "SpacetimeCallCredentialsTest", + srcs = [ + "SpacetimeCallCredentialsTest.java", + ], + resources = [ + "resources/test_private_key.pem", + ], + deps = [ + "//api/nbi/v1alpha:nbi_java_grpc", + "//java/com/aalyria/spacetime/authentication:Authentication", + "@maven//:com_google_code_gson_gson", + "@maven//:io_grpc_grpc_api", + "@maven//:io_grpc_grpc_inprocess", + "@maven//:io_grpc_grpc_testing", + "@maven//:io_helidon_grpc_helidon_grpc_core", + "@maven//:junit_junit", + "@maven//:org_mockito_mockito_core", + ], +) diff --git a/javatests/com/aalyria/spacetime/authentication/SpacetimeCallCredentialsTest.java b/javatests/com/aalyria/spacetime/authentication/SpacetimeCallCredentialsTest.java new file mode 100644 index 0000000..b1f40d1 --- /dev/null +++ b/javatests/com/aalyria/spacetime/authentication/SpacetimeCallCredentialsTest.java @@ -0,0 +1,271 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.aalyria.spacetime.authentication; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import com.aalyria.spacetime.api.nbi.v1alpha.Nbi.ListEntitiesRequest; +import com.aalyria.spacetime.api.nbi.v1alpha.Nbi.ListEntitiesResponse; +import com.aalyria.spacetime.api.nbi.v1alpha.NetOpsGrpc; +import com.google.common.io.Resources; +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.helidon.grpc.core.ResponseHelper; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; + +public class SpacetimeCallCredentialsTest { + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + @Rule + public final MockOidcTokenHttpServer oidcTokenServer = new MockOidcTokenHttpServer(/* port= */ 0); + + // Mocks the NBI server with a no-op implementation. + NetOpsGrpc.NetOpsImplBase netOpsServiceImpl = + mock(NetOpsGrpc.NetOpsImplBase.class, delegatesTo(new NetOpsGrpc.NetOpsImplBase() {})); + + private static final String HOST = "nbi.example.spacetime.aalyria.com"; + private static final String AGENT_EMAIL = "some@example.com"; + private static final String AGENT_PRIVATE_ID = "f1f2569a4ee44663732b8740e1f3f3e92c1931b5"; + private static final String VALID_TOKEN = + "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcyMTk0YjI2MzU0YzIzYzBiYTU5YTZkNzUxZGZmYWEyNTg2NTkwNGUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjQvdG9rZW4iLCJleHAiOjE2ODE3OTIzMTksImlhdCI6MTY4MTc4ODcxOSwiaXNzIjoiY2RwaS1hZ2VudEBhNWEtc3BhY2V0aW1lLWdrZS1iYWNrLWRldi5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6ImNkcGktYWdlbnRAYTVhLXNwYWNldGltZS1na2UtYmFjay1kZXYuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJ0YXJnZXRfYXVkaWVuY2UiOiI2MDI5MjQwMzEzOS1tZTY4dGpnYWpsNWRjZGJwbmxtMmVrODMwbHZzbnNscS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSJ9.QyOi7vkFCwdmjT4ChT3_yVY4ZObUJkZkYC0q7alF_thiotdJKRiSo1ZHp_XnS0nM4WSWcQYLGHUDdAMPS0R22brFGzCl8ndgNjqI38yp_LDL8QVTqnLBGUj-m3xB5wH17Q_Dt8riBB4IE-mSS8FB-R6sqSwn-seMfMDydScC0FrtOF3-2BCYpIAlf1AQKN083QdtKgNEVDi72npPr2MmsWV3tct6ydXHWNbxG423kfSD6vCZSUTvWXAuVjuOwnbc2LHZS04U-jiLpvHxu06OwHOQ5LoGVPyd69o8Ny_Bapd2m0YCX2xJr8_HH2nw1jH7EplFf-owbBYz9ZtQoQ2YTA"; + private static final String PATH_TO_PRIVATE_KEY = + "com/aalyria/spacetime/authentication/resources/test_private_key.pem"; + private static final String MOCK_NBI_SERVER_NAME = "mockNbiServer"; + // The number of threads to spawn in order to test the synchronization logic. + private static final int NUM_THREADS = 100; + + public class MockOidcTokenHttpServer extends ExternalResource { + private final HttpServer server; + private final AtomicInteger numCalls = new AtomicInteger(0); + private final int port; + + MockOidcTokenHttpServer(int port) { + this.port = port; + try { + server = HttpServer.create(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void before() throws Throwable { + server.bind(new InetSocketAddress(port), 0); + server.createContext("/", new MockOidcTokenServiceHandler()); + server.start(); + } + + @Override + protected void after() { + server.stop(0); + } + + public void resetNumCalls() { + numCalls.set(0); + } + + private class MockOidcTokenServiceHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + String response = (new Gson()).toJson(Map.of("id_token", VALID_TOKEN)); + exchange.sendResponseHeaders(200, response.length()); + OutputStream responseBody = exchange.getResponseBody(); + responseBody.write(response.getBytes()); + responseBody.close(); + numCalls.incrementAndGet(); + } + } + } + + @Test + public void testCustomCallCredentials() throws Exception { + // Creates a mock gRPC server for the NBI with an interceptor to verify + // requests' metadata. + AtomicInteger numVerifiedOidcTokensReceived = new AtomicInteger(0); + grpcCleanup.register( + InProcessServerBuilder.forName(MOCK_NBI_SERVER_NAME) + .directExecutor() + .addService(netOpsServiceImpl) + .intercept( + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata requestHeaders, + ServerCallHandler next) { + Key proxyAuthorizationHeaderKey = + Key.of("Proxy-Authorization", Metadata.ASCII_STRING_MARSHALLER); + assertEquals( + "Bearer " + VALID_TOKEN, requestHeaders.get(proxyAuthorizationHeaderKey)); + numVerifiedOidcTokensReceived.incrementAndGet(); + return next.startCall(call, requestHeaders); + } + }) + .build() + .start()); + ManagedChannel channel = + grpcCleanup.register( + InProcessChannelBuilder.forName(MOCK_NBI_SERVER_NAME).directExecutor().build()); + + // Mocks the listEntities method in the NBI to return an empty response when invoked. + doAnswer( + invocation -> { + var streamObserver = + (StreamObserver) invocation.getArguments()[1]; + ResponseHelper.complete(streamObserver, ListEntitiesResponse.newBuilder().build()); + return null; + }) + .when(netOpsServiceImpl) + .listEntities(any(ListEntitiesRequest.class), any(StreamObserver.class)); + + String privateKey = Resources.toString(Resources.getResource(PATH_TO_PRIVATE_KEY), UTF_8); + // Because the lifetime of the OIDC Token is set to 0 seconds, each thread calls the + // mock OIDC Token server to retrieve a new token. + String oidcTokenServerUrl = "http:/" + oidcTokenServer.server.getAddress().toString() + "/"; + SpacetimeCallCredentials callCredentials = + SpacetimeCallCredentials.createFromPrivateKey( + HOST, + AGENT_EMAIL, + AGENT_PRIVATE_ID, + privateKey, + oidcTokenServerUrl, + /* oidcTokenLifetimeSeconds= */ Duration.ofSeconds(0), + /* clock= */ Clock.system(ZoneId.systemDefault())); + ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; ++i) { + executor.submit( + () -> { + NetOpsGrpc.newBlockingStub(channel) + .withCallCredentials(callCredentials) + .listEntities(ListEntitiesRequest.newBuilder().build()); + }); + } + executor.shutdown(); + // Allows a permissive timeout of 10 seconds for all threads to complete. + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + + // Verifies that each thread successfully received a token from the OIDC Token + // Service. + assertEquals(NUM_THREADS, oidcTokenServer.numCalls.get()); + // Verifies that each thread passed a valid token to the mock NBI gRPC Server. + assertEquals(NUM_THREADS, numVerifiedOidcTokensReceived.get()); + } + + @Test + public void testTokenRefreshedAtExpirationTime() throws Exception { + // Creates a mock gRPC server. + grpcCleanup.register( + InProcessServerBuilder.forName(MOCK_NBI_SERVER_NAME) + .directExecutor() + .addService(netOpsServiceImpl) + .build() + .start()); + ManagedChannel channel = + grpcCleanup.register( + InProcessChannelBuilder.forName(MOCK_NBI_SERVER_NAME).directExecutor().build()); + + // Mocks the listEntities method in the NBI to return an empty response when + // invoked. + doAnswer( + invocation -> { + var streamObserver = + (StreamObserver) invocation.getArguments()[1]; + ResponseHelper.complete(streamObserver, ListEntitiesResponse.newBuilder().build()); + return null; + }) + .when(netOpsServiceImpl) + .listEntities(any(ListEntitiesRequest.class), any(StreamObserver.class)); + + String privateKey = Resources.toString(Resources.getResource(PATH_TO_PRIVATE_KEY), UTF_8); + // Mocks the clock object so that the token's expiration time can be simulated. + Clock fakeClock = mock(Clock.class); + Instant startClockTime = Instant.ofEpochMilli(0); + doReturn(startClockTime).when(fakeClock).instant(); + Duration oidcTokenLifetime = Duration.ofHours(1); + String oidcTokenServerUrl = "http:/" + oidcTokenServer.server.getAddress().toString() + "/"; + SpacetimeCallCredentials callCredentials = + SpacetimeCallCredentials.createFromPrivateKey( + HOST, + AGENT_EMAIL, + AGENT_PRIVATE_ID, + privateKey, + oidcTokenServerUrl, + oidcTokenLifetime, + fakeClock); + + // Calls ListEntities so that an initial OIDC token is created. + NetOpsGrpc.newBlockingStub(channel) + .withCallCredentials(callCredentials) + .listEntities(ListEntitiesRequest.newBuilder().build()); + + // Sets the clock's current time into the future so that the token is now stale. + doReturn(startClockTime.plusSeconds(2 * oidcTokenLifetime.getSeconds())) + .when(fakeClock) + .instant(); + // Resets the number of calls to refresh the token as recorded by the OIDC Token + // server. + oidcTokenServer.resetNumCalls(); + + // Across all of the NUM_THREADS concurrent calls to ListEntities, the token should only + // be refreshed once, and all other threads should read the new value. + ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; ++i) { + executor.submit( + () -> { + NetOpsGrpc.newBlockingStub(channel) + .withCallCredentials(callCredentials) + .listEntities(ListEntitiesRequest.newBuilder().build()); + }); + } + executor.shutdown(); + // Allows a permissive timeout of 10 seconds for all threads to complete. + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + // Verifies that the token was only created once. + assertEquals(1, oidcTokenServer.numCalls.get()); + } +} diff --git a/javatests/com/aalyria/spacetime/authentication/resources/test_private_key.pem b/javatests/com/aalyria/spacetime/authentication/resources/test_private_key.pem new file mode 100644 index 0000000..167a600 --- /dev/null +++ b/javatests/com/aalyria/spacetime/authentication/resources/test_private_key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEAxLLIWHTZUWI8PFpGLec6DCGL7GlSPyblIKKfhYCEcRmc74W7 +9IStF0OaHeSHWD3Tv3gB0C3Ia8vJNGsPw37ZVvOQRJQKFN4v5vZL20dCULHe68RH +OmMrIZO7ylucaoDR2trOJSUkUCswZ9AYoaxN+XOrXxGHIAUEc3Sf6QwGewH6l1tv +7Seu1fy9hP1JsJYFRlmQkJtYq0VKyPNWIHeYawINvjjnrBYHC+i++k/mCr2OrquH +ffyLvPNcB9chouzAX/B35PKjrPQuXbs/YRczLwqmtRZIOQchYdPftn7eDVpIFDfL +R/SRvApxANeiAbeegu+bExCjnsVDxxWupVqblrqIy6qoZWnR8kifh1YIS7R+YTYI +OI1uyns065R8hoSrfxUX1AsKzTxPn2n0cN0fKQSuF9sk0VyNZDZN3AQ3TOlVxTJ3 +7EuCu4WIn07/gRmFqEQ0aoQ+/7lHwsE1hxATyLu5cvT2KtXMM8VWqaLjO2c+3ps3 +IqvIqBG8HNY41O2kIpF5CzQznkzrMA5RY3r60gZTE8M95QMGz+E24yVDsX1UGH38 +X8rQP6zENwO59rhCZ6Cb9C9xI5kU4MrAINxO0/Xs5rZ+2EJ49mr6fJFZ/yrTJG20 ++LUf6tPskDJVbzQAPB9B/80flwiBy8ZxtadjzbVzFKtbXgoGP3LJf7FXTjkCAwEA +AQKCAgEAnrj12gtQYc+3c6wU/W2c9bUMSBUk/TjRo+gWeZAfT3SvoshzxvhZBHDU +qFKEtLwPZm3caLTJCdND5TyPV93AW93nCK+9AuHYHbOZurRh2uPtUqrsHz1uzIV9 +/+i062xP6x6tQmQaTWbMhLjzZ2K2+RhRrUFjnH7v/Iqbj17Yy+Ho6MIsK17eQmbU +N8B7+jbUwtP7R/VTM8GSe12AnmpjX9YkaN9acw7DWOTTVwGqS/hybpiTmSJ1UF/A +X8NFrUcGZWSSGvmBAkt4LsKufhZOgyNBKtd0KDCMy3hBCe6OGJBFoar0Kng8MVTr +oxZd8KzPCDF5HYVLww7Dhp8EcwIOkcJq+B3kOMy25HaWuEGS2Rpw0ZEz1LhJW9oG +8gWOE3d0fdkhD8xWUzxEVkY1SdmOf3QpXnTJ6lvwCb+vxaEcr6NKySoDnuj4veSJ +Oq9kw74b03HWnxfCykxjDUnCFUlNAoWt/G0HWmPF3jid4hF3IWBeOP+CzD/uH69M +bhdIvTFS43QhjejxsTTu2ciIM7g+oRHs07A+a6P5kjL98VKyThJUmpYdskgrm7qN +LjxrDFPmmbw8iTaJS25g6ayFgIaZrU7uWQZ+Pw7/toHWffwx/yPxXD5xBLH3Ne9Q +AlTFrdDso1rAj1HLy/yGcvMiSYBXHy3C0egY0VJXSittAAhxVkECggEBAPnloLBF +MVY9TS77NP6stl0bZEB1ScAtKdKrPsd8mbANWqs2DGZJwX9Z8KFDo7AbUAcBC43t +sIohZWJz6PfdRr5w9w70Q2P1aoXckcVTZiGRCfMmGkVw61tNLHld7yNl3n6FH5m9 +SiLk7b705hHz3xJWyfpU8K5TLbi19J0jRF1W2HdddjnRb1uDAra39fVx1+lK7sfc +gqFN7J+HMPstIzHiIDFa+HGqm86Rj32tY2mPaSm2X11ZYBYfH8Mo4kwCJlezr71D +zfD0iHnl+nyox2Rnru9Qd8bJjw6rLJWfa2Ze04wgWpFKvhzj71lCRkXXs0ka4kz/ +/rPyIj7fKmbuxc0CggEBAMmAjb7JQOPt8SbBMpqIZWRZxW91R09iy8N/QeT9evy9 +BSdWDQ27EJD883Ph2Rb6ZL28I3z7fm8J5MQfAucCIZX6wsTJOiAJNQ0xGcqoWmNn +VmTVxMLaOsmcqu/qzmpVwWiYP6FMZgmhME6MO9B6oD2YZQkACPc7WgYXWEief5xn +SlbOaPQcnt5adHJUiFZf3Npf7dmU3xaMdUpmJzsXOtYmVUSX6fGLjHx3vP1W1FV7 +QkyTzcWrPkGMtXGMw4yXKypqDHyc7swhcBVhkwK0OO1vHiDS5aTCBJrXO4HfpwWY +8glc9syYMYFSE4a9F7sUUz/vZ7Msctu9RJf3/J6wfh0CggEBAJ4mmR91atftS9+j +09Ipc/BQ2Y2BrP8WlhVhkwWk8Y7dpLgeKJLDstXqEcO9juZxIqCMJMfZ2ZRRtlws +hw/21kLIM1Mfe1bThmrZQNflBAnSRL3BK3cvE4RgvvjAXgvE+J6RmYIurbCPuXbU +fpZ2BIMSshjfkMKCmbkATqL0Itv/jldcqunEhfkgXKNOoTaIqeBXL+8EuxdivZHo +eTM9SbzUIxOZQwqMsrZ0oJ8lEHlJ7YAcjxYA7jKR2AQm57C625E9gscBFmCcIZMj +3PbvyeDdTXTfNC52eTcLVdgLYGkVMkTIZQj2iIK4Lk4LB3ZEII7vmLCqgoNXhhzM +F4W1LzECggEAPgvLmbe2t3iJDPnobxUYw3GxDcT7FELDo4sH607yE+jQMXCZzGSR +kVOSU1hz1FN9ub643r9CC0bsnkc+SYuqc9gnKRkdQMgVAd7gpjp4uqsTOzFnyOgR +ugr3x7BxpuSJDX/z9+LieIydp1IfCO75cH4Afmj4Wch4y+9cS+AiQzK6/UfJoYE+ +mhEYUiwdXxtdkhB/2MOyfer6ItKZueRJRa/ACcMNUkc6Fwl3tDqMX1X48EOC1R8J +qH1/UVuayyuxKvpEpgpcrZaOQMcu0WJVNSdGC61k8PqdGGC1/iOz9lFGFgP+Krmi +ZMO1J4QKTpzPlZxOPimtpGP/PxwpOWP3MQKCAQEAjgVuY2oWPDk82majywOpUtIK +67My4ELlvZYDKPuApTi5tZvLxztD5JD3UaPmbgph34DVViRgbj2Ij/D7z99xYrM8 +R7BlYb9dMBkmp+6DHpQN5twpclGkzKAexMzYB+aAaHtQ6jLx8K3IuZttD91s8apZ +zcbGYot8HVVnJC+PIxjCkZKnVaHr0HYD0OCH2azkxvtKuVWJnHh6C5Kh8nUpQoAV +Qk/vb+vIJgkYhTEO20Si0GDUbwVysM8Im55Awxk8+m13dtwcxlbDf5utB3xer378 +nQEqPvg+Y/Yg4eVwvlQtZw3HdMWgsWnl58DEBVSDmYpcQjy6WPtSsQ4UdHqzoQ== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/patches/BUILD b/patches/BUILD new file mode 100644 index 0000000..abe39c8 --- /dev/null +++ b/patches/BUILD @@ -0,0 +1,18 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exports_files([ + "protobuf.patch", + "rules_proto_grpc_python.patch", +]) diff --git a/patches/protobuf.patch b/patches/protobuf.patch new file mode 100644 index 0000000..32f3194 --- /dev/null +++ b/patches/protobuf.patch @@ -0,0 +1,191 @@ +diff --git a/MODULE.bazel b/MODULE.bazel +index fb3065230..3758ec075 100644 +--- a/MODULE.bazel ++++ b/MODULE.bazel +@@ -18,7 +18,8 @@ bazel_dep(name = "jsoncpp", version = "1.9.5") + bazel_dep(name = "rules_cc", version = "0.0.9") + bazel_dep(name = "rules_fuzzing", version = "0.5.2") + bazel_dep(name = "rules_java", version = "5.3.5") +-bazel_dep(name = "rules_jvm_external", version = "5.1") ++bazel_dep(name = "rules_jvm_external", version = "6.0") ++bazel_dep(name = "rules_kotlin", version = "1.9.0") + bazel_dep(name = "rules_pkg", version = "0.7.0") + bazel_dep(name = "rules_python", version = "0.28.0") + bazel_dep(name = "rules_rust", version = "0.45.1") +@@ -70,3 +71,29 @@ crate.spec( + ) + crate.from_specs() + use_repo(crate, crate_index = "crates") ++ ++maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") ++maven.install( ++ artifacts = [ ++ "com.google.caliper:caliper:1.0-beta-3", ++ "com.google.code.findbugs:jsr305:3.0.2", ++ "com.google.code.gson:gson:2.8.9", ++ "com.google.errorprone:error_prone_annotations:2.5.1", ++ "com.google.j2objc:j2objc-annotations:2.8", ++ "com.google.guava:guava:32.0.1-jre", ++ "com.google.guava:guava-testlib:32.0.1-jre", ++ "com.google.truth:truth:1.1.2", ++ "junit:junit:4.13.2", ++ "org.mockito:mockito-core:4.3.1", ++ "biz.aQute.bnd:biz.aQute.bndlib:6.4.0", ++ "info.picocli:picocli:4.6.3", ++ ], ++ repositories = [ ++ "https://repo1.maven.org/maven2", ++ "https://repo.maven.apache.org/maven2", ++ ], ++) ++use_repo(maven, "maven") ++ ++# Development dependencies ++bazel_dep(name = "googletest", version = "1.14.0", repo_name = "com_google_googletest", dev_dependency = True) +diff --git a/java/kotlin-lite/BUILD.bazel b/java/kotlin-lite/BUILD.bazel +index 4d5577048..431839fdd 100644 +--- a/java/kotlin-lite/BUILD.bazel ++++ b/java/kotlin-lite/BUILD.bazel +@@ -50,7 +50,7 @@ kt_jvm_library( + kt_jvm_export( + name = "kotlin-lite_mvn", + deploy_env = [ +- "@com_github_jetbrains_kotlin//:kotlin-stdlib", ++ "@rules_kotlin//kotlin/compiler:kotlin-stdlib", + "//java/lite", + ], + maven_coordinates = "com.google.protobuf:protobuf-kotlin-lite:%s" % PROTOBUF_JAVA_VERSION, +@@ -99,7 +99,7 @@ kt_jvm_library( + "//java/kotlin:only_for_use_in_proto_generated_code_its_generator_and_tests", + "//java/kotlin:shared_runtime", + "//java/lite", +- "@com_github_jetbrains_kotlin//:kotlin-test", ++ "@rules_kotlin//kotlin/compiler:kotlin-test", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", + ], +diff --git a/java/kotlin/BUILD.bazel b/java/kotlin/BUILD.bazel +index 20dd242ae..3af561b74 100644 +--- a/java/kotlin/BUILD.bazel ++++ b/java/kotlin/BUILD.bazel +@@ -1,5 +1,5 @@ +-load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + load("@rules_jvm_external//:kt_defs.bzl", "kt_jvm_export") ++load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") + load("//:protobuf.bzl", "internal_gen_kt_protos") + load("//:protobuf_version.bzl", "PROTOBUF_JAVA_VERSION") +@@ -53,7 +53,7 @@ kt_jvm_library( + kt_jvm_export( + name = "kotlin_mvn", + deploy_env = [ +- "@com_github_jetbrains_kotlin//:kotlin-stdlib", ++ "@rules_kotlin//kotlin/compiler:kotlin-stdlib", + "//java/core", + ], + maven_coordinates = "com.google.protobuf:protobuf-kotlin:%s" % PROTOBUF_JAVA_VERSION, +@@ -101,9 +101,9 @@ kt_jvm_library( + deps = [ + ":bytestring_lib", + "//java/lite", +- "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", ++ "@rules_kotlin//kotlin/compiler:kotlin-test", + ], + ) + +@@ -136,10 +136,10 @@ kt_jvm_library( + ":example_extensible_message_java_proto", + ":only_for_use_in_proto_generated_code_its_generator_and_tests", + ":shared_runtime", +- "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:com_google_guava_guava_testlib", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", ++ "@rules_kotlin//kotlin/compiler:kotlin-test", + ], + ) + +@@ -162,9 +162,9 @@ kt_jvm_library( + ":only_for_use_in_proto_generated_code_its_generator_and_tests", + ":shared_runtime", + "//java/core", +- "@com_github_jetbrains_kotlin//:kotlin-test", + "@maven//:com_google_truth_truth", + "@maven//:junit_junit", ++ "@rules_kotlin//kotlin/compiler:kotlin-test", + ], + ) + +diff --git a/protobuf_deps.bzl b/protobuf_deps.bzl +index ae2aa5e72..b8f5b989b 100644 +--- a/protobuf_deps.bzl ++++ b/protobuf_deps.bzl +@@ -80,11 +80,27 @@ def protobuf_deps(): + ) + + if not native.existing_rule("rules_java"): +- http_archive( +- name = "rules_java", +- url = "https://github.com/bazelbuild/rules_java/releases/download/6.0.0/rules_java-6.0.0.tar.gz", +- sha256 = "469b7f3b580b4fcf8112f4d6d0d5a4ce8e1ad5e21fee67d8e8335d5f8b3debab", +- ) ++ bazel_version = native.bazel_version or "999999.999999.999999" ++ version_parts = bazel_version.split("-")[0].split(".") ++ if len(version_parts) != 3: ++ fail("invalid Bazel version '{}': got {} dot-separated segments, want 3".format(bazel_version, len(version_parts))) ++ major_version_int = int(version_parts[0]) ++ minor_version_int = int(version_parts[1]) ++ ++ if major_version_int < 6 or (major_version_int == 6 and minor_version_int <= 3): ++ # Works with Bazel 6.3.0, but not higher ++ http_archive( ++ name = "rules_java", ++ url = "https://github.com/bazelbuild/rules_java/releases/download/6.0.0/rules_java-6.0.0.tar.gz", ++ sha256 = "469b7f3b580b4fcf8112f4d6d0d5a4ce8e1ad5e21fee67d8e8335d5f8b3debab", ++ ) ++ else: ++ # Version 6.5.2 works both with Bazel 6.4.0 and Bazel 7 ++ http_archive( ++ name = "rules_java", ++ url = "https://github.com/bazelbuild/rules_java/releases/download/6.5.0/rules_java-6.5.0.tar.gz", ++ sha256 = "160d1ebf33763124766fb35316329d907ca67f733238aa47624a8e3ff3cf2ef4", ++ ) + + # TODO: remove after toolchain types are moved to protobuf + if not native.existing_rule("rules_proto"): +@@ -112,11 +128,12 @@ def protobuf_deps(): + ) + + if not native.existing_rule("rules_jvm_external"): +- _github_archive( ++ # Version 6.0 is the lowest that works with rules_kotlin 1.9.0 ++ http_archive( + name = "rules_jvm_external", +- repo = "https://github.com/bazelbuild/rules_jvm_external", +- commit = "906875b0d5eaaf61a8ca2c9c3835bde6f435d011", +- sha256 = "744bd7436f63af7e9872948773b8b106016dc164acb3960b4963f86754532ee7", ++ strip_prefix = "rules_jvm_external-6.0", ++ sha256 = "85fd6bad58ac76cc3a27c8e051e4255ff9ccd8c92ba879670d195622e7c0a9b7", ++ url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/6.0/rules_jvm_external-6.0.tar.gz", + ) + + if not native.existing_rule("rules_pkg"): +@@ -143,11 +160,12 @@ def protobuf_deps(): + url = "https://github.com/bazelbuild/apple_support/releases/download/1.12.0/apple_support.1.12.0.tar.gz", + ) + +- if not native.existing_rule("io_bazel_rules_kotlin"): ++ if not native.existing_rule("rules_kotlin"): ++ # Version 1.9.0 is the lowest available on BCR + http_archive( +- name = "io_bazel_rules_kotlin", +- urls = ["https://github.com/bazelbuild/rules_kotlin/releases/download/v1.8.1/rules_kotlin_release.tgz"], +- sha256 = "a630cda9fdb4f56cf2dc20a4bf873765c41cf00e9379e8d59cd07b24730f4fde", ++ name = "rules_kotlin", ++ sha256 = "5766f1e599acf551aa56f49dab9ab9108269b03c557496c54acaf41f98e2b8d6", ++ url = "https://github.com/bazelbuild/rules_kotlin/releases/download/v1.9.0/rules_kotlin-v1.9.0.tar.gz", + ) + + # Python Downloads diff --git a/patches/rules_proto_grpc_python.patch b/patches/rules_proto_grpc_python.patch new file mode 100644 index 0000000..7ea0766 --- /dev/null +++ b/patches/rules_proto_grpc_python.patch @@ -0,0 +1,49 @@ +diff --git a/requirements.in b/requirements.in +index fe5ee1a..15f490a 100644 +--- a/requirements.in ++++ b/requirements.in +@@ -1,3 +1,3 @@ + grpcio==1.64.1 # TODO: remove once pulling grpc from BCR @grpc works + grpclib==0.4.7 +-protobuf==5.27.2 ++protobuf==5.28.0rc3 +diff --git a/requirements.txt b/requirements.txt +index 56f8c93..6aa360a 100644 +--- a/requirements.txt ++++ b/requirements.txt +@@ -1,5 +1,5 @@ + # +-# This file is autogenerated by pip-compile with Python 3.11 ++# This file is autogenerated by pip-compile with Python 3.12 + # by the following command: + # + # bazel run //:requirements.update +@@ -159,16 +159,16 @@ multidict==6.0.5 \ + --hash=sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423 \ + --hash=sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef + # via grpclib +-protobuf==5.27.2 \ +- --hash=sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505 \ +- --hash=sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b \ +- --hash=sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38 \ +- --hash=sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863 \ +- --hash=sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470 \ +- --hash=sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6 \ +- --hash=sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce \ +- --hash=sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca \ +- --hash=sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5 \ +- --hash=sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e \ +- --hash=sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714 ++protobuf==5.28.2 \ ++ --hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \ ++ --hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \ ++ --hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \ ++ --hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \ ++ --hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \ ++ --hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \ ++ --hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \ ++ --hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \ ++ --hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \ ++ --hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \ ++ --hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d + # via -r requirements.in diff --git a/py/authentication/BUILD b/py/authentication/BUILD new file mode 100644 index 0000000..3a7c3b5 --- /dev/null +++ b/py/authentication/BUILD @@ -0,0 +1,29 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@python_deps//:requirements.bzl", "requirement") +load("@rules_python//python:defs.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "authentication", + srcs = [ + "jwt_manager.py", + "spacetime_call_credentials.py", + ], + deps = [ + requirement("cryptography"), + ], +) diff --git a/py/authentication/jwt_manager.py b/py/authentication/jwt_manager.py new file mode 100644 index 0000000..d7f28cd --- /dev/null +++ b/py/authentication/jwt_manager.py @@ -0,0 +1,101 @@ +''' +Copyright 2023 Aalyria Technologies, Inc., and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import base64 +import json +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from datetime import timedelta, datetime, timezone + + +class JwtManager: + + def __init__( + self, + lifetime=timedelta(seconds=0), + audience="", + issuer="", + subject="", + target_audience="", + private_key_id="", + private_key_pem="", + ): + self.lifetime = lifetime + self.audience = audience + self.issuer = issuer + self.subject = subject if subject else issuer + self.target_audience = target_audience + self.private_key_id = private_key_id + self.private_key = self.__decode_private_key(private_key_pem) + + def generate_jwt(self) -> str: + header = self.__generate_header() + payload = self.__generate_payload() + signature = self.__generate_signature(header, payload, self.private_key) + + jwt = b".".join([header, payload, signature]) + return jwt.decode() + + def __decode_private_key(self, private_key_pem: str) -> rsa.RSAPrivateKey: + try: + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), None) + except (ValueError, TypeError) as e: + raise ValueError("Invalid private key.") from e + + return private_key + + def __generate_header(self) -> bytes: + header = { + "alg": "RS256", + "typ": "JWT", + "kid": self.private_key_id, + } + encoded_header = json.dumps(header).encode() + return base64.urlsafe_b64encode(encoded_header) + + def __generate_payload(self) -> bytes: + now = datetime.now(timezone.utc) + issue_time = int(now.timestamp()) + expiration_time = int((now + self.lifetime).timestamp()) + + payload = { + "aud": self.audience, + "exp": expiration_time, + "iat": issue_time, + "iss": self.issuer, + "sub": self.subject, + } + if self.target_audience: + payload["target_audience"] = self.target_audience + + encoded_payload = json.dumps(payload).encode() + return base64.urlsafe_b64encode(encoded_payload) + + def __generate_signature( + self, + header: bytes, + payload: bytes, + private_key: rsa.RSAPrivateKey, + ) -> bytes: + message = b".".join([header, payload]) + signature = private_key.sign( + message, + padding.PKCS1v15(), + hashes.SHA256(), + ) + return base64.urlsafe_b64encode(signature) diff --git a/py/authentication/spacetime_call_credentials.py b/py/authentication/spacetime_call_credentials.py new file mode 100644 index 0000000..63dfa02 --- /dev/null +++ b/py/authentication/spacetime_call_credentials.py @@ -0,0 +1,128 @@ +''' +Copyright 2023 Aalyria Technologies, Inc., and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import grpc +import http.client +import json +from urllib.parse import urlencode +from datetime import timedelta, datetime, timezone + +from py.authentication.jwt_manager import JwtManager + + +class SpacetimeCallCredentials(grpc.AuthMetadataPlugin): + """A class that supplies per-RPC credentials, which are based on + two signed JWTs, one for authenticating to the Spacetime backend + and one for authenticating through the secure proxy. + """ + + # OIDC tokens are provided by Google's OAuth endpoint by default. + DEFAULT_GCP_OIDC_TOKEN_CREATION_HOST = "www.googleapis.com" + DEFAULT_GCP_OIDC_TOKEN_CREATION_PATH = "/oauth2/v4/token" + # The OIDC tokens are valid for 1 hour by default. + DEFAULT_OIDC_TOKEN_LIFETIME = timedelta(hours=1) + PROXY_TARGET_AUDIENCE = "60292403139-me68tjgajl5dcdbpnlm2ek830lvsnslq.apps.googleusercontent.com" + # If the OIDC token will expire within this margin, it will be re-created. + OIDC_TOKEN_EXPIRATION_TIME_MARGIN = timedelta(minutes=5) + + def __init__( + self, + spacetime_auth_jwt_manager, + proxy_auth_jwt_manager, + gcp_oidc_token_creation_host, + gcp_oidc_token_creation_path, + oidc_token_lifetime, + ): + self.spacetime_auth_jwt_manager = spacetime_auth_jwt_manager + self.proxy_auth_jwt_manager = proxy_auth_jwt_manager + self.gcp_oidc_token_creation_host = gcp_oidc_token_creation_host + self.gcp_oidc_token_creation_path = gcp_oidc_token_creation_path + self.oidc_token_lifetime = oidc_token_lifetime + + self.oidc_token = "" + self.oidc_token_expiration_time = datetime.now(timezone.utc) + + @classmethod + def create_from_private_key(cls, host: str, agent_email: str, + private_key_id: str, private_key: str): + spacetime_auth_jwt_manager = JwtManager(lifetime=timedelta(hours=1), + issuer=agent_email, + subject=agent_email, + audience=host, + private_key_id=private_key_id, + private_key_pem=private_key) + proxy_auth_jwt_manager = JwtManager( + lifetime=timedelta(hours=1), + issuer=agent_email, + subject=agent_email, + audience="https://{}{}".format( + cls.DEFAULT_GCP_OIDC_TOKEN_CREATION_HOST, + cls.DEFAULT_GCP_OIDC_TOKEN_CREATION_PATH), + target_audience=cls.PROXY_TARGET_AUDIENCE, + private_key_id=private_key_id, + private_key_pem=private_key) + return cls(spacetime_auth_jwt_manager, proxy_auth_jwt_manager, + cls.DEFAULT_GCP_OIDC_TOKEN_CREATION_HOST, + cls.DEFAULT_GCP_OIDC_TOKEN_CREATION_PATH, + cls.DEFAULT_OIDC_TOKEN_LIFETIME) + + @classmethod + def create_from_jwt(cls, spacetime_auth_jwt: str, proxy_auth_jwt: str): + spacetime_auth_jwt_manager = JwtManager(lifetime=timedelta.max, + jwt_value=spacetime_auth_jwt) + proxy_auth_jwt_manager = JwtManager(lifetime=timedelta.max, + jwt_value=proxy_auth_jwt) + return cls(spacetime_auth_jwt_manager, proxy_auth_jwt_manager, + cls.DEFAULT_GCP_OIDC_TOKEN_CREATION_HOST, + cls.DEFAULT_GCP_OIDC_TOKEN_CREATION_PATH, + cls.DEFAULT_OIDC_TOKEN_LIFETIME) + + # Supplies the authorization credentials in gRPC calls. + def __call__(self, context, callback): + now = datetime.now(timezone.utc) + if self.is_oidc_token_expired(now): + proxy_auth_jwt = self.proxy_auth_jwt_manager.generate_jwt() + self.oidc_token_expiration_time = now + self.oidc_token_lifetime + self.oidc_token = self._exchange_proxy_auth_jwt_for_oidc_token( + proxy_auth_jwt) + + callback([ + ("authorization", "Bearer {}".format( + self.spacetime_auth_jwt_manager.generate_jwt())), + ("proxy-authorization", "Bearer {}".format(self.oidc_token)), + ], None) + + def _exchange_proxy_auth_jwt_for_oidc_token(self, proxy_auth_jwt_value): + data = { + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": proxy_auth_jwt_value + } + headers = {"Content-Type": "application/x-www-form-urlencoded"} + connection = http.client.HTTPSConnection( + self.gcp_oidc_token_creation_host) + connection.request("POST", + self.gcp_oidc_token_creation_path, + body=urlencode(data), + headers=headers) + response = connection.getresponse() + response_data = response.read() + response_json = json.loads(response_data.decode("utf-8")) + return response_json["id_token"] + + def is_oidc_token_expired(self, now): + return (not self.oidc_token or now + + SpacetimeCallCredentials.OIDC_TOKEN_EXPIRATION_TIME_MARGIN + > self.oidc_token_expiration_time) diff --git a/py/codesamples/BUILD b/py/codesamples/BUILD new file mode 100644 index 0000000..04a090e --- /dev/null +++ b/py/codesamples/BUILD @@ -0,0 +1,24 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_python//python:defs.bzl", "py_binary") + +py_binary( + name = "list_entities", + srcs = ["list_entities.py"], + deps = [ + "//api/nbi/v1alpha:nbi_python_grpc", + "//py/authentication", + ], +) diff --git a/py/codesamples/README.md b/py/codesamples/README.md new file mode 100644 index 0000000..86229a4 --- /dev/null +++ b/py/codesamples/README.md @@ -0,0 +1,21 @@ +# Northbound Interface (NBI) Client Code Samples + +These binaries are sample implementations of how to interact with Spacetime's +NBI. + +Start by creating a public and private key pair (see instructions [here](https://docs.spacetime.aalyria.com/authentication)). +Share the public key certificate with your contact on the Aalyria team, and they will provide you with a private key ID for +your application. + +```sh + DOMAIN="${DOMAIN:?should be provided by your Aalyria contact}" + AGENT_EMAIL="${AGENT_EMAIL:?should be provided by your Aalyria contact}" + AGENT_PRIV_KEY_ID="${AGENT_PRIV_KEY_ID:?should be provided by your Aalyria contact}" + # This is the path to your private key. + AGENT_PRIV_KEY_FILE="/path/to/your/private/key/in/PKSC8/format.pem" +``` + +To run the `list_entities` sample, run: +```sh + bazel run //py/codesamples:list_entities -- "$DOMAIN" "$AGENT_EMAIL" "$AGENT_PRIV_KEY_ID" "$AGENT_PRIV_KEY_FILE" +``` diff --git a/py/codesamples/list_entities.py b/py/codesamples/list_entities.py new file mode 100644 index 0000000..1d5be24 --- /dev/null +++ b/py/codesamples/list_entities.py @@ -0,0 +1,66 @@ +''' +Copyright 2023 Aalyria Technologies, Inc., and its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +''' + +import grpc +import sys +from pathlib import Path + +import api.nbi.v1alpha.nbi_pb2 as Nbi +import api.nbi.v1alpha.nbi_pb2_grpc as NetOpsGrpc +from py.authentication.spacetime_call_credentials import SpacetimeCallCredentials + + +def main(): + if len(sys.argv) < 4: + print("Error parsing arguments. Provide a value for host, " + + "agent email, agent private key ID, and agent private key file.", + file=sys.stderr) + sys.exit(-1) + + HOST = sys.argv[1] + AGENT_EMAIL = sys.argv[2] + AGENT_PRIV_KEY_ID = sys.argv[3] + AGENT_PRIV_KEY_FILE = sys.argv[4] + PORT = 443 + + # The private key should start with "-----BEGIN RSA PRIVATE KEY-----" and + # end with "-----END RSA PRIVATE KEY-----". In between, there should be newline-delimited + # strings of characters. + private_key = Path(AGENT_PRIV_KEY_FILE).read_text() + + # Sets up the channel using the two signed JWTs for RPCs to the NBI. + credentials = grpc.metadata_call_credentials( + SpacetimeCallCredentials.create_from_private_key( + HOST, AGENT_EMAIL, AGENT_PRIV_KEY_ID, private_key)) + channel = grpc.secure_channel( + f"{HOST}:{PORT}", + grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), + credentials), + [ + ("grpc.max_receive_message_length", 1024*1024*256,), + ]) + + # Sets up a stub to invoke RPCs against the NBI's NetOps service. + stub = NetOpsGrpc.NetOpsStub(channel) + + # This stub can now be used to call any method in the NetOps service. + request = Nbi.ListEntitiesRequest(type="PLATFORM_DEFINITION") + entities = stub.ListEntities(request) + print("ListEntitiesResponse received:\n", entities) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6926972 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,94 @@ +cryptography==42.0.5 \ + --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ + --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ + --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ + --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ + --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ + --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ + --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ + --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ + --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ + --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ + --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ + --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ + --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ + --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ + --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ + --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ + --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ + --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ + --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ + --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ + --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ + --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ + --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ + --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ + --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ + --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ + --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ + --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ + --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ + --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ + --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ + --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 + # via -r requirements.in +cffi==1.16.0 \ + --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ + --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ + --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ + --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ + --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ + --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ + --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ + --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ + --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ + --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ + --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ + --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ + --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ + --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ + --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ + --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ + --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ + --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ + --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ + --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ + --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ + --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ + --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ + --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ + --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ + --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ + --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ + --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ + --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ + --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ + --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ + --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ + --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ + --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ + --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ + --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ + --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ + --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ + --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ + --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ + --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ + --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ + --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ + --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ + --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ + --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ + --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ + --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ + --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ + --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ + --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ + --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 + # via + # argon2-cffi-bindings + # cryptography +pycparser==2.21 \ + --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ + --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 + # via cffi \ No newline at end of file diff --git a/third_party/java/google_java_format/BUILD b/third_party/java/google_java_format/BUILD new file mode 100644 index 0000000..f37f5c7 --- /dev/null +++ b/third_party/java/google_java_format/BUILD @@ -0,0 +1,28 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +java_binary( + name = "google_java_format", + jvm_flags = [ + "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ], + main_class = "com.google.googlejavaformat.java.Main", + visibility = ["//visibility:public"], + runtime_deps = ["@maven//:com_google_googlejavaformat_google_java_format"], +) diff --git a/tools/gopackagesdriver.sh b/tools/gopackagesdriver.sh new file mode 100755 index 0000000..9d85eb0 --- /dev/null +++ b/tools/gopackagesdriver.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exec bazel run -- @rules_go//go/tools/gopackagesdriver "${@}" diff --git a/tools/nbictl/BUILD b/tools/nbictl/BUILD new file mode 100644 index 0000000..a98a85f --- /dev/null +++ b/tools/nbictl/BUILD @@ -0,0 +1,111 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build rules for the Go CDPI agent. These shouldn't depend on any other +# internal packages apart from those found in //api. + +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "nbictl", + srcs = [ + "config.go", + "connection.go", + "generate_auth_token.go", + "generate_rsa_key.go", + "grpcurl.go", + "model.go", + "nbictl.go", + ], + importpath = "aalyria.com/spacetime/github/tools/nbictl", + visibility = ["//visibility:public"], + deps = [ + "//api/common:common_go_proto", + "//api/model/v1alpha:v1alpha_go_proto", + "//api/nbi/v1alpha:v1alpha_go_proto", + "//api/nbi/v1alpha/resources:nbi_resources_go_grpc", + "//auth", + "//tools/nbictl/proto:nbictl_go_proto", + "@com_github_fullstorydev_grpcurl//:grpcurl", + "@com_github_golang_jwt_jwt_v5//:jwt", + "@com_github_jhump_protoreflect//desc", + "@com_github_jhump_protoreflect//grpcreflect", + "@com_github_jonboulle_clockwork//:clockwork", + "@com_github_urfave_cli_v2//:cli", + "@org_golang_google_genproto//googleapis/type/interval", + "@org_golang_google_grpc//:grpc", + "@org_golang_google_grpc//credentials", + "@org_golang_google_grpc//credentials/insecure", + "@org_golang_google_grpc//encoding/gzip", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//reflect/protoreflect", + "@org_golang_google_protobuf//types/descriptorpb", + "@org_golang_google_protobuf//types/known/durationpb", + "@org_golang_google_protobuf//types/known/timestamppb", + "@org_golang_x_sync//errgroup", + "@org_outernetcouncil_nmts//proto:nmts_go_proto", + ], +) + +go_test( + name = "nbictl_test", + srcs = [ + "config_test.go", + "connection_test.go", + "fake_modelapi_server_test.go", + "fake_nbi_server_test.go", + "generate_auth_token_test.go", + "generate_rsa_key_test.go", + "model_test.go", + "nbictl_test.go", + ], + embed = [":nbictl"], + deps = [ + "//api/common:common_go_proto", + "//api/model/v1alpha:v1alpha_go_proto", + "//api/nbi/v1alpha:v1alpha_go_proto", + "//api/nbi/v1alpha/resources:nbi_resources_go_grpc", + "//auth/authtest", + "//tools/nbictl/proto:nbictl_go_proto", + "@com_github_golang_jwt_jwt_v5//:jwt", + "@com_github_google_go_cmp//cmp", + "@com_github_urfave_cli_v2//:cli", + "@org_golang_google_grpc//:grpc", + "@org_golang_google_grpc//metadata", + "@org_golang_google_grpc//reflection", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//testing/protocmp", + "@org_golang_google_protobuf//types/known/emptypb", + "@org_golang_google_protobuf//types/known/fieldmaskpb", + "@org_golang_x_sync//errgroup", + "@org_outernetcouncil_nmts//proto:nmts_go_proto", + "@org_outernetcouncil_nmts//proto/ek/physical:physical_go_proto", + "@rules_go//go/tools/bazel", + ], +) + +# Check that the README.md file matches the output of the nbictl tool's +# "readme" subcommand exactly. Normally the README file would be a `genfile` +# rule, but for ease of browsing documentation we want it to be checked in. +sh_test( + name = "readme_test", + size = "small", + srcs = ["readme_test.sh"], + data = [ + ":README.md", + "//tools/nbictl/cmd/nbictl", + ], +) diff --git a/tools/nbictl/README.md b/tools/nbictl/README.md new file mode 100644 index 0000000..feae2cd --- /dev/null +++ b/tools/nbictl/README.md @@ -0,0 +1,217 @@ + + +# NAME + +nbictl - Interact with the Spacetime NBI service from the command line. + +# SYNOPSIS + +``` +nbictl [--profile=value] [--context=value] [--config_dir=value] [--help] [-h] [COMMAND OPTIONS] [ARGUMENTS...] +``` + +# GLOBAL OPTIONS + +**--config_dir**="": Directory to use for configuration. (default: $XDG_CONFIG_HOME/nbictl) + +**--help, -h**: show help + +**--profile, --context**="": Configuration profile to use. + +# COMMANDS + +## get + +Gets the entity with the given type and ID. + +**--id**="": [REQUIRED] ID of entity to delete. + +**--type, -t**="": [REQUIRED] Type of entity to delete. Allowed values: [ANTENNA_PATTERN, BAND_PROFILE, COMPUTED_MOTION, DEVICES_IN_REGION, INTENT, INTERFACE_LINK_REPORT, INTERFERENCE_CONSTRAINT, NETWORK_NODE, NETWORK_STATS_REPORT, PLATFORM_DEFINITION, SERVICE_REQUEST, STATION_SET, SURFACE_REGION, TRANSCEIVER_LINK_REPORT] + +## create + +Create one or more entities described in textproto files. + +**--files, -f**="": [REQUIRED] Glob of textproto files that represent one or more Entity messages. + +## edit + +Opens the specified entity as a textproto in $EDITOR, then updates the NBI's version with any updates made. + +**--id**="": [REQUIRED] ID of entity to edit. + +**--type, -t**="": [REQUIRED] Type of entity to edit. Allowed values: [ANTENNA_PATTERN, BAND_PROFILE, COMPUTED_MOTION, DEVICES_IN_REGION, INTENT, INTERFACE_LINK_REPORT, INTERFERENCE_CONSTRAINT, NETWORK_NODE, NETWORK_STATS_REPORT, PLATFORM_DEFINITION, SERVICE_REQUEST, STATION_SET, SURFACE_REGION, TRANSCEIVER_LINK_REPORT] + +## update + +Updates, or creates if missing, one or more entities described in textproto files. + +**--files, -f**="": [REQUIRED] Glob of textproto files that represent one or more Entity messages. + +**--ignore_consistency_check**: Always update or create the entity, without verifying that the provided `commit_timestamp` matches the currently stored entity. + +## list + +Lists all entities of a given type. + +**--field_masks**="": Comma-separated allow-list of fields to include in the response; see the aalyria.spacetime.api.nbi.v1alpha.EntityFilter.field_masks documentation for usage details. + +**--type, -t**="": [REQUIRED] Type of entities to query. Allowed values: [ANTENNA_PATTERN, BAND_PROFILE, COMPUTED_MOTION, DEVICES_IN_REGION, INTENT, INTERFACE_LINK_REPORT, INTERFERENCE_CONSTRAINT, NETWORK_NODE, NETWORK_STATS_REPORT, PLATFORM_DEFINITION, SERVICE_REQUEST, STATION_SET, SURFACE_REGION, TRANSCEIVER_LINK_REPORT] + +## delete + +Deletes one or more entities. Provide the type and ID to delete a single entity, or a directory of Entity textproto files to delete multiple entities. + +**--files, -f**="": Glob of textproto files that represent one or more Entity messages. + +**--id**="": ID of entity to delete. + +**--ignore_consistency_check**: Always update or create the entity, without verifying that the provided `commit_timestamp` matches the value in the currently stored entity. + +**--last_commit_timestamp**="": Delete the entity only if `last_commit_timestamp` matches the `commit_timestamp` of the currently stored entity. (default: 0) + +**--type, -t**="": Type of entity to delete. Allowed values: [ANTENNA_PATTERN, BAND_PROFILE, COMPUTED_MOTION, DEVICES_IN_REGION, INTENT, INTERFACE_LINK_REPORT, INTERFERENCE_CONSTRAINT, NETWORK_NODE, NETWORK_STATS_REPORT, PLATFORM_DEFINITION, SERVICE_REQUEST, STATION_SET, SURFACE_REGION, TRANSCEIVER_LINK_REPORT] + +## get-link-budget + +Gets link budget details + +**--analysis_end_timestamp**="": An RFC3339 formatted timestamp for the end of the interval to evaluate the signal propagation. If unset, the signal propagation is evaluated at the instant of the `analysis_start_timestamp.` + +**--analysis_start_timestamp**="": An RFC3339 formatted timestamp for the beginning of the interval to evaluate the signal propagation. Defaults to the current local timestamp. + +**--band_profile_id**="": The Entity ID of the BandProfile used for this link. + +**--explain_inaccessibility**: If true, the server will spend additional computational time determining the specific set of access constraints that were not satisfied and including these reasons in the response. + +**--input_file**="": A path to a textproto file containing a SignalPropagationRequest message. If set, it will be used as the request to the SignalPropagation service. If unset, the request will be built from the other flags. + +**--output_file**="": Path to a textproto file to write the response. If unset, defaults to stdout. (default: /dev/stdout) + +**--reference_data_timestamp**="": An RFC3339 formatted timestamp for the instant at which to reference the versions of the platforms. Defaults to `analysis_start_timestamp`. (default: analysis_start_timestamp) + +**--spatial_propagation_step_size**="": The analysis step size for spatial propagation metrics. (default: 1m) + +**--step_size**="": The analysis step size and the temporal resolution of the response. (default: 1m) + +**--target_platform_id**="": The Entity ID of the PlatformDefinition that represents the target. Leave unset if the antenna is fixed or non-steerable, in which case coverage calculations will be returned. + +**--target_transceiver_model_id**="": The ID of the transceiver model on the target.Leave unset if the antenna is fixed or non-steerable, in which case coverage calculations will be returned. + +**--tx_platform_id**="": The Entity ID of the PlatformDefinition that represents the transmitter. + +**--tx_transceiver_model_id**="": The ID of the transceiver model on the transmitter. + +## generate-keys + +Generate RSA keys to use for authentication with the Spacetime APIs. + +>After creating the Private-Public keypair, you will need to request API access by sharing the `.crt` file (a self-signed x509 certificate containing the public key) with Aalyria to receive the `USER_ID` and a `KEY_ID` needed to complete the nbictl configuration. Only share the public certificate (`.crt`) with Aalyria or third-parties. The private key (`.key`) must be protected and should never be sent by email or communicated to others. + +**--country**="": Country of certificate. + +**--dir, --directory**="": Directory to store the generated RSA keys in. (default: ~/.config/nbictl/keys) + +**--location**="": Location of certificate. + +**--org, --organization**="": [REQUIRED] Organization of certificate. + +**--state**="": State of certificate. + +## config + +Provides subcommands for managing nbictl configuration. + +### list-profiles + +List all configuration profiles (ignores any `--profile` flag) + +### describe + +Prints the NBI connection settings associated with the configuration profile given by the `--profile` flag (defaults to "DEFAULT"). + +### set + +Sets or updates a configuration profile settings. You can create multiple profiles by specifying the `--profile` flag (defaults to "DEFAULT"). + +**--key_id**="": Key ID associated with the private key provided by Aalyria. + +**--priv_key**="": Path to the private key to use for authentication. + +**--transport_security**="": Transport security to use when connecting to the NBI service. Allowed values: [insecure, system_cert_pool] + +**--url**="": NBI endpoint specified as `host[:port]` (port is optional and defaults to 443). + +**--user_id**="": User ID associated with the private key provided by Aalyria. + +## model + +Provides subcommands for accessing and managing the model elements comprising the digital twin. + +### upsert-entity + +Upsert the model NMTS Entity contained within the file provided on the command line ('-' reads from stdin). + +### update-entity + +Update the model using NMTS PartialEntity contained within the file provided on the command line ('-' reads from stdin). + +### delete-entity + +Delete the model NMTS Entity associated with the entity ID provided on the command line, along with any relationships in which it participates. + +### get-entity + +Get the model NMTS Entity associated with the entity ID given on the command line. + +### insert-relationship + +Insert the model NMTS Relationship contained within the file provided on the command line ('-' reads from stdin). + +### delete-relationship + +Delete the model NMTS Relationship contained within the file provided on the command line ('-' reads from stdin). + +### upsert-fragment + +Upsert the model NMTS Fragment contained within the file provided on the command line ('-' reads from stdin). + +### list-elements + +List all model elements (NMTS Entities and Relationships). + +## grpcurl + +Provides curl-like equivalents for interacting with the NBI. + +### describe + +Takes an optional fully-qualified symbol (service, enum, or message). If provided, the descriptor for that symbol is shown. If not provided, the descriptor for all exposed or known services are shown. + +### list + +Takes an optional fully-qualified service name. If provided, lists all methods of that service. If not provided, all exposed services are listed. + +### call, invoke + +Takes a fully-qualified method name in 'service.method' or 'service/method' format. Invokes the method using the provided request body. + +**--format, -f**="": Protobuf format to use for input and output. Allowed values: [text, json] (default: json) + +**--request, -r**="": File containing the request to make encoded in the selected --format. Defaults to -, which uses stdin. (default: -) + +## generate-auth-token + +Generate a self-signed JWT token for API authentication. + +**--audience, --aud**="": The audience (aud) to set in the JWT token. + +**--expiration, --exp**="": The validity duration of token, from the time of creation. (default: 1h) + +## help, h + +Shows a list of commands or help for one command + diff --git a/tools/nbictl/cmd/nbictl/BUILD b/tools/nbictl/cmd/nbictl/BUILD new file mode 100644 index 0000000..f791d6f --- /dev/null +++ b/tools/nbictl/cmd/nbictl/BUILD @@ -0,0 +1,31 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build rules for the Go CDPI agent. These shouldn't depend on any other +# internal packages apart from those found in //api. + +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "nbictl_lib", + srcs = ["nbictl.go"], + importpath = "aalyria.com/spacetime/github/tools/nbictl/cmd/nbictl", + deps = ["//tools/nbictl"], +) + +go_binary( + name = "nbictl", + embed = [":nbictl_lib"], + visibility = ["//visibility:public"], +) diff --git a/tools/nbictl/cmd/nbictl/nbictl.go b/tools/nbictl/cmd/nbictl/nbictl.go new file mode 100644 index 0000000..2c0eaf0 --- /dev/null +++ b/tools/nbictl/cmd/nbictl/nbictl.go @@ -0,0 +1,29 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + "aalyria.com/spacetime/github/tools/nbictl" +) + +func main() { + if err := nbictl.App().Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "fatal error: %v\n", err) + os.Exit(1) + } +} diff --git a/tools/nbictl/config.go b/tools/nbictl/config.go new file mode 100644 index 0000000..96639b9 --- /dev/null +++ b/tools/nbictl/config.go @@ -0,0 +1,257 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "aalyria.com/spacetime/github/tools/nbictl/nbictlpb" + "github.com/urfave/cli/v2" + "google.golang.org/protobuf/encoding/prototext" +) + +func getAppConfDir(appCtx *cli.Context) (string, error) { + if appCtx.IsSet("config_dir") { + appConfDir := appCtx.String("config_dir") + if appConfDir == "" { + return "", errors.New("--config_dir can't be empty") + } + return appConfDir, nil + } + + confDir, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("unable to obtain the default config directory: %w", err) + } + return filepath.Join(confDir, appCtx.App.Name), nil +} + +func readConfig(context, confFilePath string) (*nbictlpb.Config, error) { + confs, err := readConfigs(confFilePath) + if err != nil { + return nil, fmt.Errorf("unable to get config contexts: %w", err) + } + + // if the context name is not specified and there is only one context in the config file + // the function will return that configuration context + if context == "" { + switch { + case len(confs.GetConfigs()) == 1: + return confs.GetConfigs()[0], nil + default: + return nil, errors.New("--context flag required because there are multiple contexts defined in the configuration.") + } + } + + var confNames []string + for _, conf := range confs.GetConfigs() { + if conf.GetName() == context { + return conf, nil + } + confNames = append(confNames, conf.GetName()) + } + return nil, fmt.Errorf("unable to get the context with the name: %q (expected one of [%s])", context, strings.Join(confNames, ", ")) +} + +func readConfigs(confFilePath string) (*nbictlpb.AppConfig, error) { + confProto := &nbictlpb.AppConfig{} + confBytes, err := os.ReadFile(confFilePath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("unable to read file: %w.\nSee `%s config -h` to learn how to configure the tool.", err, appName) + } + return nil, fmt.Errorf("unable to read file: %w", err) + } + + if err := prototext.Unmarshal(confBytes, confProto); err != nil { + return nil, fmt.Errorf("invalid file content: %w", err) + } + return confProto, nil +} + +func getConfFileForContext(appCtx *cli.Context) (string, error) { + confDir, err := getAppConfDir(appCtx) + if err != nil { + return "", fmt.Errorf("unable to obtain the default config directory: %w", err) + } + + return filepath.Join(confDir, confFileName), nil +} + +func ListConfigs(appCtx *cli.Context) error { + confFile, err := getConfFileForContext(appCtx) + if err != nil { + return err + } + confProto, err := readConfigs(confFile) + if err != nil { + return err + } + + for _, profile := range confProto.GetConfigs() { + fmt.Fprintln(appCtx.App.Writer, profile.GetName()) + } + + return nil +} + +func GetConfig(appCtx *cli.Context) error { + confFile, err := getConfFileForContext(appCtx) + if err != nil { + return err + } + confProto, err := readConfigs(confFile) + if err != nil { + return err + } + + confName := "DEFAULT" + if appCtx.IsSet("context") { + confName = appCtx.String("context") + } + + for _, profile := range confProto.GetConfigs() { + if profile.GetName() == confName { + protoMessage, err := prototext.MarshalOptions{Multiline: true}.Marshal(profile) + if err != nil { + return err + } + fmt.Fprint(appCtx.App.Writer, string(protoMessage)) + return nil + } + } + + return fmt.Errorf("unable to find config %q in file %q.", confName, confFile) +} + +func SetConfig(appCtx *cli.Context) error { + confName := "DEFAULT" + if appCtx.IsSet("context") { + confName = appCtx.String("context") + } + privKey := appCtx.String("priv_key") + keyID := appCtx.String("key_id") + userID := appCtx.String("user_id") + url := appCtx.String("url") + transportSecurity := appCtx.String("transport_security") + + confPath, err := getConfFileForContext(appCtx) + if err != nil { + return err + } + + var transportSecurityPb *nbictlpb.Config_TransportSecurity + + switch transportSecurity { + case "insecure": + transportSecurityPb = &nbictlpb.Config_TransportSecurity{ + Type: &nbictlpb.Config_TransportSecurity_Insecure{}, + } + + case "system_cert_pool": + transportSecurityPb = &nbictlpb.Config_TransportSecurity{ + Type: &nbictlpb.Config_TransportSecurity_SystemCertPool{}, + } + + case "": + transportSecurityPb = nil + + default: + return fmt.Errorf("unexpected transport security selection: %s", transportSecurity) + } + + contextToCreate := &nbictlpb.Config{ + Name: confName, + KeyId: keyID, + Email: userID, + PrivKey: privKey, + Url: url, + TransportSecurity: transportSecurityPb, + } + + return setConfig(appCtx.App.Writer, appCtx.App.ErrWriter, contextToCreate, confPath) +} + +func setConfig(outWriter, errWriter io.Writer, confToCreate *nbictlpb.Config, confFile string) error { + if confToCreate.GetName() == "" { + return errors.New("missing required --context flag") + } + + confProto, err := readConfigs(confFile) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + confProto = &nbictlpb.AppConfig{} + } else { + return fmt.Errorf("unable to get configs from file %s: %w", confFile, err) + } + } + + found := false + for _, confProto := range confProto.GetConfigs() { + if confProto.GetName() != confToCreate.GetName() { + continue + } + if confToCreate.GetEmail() != "" { + confProto.Email = confToCreate.GetEmail() + } + if confToCreate.GetPrivKey() != "" { + confProto.PrivKey = confToCreate.GetPrivKey() + } + if confToCreate.GetKeyId() != "" { + confProto.KeyId = confToCreate.GetKeyId() + } + if confToCreate.GetUrl() != "" { + confProto.Url = confToCreate.GetUrl() + } + if confToCreate.GetTransportSecurity() != nil { + confProto.TransportSecurity = confToCreate.GetTransportSecurity() + } + found = true + confToCreate = confProto + break + } + + if !found { + confProto.Configs = append(confProto.Configs, confToCreate) + } + + nbiConfigTextProto, err := prototext.MarshalOptions{Multiline: true}.Marshal(confProto) + if err != nil { + return fmt.Errorf("unable to convert proto into textproto format: %w", err) + } + + contextDir := filepath.Dir(confFile) + if err = os.MkdirAll(contextDir, 0o777); err != nil { + return fmt.Errorf("unable to create directory: %w", err) + } + + if err = os.WriteFile(confFile, nbiConfigTextProto, 0o777); err != nil { + return fmt.Errorf("unable to update the configuration information: %w", err) + } + + protoMessage, err := prototext.MarshalOptions{Multiline: true}.Marshal(confToCreate) + if err != nil { + return fmt.Errorf("unable to convert the nbictl context into textproto format: %w", err) + } + fmt.Fprintf(errWriter, "configuration successfully updated; the configuration file is stored under: %s\n", confFile) + fmt.Fprint(outWriter, string(protoMessage)) + return nil +} diff --git a/tools/nbictl/config_test.go b/tools/nbictl/config_test.go new file mode 100644 index 0000000..298847f --- /dev/null +++ b/tools/nbictl/config_test.go @@ -0,0 +1,276 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + + "aalyria.com/spacetime/github/tools/nbictl/nbictlpb" + "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +var ( + testConfig = &nbictlpb.Config{ + Name: "unit_testing", + KeyId: "privateKey.id", + Email: "privateKey.userID", + PrivKey: "privateKey.path", + Url: "test_url", + } + testConfigForUpdate = &nbictlpb.Config{ + Name: "test update", + KeyId: "update_key_id", + Email: "update_user_id", + PrivKey: "update_priv_key", + Url: "update_url", + } + testConfigs = &nbictlpb.AppConfig{ + Configs: []*nbictlpb.Config{testConfig}, + } +) + +func TestReadContext(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + + got, err := readConfig(testConfig.GetName(), confFile) + checkErr(t, err) + + assertProtosEqual(t, testConfig, got) +} + +func TestReadContext_WithOnlyOneContextInConfig(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + + got, err := readConfig("", confFile) + checkErr(t, err) + + assertProtosEqual(t, testConfig, got) +} + +func TestReadContext_WithNoContextWithMultipleContextInConfig(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + for _, contextToCreate := range []string{"test_1", "test_2", "test_3"} { + checkErr(t, setConfig(io.Discard, io.Discard, &nbictlpb.Config{Name: contextToCreate}, confFile)) + } + + _, err = readConfig("", confFile) + want := "--context flag required because there are multiple contexts defined in the configuration" + if got := err.Error(); !strings.Contains(got, want) { + t.Fatalf("want: %s, got %s", want, got) + } +} + +func TestReadContexts_WithFileWithNoPermission(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + file, err := os.Create(confFile) + checkErr(t, err) + defer file.Close() + + checkErr(t, os.Chmod(confFile, 0000)) + + if _, err = readConfigs(confFile); err == nil { + t.Fatal("unable to detect that issues with selected config file") + } +} + +func TestReadContext_WithNonExistingContextName(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + for _, contextToCreate := range []string{"test_1", "test_2", "test_3"} { + checkErr(t, setConfig(io.Discard, io.Discard, &nbictlpb.Config{Name: contextToCreate}, confFile)) + } + + _, err = readConfig("non_existing", confFile) + wantErrMsg := `unable to get the context with the name: "non_existing" (expected one of [test_1, test_2, test_3])` + if gotErrMsg := err.Error(); !strings.Contains(gotErrMsg, wantErrMsg) { + t.Fatalf("want: %s, got %s", wantErrMsg, gotErrMsg) + } +} + +func TestSetConfig_WithNoUpdate(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + wantContexts, err := readConfigs(confFile) + checkErr(t, err) + assertProtosEqual(t, testConfigs, wantContexts) + + contextWithNoChange := &nbictlpb.Config{Name: testConfig.GetName()} + checkErr(t, setConfig(io.Discard, io.Discard, contextWithNoChange, confFile)) + + gotContexts, err := readConfigs(confFile) + checkErr(t, err) + assertProtosEqual(t, wantContexts, gotContexts) +} + +func TestSetConfig_UpdatePrivateKey(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + // initial setup + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + checkErr(t, setConfig(io.Discard, io.Discard, testConfigForUpdate, confFile)) + + // update the existing context with a new private key + checkErr(t, setConfig(io.Discard, io.Discard, &nbictlpb.Config{ + Name: testConfig.GetName(), + PrivKey: "private_key.updated", + }, confFile)) + gotContexts, err := readConfigs(confFile) + checkErr(t, err) + + // check that the private key is updated + updatedContext := proto.Clone(testConfig).(*nbictlpb.Config) + updatedContext.PrivKey = "private_key.updated" + wantContexts := &nbictlpb.AppConfig{ + Configs: []*nbictlpb.Config{updatedContext, testConfigForUpdate}, + } + assertProtosEqual(t, wantContexts, gotContexts) +} + +func TestSetConfig_UpdateKeyId(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + // initial setup + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + checkErr(t, setConfig(io.Discard, io.Discard, testConfigForUpdate, confFile)) + + // update the existing context with a new key id + checkErr(t, setConfig(io.Discard, io.Discard, &nbictlpb.Config{ + Name: testConfigForUpdate.GetName(), + KeyId: "key_id.updated", + }, confFile)) + gotContexts, err := readConfigs(confFile) + checkErr(t, err) + + // check that the key id is updated + updatedContext := proto.Clone(testConfigForUpdate).(*nbictlpb.Config) + updatedContext.KeyId = "key_id.updated" + wantContexts := &nbictlpb.AppConfig{ + Configs: []*nbictlpb.Config{testConfig, updatedContext}, + } + assertProtosEqual(t, wantContexts, gotContexts) +} + +func TestSetConfig_UpdateUserID(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + // initial setup + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + checkErr(t, setConfig(io.Discard, io.Discard, testConfigForUpdate, confFile)) + + // update the existing context with a new user id + checkErr(t, setConfig(io.Discard, io.Discard, &nbictlpb.Config{ + Name: testConfig.GetName(), + Email: "email.updated", + }, confFile)) + gotContexts, err := readConfigs(confFile) + checkErr(t, err) + + // check that the user id is updated + updatedContext := proto.Clone(testConfig).(*nbictlpb.Config) + updatedContext.Email = "email.updated" + wantContexts := &nbictlpb.AppConfig{ + Configs: []*nbictlpb.Config{updatedContext, testConfigForUpdate}, + } + assertProtosEqual(t, wantContexts, gotContexts) +} + +func TestSetConfig_UpdateUrl(t *testing.T) { + t.Parallel() + + confDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + confFile := filepath.Join(confDir, confFileName) + + // initial setup + checkErr(t, setConfig(io.Discard, io.Discard, testConfig, confFile)) + checkErr(t, setConfig(io.Discard, io.Discard, testConfigForUpdate, confFile)) + checkErr(t, setConfig(io.Discard, io.Discard, &nbictlpb.Config{ + Name: testConfig.GetName(), + Url: "url.updated", + }, confFile)) + gotContexts, err := readConfigs(confFile) + checkErr(t, err) + + // check that the url is updated + updatedContext := proto.Clone(testConfig).(*nbictlpb.Config) + updatedContext.Url = "url.updated" + wantContexts := &nbictlpb.AppConfig{ + Configs: []*nbictlpb.Config{updatedContext, testConfigForUpdate}, + } + assertProtosEqual(t, wantContexts, gotContexts) +} + +func checkErr(t *testing.T, err error) { + t.Helper() + + if err != nil { + t.Fatal(err) + } +} + +func assertProtosEqual(t *testing.T, want, got interface{}) { + t.Helper() + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Fatalf("proto mismatch: (-want +got):\n%s", diff) + } +} diff --git a/tools/nbictl/connection.go b/tools/nbictl/connection.go new file mode 100644 index 0000000..b7203f1 --- /dev/null +++ b/tools/nbictl/connection.go @@ -0,0 +1,158 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "bytes" + "context" + "crypto/x509" + "errors" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/jonboulle/clockwork" + "github.com/urfave/cli/v2" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding/gzip" + _ "google.golang.org/grpc/encoding/gzip" // Install the gzip compressor + + "aalyria.com/spacetime/auth" + "aalyria.com/spacetime/github/tools/nbictl/nbictlpb" +) + +func openConnection(appCtx *cli.Context) (*grpc.ClientConn, error) { + return openAPIConnection(appCtx, "") +} + +func openAPIConnection(appCtx *cli.Context, apiSubDomain string) (*grpc.ClientConn, error) { + ctxName := appCtx.String("context") + + appConfDir, err := getAppConfDir(appCtx) + if err != nil { + return nil, err + } + setting, err := readConfig(ctxName, filepath.Join(appConfDir, confFileName)) + if err != nil { + return nil, fmt.Errorf("unable to obtain context information: %w", err) + } + var containsDnsSchema bool + setting.Url, containsDnsSchema = strings.CutPrefix(setting.GetUrl(), "dns://") + if containsDnsSchema { + fmt.Fprintf(appCtx.App.ErrWriter, "Warning: the URL setting should not contain the dns:// prefix, please provide only host[:port]\n") + } + setting.Url = adjustURLForAPISubDomain(setting.GetUrl(), apiSubDomain) + return dial(appCtx.Context, setting, nil) +} + +func adjustURLForAPISubDomain(url string, apiSubDomain string) string { + // Unexpectedly empty arguments or already the subdomain sought. + if url == "" || apiSubDomain == "" || strings.HasPrefix(url, apiSubDomain+".") { + return url + } + + // If the |url| is an ip:port then best to leave it alone. + if host, _, err := net.SplitHostPort(url); err == nil { + if net.ParseIP(host) != nil { + return url + } + } + + // Earlier uses recommended setting the URL to "nbi.". + url = strings.TrimPrefix(url, "nbi.") + + return apiSubDomain + "." + url +} + +func dial(ctx context.Context, setting *nbictlpb.Config, httpClient *http.Client) (*grpc.ClientConn, error) { + dialOpts, err := getDialOpts(ctx, setting, httpClient) + if err != nil { + return nil, fmt.Errorf("unable to construct dial options: %w", err) + } + conn, err := grpc.NewClient(setting.GetUrl(), dialOpts...) + if err != nil { + return nil, fmt.Errorf("unable to connect to the server: %w", err) + } + return conn, nil +} + +func getDialOpts(ctx context.Context, setting *nbictlpb.Config, httpClient *http.Client) ([]grpc.DialOption, error) { + dialOpts := []grpc.DialOption{ + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*256), grpc.UseCompressor(gzip.Name)), + } + + switch t := setting.GetTransportSecurity().GetType().(type) { + case *nbictlpb.Config_TransportSecurity_Insecure: + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + + case *nbictlpb.Config_TransportSecurity_ServerCertificate_: + clientTLSFromFile, err := credentials.NewClientTLSFromFile(t.ServerCertificate.GetCertFilePath(), "") + if err != nil { + return nil, fmt.Errorf("creating TLS credentials from certificate file: %w", err) + } + dialOpts = append(dialOpts, grpc.WithTransportCredentials(clientTLSFromFile)) + + // SystemCertPoll is the default option in case transport_security is not set (nil). + case nil, *nbictlpb.Config_TransportSecurity_SystemCertPool: + cp, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("reading system tls cert pool: %w", err) + } + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(cp, ""))) + + default: + return nil, fmt.Errorf("unexpected transport security selection: %T", t) + } + + // Unless transport-security is set to Insecure, add Spacetime PerRPCCredentials. + if _, insecure := setting.GetTransportSecurity().GetType().(*nbictlpb.Config_TransportSecurity_Insecure); !insecure { + host, _, err := net.SplitHostPort(setting.GetUrl()) + if err != nil { + return nil, fmt.Errorf("parsing %q: %w", setting.GetUrl(), err) + } + if setting.GetPrivKey() == "" { + return nil, errors.New("no private key set for chosen context") + } + pkeyBytes, err := os.ReadFile(setting.GetPrivKey()) + if err != nil { + return nil, fmt.Errorf("unable to read the file: %w", err) + } + privateKey := bytes.NewBuffer(pkeyBytes) + clock := clockwork.NewRealClock() + + config := auth.Config{ + Client: httpClient, + Clock: clock, + PrivateKey: privateKey, + PrivateKeyID: setting.GetKeyId(), + Email: setting.GetEmail(), + Host: host, + } + + creds, err := auth.NewCredentials(ctx, config) + if err != nil { + return nil, fmt.Errorf("unable to get new credentials with provided information: %w", err) + } + + dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(creds)) + } + + return dialOpts, nil +} diff --git a/tools/nbictl/connection_test.go b/tools/nbictl/connection_test.go new file mode 100644 index 0000000..86f0bdc --- /dev/null +++ b/tools/nbictl/connection_test.go @@ -0,0 +1,204 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "context" + "crypto/tls" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/bazelbuild/rules_go/go/tools/bazel" + "golang.org/x/sync/errgroup" + + nbi "aalyria.com/spacetime/api/nbi/v1alpha" + "aalyria.com/spacetime/auth/authtest" + "aalyria.com/spacetime/github/tools/nbictl/nbictlpb" +) + +const ( + authHeader = "authorization" + proxyAuthHeader = "proxy-authorization" + oidcToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjcyMTk0YjI2MzU0YzIzYzBiYTU5YTZkNzUxZGZmYWEyNTg2NTkwNGUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjQvdG9rZW4iLCJleHAiOjE2ODE3OTIzMTksImlhdCI6MTY4MTc4ODcxOSwiaXNzIjoiY2RwaS1hZ2VudEBhNWEtc3BhY2V0aW1lLWdrZS1iYWNrLWRldi5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6ImNkcGktYWdlbnRAYTVhLXNwYWNldGltZS1na2UtYmFjay1kZXYuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJ0YXJnZXRfYXVkaWVuY2UiOiI2MDI5MjQwMzEzOS1tZTY4dGpnYWpsNWRjZGJwbmxtMmVrODMwbHZzbnNscS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSJ9.QyOi7vkFCwdmjT4ChT3_yVY4ZObUJkZkYC0q7alF_thiotdJKRiSo1ZHp_XnS0nM4WSWcQYLGHUDdAMPS0R22brFGzCl8ndgNjqI38yp_LDL8QVTqnLBGUj-m3xB5wH17Q_Dt8riBB4IE-mSS8FB-R6sqSwn-seMfMDydScC0FrtOF3-2BCYpIAlf1AQKN083QdtKgNEVDi72npPr2MmsWV3tct6ydXHWNbxG423kfSD6vCZSUTvWXAuVjuOwnbc2LHZS04U-jiLpvHxu06OwHOQ5LoGVPyd69o8Ny_Bapd2m0YCX2xJr8_HH2nw1jH7EplFf-owbBYz9ZtQoQ2YTA` +) + +// LocalhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw +MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r +bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U +aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P +YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk +POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu +h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE +AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv +bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI +5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv +cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2 ++tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B +grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK +5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/ +WkBKOclmOV2xlTVuPw== +-----END CERTIFICATE-----`) + +// LocalhostKey is the private key for LocalhostCert. +var LocalhostKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi +4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS +gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW +URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX +AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy +VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK +x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk +lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL +dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89 +EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq +XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki +6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O +3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s +uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ +Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ +w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo ++bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP +OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA +brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv +m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y +LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN +/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN +s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ +Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0 +xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/ +ZboOWVe3icTy64BT3OQhmg== +-----END RSA TESTING KEY-----`)) + +func TestDial_insecure(t *testing.T) { + t.Parallel() + + // Start fake NBI server and a timeout for the test + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + g, ctx := errgroup.WithContext(ctx) + defer func() { checkErr(t, g.Wait()) }() + defer cancel() + + srv := startInsecureServer(ctx, t, g) + // Invoke OpenConnection + nbiConf := &nbictlpb.Config{ + Url: srv.listener.Addr().String(), + TransportSecurity: &nbictlpb.Config_TransportSecurity{ + Type: &nbictlpb.Config_TransportSecurity_Insecure{}, + }, + } + conn, err := dial(ctx, nbiConf, nil) + checkErr(t, err) + defer conn.Close() + + // Test a gRPC method invocation + client := nbi.NewNetOpsClient(conn) + _, err = client.ListEntities(ctx, &nbi.ListEntitiesRequest{Type: nbi.EntityType_ANTENNA_PATTERN.Enum()}) + checkErr(t, err) + + // Verify that the method invocation reached the server + if srv.NumCallsListEntities.Load() != 1 { + t.Fatal("ListEntities has not been invoked correctly") + } + // Verify that when transportSecurity = insecure, the gRPC headers + // authHeader and proxyAuthHeader are NOT transmitted to the server. + if len(srv.IncomingMetadata[0].Get(authHeader)) > 0 { + t.Fatal("Unexpected Incoming Metadata: ", authHeader) + } + if len(srv.IncomingMetadata[0].Get(proxyAuthHeader)) > 0 { + t.Fatal("Unexpected Incoming Metadata: ", proxyAuthHeader) + } +} + +func TestDial_serverCertificate(t *testing.T) { + t.Parallel() + + // Store on filesystem the localhost certificate and the user priv/pub key + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + serverCertPath := filepath.Join(tmpDir, "localhost.crt.tls") + checkErr(t, os.WriteFile(serverCertPath, LocalhostCert, 0o644)) + + userKeys := generateKeysForTesting(t, tmpDir, "--org", "user.organization") + + // Start fake NBI server WITH TLS and a timeout for the test + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + g, ctx := errgroup.WithContext(ctx) + defer func() { checkErr(t, g.Wait()) }() + defer cancel() + cert, _ := tls.X509KeyPair(LocalhostCert, LocalhostKey) + lis, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{cert}}) + checkErr(t, err) + fakeGrpcServer, err := startFakeNbiServer(ctx, g, lis) + checkErr(t, err) + + // Start a fake OIDCServer + ts := authtest.NewOIDCServer(oidcToken) + defer ts.Close() + + // Invoke OpenConnection + nbiConf := &nbictlpb.Config{ + Url: lis.Addr().String(), + PrivKey: userKeys.key, + Name: "test", + KeyId: "1", + Email: "some@example.com", + TransportSecurity: &nbictlpb.Config_TransportSecurity{ + Type: &nbictlpb.Config_TransportSecurity_ServerCertificate_{ + ServerCertificate: &nbictlpb.Config_TransportSecurity_ServerCertificate{ + CertFilePath: serverCertPath, + }, + }, + }, + } + conn, err := dial(ctx, nbiConf, ts.Client()) + checkErr(t, err) + defer conn.Close() + + // Test a gRPC method invocation + client := nbi.NewNetOpsClient(conn) + _, err = client.ListEntities(ctx, &nbi.ListEntitiesRequest{Type: nbi.EntityType_ANTENNA_PATTERN.Enum()}) + checkErr(t, err) + + // Verify that the method invocation reached the server + if fakeGrpcServer.NumCallsListEntities.Load() != 1 { + t.Fatal("ListEntities has not been invoked correctly") + } + // Verify that when transportSecurity != insecure, the gRPC headers + // authHeader and proxyAuthHeader are transmitted to the server. + if len(fakeGrpcServer.IncomingMetadata[0].Get(authHeader)) != 1 { + t.Fatal("Missing incoming metadata: ", authHeader) + } + if len(fakeGrpcServer.IncomingMetadata[0].Get(proxyAuthHeader)) != 1 { + t.Fatal("Missing incoming metadata: ", proxyAuthHeader) + } + if want, got := "Bearer "+oidcToken, fakeGrpcServer.IncomingMetadata[0].Get(proxyAuthHeader)[0]; want != got { + t.Fatalf("fakeGrpcServer received the wrong proxyAuthHeader header: got %+v, wanted %+v ", got, want) + } +} + +func testingKey(s string) string { + return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") +} diff --git a/tools/nbictl/fake_modelapi_server_test.go b/tools/nbictl/fake_modelapi_server_test.go new file mode 100644 index 0000000..627b708 --- /dev/null +++ b/tools/nbictl/fake_modelapi_server_test.go @@ -0,0 +1,117 @@ +// Copyright (c) Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "context" + "net" + + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" + + modelpb "aalyria.com/spacetime/api/model/v1alpha" +) + +type FakeModelServer struct { + listener net.Listener + + modelpb.UnimplementedModelServer + ResponseError error + ResponseMessage proto.Message + RequestMessage proto.Message +} + +func (s *FakeModelServer) Reset() { + s.ResponseError = nil + s.ResponseMessage = nil + s.RequestMessage = nil +} + +// Handle any of the gRPC calls for simplistic testing: +// +// - store the request for inspecton by the test +// - if a response error is present, return the error +// - else return the response message +func handleCall[RespT proto.Message](s *FakeModelServer, ctx context.Context, req proto.Message) (RespT, error) { + var nilRespT RespT + + s.RequestMessage = req + if s.ResponseError != nil { + return nilRespT, s.ResponseError + } else { + resp := s.ResponseMessage.(RespT) + return resp, nil + } +} + +func (s *FakeModelServer) UpsertEntity(ctx context.Context, req *modelpb.UpsertEntityRequest) (*modelpb.UpsertEntityResponse, error) { + s.RequestMessage = req + if s.ResponseError != nil { + return nil, s.ResponseError + } else { + resp := s.ResponseMessage.(*modelpb.UpsertEntityResponse) + return resp, nil + } +} + +func (s *FakeModelServer) UpdateEntity(ctx context.Context, req *modelpb.UpdateEntityRequest) (*modelpb.UpdateEntityResponse, error) { + return handleCall[*modelpb.UpdateEntityResponse](s, ctx, req) +} + +func (s *FakeModelServer) DeleteEntity(ctx context.Context, req *modelpb.DeleteEntityRequest) (*modelpb.DeleteEntityResponse, error) { + return handleCall[*modelpb.DeleteEntityResponse](s, ctx, req) +} + +func (s *FakeModelServer) InsertRelationship(ctx context.Context, req *modelpb.InsertRelationshipRequest) (*modelpb.InsertRelationshipResponse, error) { + return handleCall[*modelpb.InsertRelationshipResponse](s, ctx, req) +} + +func (s *FakeModelServer) DeleteRelationship(ctx context.Context, req *modelpb.DeleteRelationshipRequest) (*emptypb.Empty, error) { + return handleCall[*emptypb.Empty](s, ctx, req) +} + +func (s *FakeModelServer) GetEntity(ctx context.Context, req *modelpb.GetEntityRequest) (*modelpb.GetEntityResponse, error) { + return handleCall[*modelpb.GetEntityResponse](s, ctx, req) +} + +func (s *FakeModelServer) ListElements(ctx context.Context, req *modelpb.ListElementsRequest) (*modelpb.ListElementsResponse, error) { + return handleCall[*modelpb.ListElementsResponse](s, ctx, req) +} + +func startFakeModelServer(ctx context.Context, g *errgroup.Group, listener net.Listener) (*FakeModelServer, error) { + fakeModelServer := &FakeModelServer{ + listener: listener, + } + fakeModelServer.Reset() + server := grpc.NewServer() + modelpb.RegisterModelServer(server, fakeModelServer) + reflection.Register(server) + + g.Go(func() error { + return server.Serve(listener) + }) + + g.Go(func() error { + <-ctx.Done() + server.GracefulStop() + listener.Close() + return nil + }) + + return fakeModelServer, nil +} diff --git a/tools/nbictl/fake_nbi_server_test.go b/tools/nbictl/fake_nbi_server_test.go new file mode 100644 index 0000000..5be5a8d --- /dev/null +++ b/tools/nbictl/fake_nbi_server_test.go @@ -0,0 +1,147 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "context" + "net" + "sync" + "sync/atomic" + + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/reflection" + "google.golang.org/protobuf/proto" + + nbi "aalyria.com/spacetime/api/nbi/v1alpha" +) + +const DEFAULT_COMMIT_TIMESTAMP = int64(123456) + +type FakeNetOpsServer struct { + listener net.Listener + + nbi.UnimplementedNetOpsServer + IncomingMetadata []metadata.MD + NumCallsListEntities *atomic.Int64 + ListEntityResponse *nbi.ListEntitiesResponse + LatestRequest proto.Message + + EntityIDsModified map[string]struct{} + // Synchronizes access to EntityIDsModified. + mu sync.Mutex +} + +func (s *FakeNetOpsServer) ListEntities(ctx context.Context, req *nbi.ListEntitiesRequest) (*nbi.ListEntitiesResponse, error) { + md := make(metadata.MD) + md, _ = metadata.FromIncomingContext(ctx) + s.LatestRequest = req + s.IncomingMetadata = append(s.IncomingMetadata, md) + s.NumCallsListEntities.Add(1) + return s.ListEntityResponse, nil +} + +// Returns the Entity in the request, with the default commit timestamp. +// Assumes that the ID has been set in the Entity within the CreateEntityRequest. +func (s *FakeNetOpsServer) CreateEntity(ctx context.Context, req *nbi.CreateEntityRequest) (*nbi.Entity, error) { + md := make(metadata.MD) + md, _ = metadata.FromIncomingContext(ctx) + s.LatestRequest = req + s.IncomingMetadata = append(s.IncomingMetadata, md) + + s.mu.Lock() + defer s.mu.Unlock() + s.EntityIDsModified[req.GetEntity().GetId()] = struct{}{} + + res := *req.GetEntity() + res.CommitTimestamp = proto.Int64(DEFAULT_COMMIT_TIMESTAMP) + return &res, nil +} + +// Returns an Entity with the same type and ID as in the request, +// along with the default commit timestamp. +// This method does not increment EntityIDsModified. +func (s *FakeNetOpsServer) GetEntity(ctx context.Context, req *nbi.GetEntityRequest) (*nbi.Entity, error) { + md := make(metadata.MD) + md, _ = metadata.FromIncomingContext(ctx) + s.LatestRequest = req + s.IncomingMetadata = append(s.IncomingMetadata, md) + + res := &nbi.Entity{ + Id: req.Id, + Group: &nbi.EntityGroup{ + Type: req.Type, + }, + CommitTimestamp: proto.Int64(DEFAULT_COMMIT_TIMESTAMP), + } + return res, nil +} + +// Returns the Entity in the request, with the default commit timestamp. +func (s *FakeNetOpsServer) UpdateEntity(ctx context.Context, req *nbi.UpdateEntityRequest) (*nbi.Entity, error) { + md := make(metadata.MD) + md, _ = metadata.FromIncomingContext(ctx) + s.LatestRequest = req + s.IncomingMetadata = append(s.IncomingMetadata, md) + + s.mu.Lock() + defer s.mu.Unlock() + s.EntityIDsModified[req.GetEntity().GetId()] = struct{}{} + + res := *req.GetEntity() + res.CommitTimestamp = proto.Int64(DEFAULT_COMMIT_TIMESTAMP) + return &res, nil +} + +// Returns a DeleteEntityResponse regardless of the request. +func (s *FakeNetOpsServer) DeleteEntity(ctx context.Context, req *nbi.DeleteEntityRequest) (*nbi.DeleteEntityResponse, error) { + md := make(metadata.MD) + md, _ = metadata.FromIncomingContext(ctx) + s.LatestRequest = req + s.IncomingMetadata = append(s.IncomingMetadata, md) + + s.mu.Lock() + defer s.mu.Unlock() + s.EntityIDsModified[req.GetId()] = struct{}{} + + return &nbi.DeleteEntityResponse{}, nil +} + +func startFakeNbiServer(ctx context.Context, g *errgroup.Group, listener net.Listener) (*FakeNetOpsServer, error) { + fakeNbiServer := &FakeNetOpsServer{ + IncomingMetadata: make([]metadata.MD, 0), + NumCallsListEntities: &atomic.Int64{}, + EntityIDsModified: make(map[string]struct{}, 0), + listener: listener, + ListEntityResponse: &nbi.ListEntitiesResponse{}, + } + server := grpc.NewServer() + nbi.RegisterNetOpsServer(server, fakeNbiServer) + reflection.Register(server) + + g.Go(func() error { + return server.Serve(listener) + }) + + g.Go(func() error { + <-ctx.Done() + server.GracefulStop() + listener.Close() + return nil + }) + + return fakeNbiServer, nil +} diff --git a/tools/nbictl/generate_auth_token.go b/tools/nbictl/generate_auth_token.go new file mode 100644 index 0000000..cfd2d76 --- /dev/null +++ b/tools/nbictl/generate_auth_token.go @@ -0,0 +1,107 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/urfave/cli/v2" +) + +func GenerateAuthToken(appCtx *cli.Context) error { + ctxName := appCtx.String("context") + confFile, err := getConfFileForContext(appCtx) + if err != nil { + return err + } + c, err := readConfig(ctxName, confFile) + if err != nil { + return fmt.Errorf("unable to obtain context information: %w", err) + } + if c.GetEmail() == "" { + return errors.New("no user_id set for chosen context") + } + if c.GetKeyId() == "" { + return errors.New("no key_id set for chosen context") + } + if c.GetPrivKey() == "" { + return errors.New("no priv_key set for chosen context") + } + pkeyBytes, err := os.ReadFile(c.GetPrivKey()) + if err != nil { + return fmt.Errorf("unable to read the priv_key file: %w", err) + } + pkeyBlock, _ := pem.Decode(pkeyBytes) + if pkeyBlock == nil { + return errors.New("PrivateKey not PEM-encoded") + } + pkey, err := parsePrivateKey(pkeyBlock.Bytes) + if err != nil { + return fmt.Errorf("error while parsing priv_key file (%s): %w", c.GetPrivKey(), err) + } + + now := time.Now() + claims := jwt.MapClaims{ + "iss": c.GetEmail(), + "sub": c.GetEmail(), + "iat": jwt.NewNumericDate(now), + "exp": jwt.NewNumericDate(now.Add(appCtx.Duration("expiration"))), + } + if aud := appCtx.String("audience"); aud != "" { + claims["aud"] = aud + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = c.GetKeyId() + tokenString, err := token.SignedString(pkey) + if err != nil { + return fmt.Errorf("signing auth token: %w", err) + } + fmt.Fprintln(appCtx.App.Writer, tokenString) + return nil +} + +func parsePrivateKey(data []byte) (any, error) { + var pkey any + ok := false + parseErrs := []error{} + for algName, parse := range map[string]func([]byte) (any, error){ + "pkcs1": func(d []byte) (any, error) { + k, err := x509.ParsePKCS1PrivateKey(d) + return any(k), err + }, + "pkcs8": x509.ParsePKCS8PrivateKey, + } { + k, err := parse(data) + if err != nil { + parseErrs = append(parseErrs, fmt.Errorf("%s: %w", algName, err)) + continue + } + + pkey = k + ok = true + } + + if !ok { + return nil, errors.Join(parseErrs...) + } + return pkey, nil +} diff --git a/tools/nbictl/generate_auth_token_test.go b/tools/nbictl/generate_auth_token_test.go new file mode 100644 index 0000000..302e795 --- /dev/null +++ b/tools/nbictl/generate_auth_token_test.go @@ -0,0 +1,145 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "crypto/x509" + "encoding/pem" + "os" + "strings" + "testing" + "time" + + "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/golang-jwt/jwt/v5" +) + +func TestGenerateAuthToken_requiresUserId(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + // Set key_id so that the config file is created. + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--key_id", "key1", + })) + + switch want, err := `no user_id set for chosen context`, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "generate-auth-token", + }); { + case err == nil: + t.Fatal("expected missing user_id setting to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestGenerateAuthToken_requiresKeyId(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + // Set user_id, but not key_id + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--user_id", "user1", + })) + + switch want, err := `no key_id set for chosen context`, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "generate-auth-token", + }); { + case err == nil: + t.Fatal("expected missing user_id setting to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestGenerateAuthToken_requiresPrivKey(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + // Set user_id, and key_id but no priv_key + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--user_id", "user1", + })) + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--key_id", "key1", + })) + + switch want, err := `no priv_key set for chosen context`, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "generate-auth-token", + }); { + case err == nil: + t.Fatal("expected missing user_id setting to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestGenerateAuthToken_happyPath(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + keys := generateKeysForTesting(t, tmpDir, "--org", "user.organization") + certBytes, err := os.ReadFile(keys.cert) + checkErr(t, err) + pemCrtBlock, _ := pem.Decode(certBytes) + cert, err := x509.ParseCertificate(pemCrtBlock.Bytes) + checkErr(t, err) + + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--user_id", "user1", + })) + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--key_id", "key1", + })) + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, "config", "set", "--priv_key", keys.key, + })) + + app := newTestApp() + checkErr(t, app.Run([]string{ + "nbictl", "--config_dir", tmpDir, "generate-auth-token", "--audience", "providedAudience", + })) + + token, err := jwt.Parse(string(app.stdout.Bytes()), func(token *jwt.Token) (interface{}, error) { + return cert.PublicKey, nil + }, + jwt.WithSubject("user1"), + jwt.WithIssuer("user1"), + jwt.WithIssuedAt(), + jwt.WithExpirationRequired()) + checkErr(t, err) + if token.Header["kid"] != "key1" { + t.Errorf("header[kid] invalid. Expected: key1, actual: %s", token.Header["kid"]) + } + exp, err := token.Claims.GetExpirationTime() + checkErr(t, err) + iat, err := token.Claims.GetIssuedAt() + checkErr(t, err) + if exp.Time.Sub(iat.Time) != time.Hour { + t.Errorf("exp is not 1h from iat. exp: %s, iat: %s", exp.Time, iat.Time) + } + aud, err := token.Claims.GetAudience() + if aud[0] != "providedAudience" { + t.Errorf("aud is not correct. Expected: providedAudience, Actual: %s", aud) + } +} diff --git a/tools/nbictl/generate_rsa_key.go b/tools/nbictl/generate_rsa_key.go new file mode 100644 index 0000000..884e406 --- /dev/null +++ b/tools/nbictl/generate_rsa_key.go @@ -0,0 +1,173 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "math" + "math/big" + "os" + "path/filepath" + "time" + + "github.com/urfave/cli/v2" +) + +const ( + rsaKeysBitSize = 4096 + generatedKeysDirDefault = "keys" + defaultExpirationInYears = 1 + lenKeyFileName = 12 + generatedKeysDirPerm = os.FileMode(0700) + privateKeysFilePerm = os.FileMode(0600) + pubCertFilePerm = os.FileMode(0644) +) + +type RSAKeyPath struct { + PrivateKeyPath string + CertificatePath string +} + +func GenerateKeys(appCtx *cli.Context) error { + directory := appCtx.String("dir") + country := appCtx.String("country") + org := appCtx.String("org") + state := appCtx.String("state") + location := appCtx.String("location") + + certIssuer := pkix.Name{} + + if org == "" { + return errors.New("missing required key --org: organization for the certification must be provided") + } else { + certIssuer.Organization = []string{org} + } + + if country != "" { + certIssuer.Country = []string{country} + } + if state != "" { + certIssuer.Province = []string{state} + } + if location != "" { + certIssuer.Locality = []string{location} + } + + generatedKeysDir := directory + if generatedKeysDir == "" { + configDir, err := os.UserConfigDir() + if err != nil { + return err + } + generatedKeysDir = filepath.Join(configDir, appCtx.App.Name, generatedKeysDirDefault) + } + + if err := os.MkdirAll(generatedKeysDir, generatedKeysDirPerm); err != nil { + return err + } + + dirInfo, err := os.Stat(generatedKeysDir) + if err != nil { + return fmt.Errorf("unable to get directory info: %w", err) + } + + if dirPerm := dirInfo.Mode().Perm(); dirPerm != generatedKeysDirPerm { + return fmt.Errorf("directory does not have an appropriate permission: must have %v but have %v", generatedKeysDirPerm, dirPerm) + } + + now := time.Now() + certSerialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return err + } + + privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeysBitSize) + if err != nil { + return fmt.Errorf("unable to generate private key: %w", err) + } + + publicKey := privateKey.PublicKey + publicKeyBytes := x509.MarshalPKCS1PublicKey(&publicKey) + shaPubKey := sha1.Sum(publicKeyBytes) + + authorityKeyId := shaPubKey[:] + + certTemplate := &x509.Certificate{ + SerialNumber: certSerialNumber, + Subject: certIssuer, + Issuer: certIssuer, + NotBefore: now, + NotAfter: now.AddDate(defaultExpirationInYears, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{}, + AuthorityKeyId: authorityKeyId, + BasicConstraintsValid: true, + IsCA: true, + } + + cert, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &publicKey, privateKey) + if err != nil { + return fmt.Errorf("unable to create certificate: %w", err) + } + + pemPrivateBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + } + + pemCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + } + + shaCert := sha256.Sum256(cert) + + rsaKeyPaths := RSAKeyPath{ + PrivateKeyPath: filepath.Join(generatedKeysDir, hex.EncodeToString(shaCert[:lenKeyFileName])+".key"), + CertificatePath: filepath.Join(generatedKeysDir, hex.EncodeToString(shaCert[:lenKeyFileName])+".crt"), + } + + privFile, err := os.OpenFile(rsaKeyPaths.PrivateKeyPath, os.O_CREATE|os.O_RDWR|os.O_EXCL, privateKeysFilePerm) + if err != nil { + return fmt.Errorf("unable to create file: %w", err) + } + defer privFile.Close() + + pubFile, err := os.OpenFile(rsaKeyPaths.CertificatePath, os.O_CREATE|os.O_RDWR|os.O_EXCL, pubCertFilePerm) + if err != nil { + return fmt.Errorf("unable to create file: %w", err) + } + defer pubFile.Close() + + if err = pem.Encode(privFile, pemPrivateBlock); err != nil { + return fmt.Errorf("unable to encode private key: %w", err) + } + + if err := pem.Encode(pubFile, pemCertBlock); err != nil { + return fmt.Errorf("unable to encode certificate: %w", err) + } + + fmt.Fprintf(appCtx.App.ErrWriter, "private key is stored under: %s\n", rsaKeyPaths.PrivateKeyPath) + fmt.Fprintf(appCtx.App.ErrWriter, "certificate is stored under: %s\n", rsaKeyPaths.CertificatePath) + return nil +} diff --git a/tools/nbictl/generate_rsa_key_test.go b/tools/nbictl/generate_rsa_key_test.go new file mode 100644 index 0000000..8aae61a --- /dev/null +++ b/tools/nbictl/generate_rsa_key_test.go @@ -0,0 +1,199 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel" +) + +const ( + exampleCertCountry = "example.country" + exampleCertOrganization = "example.organization" + exampleCertState = "example.state" + exampleCertLocation = "example.location" +) + +type testKeyPath struct{ key, cert string } + +func generateKeysForTesting(t *testing.T, tmpDir string, args ...string) testKeyPath { + if err := newTestApp().Run(append([]string{"nbictl", "--config_dir", tmpDir, "generate-keys", "--dir", tmpDir}, args...)); err != nil { + t.Fatalf("unable to generate RSA keys: %v", err) + } + + privKeyPaths, err := filepath.Glob(filepath.Join(tmpDir, "*.key")) + if err != nil { + t.Fatal(err) + } else if len(privKeyPaths) != 1 { + t.Fatalf("expected to generate 1 private key, got %v", privKeyPaths) + } + certPaths, err := filepath.Glob(filepath.Join(tmpDir, "*.crt")) + if err != nil { + t.Fatal(err) + } else if len(certPaths) != 1 { + t.Fatalf("expected to generate 1 cert, got %v", certPaths) + } + + return testKeyPath{ + key: privKeyPaths[0], + cert: certPaths[0], + } +} + +func TestGenerateKey_ValidateWithOpenSSL(t *testing.T) { + t.Parallel() + + if _, err := exec.LookPath("openssl"); err != nil { + t.Skipf("unable to find path to openssl binary: %v", err) + } + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + keys := generateKeysForTesting(t, tmpDir, "--org", exampleCertOrganization) + privCmd := exec.Command("openssl", "rsa", "-noout", "-modulus", "-in", keys.key) + certCmd := exec.Command("openssl", "x509", "-noout", "-modulus", "-in", keys.cert) + + privOutput, err := privCmd.Output() + if err != nil { + t.Fatalf("unable to run the openssl command for private key: %v", err) + } + pubOutput, err := certCmd.Output() + if err != nil { + t.Fatalf("unable to run the openssl command for public key: %v", err) + } + + if !bytes.Equal(privOutput, pubOutput) { + t.Fatalf("modulus mismatch: got %v from key but %v from cert", privOutput, pubOutput) + } +} + +func TestGenerateKey_ValidateWithGoLib(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + keys := generateKeysForTesting(t, tmpDir, "--org", exampleCertOrganization) + rawPrivateKey, err := os.ReadFile(keys.key) + if err != nil { + t.Fatalf("failed to read file containing private key: %v", err) + } + rawCert, err := os.ReadFile(keys.cert) + if err != nil { + t.Fatalf("failed to read file containing certificate: %v", err) + } + + pemPrivBlock, _ := pem.Decode(rawPrivateKey) + pemCrtBlock, _ := pem.Decode(rawCert) + + if _, err := x509.ParsePKCS1PrivateKey(pemPrivBlock.Bytes); err != nil { + t.Fatalf("failed to parse private key. not a valid private key: %v", err) + } + + if _, err = x509.ParseCertificate(pemCrtBlock.Bytes); err != nil { + t.Fatalf("failed to parse certificate: not a valid certificate: %v", err) + } +} + +func TestGenerateKey_ValidateSubjectAndIssuer(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + keys := generateKeysForTesting(t, tmpDir, + "--org", exampleCertOrganization, + "--country", exampleCertCountry, + "--state", exampleCertState, + "--location", exampleCertLocation) + + rawCert, err := os.ReadFile(keys.cert) + if err != nil { + t.Fatalf("failed to read file containing certificate: %v", err) + } + + pemCrtBlock, _ := pem.Decode(rawCert) + + cert, err := x509.ParseCertificate(pemCrtBlock.Bytes) + if err != nil { + t.Fatalf("failed to parse certificate: %v", err) + } + + switch { + case cert.Subject.Country[0] != exampleCertCountry: + t.Fatalf("subject country mismatch: want %s got %s", exampleCertCountry, cert.Subject.Country[0]) + case cert.Subject.Organization[0] != exampleCertOrganization: + t.Fatalf("subject organization mismatch: want %s got %s", exampleCertOrganization, cert.Subject.Organization[0]) + case cert.Subject.Province[0] != exampleCertState: + t.Fatalf("subject state mismatch: want %s got %s", exampleCertState, cert.Subject.Province[0]) + case cert.Subject.Locality[0] != exampleCertLocation: + t.Fatalf("subject location mismatch: want %s got %s", exampleCertLocation, cert.Subject.Locality[0]) + } + + switch { + case cert.Issuer.Country[0] != exampleCertCountry: + t.Fatalf("issuer country mismatch: want %s got %s", exampleCertCountry, cert.Subject.Country[0]) + case cert.Issuer.Organization[0] != exampleCertOrganization: + t.Fatalf("issuer organization mismatch: want %s got %s", exampleCertOrganization, cert.Subject.Organization[0]) + case cert.Issuer.Province[0] != exampleCertState: + t.Fatalf("issuer state mismatch: want %s got %s", exampleCertState, cert.Subject.Province[0]) + case cert.Issuer.Locality[0] != exampleCertLocation: + t.Fatalf("issuer location mismatch: want %s got %s", exampleCertLocation, cert.Subject.Locality[0]) + } +} + +func TestGenerateKey_FilePermission(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + keys := generateKeysForTesting(t, tmpDir, "--org", exampleCertOrganization) + + privKeyInfo, err := os.Stat(keys.key) + if err != nil { + t.Fatalf("unable to get file info: %v", err) + } + if privFilePerm := privKeyInfo.Mode().Perm(); privFilePerm != os.FileMode(privateKeysFilePerm) { + t.Errorf("file must have permission %d, but has %s", privateKeysFilePerm, privFilePerm.String()) + } + + pubCertKeyInfo, err := os.Stat(keys.cert) + if err != nil { + t.Errorf("unable to get file info: %v", err) + } + if pubCertPerm := pubCertKeyInfo.Mode().Perm(); pubCertPerm != os.FileMode(pubCertFilePerm) { + t.Errorf("file must have permission %d, but has %s", pubCertFilePerm, pubCertPerm.String()) + } +} + +func TestGenerateKey_DirPermision(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + checkErr(t, os.Chmod(tmpDir, 0755)) + + if err := newTestApp().Run([]string{"nbictl", "generate-keys", "--dir", tmpDir, "--org", exampleCertOrganization}); err == nil { + t.Fatal("unable to detect wrong directory permission (expected non-nil error, got nil)") + } +} diff --git a/tools/nbictl/grpcurl.go b/tools/nbictl/grpcurl.go new file mode 100644 index 0000000..9fb8523 --- /dev/null +++ b/tools/nbictl/grpcurl.go @@ -0,0 +1,257 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "fmt" + "io" + "os" + "sort" + + nbipb "aalyria.com/spacetime/api/nbi/v1alpha" + "github.com/fullstorydev/grpcurl" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/grpcreflect" + "github.com/urfave/cli/v2" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" +) + +func GRPCDescribe(appCtx *cli.Context) error { + switch args := appCtx.Args(); args.Len() { + case 0: + return GRPCDescribeServices(appCtx) + case 1: + return GRPCDescribeSymbol(appCtx, args.First()) + default: + return fmt.Errorf("expected 0 or 1 arguments, got %d", args.Len()) + } +} + +func GRPCDescribeServices(appCtx *cli.Context) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + refClient := grpcreflect.NewClientAuto(appCtx.Context, conn) + + svcs, err := refClient.ListServices() + if err != nil { + return err + } + + for _, svc := range svcs { + if err := describeSymbol(appCtx, refClient, svc); err != nil { + return err + } + } + return nil +} + +func GRPCDescribeSymbol(appCtx *cli.Context, symbol string) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + refClient := grpcreflect.NewClientAuto(appCtx.Context, conn) + return describeSymbol(appCtx, refClient, symbol) +} + +func describeSymbol(appCtx *cli.Context, refClient *grpcreflect.Client, symbol string) error { + f, err := refClient.FileContainingSymbol(symbol) + if err != nil { + return err + } + dsc := f.FindSymbol(symbol) + if dsc == nil { + return fmt.Errorf("couldn't find symbol %q", symbol) + } + + elementType := "" + switch d := dsc.(type) { + case *desc.MessageDescriptor: + elementType = "a message" + if parent, ok := d.GetParent().(*desc.MessageDescriptor); ok { + if d.IsMapEntry() { + for _, f := range parent.GetFields() { + if f.IsMap() && f.GetMessageType() == d { + // found it: describe the map field instead + elementType = "the entry type for a map field" + dsc = f + break + } + } + } else { + // see if it's a group + for _, f := range parent.GetFields() { + if f.GetType() == descriptorpb.FieldDescriptorProto_TYPE_GROUP && f.GetMessageType() == d { + // found it: describe the group field instead + elementType = "the type of a group field" + dsc = f + break + } + } + } + } + case *desc.FieldDescriptor: + switch { + case d.GetType() == descriptorpb.FieldDescriptorProto_TYPE_GROUP: + elementType = "a group field" + case d.IsExtension(): + elementType = "an extension" + default: + elementType = "a field" + } + + case *desc.OneOfDescriptor: + elementType = "a one-of" + case *desc.EnumDescriptor: + elementType = "an enum" + case *desc.EnumValueDescriptor: + elementType = "an enum value" + case *desc.ServiceDescriptor: + elementType = "a service" + case *desc.MethodDescriptor: + elementType = "a method" + default: + return fmt.Errorf("descriptor has unrecognized type %T", dsc) + } + + fmt.Fprintf(appCtx.App.Writer, "%s is %s\n", dsc.GetFullyQualifiedName(), elementType) + return nil +} + +func GRPCList(appCtx *cli.Context) error { + switch args := appCtx.Args(); args.Len() { + case 0: + return GRPCListServices(appCtx) + case 1: + return GRPCListMethods(appCtx, args.First()) + default: + return fmt.Errorf("expected 0 or 1 arguments, got %d", args.Len()) + } +} + +func GRPCListServices(appCtx *cli.Context) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + refClient := grpcreflect.NewClientAuto(appCtx.Context, conn) + + svcs, err := refClient.ListServices() + if err != nil { + return err + } + + for _, svc := range svcs { + fmt.Fprintln(appCtx.App.Writer, svc) + } + return nil +} + +func GRPCListMethods(appCtx *cli.Context, svc string) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + refClient := grpcreflect.NewClientAuto(appCtx.Context, conn) + + serviceDesc, err := refClient.ResolveService(svc) + if err != nil { + return fmt.Errorf("resolving service %q: %w", svc, err) + } + + methods := make([]string, 0, len(serviceDesc.GetMethods())) + for _, meth := range serviceDesc.GetMethods() { + methods = append(methods, meth.GetFullyQualifiedName()) + } + sort.Strings(methods) + for _, meth := range methods { + fmt.Fprintln(appCtx.App.Writer, meth) + } + return nil +} + +func GRPCCall(appCtx *cli.Context) error { + if ln := appCtx.Args().Len(); ln != 1 { + return fmt.Errorf("call expects exactly 1 argument, got %d", ln) + } + method := appCtx.Args().First() + + var r io.Reader + switch reqFile := appCtx.String("request"); reqFile { + case "-", "": + r = appCtx.App.Reader + default: + rdr, err := os.Open(reqFile) + if err != nil { + return err + } + defer rdr.Close() + r = rdr + } + + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + + svcDescriptors, err := desc.WrapFiles([]protoreflect.FileDescriptor{ + nbipb.File_api_nbi_v1alpha_nbi_proto, + nbipb.File_api_nbi_v1alpha_signal_propagation_proto, + }) + if err != nil { + return err + } + + descSrc, err := grpcurl.DescriptorSourceFromFileDescriptors(svcDescriptors...) + if err != nil { + return err + } + + var format grpcurl.Format + if appCtx.IsSet("format") { + format = grpcurl.Format(appCtx.String("format")) + } else { + format = grpcurl.Format("json") + } + + reqParser, formatter, err := grpcurl.RequestParserAndFormatter(format, descSrc, r, grpcurl.FormatOptions{ + IncludeTextSeparator: true, + }) + if err != nil { + return err + } + + return grpcurl.InvokeRPC( + appCtx.Context, + descSrc, + conn, + method, + []string{}, + &grpcurl.DefaultEventHandler{ + Out: appCtx.App.Writer, + Formatter: formatter, + VerbosityLevel: 0, + }, + reqParser.Next, + ) +} diff --git a/tools/nbictl/model.go b/tools/nbictl/model.go new file mode 100644 index 0000000..74ac680 --- /dev/null +++ b/tools/nbictl/model.go @@ -0,0 +1,278 @@ +// Copyright (c) Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "fmt" + "io" + "os" + + modelpb "aalyria.com/spacetime/api/model/v1alpha" + "github.com/urfave/cli/v2" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + nmtspb "outernetcouncil.org/nmts/proto" +) + +const modelAPISubDomain = "model" + +func prettyPrintProto[ProtoT proto.Message](appCtx *cli.Context, msg ProtoT) error { + txt, err := prototext.MarshalOptions{Multiline: true}.Marshal(msg) + if err != nil { + return err + } + fmt.Fprint(appCtx.App.Writer, string(txt)) + return nil +} + +func readDataFromCommandLineFilenameArgument(appCtx *cli.Context) ([]byte, error) { + if appCtx.Args().Len() != 1 { + return nil, fmt.Errorf("need one and only one filename argument ('-' reads from stdin)") + } + + fileName := appCtx.Args().First() + if fileName == "-" { + return io.ReadAll(appCtx.App.Reader) + } else { + return os.ReadFile(fileName) + } +} + +func readProtoFromCommandLineFilenameArgument[ProtoT proto.Message](appCtx *cli.Context, msg ProtoT) error { + data, err := readDataFromCommandLineFilenameArgument(appCtx) + if err != nil { + return err + } + return prototext.Unmarshal(data, msg) +} + +func ModelUpsertEntity(appCtx *cli.Context) error { + nmtsEntity := &nmtspb.Entity{} + if err := readProtoFromCommandLineFilenameArgument(appCtx, nmtsEntity); err != nil { + return err + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + _, err = modelClient.UpsertEntity( + appCtx.Context, + &modelpb.UpsertEntityRequest{ + Entity: nmtsEntity, + }) + if err == nil { + fmt.Fprintln(appCtx.App.ErrWriter, "# OK") + } + return err +} + +func ModelUpdateEntity(appCtx *cli.Context) error { + nmtsPartialEntity := &nmtspb.PartialEntity{} + if err := readProtoFromCommandLineFilenameArgument(appCtx, nmtsPartialEntity); err != nil { + return err + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + _, err = modelClient.UpdateEntity( + appCtx.Context, + &modelpb.UpdateEntityRequest{ + Patch: nmtsPartialEntity, + }) + if err == nil { + fmt.Fprintln(appCtx.App.ErrWriter, "# OK") + } + return err +} + +func ModelDeleteEntity(appCtx *cli.Context) error { + if appCtx.Args().Len() != 1 { + return fmt.Errorf("need one and only one Entity ID argument") + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + resp, err := modelClient.DeleteEntity( + appCtx.Context, + &modelpb.DeleteEntityRequest{ + EntityId: appCtx.Args().First(), + }) + if err == nil { + fmt.Fprintf(appCtx.App.ErrWriter, "# also deleted %d relationship/s\n", len(resp.DeletedRelationships)) + } + return err +} + +func ModelInsertRelationship(appCtx *cli.Context) error { + nmtsRelationship := &nmtspb.Relationship{} + if err := readProtoFromCommandLineFilenameArgument(appCtx, nmtsRelationship); err != nil { + return err + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + _, err = modelClient.InsertRelationship( + appCtx.Context, + &modelpb.InsertRelationshipRequest{ + Relationship: nmtsRelationship, + }) + if err == nil { + fmt.Fprintln(appCtx.App.ErrWriter, "# OK") + } + return err +} + +func ModelDeleteRelationship(appCtx *cli.Context) error { + nmtsRelationship := &nmtspb.Relationship{} + if err := readProtoFromCommandLineFilenameArgument(appCtx, nmtsRelationship); err != nil { + return err + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + _, err = modelClient.DeleteRelationship( + appCtx.Context, + &modelpb.DeleteRelationshipRequest{ + Relationship: nmtsRelationship, + }) + if err == nil { + fmt.Fprintln(appCtx.App.ErrWriter, "# OK") + } + return err +} + +// TODO: turn these into one atomic RPC call in the modelfe. +func ModelUpsertFragment(appCtx *cli.Context) error { + nmtsFragment := &nmtspb.Fragment{} + if err := readProtoFromCommandLineFilenameArgument(appCtx, nmtsFragment); err != nil { + return err + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + for _, nmtsEntity := range nmtsFragment.GetEntity() { + _, err = modelClient.UpsertEntity( + appCtx.Context, + &modelpb.UpsertEntityRequest{ + Entity: nmtsEntity, + }) + if err != nil { + return err + } + } + + for _, nmtsRelationship := range nmtsFragment.GetRelationship() { + _, err = modelClient.InsertRelationship( + appCtx.Context, + &modelpb.InsertRelationshipRequest{ + Relationship: nmtsRelationship, + }) + if err != nil { + return err + } + } + + fmt.Fprintln(appCtx.App.ErrWriter, "# OK") + return nil +} + +func ModelGetEntity(appCtx *cli.Context) error { + if appCtx.Args().Len() != 1 { + return fmt.Errorf("need one and only one Entity ID argument") + } + + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + response, err := modelClient.GetEntity( + appCtx.Context, + &modelpb.GetEntityRequest{ + EntityId: appCtx.Args().First(), + }) + if err != nil { + return err + } + + prettyPrintProto(appCtx, response) + debugPrintNMTSEntityEdges(appCtx.App.ErrWriter, response.GetEntityEdges()) + return nil +} + +func debugPrintNMTSEntityEdges(stderr io.Writer, entityEdges *nmtspb.EntityEdges) { + numEntities, numRelationships := 0, 0 + if entityEdges != nil { + numEntities, numRelationships = 1, len(entityEdges.Relationship) + } + fmt.Fprintf(stderr, "# %v entity/ies, %v relationship/s\n", numEntities, numRelationships) +} + +func ModelListElements(appCtx *cli.Context) error { + conn, err := openAPIConnection(appCtx, modelAPISubDomain) + if err != nil { + return err + } + defer conn.Close() + modelClient := modelpb.NewModelClient(conn) + + response, err := modelClient.ListElements(appCtx.Context, &modelpb.ListElementsRequest{}) + if err != nil { + return err + } + + prettyPrintProto(appCtx, response) + debugPrintNMTSFragment(appCtx.App.ErrWriter, response.GetElements()) + return nil +} + +func debugPrintNMTSFragment(stderr io.Writer, fragment *nmtspb.Fragment) { + numEntities, numRelationships := 0, 0 + if fragment != nil { + numEntities, numRelationships = len(fragment.Entity), len(fragment.Relationship) + } + fmt.Fprintf(stderr, "# %v entity/ies, %v relationship/s\n", numEntities, numRelationships) +} diff --git a/tools/nbictl/model_test.go b/tools/nbictl/model_test.go new file mode 100644 index 0000000..c82e47f --- /dev/null +++ b/tools/nbictl/model_test.go @@ -0,0 +1,447 @@ +// Copyright (c) Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "context" + "net" + "os" + "path/filepath" + "testing" + "time" + + modelpb "aalyria.com/spacetime/api/model/v1alpha" + "github.com/bazelbuild/rules_go/go/tools/bazel" + gcmp "github.com/google/go-cmp/cmp" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/fieldmaskpb" + nmtspb "outernetcouncil.org/nmts/proto" + nmtsphypb "outernetcouncil.org/nmts/proto/ek/physical" +) + +type testCase struct { + desc string + fileContents map[string]string + responseError error + responseMessage proto.Message + cmdLineArgs []string + wantAppError bool + wantRequest proto.Message +} + +func (tc *testCase) Run(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + g, ctx := errgroup.WithContext(ctx) + defer func() { checkErr(t, g.Wait()) }() + defer cancel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + os.Chdir(tmpDir) + + app := newTestApp() + for key, value := range tc.fileContents { + if key == "-" { + app.stdin.WriteString(value) + } else { + absFile, err := filepath.Abs(filepath.Join(tmpDir, key)) + checkErr(t, err) + _, err = filepath.Rel(tmpDir, absFile) + checkErr(t, err) + checkErr(t, os.WriteFile(absFile, []byte(value), 0644)) + } + } + + lis, err := net.Listen("tcp", ":0") + checkErr(t, err) + srv, err := startFakeModelServer(ctx, g, lis) + checkErr(t, err) + + argsPrefix := []string{"nbictl", "--config_dir", tmpDir, "--context", "DEFAULT"} + + keys := generateKeysForTesting(t, tmpDir, "--org", "example org") + checkErr(t, newTestApp().Run(append(argsPrefix, []string{ + "config", + "set", + "--transport_security", "insecure", + "--user_id", "usr1", + "--key_id", "key1", + "--priv_key", keys.key, + "--url", srv.listener.Addr().String(), + }...))) + + srv.ResponseMessage = tc.responseMessage + args := append(argsPrefix, tc.cmdLineArgs...) + err = app.Run(args) + if tc.wantAppError { + if err != nil { + return + } else { + t.Errorf("[%v] wanted App error but got success", tc.desc) + } + } + checkErr(t, err) + if diff := gcmp.Diff(tc.wantRequest, srv.RequestMessage, + protocmp.Transform(), + ); diff != "" { + t.Errorf("[%v] mismatch (-want +got):\n%s", tc.desc, diff) + } +} + +var testCases = []testCase{ + { + desc: "'model upsert-entity' without arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "upsert-entity"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model upsert-entity' with too many arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "upsert-entity", "uuid-1234.txtpb", "uuid-5678.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model upsert-entity' with one argument errors if file is missing", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "upsert-entity", "uuid-1234.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model upsert-entity' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "-": "id: \"uuid-1234\" ek_platform{}", + }, + responseError: nil, + responseMessage: &modelpb.UpsertEntityResponse{}, + cmdLineArgs: []string{"model", "upsert-entity", "-"}, + wantAppError: false, + wantRequest: &modelpb.UpsertEntityRequest{ + Entity: &nmtspb.Entity{ + Id: "uuid-1234", + Kind: &nmtspb.Entity_EkPlatform{ + EkPlatform: &nmtsphypb.Platform{}, + }, + }, + }, + }, + { + desc: "'model upsert-entity' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "uuid-1234.txtpb": "id: \"uuid-1234\" ek_platform{}", + }, + responseError: nil, + responseMessage: &modelpb.UpsertEntityResponse{}, + cmdLineArgs: []string{"model", "upsert-entity", "uuid-1234.txtpb"}, + wantAppError: false, + wantRequest: &modelpb.UpsertEntityRequest{ + Entity: &nmtspb.Entity{ + Id: "uuid-1234", + Kind: &nmtspb.Entity_EkPlatform{ + EkPlatform: &nmtsphypb.Platform{}, + }, + }, + }, + }, + { + desc: "'model update-entity' without arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "update-entity"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model update-entity' with too many arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "update-entity", "uuid-1234.txtpb", "uuid-5678.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model update-entity' with one argument errors if file is missing", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "update-entity", "uuid-1234.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model update-entity' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "-": "entity{ id: \"uuid-1234\" ek_platform{ name: \"platform_1234\" } } mask: { paths: \"ek_platform.name\" }", + }, + responseError: nil, + responseMessage: &modelpb.UpdateEntityResponse{}, + cmdLineArgs: []string{"model", "update-entity", "-"}, + wantAppError: false, + wantRequest: &modelpb.UpdateEntityRequest{ + Patch: &nmtspb.PartialEntity{ + Entity: &nmtspb.Entity{ + Id: "uuid-1234", + Kind: &nmtspb.Entity_EkPlatform{ + EkPlatform: &nmtsphypb.Platform{ + Name: "platform_1234", + }, + }, + }, + Mask: &fieldmaskpb.FieldMask{ + Paths: []string{ + "ek_platform.name", + }, + }, + }, + }, + }, + { + desc: "'model update-entity' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "uuid-1234.txtpb": "entity{ id: \"uuid-1234\" ek_platform{ name: \"platform_1234\" } } mask: { paths: \"ek_platform.name\" }", + }, + responseError: nil, + responseMessage: &modelpb.UpdateEntityResponse{}, + cmdLineArgs: []string{"model", "update-entity", "uuid-1234.txtpb"}, + wantAppError: false, + wantRequest: &modelpb.UpdateEntityRequest{ + Patch: &nmtspb.PartialEntity{ + Entity: &nmtspb.Entity{ + Id: "uuid-1234", + Kind: &nmtspb.Entity_EkPlatform{ + EkPlatform: &nmtsphypb.Platform{ + Name: "platform_1234", + }, + }, + }, + Mask: &fieldmaskpb.FieldMask{ + Paths: []string{ + "ek_platform.name", + }, + }, + }, + }, + }, + { + desc: "'model delete-entity' without arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "delete-entity"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model delete-entity' with too many arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "delete-entity", "uuid-1234", "uuid-5678"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model delete-entity' with one argument calls API as expected", + fileContents: nil, + responseError: nil, + responseMessage: &modelpb.DeleteEntityResponse{}, + cmdLineArgs: []string{"model", "delete-entity", "uuid-1234"}, + wantAppError: false, + wantRequest: &modelpb.DeleteEntityRequest{ + EntityId: "uuid-1234", + }, + }, + { + desc: "'model insert-relationship' without arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "insert-relationship"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model insert-relationship' with too many arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "insert-relationship", "uuid-1234.txtpb", "uuid-5678.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model insert-relationship' with one argument errors if file is missing", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "insert-relationship", "uuid-1234.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model insert-relationship' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "-": "a: \"uuid-1234\" kind: RK_CONTAINS z: \"uuid-5678\"", + }, + responseError: nil, + responseMessage: &modelpb.InsertRelationshipResponse{}, + cmdLineArgs: []string{"model", "insert-relationship", "-"}, + wantAppError: false, + wantRequest: &modelpb.InsertRelationshipRequest{ + Relationship: &nmtspb.Relationship{ + A: "uuid-1234", + Kind: nmtspb.RK_RK_CONTAINS, + Z: "uuid-5678", + }, + }, + }, + { + desc: "'model insert-relationship' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "uuid-1234.txtpb": "a: \"uuid-1234\" kind: RK_CONTAINS z: \"uuid-5678\"", + }, + responseError: nil, + responseMessage: &modelpb.InsertRelationshipResponse{}, + cmdLineArgs: []string{"model", "insert-relationship", "uuid-1234.txtpb"}, + wantAppError: false, + wantRequest: &modelpb.InsertRelationshipRequest{ + Relationship: &nmtspb.Relationship{ + A: "uuid-1234", + Kind: nmtspb.RK_RK_CONTAINS, + Z: "uuid-5678", + }, + }, + }, + { + desc: "'model delete-relationship' without arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "delete-relationship"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model delete-relationship' with too many arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "delete-relationship", "uuid-1234.txtpb", "uuid-5678.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model delete-relationship' with one argument errors if file is missing", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "delete-relationship", "uuid-1234.txtpb"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model delete-relationship' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "-": "a: \"uuid-1234\" kind: RK_CONTAINS z: \"uuid-5678\"", + }, + responseError: nil, + responseMessage: &emptypb.Empty{}, + cmdLineArgs: []string{"model", "delete-relationship", "-"}, + wantAppError: false, + wantRequest: &modelpb.DeleteRelationshipRequest{ + Relationship: &nmtspb.Relationship{ + A: "uuid-1234", + Kind: nmtspb.RK_RK_CONTAINS, + Z: "uuid-5678", + }, + }, + }, + { + desc: "'model delete-relationship' with one argument calls API as expected (stdin)", + fileContents: map[string]string{ + "uuid-1234.txtpb": "a: \"uuid-1234\" kind: RK_CONTAINS z: \"uuid-5678\"", + }, + responseError: nil, + responseMessage: &emptypb.Empty{}, + cmdLineArgs: []string{"model", "delete-relationship", "uuid-1234.txtpb"}, + wantAppError: false, + wantRequest: &modelpb.DeleteRelationshipRequest{ + Relationship: &nmtspb.Relationship{ + A: "uuid-1234", + Kind: nmtspb.RK_RK_CONTAINS, + Z: "uuid-5678", + }, + }, + }, + { + desc: "'model get-entity' without arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "get-entity"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model get-entity' with too many arguments does not call API", + fileContents: nil, + responseError: nil, + responseMessage: nil, + cmdLineArgs: []string{"model", "get-entity", "uuid-1234", "uuid-5678"}, + wantAppError: true, + wantRequest: nil, + }, + { + desc: "'model get-entity' with one argument calls API as expected", + fileContents: nil, + responseError: nil, + responseMessage: &modelpb.GetEntityResponse{}, + cmdLineArgs: []string{"model", "get-entity", "uuid-1234"}, + wantAppError: false, + wantRequest: &modelpb.GetEntityRequest{ + EntityId: "uuid-1234", + }, + }, + { + desc: "'model list-elements' calls API as expected", + fileContents: nil, + responseError: nil, + responseMessage: &modelpb.ListElementsResponse{}, + cmdLineArgs: []string{"model", "list-elements"}, + wantAppError: false, + wantRequest: &modelpb.ListElementsRequest{}, + }, +} + +func TestCases(t *testing.T) { + for _, tc := range testCases { + tc.Run(t) + } +} diff --git a/tools/nbictl/nbictl.go b/tools/nbictl/nbictl.go new file mode 100644 index 0000000..ed71124 --- /dev/null +++ b/tools/nbictl/nbictl.go @@ -0,0 +1,945 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/urfave/cli/v2" + "golang.org/x/sync/errgroup" + intervalpb "google.golang.org/genproto/googleapis/type/interval" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + commonpb "aalyria.com/spacetime/api/common" + nbipb "aalyria.com/spacetime/api/nbi/v1alpha" + resourcespb "aalyria.com/spacetime/api/nbi/v1alpha/resources" +) + +const ( + confFileName = "config.textproto" + + // modified from + // https://github.com/urfave/cli/blob/c023d9bc5a3122830c9355a0a8c17137e0c8556f/template.go#L98 + readmeDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionNum }} + +{{end}}# NAME + +{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} + +# SYNOPSIS + +{{ if .SynopsisArgs }}` + "```" + ` +{{ .App.Name }} {{ range $f := .App.VisibleFlags -}}{{ range $n := $f.Names }}[{{ if len $n | lt 1 }}--{{ else }}-{{ end }}{{ $n }}{{ if $f.TakesValue }}=value{{ end }}] {{ end }}{{ end }} [COMMAND OPTIONS] [ARGUMENTS...] +` + "```" + ` +{{ end }}{{ if .GlobalArgs }} +# GLOBAL OPTIONS +{{ range $v := .GlobalArgs }} +{{ $v }}{{ end }} +{{ end }}{{ if .Commands }}# COMMANDS +{{ range $v := .Commands }} +{{ $v }}{{ end }}{{ end }}` + + appName = "nbictl" +) + +var entityTypeList = generateTypeList() + +func init() { + cli.MarkdownDocTemplate = readmeDocTemplate +} + +func App() *cli.App { + return &cli.App{ + Name: appName, + Usage: "Interact with the Spacetime NBI service from the command line.", + Description: fmt.Sprintf("`%s` is a tool that allows you to interact with the Spacetime NBI APIs from the command-line.", appName), + BashComplete: cli.DefaultAppComplete, + EnableBashCompletion: true, + Suggest: true, + Reader: os.Stdin, + Writer: os.Stdout, + ErrWriter: os.Stderr, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "profile", + Usage: "Configuration profile to use.", + Aliases: []string{"context"}, + }, + &cli.StringFlag{ + Name: "config_dir", + Usage: "Directory to use for configuration.", + DefaultText: "$XDG_CONFIG_HOME/" + appName, + }, + }, + Commands: []*cli.Command{ + { + Name: "readme", + Category: "help", + Usage: "Prints the help information as Markdown.", + Hidden: true, + Action: func(appCtx *cli.Context) error { + md, err := appCtx.App.ToMarkdown() + if err != nil { + return err + } + fmt.Fprintln(appCtx.App.Writer, `") + fmt.Fprintln(appCtx.App.Writer) + + fmt.Fprintln(appCtx.App.Writer, md) + return nil + }, + }, + { + Name: "man", + Category: "help", + Usage: "Prints the help information as a man page.", + Hidden: true, + Action: func(appCtx *cli.Context) error { + man, err := appCtx.App.ToMan() + if err != nil { + return err + } + fmt.Fprintln(appCtx.App.Writer, man) + return nil + }, + }, + { + Name: "get", + Category: "entities", + Usage: "Gets the entity with the given type and ID.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: fmt.Sprintf("[REQUIRED] Type of entity to delete. Allowed values: [%s]", strings.Join(entityTypeList, ", ")), + Aliases: []string{"t"}, + Required: true, + Action: validateEntityType, + }, + &cli.StringFlag{ + Name: "id", + Usage: "[REQUIRED] ID of entity to delete.", + Aliases: []string{}, + Required: true, + }, + }, + Action: Get, + }, + { + Name: "create", + Category: "entities", + Usage: "Create one or more entities described in textproto files.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "files", + Usage: "[REQUIRED] Glob of textproto files that represent one or more Entity messages.", + Aliases: []string{"f"}, + Required: true, + }, + }, + Action: Create, + }, + { + Name: "edit", + Category: "entities", + Usage: "Opens the specified entity as a textproto in $EDITOR, then updates the NBI's version with any updates made.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: fmt.Sprintf("[REQUIRED] Type of entity to edit. Allowed values: [%s]", strings.Join(entityTypeList, ", ")), + Aliases: []string{"t"}, + Required: true, + Action: validateEntityType, + }, + &cli.StringFlag{ + Name: "id", + Usage: "[REQUIRED] ID of entity to edit.", + Aliases: []string{}, + Required: true, + }, + }, + Action: Edit, + }, + { + Name: "update", + Category: "entities", + Usage: "Updates, or creates if missing, one or more entities described in textproto files.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "files", + Usage: "[REQUIRED] Glob of textproto files that represent one or more Entity messages.", + Aliases: []string{"f"}, + Required: true, + }, + &cli.BoolFlag{ + Name: "ignore_consistency_check", + DefaultText: "false", + Usage: "Always update or create the entity, without verifying that the provided `commit_timestamp` matches the currently stored entity.", + }, + }, + Action: Update, + }, + { + Name: "list", + Category: "entities", + Usage: "Lists all entities of a given type.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: fmt.Sprintf("[REQUIRED] Type of entities to query. Allowed values: [%s]", strings.Join(entityTypeList, ", ")), + Aliases: []string{"t"}, + Required: true, + Action: validateEntityType, + }, + &cli.StringFlag{ + Name: "field_masks", + Usage: "Comma-separated allow-list of fields to include in the response; see the aalyria.spacetime.api.nbi.v1alpha.EntityFilter.field_masks documentation for usage details.", + Required: false, + Aliases: []string{}, + }, + }, + Action: List, + }, + { + Name: "delete", + Category: "entities", + Usage: "Deletes one or more entities. Provide the type and ID to delete a single entity, or a directory of Entity textproto files to delete multiple entities.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: fmt.Sprintf("Type of entity to delete. Allowed values: [%s]", strings.Join(entityTypeList, ", ")), + Aliases: []string{"t"}, + Action: validateEntityType, + }, + &cli.StringFlag{ + Name: "id", + Usage: "ID of entity to delete.", + Aliases: []string{}, + }, + &cli.IntFlag{ + Name: "last_commit_timestamp", + Usage: "Delete the entity only if `last_commit_timestamp` matches the `commit_timestamp` of the currently stored entity.", + Aliases: []string{}, + }, + &cli.BoolFlag{ + Name: "ignore_consistency_check", + DefaultText: "false", + Usage: "Always update or create the entity, without verifying that the provided `commit_timestamp` matches the value in the currently stored entity.", + Aliases: []string{}, + }, + &cli.StringFlag{ + Name: "files", + Usage: "Glob of textproto files that represent one or more Entity messages.", + Aliases: []string{"f"}, + }, + }, + Action: Delete, + }, + { + Name: "get-link-budget", + Usage: "Gets link budget details", + Description: "Gets link budget details for a given signal propagation request between a transmitter and a target platform.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "input_file", + Usage: "A path to a textproto file containing a SignalPropagationRequest message. If set, it will be used as the request to the SignalPropagation service. If unset, the request will be built from the other flags.", + }, + &cli.StringFlag{ + Name: "tx_platform_id", + Usage: "The Entity ID of the PlatformDefinition that represents the transmitter.", + }, + &cli.StringFlag{ + Name: "tx_transceiver_model_id", + Usage: "The ID of the transceiver model on the transmitter.", + }, + &cli.StringFlag{ + Name: "target_platform_id", + Usage: "The Entity ID of the PlatformDefinition that represents the target. Leave unset if the antenna is fixed or non-steerable, in which case coverage calculations will be returned.", + }, + &cli.StringFlag{ + Name: "target_transceiver_model_id", + Usage: "The ID of the transceiver model on the target.Leave unset if the antenna is fixed or non-steerable, in which case coverage calculations will be returned.", + }, + &cli.StringFlag{ + Name: "band_profile_id", + Usage: "The Entity ID of the BandProfile used for this link.", + }, + &cli.TimestampFlag{ + Name: "analysis_start_timestamp", + Layout: time.RFC3339, + Usage: "An RFC3339 formatted timestamp for the beginning of the interval to evaluate the signal propagation. Defaults to the current local timestamp.", + }, + &cli.TimestampFlag{ + Name: "analysis_end_timestamp", + Layout: time.RFC3339, + Usage: "An RFC3339 formatted timestamp for the end of the interval to evaluate the signal propagation. If unset, the signal propagation is evaluated at the instant of the `analysis_start_timestamp.`", + }, + &cli.DurationFlag{ + Name: "step_size", + DefaultText: "1m", + Usage: "The analysis step size and the temporal resolution of the response.", + }, + &cli.DurationFlag{ + Name: "spatial_propagation_step_size", + DefaultText: "1m", + Usage: "The analysis step size for spatial propagation metrics.", + }, + &cli.BoolFlag{ + Name: "explain_inaccessibility", + DefaultText: "false", + Usage: "If true, the server will spend additional computational time determining the specific set of access constraints that were not satisfied and including these reasons in the response.", + }, + &cli.TimestampFlag{ + Name: "reference_data_timestamp", + Layout: time.RFC3339, + Usage: "An RFC3339 formatted timestamp for the instant at which to reference the versions of the platforms. Defaults to `analysis_start_timestamp`.", + DefaultText: "analysis_start_timestamp", + }, + &cli.PathFlag{ + Name: "output_file", + Usage: "Path to a textproto file to write the response. If unset, defaults to stdout.", + DefaultText: "/dev/stdout", + }, + }, + Action: GetLinkBudget, + }, + { + Name: "generate-keys", + Usage: "Generate RSA keys to use for authentication with the Spacetime APIs.", + UsageText: "After creating the Private-Public keypair, you will need to request API access by sharing the `.crt` file (a self-signed x509 certificate containing the public key) with Aalyria to receive the `USER_ID` and a `KEY_ID` needed to complete the nbictl configuration. Only share the public certificate (`.crt`) with Aalyria or third-parties. The private key (`.key`) must be protected and should never be sent by email or communicated to others.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "dir", + Usage: "Directory to store the generated RSA keys in.", + DefaultText: "~/.config/" + appName + "/keys", + Aliases: []string{"directory"}, + }, + &cli.StringFlag{ + Name: "org", + Usage: "[REQUIRED] Organization of certificate.", + Aliases: []string{"organization"}, + Required: true, + }, + &cli.StringFlag{ + Name: "country", + Usage: "Country of certificate.", + }, + &cli.StringFlag{ + Name: "state", + Usage: "State of certificate.", + }, + &cli.StringFlag{ + Name: "location", + Usage: "Location of certificate.", + }, + }, + Action: GenerateKeys, + }, + { + Name: "config", + Usage: fmt.Sprintf("Provides subcommands for managing %s configuration.", appName), + Subcommands: []*cli.Command{ + { + Name: "list-profiles", + Usage: "List all configuration profiles (ignores any `--profile` flag)", + Action: ListConfigs, + }, + { + Name: "describe", + Usage: "Prints the NBI connection settings associated with the configuration profile given by the `--profile` flag (defaults to \"DEFAULT\").", + Action: GetConfig, + }, + { + Name: "set", + Usage: "Sets or updates a configuration profile settings. You can create multiple profiles by specifying the `--profile` flag (defaults to \"DEFAULT\").", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "priv_key", + Usage: "Path to the private key to use for authentication.", + }, + &cli.StringFlag{ + Name: "key_id", + Usage: "Key ID associated with the private key provided by Aalyria.", + }, + &cli.StringFlag{ + Name: "user_id", + Usage: "User ID associated with the private key provided by Aalyria.", + }, + &cli.StringFlag{ + Name: "url", + Usage: "NBI endpoint specified as `host[:port]` (port is optional and defaults to 443).", + }, + &cli.StringFlag{ + Name: "transport_security", + Usage: "Transport security to use when connecting to the NBI service. Allowed values: [insecure, system_cert_pool]", + }, + }, + Action: SetConfig, + }, + }, + }, + { + Name: "model", + Usage: "Provides subcommands for accessing and managing the model elements comprising the digital twin.", + Subcommands: []*cli.Command{ + { + Name: "upsert-entity", + Usage: "Upsert the model NMTS Entity contained within the file provided on the command line ('-' reads from stdin).", + Category: "model entities", + Action: ModelUpsertEntity, + }, + { + Name: "update-entity", + Usage: "Update the model using NMTS PartialEntity contained within the file provided on the command line ('-' reads from stdin).", + Category: "model entities", + Action: ModelUpdateEntity, + }, + { + Name: "delete-entity", + Usage: "Delete the model NMTS Entity associated with the entity ID provided on the command line, along with any relationships in which it participates.", + Category: "model entities", + Action: ModelDeleteEntity, + }, + { + Name: "get-entity", + Usage: "Get the model NMTS Entity associated with the entity ID given on the command line.", + Category: "model entities", + Action: ModelGetEntity, + }, + { + Name: "insert-relationship", + Usage: "Insert the model NMTS Relationship contained within the file provided on the command line ('-' reads from stdin).", + Category: "model relationships", + Action: ModelInsertRelationship, + }, + { + Name: "delete-relationship", + Usage: "Delete the model NMTS Relationship contained within the file provided on the command line ('-' reads from stdin).", + Category: "model relationships", + Action: ModelDeleteRelationship, + }, + { + Name: "upsert-fragment", + Usage: "Upsert the model NMTS Fragment contained within the file provided on the command line ('-' reads from stdin).", + Category: "model relationships", + Action: ModelUpsertFragment, + }, + { + Name: "list-elements", + Usage: "List all model elements (NMTS Entities and Relationships).", + Category: "model entities and relationships", + Action: ModelListElements, + }, + }, + }, + { + Name: "grpcurl", + Usage: "Provides curl-like equivalents for interacting with the NBI.", + Subcommands: []*cli.Command{ + { + Name: "describe", + Usage: "Takes an optional fully-qualified symbol (service, enum, or message). If provided, the descriptor for that symbol is shown. If not provided, the descriptor for all exposed or known services are shown.", + Action: GRPCDescribe, + }, + { + Name: "list", + Usage: "Takes an optional fully-qualified service name. If provided, lists all methods of that service. If not provided, all exposed services are listed.", + Action: GRPCList, + }, + { + Name: "call", + Aliases: []string{"invoke"}, + Usage: "Takes a fully-qualified method name in 'service.method' or 'service/method' format. Invokes the method using the provided request body.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "format", + Usage: "Protobuf format to use for input and output. Allowed values: [text, json]", + DefaultText: "json", + Aliases: []string{"f"}, + Action: validateProtoFormat, + }, + &cli.StringFlag{ + Name: "request", + Usage: "File containing the request to make encoded in the selected --format. Defaults to -, which uses stdin.", + DefaultText: "-", + Aliases: []string{"r"}, + }, + }, + Action: GRPCCall, + }, + }, + }, + { + Name: "generate-auth-token", + Usage: "Generate a self-signed JWT token for API authentication.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "audience", + Usage: "The audience (aud) to set in the JWT token.", + Aliases: []string{"aud"}, + }, + &cli.DurationFlag{ + Name: "expiration", + Usage: "The validity duration of token, from the time of creation.", + Aliases: []string{"exp"}, + DefaultText: "1h", + Value: 1 * time.Hour, + }, + }, + Action: GenerateAuthToken, + }, + }, + } +} + +func Create(appCtx *cli.Context) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewNetOpsClient(conn) + + createEntityFunc := func(ctx context.Context, e *nbipb.Entity) error { + req := &nbipb.CreateEntityRequest{Entity: e} + res, err := client.CreateEntity(ctx, req) + if err != nil { + return fmt.Errorf("create failed for entity %s/%s: %w", req.Entity.GetGroup().GetType(), req.GetEntity().GetId(), err) + } + fmt.Fprintf(appCtx.App.ErrWriter, "successfully created: %s/%s\n", res.GetGroup().GetType(), res.GetId()) + return nil + } + return processEntitiesFromFiles(appCtx.Context, appCtx.String("files"), createEntityFunc) +} + +func Edit(appCtx *cli.Context) error { + ed := "" + for _, env := range []string{"VISUAL", "EDITOR"} { + ed = os.Getenv(env) + if ed != "" { + break + } + } + if ed == "" { + return fmt.Errorf("No $EDITOR value set, don't know which editor to use") + } + + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewNetOpsClient(conn) + + id := appCtx.String("id") + entityType := appCtx.String("type") + et, found := nbipb.EntityType_value[entityType] + if !found { + return fmt.Errorf("invalid type: %q", entityType) + } + oldEntity, err := client.GetEntity(appCtx.Context, &nbipb.GetEntityRequest{Type: nbipb.EntityType(et).Enum(), Id: &id}) + if err != nil { + return fmt.Errorf("unable to get the entity via the NBI: %w", err) + } + + tmp, err := os.MkdirTemp("", "nbictl") + if err != nil { + return fmt.Errorf("opening tmp dir: %w", err) + } + defer os.RemoveAll(tmp) + + oldTxt, err := (prototext.MarshalOptions{ + Multiline: true, + Indent: " ", + }).Marshal(oldEntity) + if err != nil { + return fmt.Errorf("marshalling entity as textproto: %w", err) + } + + fname := filepath.Join(tmp, "entity.textproto") + if err := os.WriteFile(fname, oldTxt, 0o755); err != nil { + return fmt.Errorf("writing entity to file %s: %w", fname, err) + } + + cmd := exec.CommandContext(appCtx.Context, ed, fname) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("command `%s` exited with an error: %w", cmd.Args, err) + } + + newTxt, err := os.ReadFile(fname) + if err != nil { + return fmt.Errorf("reading modified entity from %s: %w", fname, err) + } + newEntity := &nbipb.Entity{} + if err := prototext.Unmarshal(newTxt, newEntity); err != nil { + return fmt.Errorf("unmarshalling modified entity as textproto: %w", err) + } + if _, err := client.UpdateEntity(appCtx.Context, &nbipb.UpdateEntityRequest{Entity: newEntity}); err != nil { + return fmt.Errorf("calling UpdateEntity: %w", err) + } + return nil +} + +func Update(appCtx *cli.Context) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewNetOpsClient(conn) + + updateEntityFunc := func(ctx context.Context, e *nbipb.Entity) error { + req := &nbipb.UpdateEntityRequest{Entity: e, IgnoreConsistencyCheck: proto.Bool(true)} + res, err := client.UpdateEntity(ctx, req) + if err != nil { + return fmt.Errorf("update failed for entity %s/%s: %w", req.Entity.GetGroup().GetType(), req.GetEntity().GetId(), err) + } + fmt.Fprintf(appCtx.App.ErrWriter, "successfully updated: %s/%s\n", res.GetGroup().GetType(), res.GetId()) + return nil + } + return processEntitiesFromFiles(appCtx.Context, appCtx.String("files"), updateEntityFunc) +} + +func Get(appCtx *cli.Context) error { + entityType := appCtx.String("type") + id := appCtx.String("id") + + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewNetOpsClient(conn) + + entityTypeEnumValue, found := nbipb.EntityType_value[entityType] + if !found { + return fmt.Errorf("invalid type: %q", entityType) + } + entityTypeEnum := nbipb.EntityType(entityTypeEnumValue) + entity, err := client.GetEntity(appCtx.Context, &nbipb.GetEntityRequest{Type: &entityTypeEnum, Id: &id}) + if err != nil { + return fmt.Errorf("unable to get the entity: %w", err) + } + entitiesOutput := &nbipb.TxtpbEntities{ + Entity: []*nbipb.Entity{entity}, + } + entitiesOutputTextProto, err := prototext.MarshalOptions{Multiline: true}.Marshal(entitiesOutput) + if err != nil { + return fmt.Errorf("unable to convert the response into textproto format: %w", err) + } + fmt.Fprintln(appCtx.App.Writer, string(entitiesOutputTextProto)) + return nil +} + +func Delete(appCtx *cli.Context) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewNetOpsClient(conn) + + deleteFunc := func(ctx context.Context, req *nbipb.DeleteEntityRequest) error { + if _, err := client.DeleteEntity(appCtx.Context, req); err != nil { + return fmt.Errorf("deletion failed for entity %s/%s: %w", req.Type, *req.Id, err) + } + fmt.Fprintf(appCtx.App.ErrWriter, "successfully deleted: %s/%s\n", req.Type, *req.Id) + return nil + } + + if appCtx.IsSet("type") && appCtx.IsSet("id") { + entityId := appCtx.String("id") + entityType := appCtx.String("type") + entityTypeEnumValue, found := nbipb.EntityType_value[entityType] + if !found { + return fmt.Errorf("invalid type: %q", entityType) + } + if appCtx.IsSet("last_commit_timestamp") == appCtx.Bool("ignore_consistency_check") { + return fmt.Errorf(`when deleting a single entity, either "last_commit_timestamp" or "ignore_consistency_check" flags should be set.`) + } + entityTypeEnum := nbipb.EntityType(entityTypeEnumValue) + req := &nbipb.DeleteEntityRequest{Type: &entityTypeEnum, Id: &entityId} + if appCtx.IsSet("last_commit_timestamp") { + req.LastCommitTimestamp = proto.Int64(appCtx.Int64("last_commit_timestamp")) + } + if appCtx.Bool("ignore_consistency_check") { + req.IgnoreConsistencyCheck = proto.Bool(true) + } + return deleteFunc(appCtx.Context, req) + } else if appCtx.IsSet("files") { + deleteEntityFunc := func(ctx context.Context, e *nbipb.Entity) error { + entityId := e.GetId() + entityType := e.GetGroup().GetType() + req := &nbipb.DeleteEntityRequest{Type: &entityType, Id: &entityId} + if e.CommitTimestamp != nil { + req.LastCommitTimestamp = e.CommitTimestamp + } + if appCtx.Bool("ignore_consistency_check") { + req.IgnoreConsistencyCheck = proto.Bool(true) + } + return deleteFunc(ctx, req) + } + return processEntitiesFromFiles(appCtx.Context, appCtx.String("files"), deleteEntityFunc) + } else { + return fmt.Errorf(`either the "type" and "id" flags must be set, or the "files" flag must be set.`) + } +} + +func List(appCtx *cli.Context) error { + entityType := appCtx.String("type") + fieldMasks := strings.Split(appCtx.String("field_masks"), ",") + entityTypeEnumValue, found := nbipb.EntityType_value[entityType] + if !found { + return fmt.Errorf("unknown entity type %q is not one of [%s]", entityType, strings.Join(entityTypeList, ", ")) + } + entityTypeEnum := nbipb.EntityType(entityTypeEnumValue) + entityFilter := nbipb.EntityFilter{} + for _, value := range fieldMasks { + if trimmedValue := strings.TrimSpace(value); trimmedValue != "" { + entityFilter.FieldMasks = append(entityFilter.FieldMasks, trimmedValue) + } + } + + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewNetOpsClient(conn) + + res, err := client.ListEntities(appCtx.Context, &nbipb.ListEntitiesRequest{Type: &entityTypeEnum, Filter: &entityFilter}) + if err != nil { + return fmt.Errorf("unable to list entities: %w", err) + } + entitiesOutput := &nbipb.TxtpbEntities{ + Entity: res.Entities, + } + entitiesOutputTextProto, err := prototext.MarshalOptions{Multiline: true}.Marshal(entitiesOutput) + if err != nil { + return fmt.Errorf("unable to convert the response into textproto format: %w", err) + } + fmt.Fprintln(appCtx.App.Writer, string(entitiesOutputTextProto)) + fmt.Fprintf(appCtx.App.ErrWriter, "successfully queried a list of entities. number of entities: %d\n", len(res.Entities)) + return nil +} + +func GetLinkBudget(appCtx *cli.Context) error { + conn, err := openConnection(appCtx) + if err != nil { + return err + } + defer conn.Close() + client := nbipb.NewSignalPropagationClient(conn) + + spReq := &nbipb.SignalPropagationRequest{} + if appCtx.IsSet("input_file") { + reqPath := appCtx.String("input_file") + // If the user input a textproto file, use it to build the request. + req, err := os.ReadFile(reqPath) + if err != nil { + return fmt.Errorf("invalid file path: %w", err) + } + + if err := prototext.Unmarshal(req, spReq); err != nil { + return fmt.Errorf("reading SignalPropagationRequest from file %s: %w", reqPath, err) + } + } else { + txPlatformID := appCtx.String("tx_platform_id") + txTransceiverModelID := appCtx.String("tx_transceiver_model_id") + bandProfileID := appCtx.String("band_profile_id") + + errs := []error{} + if txPlatformID == "" { + errs = append(errs, errors.New("--tx_platform_id required")) + } + if txTransceiverModelID == "" { + errs = append(errs, errors.New("--tx_transceiver_model_id required")) + } + if bandProfileID == "" { + // TODO: Output a list of valid band profile IDs. + errs = append(errs, errors.New("--band_profile_id required")) + } + + startTime := appCtx.Timestamp("analysis_start_timestamp") + if startTime == nil { + // If the user did not specify the start of the analysis interval, + // it is set to the current local time. + now := time.Now() + startTime = &now + } + + endTime := appCtx.Timestamp("analysis_end_timestamp") + if endTime == nil { + // If the user did not provide the end of the analysis interval, it + // is set to the start time. Therefore, the signal propagation will + // be evaluated at the instant of the start time. + endTime = startTime + } + + refDataTime := appCtx.Timestamp("reference_data_timestamp") + if refDataTime == nil { + // If the user did not specify a reference data time, it is set to + // the start of the analysis interval. Therefore, the version of + // the entities used in the signal propagation analysis will match + // the start of the analysis interval. + refDataTime = startTime + } + + target := &resourcespb.TransceiverProvider{} + switch { + case appCtx.IsSet("target_platform_id") && appCtx.IsSet("target_transceiver_model_id"): + target = &resourcespb.TransceiverProvider{ + Source: &resourcespb.TransceiverProvider_IdInStore{ + IdInStore: &commonpb.TransceiverModelId{ + PlatformId: proto.String(appCtx.String("target_platform_id")), + TransceiverModelId: proto.String(appCtx.String("target_transceiver_model_id")), + }, + }, + } + case !appCtx.IsSet("target_platform_id") && !appCtx.IsSet("target_transceiver_model_id"): + // When the target's platform ID and transceiver model ID are not + // specified, the target field should be left unset (as opposed to + // setting it to an empty TransceiverProvider) to model the case of + // a fixed antenna. + target = nil + case !appCtx.IsSet("target_platform_id"): + errs = append(errs, errors.New("--target_platform_id required")) + case !appCtx.IsSet("target_transceiver_model_id"): + errs = append(errs, errors.New("--target_transceiver_model_id required.")) + } + + if err := errors.Join(errs...); err != nil { + return err + } + + stepSize := appCtx.Duration("step_size") + spatialPropagationStepSize := appCtx.Duration("spatial_propagation_step_size") + explainInaccessibility := appCtx.Bool("explain_inaccessibility") + + spReq = &nbipb.SignalPropagationRequest{ + TransmitterModel: &resourcespb.TransceiverProvider{ + Source: &resourcespb.TransceiverProvider_IdInStore{ + IdInStore: &commonpb.TransceiverModelId{ + PlatformId: &txPlatformID, + TransceiverModelId: &txTransceiverModelID, + }, + }, + }, + BandProfileId: &bandProfileID, + Target: target, + AnalysisTime: &nbipb.SignalPropagationRequest_AnalysisInterval{ + AnalysisInterval: &intervalpb.Interval{ + StartTime: timestamppb.New(*startTime), + EndTime: timestamppb.New(*endTime), + }, + }, + StepSize: durationpb.New(stepSize), + SpatialPropagationStepSize: durationpb.New(spatialPropagationStepSize), + ExplainInaccessibility: &explainInaccessibility, + ReferenceDataTime: timestamppb.New(*refDataTime), + } + } + + spRes, err := client.Evaluate(appCtx.Context, spReq) + if err != nil { + return fmt.Errorf("SignalPropagation.Evaluate: %w", err) + } + spResProto, err := prototext.MarshalOptions{Multiline: true}.Marshal(spRes) + if err != nil { + return fmt.Errorf("unable to convert the response into textproto format: %w", err) + } + + if !appCtx.IsSet("output_file") { + fmt.Fprintln(appCtx.App.Writer, string(spResProto)) + } else { + outPath := appCtx.Path("output_file") + // Creates the output file, if necessary, with read and write permissions. + if err := os.WriteFile(outPath, spResProto, 0o666); err != nil { + return fmt.Errorf("writing to output file %s: %w", outPath, err) + } + } + fmt.Fprintln(appCtx.App.ErrWriter, "successfully retrieved link budget.") + return nil +} + +func generateTypeList() []string { + var typeList []string + for _, val := range nbipb.EntityType_name { + if val != "ENTITY_TYPE_UNSPECIFIED" { + typeList = append(typeList, val) + } + } + sort.Strings(typeList) + return typeList +} + +func processEntitiesFromFiles(ctx context.Context, fileGlob string, f func(context.Context, *nbipb.Entity) error) error { + files, err := filepath.Glob(fileGlob) + if err != nil { + return fmt.Errorf("unable to expand the file path %w", err) + } else if len(files) == 0 { + return fmt.Errorf("no files found under the given file path: %s", fileGlob) + } + g, gCtx := errgroup.WithContext(ctx) + for _, filePath := range files { + entities := &nbipb.TxtpbEntities{} + msg, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("invalid file path: %w", err) + } + if err := prototext.Unmarshal(msg, entities); err != nil { + return fmt.Errorf("error while parsing file %s: %w", filePath, err) + } + for _, e := range entities.Entity { + entity := e + g.Go(func() error { + return f(gCtx, entity) + }) + } + } + return g.Wait() +} + +func validateEntityType(_ *cli.Context, t string) error { + for _, et := range entityTypeList { + if t == et { + return nil + } + } + return fmt.Errorf("unknown entity type %q", t) +} + +func validateProtoFormat(_ *cli.Context, f string) error { + switch f { + case "text", "json": + return nil + default: + return fmt.Errorf("unknown format %q", f) + } +} diff --git a/tools/nbictl/nbictl_test.go b/tools/nbictl/nbictl_test.go new file mode 100644 index 0000000..ccadae1 --- /dev/null +++ b/tools/nbictl/nbictl_test.go @@ -0,0 +1,579 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nbictl + +import ( + "bytes" + "context" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/google/go-cmp/cmp" + "github.com/urfave/cli/v2" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + + commonpb "aalyria.com/spacetime/api/common" + nbipb "aalyria.com/spacetime/api/nbi/v1alpha" + respb "aalyria.com/spacetime/api/nbi/v1alpha/resources" +) + +type testApp struct { + *cli.App + + stdin *bytes.Buffer + stdout *bytes.Buffer + stderr *bytes.Buffer +} + +func newTestApp() testApp { + stdin, stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{} + app := App() + app.Reader = stdin + app.Writer = stdout + app.ErrWriter = stderr + return testApp{stdin: stdin, stdout: stdout, stderr: stderr, App: app} +} + +func TestList_rejectsUnknownEntities(t *testing.T) { + t.Parallel() + + switch want, err := `unknown entity type "UFO"`, newTestApp().Run([]string{ + "nbictl", "list", "--type", "UFO", + }); { + case err == nil: + t.Fatal("expected --type UFO to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %s, but got %s", want, err.Error()) + } +} + +func TestGet_requiresType(t *testing.T) { + t.Parallel() + + switch want, err := `Required flag "type" not set`, newTestApp().Run([]string{ + "nbictl", "get", "--id", "abc", + }); { + case err == nil: + t.Fatal("expected missing --type to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestGet_rejectsUnknownEntities(t *testing.T) { + t.Parallel() + + switch want, err := `unknown entity type "UFO"`, newTestApp().Run([]string{ + "nbictl", "get", "--type", "UFO", "--id", "boba-cafe", + }); { + case err == nil: + t.Fatal("expected --type UFO to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestGet_requiresID(t *testing.T) { + t.Parallel() + + switch want, err := `Required flag "id" not set`, newTestApp().Run([]string{ + "nbictl", "get", "--type", "NETWORK_NODE", + }); { + case err == nil: + t.Fatal("expected missing --id to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestDelete_rejectsUnknownEntities(t *testing.T) { + t.Parallel() + + switch want, err := `unknown entity type "UFO"`, newTestApp().Run([]string{ + "nbictl", "delete", "--type", "UFO", "--id", "boba-cafe", + }); { + case err == nil: + t.Fatal("expected --type UFO to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestDelete_requiresID(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + g, ctx := errgroup.WithContext(ctx) + defer func() { checkErr(t, g.Wait()) }() + defer cancel() + srv := startInsecureServer(ctx, t, g) + + keys := generateKeysForTesting(t, tmpDir, "--org", "example org") + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, + "config", + "set", + "--transport_security", "insecure", + "--user_id", "usr1", + "--key_id", "key1", + "--priv_key", keys.key, + "--url", srv.listener.Addr().String(), + })) + + args := []string{"nbictl", "--config_dir", tmpDir, "--context", "DEFAULT", "delete", "--type", "NETWORK_NODE"} + switch want, err := `either the "type" and "id" flags must be set, or the "files" flag must be set.`, newTestApp().Run(args); { + case err == nil: + t.Fatal("expected missing --id to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestDelete_requiresLastCommitTimestampOrIgnoreConsistencyCheck(t *testing.T) { + t.Parallel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + g, ctx := errgroup.WithContext(ctx) + defer func() { checkErr(t, g.Wait()) }() + defer cancel() + srv := startInsecureServer(ctx, t, g) + + keys := generateKeysForTesting(t, tmpDir, "--org", "example org") + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, + "config", + "set", + "--transport_security", "insecure", + "--user_id", "usr1", + "--key_id", "key1", + "--priv_key", keys.key, + "--url", srv.listener.Addr().String(), + })) + + args := []string{"nbictl", "--config_dir", tmpDir, "--context", "DEFAULT", "delete", "--type", "NETWORK_NODE", "--id", "abc"} + switch want, err := `when deleting a single entity, either "last_commit_timestamp" or "ignore_consistency_check" flags should be set.`, newTestApp().Run(args); { + case err == nil: + t.Fatal("expected missing both --last_commit_timestamp and --ignore_consistency_check to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } + + args = []string{"nbictl", "--config_dir", tmpDir, "--context", "DEFAULT", "delete", "--type", "NETWORK_NODE", "--id", "abc", "--last_commit_timestamp", "123", "--ignore_consistency_check"} + switch want, err := `when deleting a single entity, either "last_commit_timestamp" or "ignore_consistency_check" flags should be set.`, newTestApp().Run(args); { + case err == nil: + t.Fatal("expected providing both --last_commit_timestamp and --ignore_consistency_check to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %q, but got %q", want, err.Error()) + } +} + +func TestCreate_requiresFiles(t *testing.T) { + t.Parallel() + + switch want, err := `Required flag "files" not set`, newTestApp().Run([]string{ + "nbictl", "create", + }); { + case err == nil: + t.Fatal("expected missing --files to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %v, but got %s", want, err.Error()) + } +} + +func TestUpdate_requiresFiles(t *testing.T) { + t.Parallel() + + switch want, err := `Required flag "files" not set`, newTestApp().Run([]string{ + "nbictl", "update", + }); { + case err == nil: + t.Fatal("expected missing --files to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %v, but got %s", want, err.Error()) + } +} + +func TestGrpcurlDescribe_rejectsTooManyArgs(t *testing.T) { + t.Parallel() + + switch want, err := `expected 0 or 1 arguments, got 2`, newTestApp().Run([]string{ + "nbictl", "grpcurl", "describe", "arg1", "arg2", + }); { + case err == nil: + t.Fatal("expected too many arguments to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %v, but got %s", want, err.Error()) + } +} + +func TestGrpcurlList_rejectsTooManyArgs(t *testing.T) { + t.Parallel() + + switch want, err := `expected 0 or 1 arguments, got 2`, newTestApp().Run([]string{ + "nbictl", "grpcurl", "list", "arg1", "arg2", + }); { + case err == nil: + t.Fatal("expected too many arguments to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %v, but got %s", want, err.Error()) + } +} + +func TestGrpcurlCall_rejectsInvalidFormat(t *testing.T) { + t.Parallel() + + switch want, err := `unknown format "idk"`, newTestApp().Run([]string{ + "nbictl", "grpcurl", "call", "--format", "idk", + }); { + case err == nil: + t.Fatal("expected invalid format to cause an error, got nil") + case !strings.Contains(err.Error(), want): + t.Fatalf("expected error to contain %v, but got %s", want, err.Error()) + } +} + +func startInsecureServer(ctx context.Context, t *testing.T, g *errgroup.Group) *FakeNetOpsServer { + lis, err := net.Listen("tcp", ":0") + checkErr(t, err) + srv, err := startFakeNbiServer(ctx, g, lis) + checkErr(t, err) + + return srv +} + +// Writes entities to the given directory in files named entities-%d.textproto. +func writeEntitiesToFiles(entitiesFiles [][]*nbipb.Entity, filePath string) error { + for i, entities := range entitiesFiles { + entitiesProto := &nbipb.TxtpbEntities{Entity: entities} + textprotoBytes, err := prototext.MarshalOptions{Multiline: true}.Marshal(entitiesProto) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(filePath, fmt.Sprintf("entities-%d.textproto", i)), textprotoBytes, 0o666) + if err != nil { + return err + } + } + + return nil +} + +func readEntitiesFromFile(filePath string) ([]*nbipb.Entity, error) { + files, err := filepath.Glob(filePath) + if err != nil { + return nil, fmt.Errorf("unable to expand the file path: %w", err) + } else if len(files) == 0 { + return nil, fmt.Errorf("no files found under the given file path: %s", filePath) + } + + entities := make([]*nbipb.Entity, 0, len(files)) + for _, f := range files { + msg, err := os.ReadFile(f) + if err != nil { + return nil, fmt.Errorf("invalid file path: %w", err) + } + + entity := &nbipb.Entity{} + if err := prototext.Unmarshal(msg, entity); err != nil { + return nil, fmt.Errorf("invalid file contents: %w", err) + } + entities = append(entities, entity) + } + + return entities, nil +} + +// Returns PLATFORM_DEFINITION entities whose IDs are 'entity-{f}-{i}', +// where f runs from 0 to (numFiles - 1) and i runs from 0 to (numEntities - 1). +func buildTestEntities(numFiles int, numEntities int) [][]*nbipb.Entity { + files := make([][]*nbipb.Entity, 0) + for f := 0; f < numFiles; f++ { + entities := make([]*nbipb.Entity, 0) + for i := 0; i < numEntities; i++ { + entityIdStr := fmt.Sprintf("entity-%d-%d", f, i) + entities = append(entities, &nbipb.Entity{ + Id: proto.String(entityIdStr), + Group: &nbipb.EntityGroup{ + Type: nbipb.EntityType_PLATFORM_DEFINITION.Enum(), + }, + Value: &nbipb.Entity_Platform{ + Platform: &commonpb.PlatformDefinition{ + Name: proto.String(entityIdStr), + }, + }, + }) + files = append(files, entities) + } + } + return files +} + +func TestEndToEnd(t *testing.T) { + t.Parallel() + + type endToEndTestCase struct { + name string + cmd []string + // Entities that are provided as inputs to the test case. + // These are written to a temporary directory, which is + // passed in the --files argument. + entitiesFiles [][]*nbipb.Entity + expectFn func([]byte) error + // Whether to verify the output of the test based on the + // state of the fake server. + expectServerStateFn func(*FakeNetOpsServer) error + // Whether to verify the output of the test based on the + // contents of the files containing the entities. + expectEntityFilesFn func(string) error + changeServer func(*FakeNetOpsServer) + } + + expectLines := func(lines ...string) func([]byte) error { + return func(gotData []byte) error { + got := strings.Split(strings.TrimSpace(string(gotData)), "\n") + if diff := cmp.Diff(lines, got); diff != "" { + return fmt.Errorf("output mismatch: (-want +got):\n%s", diff) + } + return nil + } + } + + // We can't use expectLines for prototext output because the format isn't + // stable (the authors intentionally sometimes vary the format by using two + // spaces between field name + colon and value), so instead we just verify + // that the result can get unmarshalled using the prototext library. + expectTextProto := func(want proto.Message) func([]byte) error { + return func(gotData []byte) error { + got := proto.Clone(want) + if err := prototext.Unmarshal(gotData, got); err != nil { + return err + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + return fmt.Errorf("output mismatch: (-want +got):\n%s", diff) + } + return nil + } + } + + // Verifies that all of the expected entities were processed by the server. + expectEntityIDs := func(expectedEntities [][]*nbipb.Entity) func(*FakeNetOpsServer) error { + return func(testServer *FakeNetOpsServer) error { + missingEntityIDs := []string{} + for _, entities := range expectedEntities { + for _, e := range entities { + id := e.GetId() + if _, ok := testServer.EntityIDsModified[id]; !ok { + missingEntityIDs = append(missingEntityIDs, id) + } + } + } + + if len(missingEntityIDs) > 0 { + return fmt.Errorf("expected entities were not modified:\n%v", missingEntityIDs) + } + return nil + } + } + + // Verifies that the given message was received as the most recent request by the server. + expectLatestRequest := func(want proto.Message) func(*FakeNetOpsServer) error { + return func(testServer *FakeNetOpsServer) error { + if diff := cmp.Diff(want, testServer.LatestRequest, protocmp.Transform()); diff != "" { + return fmt.Errorf("output mismatch: (-want +got):\n%s", diff) + } + return nil + } + } + + defaultTestEntities := buildTestEntities(5, 10) + + listResponse := &nbipb.ListEntitiesResponse{ + Entities: []*nbipb.Entity{ + { + Group: &nbipb.EntityGroup{}, + Id: proto.String("b0ba-cafe"), + CommitTimestamp: proto.Int64(1), + NextCommitTimestamp: proto.Int64(2), + LastModifiedBy: proto.String("your friend Ciaran"), + Value: &nbipb.Entity_NetworkNode{ + NetworkNode: &respb.NetworkNode{}, + }, + }, + }, + } + listOutput := &nbipb.TxtpbEntities{ + Entity: listResponse.Entities, + } + + testCases := []endToEndTestCase{ + { + name: "grpcurl describe netops", + cmd: []string{"grpcurl", "describe", "aalyria.spacetime.api.nbi.v1alpha.NetOps"}, + expectFn: expectLines( + "aalyria.spacetime.api.nbi.v1alpha.NetOps is a service", + ), + }, + { + name: "grpcurl describe", + cmd: []string{"grpcurl", "describe"}, + expectFn: expectLines( + "aalyria.spacetime.api.nbi.v1alpha.NetOps is a service", + "grpc.reflection.v1.ServerReflection is a service", + "grpc.reflection.v1alpha.ServerReflection is a service", + ), + }, + { + name: "grpcurl list", + cmd: []string{"grpcurl", "list"}, + expectFn: expectLines( + "aalyria.spacetime.api.nbi.v1alpha.NetOps", + "grpc.reflection.v1.ServerReflection", + "grpc.reflection.v1alpha.ServerReflection", + ), + }, + { + name: "grpcurl list netops", + cmd: []string{"grpcurl", "list", "aalyria.spacetime.api.nbi.v1alpha.NetOps"}, + expectFn: expectLines( + "aalyria.spacetime.api.nbi.v1alpha.NetOps.CreateEntity", + "aalyria.spacetime.api.nbi.v1alpha.NetOps.DeleteEntity", + "aalyria.spacetime.api.nbi.v1alpha.NetOps.GetEntity", + "aalyria.spacetime.api.nbi.v1alpha.NetOps.ListEntities", + "aalyria.spacetime.api.nbi.v1alpha.NetOps.ListEntitiesOverTime", + "aalyria.spacetime.api.nbi.v1alpha.NetOps.UpdateEntity", + "aalyria.spacetime.api.nbi.v1alpha.NetOps.VersionInfo", + ), + }, + { + name: "list", + cmd: []string{"list", "-t", "NETWORK_NODE"}, + changeServer: func(srv *FakeNetOpsServer) { + srv.ListEntityResponse = listResponse + }, + expectFn: expectTextProto(listOutput), + }, + { + name: "list with field mask", + cmd: []string{"list", "-t", "NETWORK_NODE", "--field_masks", "network_node.name,network_node.type"}, + expectServerStateFn: expectLatestRequest(&nbipb.ListEntitiesRequest{ + Type: nbipb.EntityType_NETWORK_NODE.Enum(), + Filter: &nbipb.EntityFilter{FieldMasks: []string{"network_node.name", "network_node.type"}}, + }), + }, + { + name: "create", + cmd: []string{"create"}, + entitiesFiles: defaultTestEntities, + expectServerStateFn: expectEntityIDs(defaultTestEntities), + }, + { + name: "update", + cmd: []string{"update"}, + entitiesFiles: defaultTestEntities, + expectServerStateFn: expectEntityIDs(defaultTestEntities), + }, + { + name: "delete single entity", + cmd: []string{"delete", "--type", "PLATFORM_DEFINITION", "--id", "my-id", "--last_commit_timestamp", "123456"}, + expectServerStateFn: expectEntityIDs([][]*nbipb.Entity{{ + { + Group: &nbipb.EntityGroup{ + Type: nbipb.EntityType_PLATFORM_DEFINITION.Enum(), + }, + Id: proto.String("my-id"), + CommitTimestamp: proto.Int64(123456), + }, + }}), + }, + { + name: "delete from files", + cmd: []string{"delete"}, + entitiesFiles: defaultTestEntities, + expectServerStateFn: expectEntityIDs(defaultTestEntities), + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + g, ctx := errgroup.WithContext(ctx) + defer func() { checkErr(t, g.Wait()) }() + defer cancel() + + tmpDir, err := bazel.NewTmpDir("nbictl") + checkErr(t, err) + + srv := startInsecureServer(ctx, t, g) + if tc.changeServer != nil { + tc.changeServer(srv) + } + + keys := generateKeysForTesting(t, tmpDir, "--org", "example org") + checkErr(t, newTestApp().Run([]string{ + "nbictl", "--config_dir", tmpDir, + "config", + "set", + "--transport_security", "insecure", + "--user_id", "usr1", + "--key_id", "key1", + "--priv_key", keys.key, + "--url", srv.listener.Addr().String(), + })) + + app := newTestApp() + args := append([]string{"nbictl", "--config_dir", tmpDir, "--context", "DEFAULT"}, tc.cmd...) + + entitiesFilesDir, err := bazel.NewTmpDir("entities") + checkErr(t, err) + // Makes the entity files directory into a glob from which the entities are read. + entitiesFilesGlob := entitiesFilesDir + "/*" + if tc.entitiesFiles != nil { + checkErr(t, writeEntitiesToFiles(tc.entitiesFiles, entitiesFilesDir)) + args = append(args, "--files", entitiesFilesGlob) + } + + checkErr(t, app.Run(args)) + if tc.expectFn != nil { + checkErr(t, tc.expectFn(app.stdout.Bytes())) + } + if tc.expectServerStateFn != nil { + checkErr(t, tc.expectServerStateFn(srv)) + } + if tc.expectEntityFilesFn != nil { + checkErr(t, tc.expectEntityFilesFn(entitiesFilesGlob)) + } + }) + } +} diff --git a/tools/nbictl/proto/BUILD b/tools/nbictl/proto/BUILD new file mode 100644 index 0000000..a562ed5 --- /dev/null +++ b/tools/nbictl/proto/BUILD @@ -0,0 +1,34 @@ +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "nbictl_proto", + srcs = [ + "nbi_ctl_config.proto", + ], + deps = [ + "@protobuf//:empty_proto", + ], +) + +go_proto_library( + name = "nbictl_go_proto", + importpath = "aalyria.com/spacetime/github/tools/nbictl/nbictlpb", + proto = ":nbictl_proto", +) diff --git a/tools/nbictl/proto/nbi_ctl_config.proto b/tools/nbictl/proto/nbi_ctl_config.proto new file mode 100644 index 0000000..a3024dc --- /dev/null +++ b/tools/nbictl/proto/nbi_ctl_config.proto @@ -0,0 +1,52 @@ +// Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package aalyria.spacetime.github.tools.nbictl; + +import "google/protobuf/empty.proto"; + +option go_package = "aalyria.com/spacetime/github/tools/nbictl/nbictlpb"; + +message AppConfig { + repeated Config configs = 1; +} + +message Config { + string name = 1; + string key_id = 2; + string email = 3; + string priv_key = 4; + string url = 5; + + message TransportSecurity { + message ServerCertificate { + string cert_file_path = 1; + } + + oneof type { + // Don't use TLS, connect using plain-text HTTP/2. + google.protobuf.Empty insecure = 1; + + // Use the system certificate pool for TLS. + google.protobuf.Empty system_cert_pool = 2; + + // Use the provided server certificate for TLS. + ServerCertificate server_certificate = 3; + } + } + + TransportSecurity transport_security = 7; +} diff --git a/tools/nbictl/readme_test.sh b/tools/nbictl/readme_test.sh new file mode 100755 index 0000000..6cc5d64 --- /dev/null +++ b/tools/nbictl/readme_test.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Copyright 2023 Aalyria Technologies, Inc., and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +README_PATH=tools/nbictl/README.md + +main() { + local nbictl + nbictl=$(readlink tools/nbictl/cmd/nbictl/nbictl_/nbictl) + local readme + readme=$(readlink "$README_PATH") + + if ! "$nbictl" readme | diff - "$readme"; + then + echo >&2 "$README_PATH doesn't match the generated output from the nbictl." + echo >&2 "Run the following command to regenerate:" + echo >&2 "bazel run //tools/nbictl/cmd/nbictl -- readme > \$PWD/$README_PATH" + return 1 + fi +} + +main "$@"