Add first fuota storage functions / API.
Some checks are pending
CI / tests (postgres) (push) Waiting to run
CI / tests (sqlite) (push) Waiting to run
CI / dist (postgres) (push) Blocked by required conditions
CI / dist (sqlite) (push) Blocked by required conditions

This commit is contained in:
Orne Brocaar 2025-01-27 10:34:19 +00:00
parent 0a976c82f4
commit dae5ba6802
22 changed files with 2514 additions and 3 deletions

53
Cargo.lock generated
View File

@ -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"

334
api/proto/api/fuota.proto vendored Normal file
View File

@ -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;
}

1
api/rust/build.rs vendored
View File

@ -215,6 +215,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.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(),

View File

@ -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;
}

View File

@ -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"

View File

@ -0,0 +1,3 @@
drop table fuota_deployment_gateway;
drop table fuota_deployment_device;
drop table fuota_deployment;

View File

@ -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)
);

View File

@ -0,0 +1,3 @@
drop table fuota_deployment_gateway;
drop table fuota_deployment_device;
drop table fuota_deployment;

View File

@ -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)
);

View File

@ -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<i64, Error> {
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<i64, Error> {
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<i64, Error> {
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<i64, Error> {
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;
}
}

View File

@ -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::<Vec<String>>()
.join(", ");
Status::new(Code::InvalidArgument, errors)
}
}
}
}

497
chirpstack/src/api/fuota.rs Normal file
View File

@ -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<api::CreateFuotaDeploymentRequest>,
) -> Result<Response<api::CreateFuotaDeploymentResponse>, 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<api::GetFuotaDeploymentRequest>,
) -> Result<Response<api::GetFuotaDeploymentResponse>, 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<api::UpdateFuotaDeploymentRequest>,
) -> Result<Response<()>, 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<api::DeleteFuotaDeploymentRequest>,
) -> Result<Response<()>, 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<api::ListFuotaDeploymentsRequest>,
) -> Result<Response<api::ListFuotaDeploymentsResponse>, 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<api::AddDevicesToFuotaDeploymentRequest>,
) -> Result<Response<()>, 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<api::RemoveDevicesFromFuotaDeploymentRequest>,
) -> Result<Response<()>, 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<api::ListFuotaDeploymentDevicesRequest>,
) -> Result<Response<api::ListFuotaDeploymentDevicesResponse>, 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<api::AddGatewaysToFuotaDeploymentRequest>,
) -> Result<Response<()>, 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<api::RemoveGatewaysFromFuotaDeploymentRequest>,
) -> Result<Response<()>, 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<api::ListFuotaDeploymentGatewaysRequest>,
) -> Result<Response<api::ListFuotaDeploymentGatewaysResponse>, 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)
}
}

View File

@ -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::DeviceClass, metrics::Aggregation};
pub trait FromProto<T> {
@ -318,6 +320,26 @@ impl FromProto<Option<fields::device_profile::Ts005Version>> for api::Ts005Versi
}
}
impl ToProto<api::RequestFragmentationSessionStatus> 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<RequestFragmentationSessionStatus> 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<Utc>) -> prost_types::Timestamp {
let ts = dt.timestamp_nanos_opt().unwrap_or_default();

View File

@ -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());

View File

@ -1,3 +1,5 @@
#![recursion_limit = "256"]
#[macro_use]
extern crate lazy_static;
extern crate diesel_migrations;

View File

@ -33,6 +33,9 @@ pub enum Error {
#[error("Not allowed ({0})")]
NotAllowed(String),
#[error("Multiple errors")]
MultiError(Vec<Error>),
#[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 {

View File

@ -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<Self, Self::Error> {
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<DB> deserialize::FromSql<Text, DB> for RequestFragmentationSessionStatus
where
DB: Backend,
*const str: deserialize::FromSql<Text, DB>,
{
fn from_sql(value: <DB as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
let string = <*const str>::from_sql(value)?;
Ok(Self::try_from(unsafe { &*string })?)
}
}
#[cfg(feature = "postgres")]
impl serialize::ToSql<Text, diesel::pg::Pg> for RequestFragmentationSessionStatus
where
str: serialize::ToSql<Text, diesel::pg::Pg>,
{
fn to_sql<'b>(
&'b self,
out: &mut serialize::Output<'b, '_, diesel::pg::Pg>,
) -> serialize::Result {
<str as serialize::ToSql<Text, diesel::pg::Pg>>::to_sql(
&String::from(self),
&mut out.reborrow(),
)
}
}
#[cfg(feature = "sqlite")]
impl serialize::ToSql<Text, Sqlite> for RequestFragmentationSessionStatus {
fn to_sql(&self, out: &mut serialize::Output<'_, '_, Sqlite>) -> serialize::Result {
out.set_value(String::from(self));
Ok(serialize::IsNull::No)
}
}

View File

@ -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;

View File

@ -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<Utc>,
pub updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
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<u8>,
pub request_fragmentation_session_status: fields::RequestFragmentationSessionStatus,
pub payload: Vec<u8>,
}
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<Utc>,
pub updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
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<Utc>,
pub updated_at: DateTime<Utc>,
pub mc_group_setup_completed_at: Option<DateTime<Utc>>,
pub mc_session_completed_at: Option<DateTime<Utc>>,
pub frag_session_setup_completed_at: Option<DateTime<Utc>>,
pub frag_status_completed_at: Option<DateTime<Utc>>,
}
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<Utc>,
}
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<FuotaDeployment, Error> {
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<FuotaDeployment, Error> {
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<FuotaDeployment, Error> {
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<i64, Error> {
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<Vec<FuotaDeploymentListItem>, 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<EUI64>) -> Result<(), Error> {
let mut errors = Vec::new();
let dev_euis_filtered: Vec<EUI64> = 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<Vec<FuotaDeploymentDevice>, 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<EUI64>) -> 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<i64, Error> {
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<EUI64>) -> Result<(), Error> {
let mut errors = Vec::new();
let gateway_ids_filtered: Vec<EUI64> = 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<EUI64>,
) -> 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<i64, Error> {
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<Vec<FuotaDeploymentGateway>, 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()))
}

View File

@ -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;

View File

@ -181,6 +181,59 @@ diesel::table! {
}
}
diesel::table! {
fuota_deployment (id) {
id -> Uuid,
created_at -> Timestamptz,
updated_at -> Timestamptz,
started_at -> Nullable<Timestamptz>,
completed_at -> Nullable<Timestamptz>,
#[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<Timestamptz>,
mc_session_completed_at -> Nullable<Timestamptz>,
frag_session_setup_completed_at -> Nullable<Timestamptz>,
frag_status_completed_at -> Nullable<Timestamptz>,
}
}
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,

View File

@ -161,6 +161,55 @@ diesel::table! {
}
}
diesel::table! {
fuota_deployment (id) {
id -> Text,
created_at -> TimestamptzSqlite,
updated_at -> TimestamptzSqlite,
started_at -> Nullable<TimestamptzSqlite>,
completed_at -> Nullable<TimestamptzSqlite>,
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<TimestamptzSqlite>,
mc_session_completed_at -> Nullable<TimestamptzSqlite>,
frag_session_setup_completed_at -> Nullable<TimestamptzSqlite>,
frag_status_completed_at -> Nullable<TimestamptzSqlite>,
}
}
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,