From 9b735d652128339bb8403fb318019d89dacc79d4 Mon Sep 17 00:00:00 2001 From: Orne Brocaar Date: Mon, 27 Jan 2025 10:34:19 +0000 Subject: [PATCH] Add first fuota storage functions / API. --- Cargo.lock | 53 ++ api/proto/api/fuota.proto | 334 ++++++++++++ api/rust/build.rs | 1 + api/rust/proto/chirpstack/api/fuota.proto | 334 ++++++++++++ chirpstack/Cargo.toml | 1 + .../down.sql | 3 + .../up.sql | 47 ++ .../down.sql | 3 + .../up.sql | 47 ++ chirpstack/src/api/auth/validator.rs | 515 +++++++++++++++++- chirpstack/src/api/error.rs | 11 + chirpstack/src/api/fuota.rs | 497 +++++++++++++++++ chirpstack/src/api/helpers.rs | 24 +- chirpstack/src/api/mod.rs | 6 + chirpstack/src/main.rs | 2 + chirpstack/src/storage/error.rs | 6 + chirpstack/src/storage/fields/fuota.rs | 79 +++ chirpstack/src/storage/fields/mod.rs | 2 + chirpstack/src/storage/fuota.rs | 431 +++++++++++++++ chirpstack/src/storage/mod.rs | 1 + chirpstack/src/storage/schema_postgres.rs | 62 +++ chirpstack/src/storage/schema_sqlite.rs | 58 ++ 22 files changed, 2514 insertions(+), 3 deletions(-) create mode 100644 api/proto/api/fuota.proto create mode 100644 api/rust/proto/chirpstack/api/fuota.proto create mode 100644 chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql create mode 100644 chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql create mode 100644 chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql create mode 100644 chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql create mode 100644 chirpstack/src/api/fuota.rs create mode 100644 chirpstack/src/storage/fields/fuota.rs create mode 100644 chirpstack/src/storage/fuota.rs diff --git a/Cargo.lock b/Cargo.lock index 0f6a01eb..bb46105f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,6 +902,7 @@ dependencies = [ "tracing-subscriber", "urlencoding", "uuid", + "validator", "x509-parser", ] @@ -3439,6 +3440,28 @@ dependencies = [ "toml_edit 0.22.22", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -5290,6 +5313,36 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/api/proto/api/fuota.proto b/api/proto/api/fuota.proto new file mode 100644 index 00000000..ca47896d --- /dev/null +++ b/api/proto/api/fuota.proto @@ -0,0 +1,334 @@ +syntax = "proto3"; + +package api; + +option go_package = "github.com/chirpstack/chirpstack/api/go/v4/api"; +option java_package = "io.chirpstack.api"; +option java_multiple_files = true; +option java_outer_classname = "FuotaProto"; +option csharp_namespace = "Chirpstack.Api"; +option php_namespace = "Chirpstack\\Api"; +option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api"; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; +import "common/common.proto"; +import "api/multicast_group.proto"; + +// FuotaService is the service providing API methods for FUOTA deployments. +service FuotaService { + // Create the given FUOTA deployment. + rpc CreateDeployment(CreateFuotaDeploymentRequest) returns (CreateFuotaDeploymentResponse) {} + + // Get the FUOTA deployment for the given ID. + rpc GetDeployment(GetFuotaDeploymentRequest) returns (GetFuotaDeploymentResponse) {} + + // Update the given FUOTA deployment. + rpc UpdateDeployment(UpdateFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // Delete the FUOTA deployment for the given ID. + rpc DeleteDeployment(DeleteFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // List the FUOTA deployments. + rpc ListDeployments(ListFuotaDeploymentsRequest) returns (ListFuotaDeploymentsResponse) {} + + // Add the given DevEUIs to the FUOTA deployment. + rpc AddDevices(AddDevicesToFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // Remove the given DevEUIs from the FUOTA deployment. + rpc RemoveDevices(RemoveDevicesFromFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // List FUOTA Deployment devices. + rpc ListDevices(ListFuotaDeploymentDevicesRequest) returns (ListFuotaDeploymentDevicesResponse) {} + + // Add the given Gateway IDs to the FUOTA deployment. + // By default, ChirpStack will automatically select the minimum amount of + // gateways needed to cover all devices within the multicast-group. Setting + // the gateways manually overrides this behaviour. + rpc AddGateways(AddGatewaysToFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // List the gateways added to the FUOTA deployment. + rpc ListGateways(ListFuotaDeploymentGatewaysRequest) returns (ListFuotaDeploymentGatewaysResponse) {} + + // Remove the given Gateway IDs from the FUOTA deployment. + rpc RemoveGateways(RemoveGatewaysFromFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + // GetLogs returns the logs for the FUOTA deployment. +} + +enum RequestFragmentationSessionStatus { + // Do not request the fragmentation-session status. + NO_REQUEST = 0; + + // Enqueue the fragmentation-session status request command directly after + // enqueueing the fragmentation-session fragments. This is the recommended + // option for Class-A devices as the status request will stay in the + // downlink queue until the device sends its next uplink. + AFTER_FRAGMENT_ENQUEUE = 1; + + // Enqueue the fragmentation-session status request after the multicast + // session-timeout. This is the recommended option for Class-B and -C + // devices as selecting AFTER_FRAGMENT_ENQUEUE will likely cause the NS + // to schedule the downlink frame during the FUOTA multicast-session. + AFTER_SESSION_TIMEOUT = 2; + +} + +message FuotaDeployment { + // Deployment ID. + // This value is automatically set on create. + string id = 1; + + // Application ID. + string application_id = 2; + + // Device-profile ID. + string device_profile_id = 3; + + // Deployment name. + string name = 4; + + // Multicast-group type. + MulticastGroupType multicast_group_type = 5; + + // Multicast-group scheduling type (Class-C only). + MulticastGroupSchedulingType multicast_class_c_scheduling_type = 6; + + // Multicast data-rate. + uint32 multicast_dr = 7; + + // Multicast ping-slot period (Class-B only). + uint32 multicast_class_b_ping_slot_nb_k = 8; + + // Multicast frequency (Hz). + uint32 multicast_frequency = 9; + + // Multicast timeout. + // This defines the timeout of the multicast-session. + // Please refer to the Remote Multicast Setup specification as this field + // has a different meaning for Class-B and Class-C groups. + uint32 multicast_timeout = 10; + + // Unicast attempt count. + // The number of attempts before considering an unicast command + // to be failed. + uint32 unicast_attempt_count = 11; + + // Fragmentation size. + // This defines the size of each payload fragment. Please refer to the + // Regional Parameters specification for the maximum payload sizes + // per data-rate and region. + uint32 fragmentation_fragment_size = 12; + + // Fragmentation redundancy. + // The number represents the additional redundant frames to send. + uint32 fragmentation_redundancy = 13; + + // Fragmentation session index. + uint32 fragmentation_session_index = 14; + + // Fragmentation matrix. + uint32 fragmentation_matrix = 15; + + // Block ack delay. + uint32 fragmentation_block_ack_delay = 16; + + // Descriptor (4 bytes). + bytes fragmentation_descriptor = 17; + + // Request fragmentation session status. + RequestFragmentationSessionStatus request_fragmentation_session_status = 18; + + // Payload. + // The FUOTA payload to send. + bytes payload = 19; +} + +message FuotaDeploymentListItem { + // ID. + string id = 1; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 2; + + // Updated at timestamp. + google.protobuf.Timestamp updated_at = 3; + + // Started at timestamp. + google.protobuf.Timestamp started_at = 4; + + // Completed at timestamp. + google.protobuf.Timestamp completed_at = 5; + + // Name. + string name = 6; +} + +message FuotaDeploymentDeviceListItem { + // ID. + string fuota_deployment_id = 1; + + // DevEUI. + string dev_eui = 2; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 3; + + // Updated at timestamp. + google.protobuf.Timestamp updated_at = 4; + + // McGroupSetup completed at timestamp. + google.protobuf.Timestamp mc_group_setup_completed_at = 5; + + // McSession completed at timestamp. + google.protobuf.Timestamp mc_session_completed_at = 6; + + // FragSessionSetup completed at timestamp. + google.protobuf.Timestamp frag_session_setup_completed_at = 7; + + // FragStatus completed at timestamp. + google.protobuf.Timestamp frag_status_completed_at = 8; +} + +message FuotaDeploymentGatewayListItem { + // ID. + string fuota_deployment_id = 1; + + // Gateway ID. + string gateway_id = 2; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 3; +} + +message CreateFuotaDeploymentRequest { + // Deployment. + FuotaDeployment deployment = 1; +} + +message CreateFuotaDeploymentResponse { + // ID of the created deployment. + string id = 1; +} + +message GetFuotaDeploymentRequest { + // FUOTA Deployment ID. + string id = 1; +} + +message GetFuotaDeploymentResponse { + // FUOTA Deployment. + FuotaDeployment deployment = 1; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 2; + + // Updated at timestamp. + google.protobuf.Timestamp updated_at = 3; +} + +message UpdateFuotaDeploymentRequest { + // Deployment. + FuotaDeployment deployment = 1; +} + +message DeleteFuotaDeploymentRequest { + // FUOTA deployment ID. + string id = 1; +} + +message ListFuotaDeploymentsRequest { + // Max number of FUOTA deployments to return in the result-set. + // If not set, it will be treated as 0, and the response will only return the total_count. + uint32 limit = 1; + + // Offset in the result-set (for pagination). + uint32 offset = 2; + + // Application ID to list the FUOTA Deployments for. + // This filter is mandatory. + string application_id = 3; +} + +message ListFuotaDeploymentsResponse { + // Total number of FUOTA Deployments. + uint32 total_count = 1; + + // Result-test. + repeated FuotaDeploymentListItem result = 2; +} + +message AddDevicesToFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // DevEUIs. + // Note that the DevEUIs must share the same device-profile as assigned to + // the FUOTA Deployment. + repeated string dev_euis = 2; +} + +message RemoveDevicesFromFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // DevEUIs. + repeated string dev_euis = 2; +} + +message ListFuotaDeploymentDevicesRequest { + // Max number of devices to return in the result-set. + // If not set, it will be treated as 0, and the response will only return the total_count. + uint32 limit = 1; + + // Offset in the result-set (for pagination). + uint32 offset = 2; + + // FUOTA Deployment ID. + string fuota_deployment_id = 3; +} + +message ListFuotaDeploymentDevicesResponse { + // Total number of devices. + uint32 total_count = 1; + + // Result-set. + repeated FuotaDeploymentDeviceListItem result = 2; +} + +message AddGatewaysToFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // Gateway IDs. + // Note that the Gateways must be under the same tenant as the FUOTA Deployment. + repeated string gateway_ids = 2; +} + +message RemoveGatewaysFromFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // Gateway IDs. + repeated string gateway_ids = 2; +} + +message ListFuotaDeploymentGatewaysRequest { + // Max number of gateways to return in the result-set. + // If not set, it will be treated as 0, and the response will only return the total_count. + uint32 limit = 1; + + // Offset in the result-set (for pagination). + uint32 offset = 2; + + // FUOTA Deployment ID. + string fuota_deployment_id = 3; +} + +message ListFuotaDeploymentGatewaysResponse { + // Total number of gateways. + uint32 total_count = 1; + + // Result-set. + repeated FuotaDeploymentGatewayListItem result = 2; +} diff --git a/api/rust/build.rs b/api/rust/build.rs index 08691754..9834240e 100644 --- a/api/rust/build.rs +++ b/api/rust/build.rs @@ -215,6 +215,7 @@ fn main() -> Result<(), Box> { .to_str() .unwrap(), cs_dir.join("api").join("relay.proto").to_str().unwrap(), + cs_dir.join("api").join("fuota.proto").to_str().unwrap(), ], &[ proto_dir.join("chirpstack").to_str().unwrap(), diff --git a/api/rust/proto/chirpstack/api/fuota.proto b/api/rust/proto/chirpstack/api/fuota.proto new file mode 100644 index 00000000..ca47896d --- /dev/null +++ b/api/rust/proto/chirpstack/api/fuota.proto @@ -0,0 +1,334 @@ +syntax = "proto3"; + +package api; + +option go_package = "github.com/chirpstack/chirpstack/api/go/v4/api"; +option java_package = "io.chirpstack.api"; +option java_multiple_files = true; +option java_outer_classname = "FuotaProto"; +option csharp_namespace = "Chirpstack.Api"; +option php_namespace = "Chirpstack\\Api"; +option php_metadata_namespace = "GPBMetadata\\Chirpstack\\Api"; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; +import "common/common.proto"; +import "api/multicast_group.proto"; + +// FuotaService is the service providing API methods for FUOTA deployments. +service FuotaService { + // Create the given FUOTA deployment. + rpc CreateDeployment(CreateFuotaDeploymentRequest) returns (CreateFuotaDeploymentResponse) {} + + // Get the FUOTA deployment for the given ID. + rpc GetDeployment(GetFuotaDeploymentRequest) returns (GetFuotaDeploymentResponse) {} + + // Update the given FUOTA deployment. + rpc UpdateDeployment(UpdateFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // Delete the FUOTA deployment for the given ID. + rpc DeleteDeployment(DeleteFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // List the FUOTA deployments. + rpc ListDeployments(ListFuotaDeploymentsRequest) returns (ListFuotaDeploymentsResponse) {} + + // Add the given DevEUIs to the FUOTA deployment. + rpc AddDevices(AddDevicesToFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // Remove the given DevEUIs from the FUOTA deployment. + rpc RemoveDevices(RemoveDevicesFromFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // List FUOTA Deployment devices. + rpc ListDevices(ListFuotaDeploymentDevicesRequest) returns (ListFuotaDeploymentDevicesResponse) {} + + // Add the given Gateway IDs to the FUOTA deployment. + // By default, ChirpStack will automatically select the minimum amount of + // gateways needed to cover all devices within the multicast-group. Setting + // the gateways manually overrides this behaviour. + rpc AddGateways(AddGatewaysToFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + + // List the gateways added to the FUOTA deployment. + rpc ListGateways(ListFuotaDeploymentGatewaysRequest) returns (ListFuotaDeploymentGatewaysResponse) {} + + // Remove the given Gateway IDs from the FUOTA deployment. + rpc RemoveGateways(RemoveGatewaysFromFuotaDeploymentRequest) returns (google.protobuf.Empty) {} + // GetLogs returns the logs for the FUOTA deployment. +} + +enum RequestFragmentationSessionStatus { + // Do not request the fragmentation-session status. + NO_REQUEST = 0; + + // Enqueue the fragmentation-session status request command directly after + // enqueueing the fragmentation-session fragments. This is the recommended + // option for Class-A devices as the status request will stay in the + // downlink queue until the device sends its next uplink. + AFTER_FRAGMENT_ENQUEUE = 1; + + // Enqueue the fragmentation-session status request after the multicast + // session-timeout. This is the recommended option for Class-B and -C + // devices as selecting AFTER_FRAGMENT_ENQUEUE will likely cause the NS + // to schedule the downlink frame during the FUOTA multicast-session. + AFTER_SESSION_TIMEOUT = 2; + +} + +message FuotaDeployment { + // Deployment ID. + // This value is automatically set on create. + string id = 1; + + // Application ID. + string application_id = 2; + + // Device-profile ID. + string device_profile_id = 3; + + // Deployment name. + string name = 4; + + // Multicast-group type. + MulticastGroupType multicast_group_type = 5; + + // Multicast-group scheduling type (Class-C only). + MulticastGroupSchedulingType multicast_class_c_scheduling_type = 6; + + // Multicast data-rate. + uint32 multicast_dr = 7; + + // Multicast ping-slot period (Class-B only). + uint32 multicast_class_b_ping_slot_nb_k = 8; + + // Multicast frequency (Hz). + uint32 multicast_frequency = 9; + + // Multicast timeout. + // This defines the timeout of the multicast-session. + // Please refer to the Remote Multicast Setup specification as this field + // has a different meaning for Class-B and Class-C groups. + uint32 multicast_timeout = 10; + + // Unicast attempt count. + // The number of attempts before considering an unicast command + // to be failed. + uint32 unicast_attempt_count = 11; + + // Fragmentation size. + // This defines the size of each payload fragment. Please refer to the + // Regional Parameters specification for the maximum payload sizes + // per data-rate and region. + uint32 fragmentation_fragment_size = 12; + + // Fragmentation redundancy. + // The number represents the additional redundant frames to send. + uint32 fragmentation_redundancy = 13; + + // Fragmentation session index. + uint32 fragmentation_session_index = 14; + + // Fragmentation matrix. + uint32 fragmentation_matrix = 15; + + // Block ack delay. + uint32 fragmentation_block_ack_delay = 16; + + // Descriptor (4 bytes). + bytes fragmentation_descriptor = 17; + + // Request fragmentation session status. + RequestFragmentationSessionStatus request_fragmentation_session_status = 18; + + // Payload. + // The FUOTA payload to send. + bytes payload = 19; +} + +message FuotaDeploymentListItem { + // ID. + string id = 1; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 2; + + // Updated at timestamp. + google.protobuf.Timestamp updated_at = 3; + + // Started at timestamp. + google.protobuf.Timestamp started_at = 4; + + // Completed at timestamp. + google.protobuf.Timestamp completed_at = 5; + + // Name. + string name = 6; +} + +message FuotaDeploymentDeviceListItem { + // ID. + string fuota_deployment_id = 1; + + // DevEUI. + string dev_eui = 2; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 3; + + // Updated at timestamp. + google.protobuf.Timestamp updated_at = 4; + + // McGroupSetup completed at timestamp. + google.protobuf.Timestamp mc_group_setup_completed_at = 5; + + // McSession completed at timestamp. + google.protobuf.Timestamp mc_session_completed_at = 6; + + // FragSessionSetup completed at timestamp. + google.protobuf.Timestamp frag_session_setup_completed_at = 7; + + // FragStatus completed at timestamp. + google.protobuf.Timestamp frag_status_completed_at = 8; +} + +message FuotaDeploymentGatewayListItem { + // ID. + string fuota_deployment_id = 1; + + // Gateway ID. + string gateway_id = 2; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 3; +} + +message CreateFuotaDeploymentRequest { + // Deployment. + FuotaDeployment deployment = 1; +} + +message CreateFuotaDeploymentResponse { + // ID of the created deployment. + string id = 1; +} + +message GetFuotaDeploymentRequest { + // FUOTA Deployment ID. + string id = 1; +} + +message GetFuotaDeploymentResponse { + // FUOTA Deployment. + FuotaDeployment deployment = 1; + + // Created at timestamp. + google.protobuf.Timestamp created_at = 2; + + // Updated at timestamp. + google.protobuf.Timestamp updated_at = 3; +} + +message UpdateFuotaDeploymentRequest { + // Deployment. + FuotaDeployment deployment = 1; +} + +message DeleteFuotaDeploymentRequest { + // FUOTA deployment ID. + string id = 1; +} + +message ListFuotaDeploymentsRequest { + // Max number of FUOTA deployments to return in the result-set. + // If not set, it will be treated as 0, and the response will only return the total_count. + uint32 limit = 1; + + // Offset in the result-set (for pagination). + uint32 offset = 2; + + // Application ID to list the FUOTA Deployments for. + // This filter is mandatory. + string application_id = 3; +} + +message ListFuotaDeploymentsResponse { + // Total number of FUOTA Deployments. + uint32 total_count = 1; + + // Result-test. + repeated FuotaDeploymentListItem result = 2; +} + +message AddDevicesToFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // DevEUIs. + // Note that the DevEUIs must share the same device-profile as assigned to + // the FUOTA Deployment. + repeated string dev_euis = 2; +} + +message RemoveDevicesFromFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // DevEUIs. + repeated string dev_euis = 2; +} + +message ListFuotaDeploymentDevicesRequest { + // Max number of devices to return in the result-set. + // If not set, it will be treated as 0, and the response will only return the total_count. + uint32 limit = 1; + + // Offset in the result-set (for pagination). + uint32 offset = 2; + + // FUOTA Deployment ID. + string fuota_deployment_id = 3; +} + +message ListFuotaDeploymentDevicesResponse { + // Total number of devices. + uint32 total_count = 1; + + // Result-set. + repeated FuotaDeploymentDeviceListItem result = 2; +} + +message AddGatewaysToFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // Gateway IDs. + // Note that the Gateways must be under the same tenant as the FUOTA Deployment. + repeated string gateway_ids = 2; +} + +message RemoveGatewaysFromFuotaDeploymentRequest { + // FUOTA Deployment ID. + string fuota_deployment_id = 1; + + // Gateway IDs. + repeated string gateway_ids = 2; +} + +message ListFuotaDeploymentGatewaysRequest { + // Max number of gateways to return in the result-set. + // If not set, it will be treated as 0, and the response will only return the total_count. + uint32 limit = 1; + + // Offset in the result-set (for pagination). + uint32 offset = 2; + + // FUOTA Deployment ID. + string fuota_deployment_id = 3; +} + +message ListFuotaDeploymentGatewaysResponse { + // Total number of gateways. + uint32 total_count = 1; + + // Result-set. + repeated FuotaDeploymentGatewayListItem result = 2; +} diff --git a/chirpstack/Cargo.toml b/chirpstack/Cargo.toml index eada36c0..6ba9592a 100644 --- a/chirpstack/Cargo.toml +++ b/chirpstack/Cargo.toml @@ -21,6 +21,7 @@ humantime-serde = "1.1" toml = "0.8" handlebars = "6.2" + validator = { version = "0.20", features = ["derive"] } # Database email_address = "0.2" diff --git a/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql new file mode 100644 index 00000000..f90d66e8 --- /dev/null +++ b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql @@ -0,0 +1,3 @@ +drop table fuota_deployment_gateway; +drop table fuota_deployment_device; +drop table fuota_deployment; diff --git a/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql new file mode 100644 index 00000000..3a8935f3 --- /dev/null +++ b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql @@ -0,0 +1,47 @@ +create table fuota_deployment ( + id uuid primary key, + created_at timestamp with time zone not null, + updated_at timestamp with time zone not null, + started_at timestamp with time zone null, + completed_at timestamp with time zone null, + name varchar(100) not null, + application_id uuid not null references application on delete cascade, + device_profile_id uuid not null references device_profile on delete cascade, + multicast_group_type char(1) not null, + multicast_class_c_scheduling_type varchar(20) not null, + multicast_dr smallint not null, + multicast_class_b_ping_slot_nb_k smallint not null, + multicast_frequency bigint not null, + multicast_timeout smallint not null, + unicast_attempt_count smallint not null, + fragmentation_fragment_size smallint not null, + fragmentation_redundancy smallint not null, + fragmentation_session_index smallint not null, + fragmentation_matrix smallint not null, + fragmentation_block_ack_delay smallint not null, + fragmentation_descriptor bytea not null, + request_fragmentation_session_status varchar(20) not null, + payload bytea not null +); + +create table fuota_deployment_device ( + fuota_deployment_id uuid not null references fuota_deployment on delete cascade, + dev_eui bytea not null references device on delete cascade, + created_at timestamp with time zone not null, + updated_at timestamp with time zone not null, + + mc_group_setup_completed_at timestamp with time zone null, + mc_session_completed_at timestamp with time zone null, + frag_session_setup_completed_at timestamp with time zone null, + frag_status_completed_at timestamp with time zone null, + + primary key (fuota_deployment_id, dev_eui) +); + +create table fuota_deployment_gateway ( + fuota_deployment_id uuid not null references fuota_deployment on delete cascade, + gateway_id bytea not null references gateway on delete cascade, + created_at timestamp with time zone not null, + + primary key (fuota_deployment_id, gateway_id) +); diff --git a/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql new file mode 100644 index 00000000..f90d66e8 --- /dev/null +++ b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql @@ -0,0 +1,3 @@ +drop table fuota_deployment_gateway; +drop table fuota_deployment_device; +drop table fuota_deployment; diff --git a/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql new file mode 100644 index 00000000..af414964 --- /dev/null +++ b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql @@ -0,0 +1,47 @@ +create table fuota_deployment ( + id text not null primary key, + created_at datetime not null, + updated_at datetime not null, + started_at datetime null, + completed_at datetime null, + name varchar(100) not null, + application_id text not null references application on delete cascade, + device_profile_id text not null references device_profile on delete cascade, + multicast_group_type char(1) not null, + multicast_class_c_scheduling_type varchar(20) not null, + multicast_dr smallint not null, + multicast_class_b_ping_slot_nb_k smallint not null, + multicast_frequency bigint not null, + multicast_timeout smallint not null, + unicast_attempt_count smallint not null, + fragmentation_fragment_size smallint not null, + fragmentation_redundancy smallint not null, + fragmentation_session_index smallint not null, + fragmentation_matrix smallint not null, + fragmentation_block_ack_delay smallint not null, + fragmentation_descriptor blob not null, + request_fragmentation_session_status varchar(20) not null, + payload blob not null +); + +create table fuota_deployment_device ( + fuota_deployment_id text not null references fuota_deployment on delete cascade, + dev_eui blob not null references device on delete cascade, + created_at datetime not null, + updated_at datetime not null, + + mc_group_setup_completed_at datetime null, + mc_session_completed_at datetime null, + frag_session_setup_completed_at datetime null, + frag_status_completed_at datetime null, + + primary key (fuota_deployment_id, dev_eui) +); + +create table fuota_deployment_gateway ( + fuota_deployment_id text not null references fuota_deployment on delete cascade, + gateway_id blob not null references gateway on delete cascade, + created_at datetime not null, + + primary key (fuota_deployment_id, gateway_id) +); diff --git a/chirpstack/src/api/auth/validator.rs b/chirpstack/src/api/auth/validator.rs index da122cdb..8ac3c7ae 100644 --- a/chirpstack/src/api/auth/validator.rs +++ b/chirpstack/src/api/auth/validator.rs @@ -12,7 +12,8 @@ use super::error::Error; use crate::api::auth::AuthID; use crate::helpers::errors::PrintFullError; use crate::storage::schema::{ - api_key, application, device, device_profile, gateway, multicast_group, tenant_user, user, + api_key, application, device, device_profile, fuota_deployment, gateway, multicast_group, + tenant_user, user, }; use crate::storage::{fields, get_async_db_conn}; @@ -2032,11 +2033,227 @@ impl Validator for ValidateMulticastGroupQueueAccess { } } +pub struct ValidateFuotaDeploymentsAccess { + flag: Flag, + application_id: Uuid, +} + +impl ValidateFuotaDeploymentsAccess { + pub fn new(flag: Flag, application_id: Uuid) -> Self { + ValidateFuotaDeploymentsAccess { + flag, + application_id, + } + } +} + +#[async_trait] +impl Validator for ValidateFuotaDeploymentsAccess { + async fn validate_user(&self, id: &Uuid) -> Result { + let mut q = user::dsl::user + .select(dsl::count_star()) + .filter( + user::dsl::id + .eq(fields::Uuid::from(id)) + .and(user::dsl::is_active.eq(true)), + ) + .into_boxed(); + + match self.flag { + // admin user + // tenant admin + // tenant device admin + Flag::Create => { + q = + q.filter( + user::dsl::is_admin.eq(true).or(dsl::exists( + application::dsl::application + .inner_join(tenant_user::table.on( + tenant_user::dsl::tenant_id.eq(application::dsl::tenant_id), + )) + .filter( + application::dsl::id + .eq(fields::Uuid::from(self.application_id)) + .and(tenant_user::dsl::user_id.eq(user::dsl::id)) + .and( + tenant_user::dsl::is_admin + .eq(true) + .or(tenant_user::dsl::is_device_admin.eq(true)), + ), + ), + )), + ); + } + // admin user + // tenant user + Flag::List => { + q = + q.filter( + user::dsl::is_admin.eq(true).or(dsl::exists( + application::dsl::application + .inner_join(tenant_user::table.on( + tenant_user::dsl::tenant_id.eq(application::dsl::tenant_id), + )) + .filter( + application::dsl::id + .eq(fields::Uuid::from(self.application_id)) + .and(tenant_user::dsl::user_id.eq(user::dsl::id)), + ), + )), + ); + } + _ => return Ok(0), + } + + Ok(q.first(&mut get_async_db_conn().await?).await?) + } + + async fn validate_key(&self, id: &Uuid) -> Result { + let mut q = api_key::dsl::api_key + .select(dsl::count_star()) + .filter(api_key::dsl::id.eq(fields::Uuid::from(id))) + .into_boxed(); + + match self.flag { + // admin api key + // tenant api key + Flag::Create | Flag::List => { + q = q.filter( + api_key::dsl::is_admin.eq(true).or(dsl::exists( + application::dsl::application.filter( + application::dsl::id + .eq(fields::Uuid::from(self.application_id)) + .and( + api_key::dsl::tenant_id + .eq(application::dsl::tenant_id.nullable()), + ), + ), + )), + ); + } + _ => { + return Ok(0); + } + } + + Ok(q.first(&mut get_async_db_conn().await?).await?) + } +} + +pub struct ValidateFuotaDeploymentAccess { + flag: Flag, + fuota_deployment_id: Uuid, +} + +impl ValidateFuotaDeploymentAccess { + pub fn new(flag: Flag, fuota_deployment_id: Uuid) -> Self { + ValidateFuotaDeploymentAccess { + flag, + fuota_deployment_id, + } + } +} + +#[async_trait] +impl Validator for ValidateFuotaDeploymentAccess { + async fn validate_user(&self, id: &Uuid) -> Result { + let mut q = user::dsl::user + .select(dsl::count_star()) + .filter( + user::dsl::id + .eq(fields::Uuid::from(id)) + .and(user::dsl::is_active.eq(true)), + ) + .into_boxed(); + + match self.flag { + // admin user + // tenant user + Flag::Read => { + q = + q.filter( + user::dsl::is_admin.eq(true).or(dsl::exists( + fuota_deployment::dsl::fuota_deployment + .inner_join(application::table) + .inner_join(tenant_user::table.on( + tenant_user::dsl::tenant_id.eq(application::dsl::tenant_id), + )) + .filter( + fuota_deployment::dsl::id + .eq(fields::Uuid::from(self.fuota_deployment_id)) + .and(tenant_user::dsl::user_id.eq(user::dsl::id)), + ), + )), + ); + } + // admin user + // tenant admin + // tenant device admin + Flag::Update | Flag::Delete => { + q = + q.filter( + user::dsl::is_admin.eq(true).or(dsl::exists( + fuota_deployment::dsl::fuota_deployment + .inner_join(application::table) + .inner_join(tenant_user::table.on( + tenant_user::dsl::tenant_id.eq(application::dsl::tenant_id), + )) + .filter( + fuota_deployment::dsl::id + .eq(fields::Uuid::from(self.fuota_deployment_id)) + .and(tenant_user::dsl::user_id.eq(user::dsl::id)) + .and( + tenant_user::dsl::is_admin + .eq(true) + .or(tenant_user::dsl::is_device_admin.eq(true)), + ), + ), + )), + ); + } + _ => return Ok(0), + } + + Ok(q.first(&mut get_async_db_conn().await?).await?) + } + + async fn validate_key(&self, id: &Uuid) -> Result { + let mut q = api_key::dsl::api_key + .select(dsl::count_star()) + .filter(api_key::dsl::id.eq(fields::Uuid::from(id))) + .into_boxed(); + + match self.flag { + // admin api key + // tenant api key + Flag::Read | Flag::Update | Flag::Delete => { + q = q.filter( + api_key::dsl::is_admin.eq(true).or(dsl::exists( + fuota_deployment::dsl::fuota_deployment + .inner_join(application::table) + .filter( + fuota_deployment::dsl::id + .eq(fields::Uuid::from(self.fuota_deployment_id)) + .and( + api_key::dsl::tenant_id + .eq(application::dsl::tenant_id.nullable()), + ), + ), + )), + ); + } + _ => return Ok(0), + } + + Ok(q.first(&mut get_async_db_conn().await?).await?) + } +} + #[cfg(test)] pub mod test { use super::*; use crate::storage::{ - api_key, application, device, device_profile, gateway, multicast, tenant, user, + api_key, application, device, device_profile, fuota, gateway, multicast, tenant, user, }; use crate::test; use std::str::FromStr; @@ -4619,4 +4836,298 @@ pub mod test { ]; run_tests(tests).await; } + + #[tokio::test] + async fn fuota_deployment() { + let _guard = test::prepare().await; + + let user_active = user::User { + email: "user@user".into(), + is_active: true, + ..Default::default() + }; + let user_admin = user::User { + email: "admin@user".into(), + is_active: true, + is_admin: true, + ..Default::default() + }; + let tenant_admin = user::User { + email: "tenant-admin@user".into(), + is_active: true, + ..Default::default() + }; + let tenant_device_admin = user::User { + email: "tenant-device-admin@user".into(), + is_active: true, + ..Default::default() + }; + let tenant_gateway_admin = user::User { + email: "tenant-gateway-admin@user".into(), + is_active: true, + ..Default::default() + }; + let tenant_user = user::User { + email: "tenant-user@user".into(), + is_active: true, + ..Default::default() + }; + + for u in [ + &user_active, + &user_admin, + &tenant_admin, + &tenant_gateway_admin, + &tenant_device_admin, + &tenant_user, + ] { + user::create(u.clone()).await.unwrap(); + } + + let api_key_admin = api_key::test::create_api_key(true, false).await; + let api_key_tenant = api_key::test::create_api_key(false, true).await; + let api_key_other_tenant = api_key::test::create_api_key(false, true).await; + + let app = + application::test::create_application(Some(api_key_tenant.tenant_id.unwrap().into())) + .await; + + tenant::add_user(tenant::TenantUser { + tenant_id: api_key_tenant.tenant_id.unwrap(), + user_id: tenant_admin.id, + is_admin: true, + ..Default::default() + }) + .await + .unwrap(); + tenant::add_user(tenant::TenantUser { + tenant_id: api_key_tenant.tenant_id.unwrap().into(), + user_id: tenant_device_admin.id, + is_device_admin: true, + ..Default::default() + }) + .await + .unwrap(); + tenant::add_user(tenant::TenantUser { + tenant_id: api_key_tenant.tenant_id.unwrap(), + user_id: tenant_gateway_admin.id, + is_gateway_admin: true, + ..Default::default() + }) + .await + .unwrap(); + tenant::add_user(tenant::TenantUser { + tenant_id: api_key_tenant.tenant_id.unwrap(), + user_id: tenant_user.id, + ..Default::default() + }) + .await + .unwrap(); + + // fuota deployments with user + let tests = vec![ + // admin user can create and list + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::User(user_admin.id.into()), + ok: true, + }, + // tenant admin can create and list + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::User(tenant_admin.id.into()), + ok: true, + }, + // tenant device admin can create and list + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::User(tenant_device_admin.id.into()), + ok: true, + }, + // tenant user can list + ValidatorTest { + validators: vec![ValidateFuotaDeploymentsAccess::new( + Flag::List, + app.id.into(), + )], + id: AuthID::User(tenant_user.id.into()), + ok: true, + }, + // tenant user can not create + ValidatorTest { + validators: vec![ValidateFuotaDeploymentsAccess::new( + Flag::Create, + app.id.into(), + )], + id: AuthID::User(tenant_user.id.into()), + ok: false, + }, + // other user can not create or list + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::User(user_active.id.into()), + ok: false, + }, + ]; + run_tests(tests).await; + + // fuota deployments with api key + let tests = vec![ + // admin api key can create and list + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::Key(api_key_admin.id.into()), + ok: true, + }, + // tenant api key can create and list + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::Key(api_key_tenant.id.into()), + ok: true, + }, + // tenant api key can not create or list for other tenant + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentsAccess::new(Flag::Create, app.id.into()), + ValidateFuotaDeploymentsAccess::new(Flag::List, app.id.into()), + ], + id: AuthID::Key(api_key_other_tenant.id.into()), + ok: false, + }, + ]; + run_tests(tests).await; + + let dp = device_profile::create(device_profile::DeviceProfile { + tenant_id: app.tenant_id, + name: "test-dp".into(), + ..Default::default() + }) + .await + .unwrap(); + + let fuota = fuota::create_deployment(fuota::FuotaDeployment { + name: "test-fuota".into(), + application_id: app.id, + device_profile_id: dp.id, + ..Default::default() + }) + .await + .unwrap(); + + // fuota deployment with user + let tests = vec![ + // admin user can read, update and delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::User(user_admin.id.into()), + ok: true, + }, + // tenant admin can read, update and delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::User(tenant_admin.id.into()), + ok: true, + }, + // tenant device admin can read, update and delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::User(tenant_device_admin.id.into()), + ok: true, + }, + // tenant user can read + ValidatorTest { + validators: vec![ValidateFuotaDeploymentAccess::new( + Flag::Read, + fuota.id.into(), + )], + id: AuthID::User(tenant_user.id.into()), + ok: true, + }, + // tenant user can not update or delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::User(tenant_user.id.into()), + ok: false, + }, + // other user can not read, update or delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::User(user_active.id.into()), + ok: false, + }, + ]; + run_tests(tests).await; + + // fuota deployment with api key + let tests = vec![ + // admin api key can read, update and delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::Key(api_key_admin.id.into()), + ok: true, + }, + // tenant api key can read, update and delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::Key(api_key_admin.id.into()), + ok: true, + }, + // other api key can not read, update or delete + ValidatorTest { + validators: vec![ + ValidateFuotaDeploymentAccess::new(Flag::Read, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Update, fuota.id.into()), + ValidateFuotaDeploymentAccess::new(Flag::Delete, fuota.id.into()), + ], + id: AuthID::Key(api_key_other_tenant.id.into()), + ok: false, + }, + ]; + run_tests(tests).await; + } } diff --git a/chirpstack/src/api/error.rs b/chirpstack/src/api/error.rs index eb344efe..47c4533d 100644 --- a/chirpstack/src/api/error.rs +++ b/chirpstack/src/api/error.rs @@ -47,6 +47,17 @@ impl ToStatus for storage::error::Error { storage::error::Error::ProstDecode(_) => { Status::new(Code::Internal, format!("{:#}", self)) } + storage::error::Error::ValidatorValidate(_) => { + Status::new(Code::InvalidArgument, format!("{:#}", self)) + } + storage::error::Error::MultiError(errors) => { + let errors = errors + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join(", "); + Status::new(Code::InvalidArgument, errors) + } } } } diff --git a/chirpstack/src/api/fuota.rs b/chirpstack/src/api/fuota.rs new file mode 100644 index 00000000..522dc365 --- /dev/null +++ b/chirpstack/src/api/fuota.rs @@ -0,0 +1,497 @@ +use std::str::FromStr; + +use tonic::{Request, Response, Status}; +use uuid::Uuid; + +use chirpstack_api::api; +use chirpstack_api::api::fuota_service_server::FuotaService; +use lrwn::EUI64; + +use crate::api::auth::validator; +use crate::api::error::ToStatus; +use crate::api::helpers::{self, FromProto, ToProto}; +use crate::storage::fuota; + +pub struct Fuota { + validator: validator::RequestValidator, +} + +impl Fuota { + pub fn new(validator: validator::RequestValidator) -> Self { + Fuota { validator } + } +} + +#[tonic::async_trait] +impl FuotaService for Fuota { + async fn create_deployment( + &self, + request: Request, + ) -> Result, Status> { + let req_dp = match &request.get_ref().deployment { + Some(v) => v, + None => { + return Err(Status::invalid_argument("deployment is missing")); + } + }; + + let app_id = Uuid::from_str(&req_dp.application_id).map_err(|e| e.status())?; + let dp_id = Uuid::from_str(&req_dp.device_profile_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentsAccess::new(validator::Flag::Create, app_id), + ) + .await?; + + let dp = fuota::FuotaDeployment { + name: req_dp.name.clone(), + application_id: app_id.into(), + device_profile_id: dp_id.into(), + multicast_group_type: match req_dp.multicast_group_type() { + api::MulticastGroupType::ClassB => "B", + api::MulticastGroupType::ClassC => "C", + } + .to_string(), + multicast_class_c_scheduling_type: req_dp + .multicast_class_c_scheduling_type() + .from_proto(), + multicast_dr: req_dp.multicast_dr as i16, + multicast_class_b_ping_slot_nb_k: req_dp.multicast_class_b_ping_slot_nb_k as i16, + multicast_frequency: req_dp.multicast_frequency as i64, + multicast_timeout: req_dp.multicast_timeout as i16, + unicast_attempt_count: req_dp.unicast_attempt_count as i16, + fragmentation_fragment_size: req_dp.fragmentation_fragment_size as i16, + fragmentation_redundancy: req_dp.fragmentation_redundancy as i16, + fragmentation_session_index: req_dp.fragmentation_session_index as i16, + fragmentation_matrix: req_dp.fragmentation_matrix as i16, + fragmentation_block_ack_delay: req_dp.fragmentation_block_ack_delay as i16, + fragmentation_descriptor: req_dp.fragmentation_descriptor.clone(), + request_fragmentation_session_status: req_dp + .request_fragmentation_session_status() + .from_proto(), + payload: req_dp.payload.clone(), + ..Default::default() + }; + let dp = fuota::create_deployment(dp).await.map_err(|e| e.status())?; + + let mut resp = Response::new(api::CreateFuotaDeploymentResponse { + id: dp.id.to_string(), + }); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + dp.id.to_string().parse().unwrap(), + ); + + Ok(resp) + } + + async fn get_deployment( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Read, dp_id), + ) + .await?; + + let dp = fuota::get_deployment(dp_id).await.map_err(|e| e.status())?; + + let mut resp = Response::new(api::GetFuotaDeploymentResponse { + deployment: Some(api::FuotaDeployment { + id: dp.id.to_string(), + application_id: dp.application_id.to_string(), + device_profile_id: dp.device_profile_id.to_string(), + name: dp.name.clone(), + multicast_group_type: match dp.multicast_group_type.as_ref() { + "B" => api::MulticastGroupType::ClassB, + "C" => api::MulticastGroupType::ClassC, + _ => return Err(Status::invalid_argument("Invalid multicast_group_type")), + } + .into(), + multicast_class_c_scheduling_type: dp + .multicast_class_c_scheduling_type + .to_proto() + .into(), + multicast_dr: dp.multicast_dr as u32, + multicast_class_b_ping_slot_nb_k: dp.multicast_class_b_ping_slot_nb_k as u32, + multicast_frequency: dp.multicast_frequency as u32, + multicast_timeout: dp.multicast_timeout as u32, + unicast_attempt_count: dp.unicast_attempt_count as u32, + fragmentation_fragment_size: dp.fragmentation_fragment_size as u32, + fragmentation_redundancy: dp.fragmentation_redundancy as u32, + fragmentation_session_index: dp.fragmentation_session_index as u32, + fragmentation_matrix: dp.fragmentation_matrix as u32, + fragmentation_block_ack_delay: dp.fragmentation_block_ack_delay as u32, + fragmentation_descriptor: dp.fragmentation_descriptor.clone(), + request_fragmentation_session_status: dp + .request_fragmentation_session_status + .to_proto() + .into(), + payload: dp.payload.clone(), + }), + created_at: Some(helpers::datetime_to_prost_timestamp(&dp.created_at)), + updated_at: Some(helpers::datetime_to_prost_timestamp(&dp.updated_at)), + }); + resp.metadata_mut() + .insert("x-log-fuota_deployment_id", req.id.parse().unwrap()); + + Ok(resp) + } + + async fn update_deployment( + &self, + request: Request, + ) -> Result, Status> { + let req_dp = match &request.get_ref().deployment { + Some(v) => v, + None => { + return Err(Status::invalid_argument("deployment is missing")); + } + }; + + let id = Uuid::from_str(&req_dp.id).map_err(|e| e.status())?; + let app_id = Uuid::from_str(&req_dp.application_id).map_err(|e| e.status())?; + let dp_id = Uuid::from_str(&req_dp.device_profile_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Update, dp_id), + ) + .await?; + + let _ = fuota::update_deployment(fuota::FuotaDeployment { + id: id.into(), + name: req_dp.name.clone(), + application_id: app_id.into(), + device_profile_id: dp_id.into(), + multicast_group_type: match req_dp.multicast_group_type() { + api::MulticastGroupType::ClassB => "B", + api::MulticastGroupType::ClassC => "C", + } + .to_string(), + multicast_class_c_scheduling_type: req_dp + .multicast_class_c_scheduling_type() + .from_proto(), + multicast_dr: req_dp.multicast_dr as i16, + multicast_class_b_ping_slot_nb_k: req_dp.multicast_class_b_ping_slot_nb_k as i16, + multicast_frequency: req_dp.multicast_frequency as i64, + multicast_timeout: req_dp.multicast_timeout as i16, + unicast_attempt_count: req_dp.unicast_attempt_count as i16, + fragmentation_fragment_size: req_dp.fragmentation_fragment_size as i16, + fragmentation_redundancy: req_dp.fragmentation_redundancy as i16, + fragmentation_session_index: req_dp.fragmentation_session_index as i16, + fragmentation_matrix: req_dp.fragmentation_matrix as i16, + fragmentation_block_ack_delay: req_dp.fragmentation_block_ack_delay as i16, + fragmentation_descriptor: req_dp.fragmentation_descriptor.clone(), + request_fragmentation_session_status: req_dp + .request_fragmentation_session_status() + .from_proto(), + payload: req_dp.payload.clone(), + ..Default::default() + }) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut() + .insert("x-log-fuota_deployment_id", req_dp.id.parse().unwrap()); + Ok(resp) + } + + async fn delete_deployment( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let id = Uuid::from_str(&req.id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Delete, id), + ) + .await?; + + let _ = fuota::delete_deployment(id).await.map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut() + .insert("x-log-fuota_deployment_id", req.id.parse().unwrap()); + Ok(resp) + } + + async fn list_deployments( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let app_id = Uuid::from_str(&req.application_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentsAccess::new(validator::Flag::List, app_id), + ) + .await?; + + let count = fuota::get_deployment_count(app_id) + .await + .map_err(|e| e.status())?; + let items = fuota::list_deployments(app_id, req.limit as i64, req.offset as i64) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(api::ListFuotaDeploymentsResponse { + total_count: count as u32, + result: items + .iter() + .map(|d| api::FuotaDeploymentListItem { + id: d.id.to_string(), + created_at: Some(helpers::datetime_to_prost_timestamp(&d.created_at)), + updated_at: Some(helpers::datetime_to_prost_timestamp(&d.created_at)), + started_at: d + .started_at + .as_ref() + .map(|ts| helpers::datetime_to_prost_timestamp(ts)), + completed_at: d + .completed_at + .as_ref() + .map(|ts| helpers::datetime_to_prost_timestamp(ts)), + name: d.name.clone(), + }) + .collect(), + }); + resp.metadata_mut() + .insert("x-log-application_id", req.application_id.parse().unwrap()); + + Ok(resp) + } + + async fn add_devices( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.fuota_deployment_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Update, dp_id), + ) + .await?; + + let mut dev_euis = Vec::with_capacity(req.dev_euis.len()); + for dev_eui in &req.dev_euis { + dev_euis.push(EUI64::from_str(dev_eui).map_err(|e| e.status())?); + } + + fuota::add_devices(dp_id, dev_euis) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + req.fuota_deployment_id.parse().unwrap(), + ); + Ok(resp) + } + + async fn remove_devices( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.fuota_deployment_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Update, dp_id), + ) + .await?; + + let mut dev_euis = Vec::with_capacity(req.dev_euis.len()); + for dev_eui in &req.dev_euis { + dev_euis.push(EUI64::from_str(dev_eui).map_err(|e| e.status())?); + } + + fuota::remove_devices(dp_id, dev_euis) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + req.fuota_deployment_id.parse().unwrap(), + ); + Ok(resp) + } + + async fn list_devices( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.fuota_deployment_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Read, dp_id), + ) + .await?; + + let count = fuota::get_device_count(dp_id) + .await + .map_err(|e| e.status())?; + let items = fuota::get_devices(dp_id, req.limit as i64, req.offset as i64) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(api::ListFuotaDeploymentDevicesResponse { + total_count: count as u32, + result: items + .iter() + .map(|d| api::FuotaDeploymentDeviceListItem { + fuota_deployment_id: d.fuota_deployment_id.to_string(), + dev_eui: d.dev_eui.to_string(), + created_at: Some(helpers::datetime_to_prost_timestamp(&d.created_at)), + updated_at: Some(helpers::datetime_to_prost_timestamp(&d.updated_at)), + mc_group_setup_completed_at: d + .mc_group_setup_completed_at + .as_ref() + .map(|ts| helpers::datetime_to_prost_timestamp(ts)), + mc_session_completed_at: d + .mc_session_completed_at + .as_ref() + .map(|ts| helpers::datetime_to_prost_timestamp(ts)), + frag_session_setup_completed_at: d + .frag_session_setup_completed_at + .as_ref() + .map(|ts| helpers::datetime_to_prost_timestamp(ts)), + frag_status_completed_at: d + .frag_status_completed_at + .as_ref() + .map(|ts| helpers::datetime_to_prost_timestamp(ts)), + }) + .collect(), + }); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + req.fuota_deployment_id.parse().unwrap(), + ); + + Ok(resp) + } + + async fn add_gateways( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.fuota_deployment_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Update, dp_id), + ) + .await?; + + let mut gateway_ids = Vec::with_capacity(req.gateway_ids.len()); + for gateway_id in &req.gateway_ids { + gateway_ids.push(EUI64::from_str(gateway_id).map_err(|e| e.status())?); + } + + fuota::add_gateways(dp_id, gateway_ids) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + req.fuota_deployment_id.parse().unwrap(), + ); + Ok(resp) + } + + async fn remove_gateways( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.fuota_deployment_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Update, dp_id), + ) + .await?; + + let mut gateway_ids = Vec::with_capacity(req.gateway_ids.len()); + for gateway_id in &req.gateway_ids { + gateway_ids.push(EUI64::from_str(gateway_id).map_err(|e| e.status())?); + } + + fuota::remove_gateways(dp_id, gateway_ids) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + req.fuota_deployment_id.parse().unwrap(), + ); + Ok(resp) + } + + async fn list_gateways( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let dp_id = Uuid::from_str(&req.fuota_deployment_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateFuotaDeploymentAccess::new(validator::Flag::Read, dp_id), + ) + .await?; + + let count = fuota::get_gateway_count(dp_id) + .await + .map_err(|e| e.status())?; + let items = fuota::get_gateway(dp_id, req.limit as i64, req.offset as i64) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(api::ListFuotaDeploymentGatewaysResponse { + total_count: count as u32, + result: items + .iter() + .map(|gw| api::FuotaDeploymentGatewayListItem { + fuota_deployment_id: gw.fuota_deployment_id.to_string(), + gateway_id: gw.gateway_id.to_string(), + created_at: Some(helpers::datetime_to_prost_timestamp(&gw.created_at)), + }) + .collect(), + }); + resp.metadata_mut().insert( + "x-log-fuota_deployment_id", + req.fuota_deployment_id.parse().unwrap(), + ); + Ok(resp) + } +} diff --git a/chirpstack/src/api/helpers.rs b/chirpstack/src/api/helpers.rs index 3dd63956..90d35274 100644 --- a/chirpstack/src/api/helpers.rs +++ b/chirpstack/src/api/helpers.rs @@ -4,7 +4,9 @@ use chirpstack_api::{api, common}; use lrwn::region::{CommonName, MacVersion, Revision}; use crate::codec::Codec; -use crate::storage::fields::{self, MeasurementKind, MulticastGroupSchedulingType}; +use crate::storage::fields::{ + self, MeasurementKind, MulticastGroupSchedulingType, RequestFragmentationSessionStatus, +}; use crate::storage::{device, device::DeviceClass, gateway, metrics::Aggregation}; pub trait FromProto { @@ -339,6 +341,26 @@ impl FromProto> for api::Ts005Versi } } +impl ToProto for RequestFragmentationSessionStatus { + fn to_proto(self) -> api::RequestFragmentationSessionStatus { + match self { + Self::NoRequest => api::RequestFragmentationSessionStatus::NoRequest, + Self::AfterFragEnqueue => api::RequestFragmentationSessionStatus::AfterFragmentEnqueue, + Self::AfterSessTimeout => api::RequestFragmentationSessionStatus::AfterSessionTimeout, + } + } +} + +impl FromProto for api::RequestFragmentationSessionStatus { + fn from_proto(self) -> RequestFragmentationSessionStatus { + match self { + Self::NoRequest => RequestFragmentationSessionStatus::NoRequest, + Self::AfterFragmentEnqueue => RequestFragmentationSessionStatus::AfterFragEnqueue, + Self::AfterSessionTimeout => RequestFragmentationSessionStatus::AfterSessTimeout, + } + } +} + pub fn datetime_to_prost_timestamp(dt: &DateTime) -> prost_types::Timestamp { let ts = dt.timestamp_nanos_opt().unwrap_or_default(); diff --git a/chirpstack/src/api/mod.rs b/chirpstack/src/api/mod.rs index 9bd2f65d..5708db3a 100644 --- a/chirpstack/src/api/mod.rs +++ b/chirpstack/src/api/mod.rs @@ -32,6 +32,7 @@ use chirpstack_api::api::application_service_server::ApplicationServiceServer; use chirpstack_api::api::device_profile_service_server::DeviceProfileServiceServer; use chirpstack_api::api::device_profile_template_service_server::DeviceProfileTemplateServiceServer; use chirpstack_api::api::device_service_server::DeviceServiceServer; +use chirpstack_api::api::fuota_service_server::FuotaServiceServer; use chirpstack_api::api::gateway_service_server::GatewayServiceServer; use chirpstack_api::api::internal_service_server::InternalServiceServer; use chirpstack_api::api::multicast_group_service_server::MulticastGroupServiceServer; @@ -53,6 +54,7 @@ pub mod device; pub mod device_profile; pub mod device_profile_template; pub mod error; +pub mod fuota; pub mod gateway; mod grpc_multiplex; pub mod helpers; @@ -175,6 +177,10 @@ pub async fn setup() -> Result<()> { .add_service(RelayServiceServer::with_interceptor( relay::Relay::new(validator::RequestValidator::new()), auth::auth_interceptor, + )) + .add_service(FuotaServiceServer::with_interceptor( + fuota::Fuota::new(validator::RequestValidator::new()), + auth::auth_interceptor, )); let backend_handle = tokio::spawn(backend::setup()); diff --git a/chirpstack/src/main.rs b/chirpstack/src/main.rs index 12c5971b..95e79a2e 100644 --- a/chirpstack/src/main.rs +++ b/chirpstack/src/main.rs @@ -1,3 +1,5 @@ +#![recursion_limit = "256"] + #[macro_use] extern crate lazy_static; extern crate diesel_migrations; diff --git a/chirpstack/src/storage/error.rs b/chirpstack/src/storage/error.rs index 4d70a6cf..e8fd03d1 100644 --- a/chirpstack/src/storage/error.rs +++ b/chirpstack/src/storage/error.rs @@ -33,6 +33,9 @@ pub enum Error { #[error("Not allowed ({0})")] NotAllowed(String), + #[error("Multiple errors")] + MultiError(Vec), + #[error(transparent)] Diesel(#[from] diesel::result::Error), @@ -50,6 +53,9 @@ pub enum Error { #[error(transparent)] ProstDecode(#[from] prost::DecodeError), + + #[error(transparent)] + ValidatorValidate(#[from] validator::ValidationErrors), } impl Error { diff --git a/chirpstack/src/storage/fields/fuota.rs b/chirpstack/src/storage/fields/fuota.rs new file mode 100644 index 00000000..8852ea54 --- /dev/null +++ b/chirpstack/src/storage/fields/fuota.rs @@ -0,0 +1,79 @@ +use anyhow::Error; +use diesel::backend::Backend; +use diesel::sql_types::Text; +#[cfg(feature = "sqlite")] +use diesel::sqlite::Sqlite; +use diesel::{deserialize, serialize}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, AsExpression, FromSqlRow)] +#[diesel(sql_type = Text)] +pub enum RequestFragmentationSessionStatus { + NoRequest, + AfterFragEnqueue, + AfterSessTimeout, +} + +impl From<&RequestFragmentationSessionStatus> for String { + fn from(value: &RequestFragmentationSessionStatus) -> Self { + match value { + RequestFragmentationSessionStatus::NoRequest => "NO_REQUEST", + RequestFragmentationSessionStatus::AfterFragEnqueue => "AFTER_FRAG_ENQUEUE", + RequestFragmentationSessionStatus::AfterSessTimeout => "AFTER_SESS_TIMEOUT", + } + .to_string() + } +} + +impl TryFrom<&str> for RequestFragmentationSessionStatus { + type Error = Error; + + fn try_from(value: &str) -> Result { + Ok(match value { + "NO_REQUEST" => Self::NoRequest, + "AFTER_FRAG_ENQUEUE" => Self::AfterFragEnqueue, + "AFTER_SESS_TIMEOUT" => Self::AfterSessTimeout, + _ => { + return Err(anyhow!( + "Invalid RequestFragmentationSessionStatus value: {}", + value + )) + } + }) + } +} + +impl deserialize::FromSql for RequestFragmentationSessionStatus +where + DB: Backend, + *const str: deserialize::FromSql, +{ + fn from_sql(value: ::RawValue<'_>) -> deserialize::Result { + let string = <*const str>::from_sql(value)?; + Ok(Self::try_from(unsafe { &*string })?) + } +} + +#[cfg(feature = "postgres")] +impl serialize::ToSql for RequestFragmentationSessionStatus +where + str: serialize::ToSql, +{ + fn to_sql<'b>( + &'b self, + out: &mut serialize::Output<'b, '_, diesel::pg::Pg>, + ) -> serialize::Result { + >::to_sql( + &String::from(self), + &mut out.reborrow(), + ) + } +} + +#[cfg(feature = "sqlite")] +impl serialize::ToSql for RequestFragmentationSessionStatus { + fn to_sql(&self, out: &mut serialize::Output<'_, '_, Sqlite>) -> serialize::Result { + out.set_value(String::from(self)); + Ok(serialize::IsNull::No) + } +} diff --git a/chirpstack/src/storage/fields/mod.rs b/chirpstack/src/storage/fields/mod.rs index d4e2dbb2..a436d574 100644 --- a/chirpstack/src/storage/fields/mod.rs +++ b/chirpstack/src/storage/fields/mod.rs @@ -2,6 +2,7 @@ mod big_decimal; mod dev_nonces; pub mod device_profile; mod device_session; +mod fuota; mod key_value; mod measurements; mod multicast_group_scheduling_type; @@ -11,6 +12,7 @@ pub use big_decimal::BigDecimal; pub use dev_nonces::DevNonces; pub use device_profile::{AbpParams, AppLayerParams, ClassBParams, ClassCParams, RelayParams}; pub use device_session::DeviceSession; +pub use fuota::RequestFragmentationSessionStatus; pub use key_value::KeyValue; pub use measurements::*; pub use multicast_group_scheduling_type::MulticastGroupSchedulingType; diff --git a/chirpstack/src/storage/fuota.rs b/chirpstack/src/storage/fuota.rs new file mode 100644 index 00000000..3ac0f83a --- /dev/null +++ b/chirpstack/src/storage/fuota.rs @@ -0,0 +1,431 @@ +use anyhow::Result; +use chrono::{DateTime, Utc}; +use diesel::{dsl, prelude::*}; +use diesel_async::RunQueryDsl; +use tracing::info; +use uuid::Uuid; +use validator::Validate; + +use crate::storage::error::Error; +use crate::storage::schema::{ + application, device, fuota_deployment, fuota_deployment_device, fuota_deployment_gateway, + gateway, tenant, +}; +use crate::storage::{self, device_profile, fields, get_async_db_conn}; +use lrwn::EUI64; + +#[derive(Clone, Queryable, Insertable, Debug, PartialEq, Eq, Validate)] +#[diesel(table_name = fuota_deployment)] +pub struct FuotaDeployment { + pub id: fields::Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub name: String, + pub application_id: fields::Uuid, + pub device_profile_id: fields::Uuid, + pub multicast_group_type: String, + pub multicast_class_c_scheduling_type: fields::MulticastGroupSchedulingType, + pub multicast_dr: i16, + pub multicast_class_b_ping_slot_nb_k: i16, + pub multicast_frequency: i64, + pub multicast_timeout: i16, + pub unicast_attempt_count: i16, + pub fragmentation_fragment_size: i16, + pub fragmentation_redundancy: i16, + pub fragmentation_session_index: i16, + pub fragmentation_matrix: i16, + pub fragmentation_block_ack_delay: i16, + pub fragmentation_descriptor: Vec, + pub request_fragmentation_session_status: fields::RequestFragmentationSessionStatus, + pub payload: Vec, +} + +impl Default for FuotaDeployment { + fn default() -> Self { + let now = Utc::now(); + + Self { + id: Uuid::new_v4().into(), + created_at: now, + updated_at: now, + started_at: None, + completed_at: None, + name: "".into(), + application_id: Uuid::nil().into(), + device_profile_id: Uuid::nil().into(), + multicast_group_type: "".into(), + multicast_class_c_scheduling_type: fields::MulticastGroupSchedulingType::DELAY, + multicast_dr: 0, + multicast_class_b_ping_slot_nb_k: 0, + multicast_frequency: 0, + multicast_timeout: 0, + unicast_attempt_count: 0, + fragmentation_fragment_size: 0, + fragmentation_redundancy: 0, + fragmentation_session_index: 0, + fragmentation_matrix: 0, + fragmentation_block_ack_delay: 0, + fragmentation_descriptor: Vec::new(), + request_fragmentation_session_status: + fields::RequestFragmentationSessionStatus::NoRequest, + payload: Vec::new(), + } + } +} + +#[derive(Queryable, PartialEq, Eq, Debug)] +pub struct FuotaDeploymentListItem { + pub id: fields::Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub started_at: Option>, + pub completed_at: Option>, + pub name: String, +} + +#[derive(Clone, Queryable, Insertable, Debug, PartialEq, Eq)] +#[diesel(table_name = fuota_deployment_device)] +pub struct FuotaDeploymentDevice { + pub fuota_deployment_id: fields::Uuid, + pub dev_eui: EUI64, + pub created_at: DateTime, + pub updated_at: DateTime, + pub mc_group_setup_completed_at: Option>, + pub mc_session_completed_at: Option>, + pub frag_session_setup_completed_at: Option>, + pub frag_status_completed_at: Option>, +} + +impl Default for FuotaDeploymentDevice { + fn default() -> Self { + let now = Utc::now(); + + Self { + fuota_deployment_id: Uuid::nil().into(), + dev_eui: EUI64::default(), + created_at: now, + updated_at: now, + mc_group_setup_completed_at: None, + mc_session_completed_at: None, + frag_session_setup_completed_at: None, + frag_status_completed_at: None, + } + } +} + +#[derive(Clone, Queryable, Insertable, Debug, PartialEq, Eq)] +#[diesel(table_name = fuota_deployment_gateway)] +pub struct FuotaDeploymentGateway { + pub fuota_deployment_id: fields::Uuid, + pub gateway_id: EUI64, + pub created_at: DateTime, +} + +impl Default for FuotaDeploymentGateway { + fn default() -> Self { + Self { + fuota_deployment_id: Uuid::nil().into(), + gateway_id: EUI64::default(), + created_at: Utc::now(), + } + } +} + +pub async fn create_deployment(d: FuotaDeployment) -> Result { + d.validate()?; + + let app = storage::application::get(&d.application_id).await?; + let dp = device_profile::get(&d.device_profile_id).await?; + if app.tenant_id != dp.tenant_id { + return Err(Error::Validation( + "The application and device-profile must be under the samen tenant".into(), + )); + } + + let d: FuotaDeployment = diesel::insert_into(fuota_deployment::table) + .values(&d) + .get_result(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, d.id.to_string()))?; + + info!(id = %d.id, "FUOTA deployment created"); + Ok(d) +} + +pub async fn get_deployment(id: Uuid) -> Result { + fuota_deployment::dsl::fuota_deployment + .find(&fields::Uuid::from(id)) + .first(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, id.to_string())) +} + +pub async fn update_deployment(d: FuotaDeployment) -> Result { + d.validate()?; + + let d: FuotaDeployment = diesel::update(fuota_deployment::dsl::fuota_deployment.find(&d.id)) + .set(( + fuota_deployment::updated_at.eq(&Utc::now()), + fuota_deployment::started_at.eq(&d.started_at), + fuota_deployment::completed_at.eq(&d.completed_at), + fuota_deployment::name.eq(&d.name), + fuota_deployment::multicast_group_type.eq(&d.multicast_group_type), + fuota_deployment::multicast_class_c_scheduling_type + .eq(&d.multicast_class_c_scheduling_type), + fuota_deployment::multicast_dr.eq(&d.multicast_dr), + fuota_deployment::multicast_class_b_ping_slot_nb_k + .eq(&d.multicast_class_b_ping_slot_nb_k), + fuota_deployment::multicast_frequency.eq(&d.multicast_frequency), + fuota_deployment::multicast_timeout.eq(&d.multicast_timeout), + fuota_deployment::unicast_attempt_count.eq(&d.unicast_attempt_count), + fuota_deployment::fragmentation_fragment_size.eq(&d.fragmentation_fragment_size), + fuota_deployment::fragmentation_redundancy.eq(&d.fragmentation_redundancy), + fuota_deployment::fragmentation_session_index.eq(&d.fragmentation_session_index), + fuota_deployment::fragmentation_matrix.eq(&d.fragmentation_matrix), + fuota_deployment::fragmentation_block_ack_delay.eq(&d.fragmentation_block_ack_delay), + fuota_deployment::fragmentation_descriptor.eq(&d.fragmentation_descriptor), + fuota_deployment::request_fragmentation_session_status + .eq(&d.request_fragmentation_session_status), + fuota_deployment::payload.eq(&d.payload), + )) + .get_result(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, d.id.to_string()))?; + + info!(id = %d.id, "FUOTA deployment updated"); + Ok(d) +} + +pub async fn delete_deployment(id: Uuid) -> Result<(), Error> { + let ra = diesel::delete(fuota_deployment::dsl::fuota_deployment.find(&fields::Uuid::from(id))) + .execute(&mut get_async_db_conn().await?) + .await?; + if ra == 0 { + return Err(Error::NotFound(id.to_string())); + } + info!(id = %id, "FUOTA deployment deleted"); + Ok(()) +} + +pub async fn get_deployment_count(application_id: Uuid) -> Result { + fuota_deployment::dsl::fuota_deployment + .select(dsl::count_star()) + .filter(fuota_deployment::dsl::application_id.eq(fields::Uuid::from(application_id))) + .first(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into())) +} + +pub async fn list_deployments( + application_id: Uuid, + limit: i64, + offset: i64, +) -> Result, Error> { + fuota_deployment::dsl::fuota_deployment + .select(( + fuota_deployment::id, + fuota_deployment::created_at, + fuota_deployment::updated_at, + fuota_deployment::started_at, + fuota_deployment::completed_at, + fuota_deployment::name, + )) + .filter(fuota_deployment::dsl::application_id.eq(fields::Uuid::from(application_id))) + .order_by(fuota_deployment::dsl::name) + .limit(limit) + .offset(offset) + .load(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into())) +} + +pub async fn add_devices(fuota_deployment_id: Uuid, dev_euis: Vec) -> Result<(), Error> { + let mut errors = Vec::new(); + + let dev_euis_filtered: Vec = device::dsl::device + .select(device::dsl::dev_eui) + .inner_join( + fuota_deployment::table + .on(fuota_deployment::dsl::device_profile_id.eq(device::dsl::device_profile_id)), + ) + .filter(fuota_deployment::dsl::id.eq(fields::Uuid::from(fuota_deployment_id))) + .filter(device::dsl::dev_eui.eq_any(&dev_euis)) + .load(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into()))?; + + if dev_euis_filtered.len() != dev_euis.len() { + return Err(Error::Validation( + "All devices must have the same device-profile as the FUOTA deployment".into(), + )); + } + + for dev_eui in dev_euis { + let res = diesel::insert_into(fuota_deployment_device::table) + .values(&FuotaDeploymentDevice { + fuota_deployment_id: fuota_deployment_id.into(), + dev_eui: dev_eui, + ..Default::default() + }) + .execute(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, dev_eui.to_string())); + + if let Err(e) = res { + errors.push(e); + } + } + + info!(fuota_deployment_id = %fuota_deployment_id, "Added DeEUIs to FUOTA Deployment"); + + if errors.is_empty() { + Ok(()) + } else { + Err(Error::MultiError(errors)) + } +} + +pub async fn get_devices( + fuota_deployment_id: Uuid, + limit: i64, + offset: i64, +) -> Result, Error> { + fuota_deployment_device::dsl::fuota_deployment_device + .filter( + fuota_deployment_device::dsl::fuota_deployment_id + .eq(fields::Uuid::from(fuota_deployment_id)), + ) + .order_by(fuota_deployment_device::dsl::dev_eui) + .limit(limit) + .offset(offset) + .load(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into())) +} + +pub async fn remove_devices(fuota_deployment_id: Uuid, dev_euis: Vec) -> Result<(), Error> { + diesel::delete( + fuota_deployment_device::table + .filter( + fuota_deployment_device::dsl::fuota_deployment_id + .eq(fields::Uuid::from(fuota_deployment_id)), + ) + .filter(fuota_deployment_device::dsl::dev_eui.eq_any(&dev_euis)), + ) + .execute(&mut get_async_db_conn().await?) + .await?; + + info!(fuota_deployment_id = %fuota_deployment_id, "DevEUIs removed from FUOTA Deployment"); + Ok(()) +} + +pub async fn get_device_count(fuota_deployment_id: Uuid) -> Result { + fuota_deployment_device::dsl::fuota_deployment_device + .select(dsl::count_star()) + .filter( + fuota_deployment_device::dsl::fuota_deployment_id + .eq(fields::Uuid::from(fuota_deployment_id)), + ) + .first(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into())) +} + +pub async fn add_gateways(fuota_deployment_id: Uuid, gateway_ids: Vec) -> Result<(), Error> { + let mut errors = Vec::new(); + + let gateway_ids_filtered: Vec = gateway::dsl::gateway + .select(gateway::dsl::gateway_id) + .inner_join(tenant::table.on(tenant::dsl::id.eq(gateway::dsl::tenant_id))) + .inner_join(application::table.on(application::dsl::tenant_id.eq(tenant::dsl::id))) + .inner_join( + fuota_deployment::table + .on(fuota_deployment::dsl::application_id.eq(application::dsl::id)), + ) + .filter(fuota_deployment::dsl::id.eq(fields::Uuid::from(fuota_deployment_id))) + .filter(gateway::dsl::gateway_id.eq_any(&gateway_ids)) + .load(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into()))?; + + if gateway_ids_filtered.len() != gateway_ids.len() { + return Err(Error::Validation( + "All gateways must be under the same tenant as the FUOTA deployment".into(), + )); + } + + for gateway_id in gateway_ids { + let res = diesel::insert_into(fuota_deployment_gateway::table) + .values(&FuotaDeploymentGateway { + fuota_deployment_id: fuota_deployment_id.into(), + gateway_id: gateway_id, + ..Default::default() + }) + .execute(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, gateway_id.to_string())); + + if let Err(e) = res { + errors.push(e); + } + } + + info!(fuota_deployment_id = %fuota_deployment_id, "Added Gateway IDs to FUOTA Deployment"); + + if errors.is_empty() { + Ok(()) + } else { + Err(Error::MultiError(errors)) + } +} + +pub async fn remove_gateways( + fuota_deployment_id: Uuid, + gateway_ids: Vec, +) -> Result<(), Error> { + diesel::delete( + fuota_deployment_gateway::table + .filter( + fuota_deployment_gateway::dsl::fuota_deployment_id + .eq(fields::Uuid::from(fuota_deployment_id)), + ) + .filter(fuota_deployment_gateway::dsl::gateway_id.eq_any(gateway_ids)), + ) + .execute(&mut get_async_db_conn().await?) + .await?; + + info!(fuota_deployment_id = %fuota_deployment_id, "Gateway IDs removed from FUOTA Deployment"); + Ok(()) +} + +pub async fn get_gateway_count(fuota_deployment_id: Uuid) -> Result { + fuota_deployment_gateway::dsl::fuota_deployment_gateway + .select(dsl::count_star()) + .filter( + fuota_deployment_gateway::dsl::fuota_deployment_id + .eq(fields::Uuid::from(fuota_deployment_id)), + ) + .first(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into())) +} + +pub async fn get_gateway( + fuota_deployment_id: Uuid, + limit: i64, + offset: i64, +) -> Result, Error> { + fuota_deployment_gateway::dsl::fuota_deployment_gateway + .filter( + fuota_deployment_gateway::dsl::fuota_deployment_id + .eq(fields::Uuid::from(fuota_deployment_id)), + ) + .order_by(fuota_deployment_gateway::dsl::gateway_id) + .limit(limit) + .offset(offset) + .load(&mut get_async_db_conn().await?) + .await + .map_err(|e| Error::from_diesel(e, "".into())) +} diff --git a/chirpstack/src/storage/mod.rs b/chirpstack/src/storage/mod.rs index 721418e4..53b740f8 100644 --- a/chirpstack/src/storage/mod.rs +++ b/chirpstack/src/storage/mod.rs @@ -24,6 +24,7 @@ pub mod device_session; pub mod downlink_frame; pub mod error; pub mod fields; +pub mod fuota; pub mod gateway; pub mod helpers; pub mod mac_command; diff --git a/chirpstack/src/storage/schema_postgres.rs b/chirpstack/src/storage/schema_postgres.rs index e969fcc2..58f6036f 100644 --- a/chirpstack/src/storage/schema_postgres.rs +++ b/chirpstack/src/storage/schema_postgres.rs @@ -181,6 +181,59 @@ diesel::table! { } } +diesel::table! { + fuota_deployment (id) { + id -> Uuid, + created_at -> Timestamptz, + updated_at -> Timestamptz, + started_at -> Nullable, + completed_at -> Nullable, + #[max_length = 100] + name -> Varchar, + application_id -> Uuid, + device_profile_id -> Uuid, + #[max_length = 1] + multicast_group_type -> Bpchar, + #[max_length = 20] + multicast_class_c_scheduling_type -> Varchar, + multicast_dr -> Int2, + multicast_class_b_ping_slot_nb_k -> Int2, + multicast_frequency -> Int8, + multicast_timeout -> Int2, + unicast_attempt_count -> Int2, + fragmentation_fragment_size -> Int2, + fragmentation_redundancy -> Int2, + fragmentation_session_index -> Int2, + fragmentation_matrix -> Int2, + fragmentation_block_ack_delay -> Int2, + fragmentation_descriptor -> Bytea, + #[max_length = 20] + request_fragmentation_session_status -> Varchar, + payload -> Bytea, + } +} + +diesel::table! { + fuota_deployment_device (fuota_deployment_id, dev_eui) { + fuota_deployment_id -> Uuid, + dev_eui -> Bytea, + created_at -> Timestamptz, + updated_at -> Timestamptz, + mc_group_setup_completed_at -> Nullable, + mc_session_completed_at -> Nullable, + frag_session_setup_completed_at -> Nullable, + frag_status_completed_at -> Nullable, + } +} + +diesel::table! { + fuota_deployment_gateway (fuota_deployment_id, gateway_id) { + fuota_deployment_id -> Uuid, + gateway_id -> Bytea, + created_at -> Timestamptz, + } +} + diesel::table! { gateway (gateway_id) { gateway_id -> Bytea, @@ -333,6 +386,12 @@ diesel::joinable!(device -> device_profile (device_profile_id)); diesel::joinable!(device_keys -> device (dev_eui)); diesel::joinable!(device_profile -> tenant (tenant_id)); diesel::joinable!(device_queue_item -> device (dev_eui)); +diesel::joinable!(fuota_deployment -> application (application_id)); +diesel::joinable!(fuota_deployment -> device_profile (device_profile_id)); +diesel::joinable!(fuota_deployment_device -> device (dev_eui)); +diesel::joinable!(fuota_deployment_device -> fuota_deployment (fuota_deployment_id)); +diesel::joinable!(fuota_deployment_gateway -> fuota_deployment (fuota_deployment_id)); +diesel::joinable!(fuota_deployment_gateway -> gateway (gateway_id)); diesel::joinable!(gateway -> tenant (tenant_id)); diesel::joinable!(multicast_group -> application (application_id)); diesel::joinable!(multicast_group_device -> device (dev_eui)); @@ -354,6 +413,9 @@ diesel::allow_tables_to_appear_in_same_query!( device_profile, device_profile_template, device_queue_item, + fuota_deployment, + fuota_deployment_device, + fuota_deployment_gateway, gateway, multicast_group, multicast_group_device, diff --git a/chirpstack/src/storage/schema_sqlite.rs b/chirpstack/src/storage/schema_sqlite.rs index 4cd4b876..9a9ed0a4 100644 --- a/chirpstack/src/storage/schema_sqlite.rs +++ b/chirpstack/src/storage/schema_sqlite.rs @@ -161,6 +161,55 @@ diesel::table! { } } +diesel::table! { + fuota_deployment (id) { + id -> Text, + created_at -> TimestamptzSqlite, + updated_at -> TimestamptzSqlite, + started_at -> Nullable, + completed_at -> Nullable, + name -> Text, + application_id -> Text, + device_profile_id -> Text, + multicast_group_type -> Text, + multicast_class_c_scheduling_type -> Text, + multicast_dr -> SmallInt, + multicast_class_b_ping_slot_nb_k -> SmallInt, + multicast_frequency -> BigInt, + multicast_timeout -> SmallInt, + unicast_attempt_count -> SmallInt, + fragmentation_fragment_size -> SmallInt, + fragmentation_redundancy -> SmallInt, + fragmentation_session_index -> SmallInt, + fragmentation_matrix -> SmallInt, + fragmentation_block_ack_delay -> SmallInt, + fragmentation_descriptor -> Binary, + request_fragmentation_session_status -> Text, + payload -> Binary, + } +} + +diesel::table! { + fuota_deployment_device (fuota_deployment_id, dev_eui) { + fuota_deployment_id -> Text, + dev_eui -> Binary, + created_at -> TimestamptzSqlite, + updated_at -> TimestamptzSqlite, + mc_group_setup_completed_at -> Nullable, + mc_session_completed_at -> Nullable, + frag_session_setup_completed_at -> Nullable, + frag_status_completed_at -> Nullable, + } +} + +diesel::table! { + fuota_deployment_gateway (fuota_deployment_id, gateway_id) { + fuota_deployment_id -> Text, + gateway_id -> Binary, + created_at -> TimestamptzSqlite, + } +} + diesel::table! { gateway (gateway_id) { gateway_id -> Binary, @@ -304,6 +353,12 @@ diesel::joinable!(device -> device_profile (device_profile_id)); diesel::joinable!(device_keys -> device (dev_eui)); diesel::joinable!(device_profile -> tenant (tenant_id)); diesel::joinable!(device_queue_item -> device (dev_eui)); +diesel::joinable!(fuota_deployment -> application (application_id)); +diesel::joinable!(fuota_deployment -> device_profile (device_profile_id)); +diesel::joinable!(fuota_deployment_device -> device (dev_eui)); +diesel::joinable!(fuota_deployment_device -> fuota_deployment (fuota_deployment_id)); +diesel::joinable!(fuota_deployment_gateway -> fuota_deployment (fuota_deployment_id)); +diesel::joinable!(fuota_deployment_gateway -> gateway (gateway_id)); diesel::joinable!(gateway -> tenant (tenant_id)); diesel::joinable!(multicast_group -> application (application_id)); diesel::joinable!(multicast_group_device -> device (dev_eui)); @@ -325,6 +380,9 @@ diesel::allow_tables_to_appear_in_same_query!( device_profile, device_profile_template, device_queue_item, + fuota_deployment, + fuota_deployment_device, + fuota_deployment_gateway, gateway, multicast_group, multicast_group_device,