Update fuota + device-keys structs / storage.

This add the gen_app_key to the device keys which is needed for FUOTA
and adds a random multicast address + key to the fuota deployment. To
the FUOTA job structure, this adds a return msg such that errors can
be captured in the database.
This commit is contained in:
Orne Brocaar 2025-02-18 11:51:59 +00:00
parent aa11db15de
commit 8e47ea1483
16 changed files with 114 additions and 36 deletions

View File

@ -278,6 +278,11 @@ message DeviceKeys {
// Application root key (128 bit).
// Note: This field only needs to be set for LoRaWAN 1.1.x devices!
string app_key = 3;
// Gen App Key (128 bit).
// Note: This field only needs to be set for LoRaWAN 1.0.x devices that
// implement TS005 (remote multicast setup).
string gen_app_key = 4;
}
message CreateDeviceRequest {

View File

@ -278,6 +278,11 @@ message DeviceKeys {
// Application root key (128 bit).
// Note: This field only needs to be set for LoRaWAN 1.1.x devices!
string app_key = 3;
// Gen App Key (128 bit).
// Note: This field only needs to be set for LoRaWAN 1.0.x devices that
// implement TS005 (remote multicast setup).
string gen_app_key = 4;
}
message CreateDeviceRequest {

View File

@ -1,3 +1,6 @@
alter table device_keys
drop column gen_app_key;
drop table fuota_deployment_job;
drop table fuota_deployment_gateway;
drop table fuota_deployment_device;

View File

@ -7,6 +7,8 @@ create table fuota_deployment (
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_addr bytea not null,
multicast_key bytea not null,
multicast_group_type char(1) not null,
multicast_class_c_scheduling_type varchar(20) not null,
multicast_dr smallint not null,
@ -54,9 +56,16 @@ create table fuota_deployment_job (
max_retry_count smallint not null,
attempt_count smallint not null,
scheduler_run_after timestamp with time zone not null,
return_msg text not null,
primary key (fuota_deployment_id, job)
);
create index idx_fuota_deployment_job_completed_at on fuota_deployment_job(completed_at);
create index idx_fuota_deployment_job_scheduler_run_after on fuota_deployment_job(scheduler_run_after);
alter table device_keys
add column gen_app_key bytea not null default decode('00000000000000000000000000000000', 'hex');
alter table device_keys
alter column gen_app_key drop default;

View File

@ -1,3 +1,6 @@
alter table device_keys
drop column gen_app_key;
drop table fuota_deployment_job;
drop table fuota_deployment_gateway;
drop table fuota_deployment_device;

View File

@ -7,6 +7,8 @@ create table fuota_deployment (
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_addr blob not null,
multicast_key blob not null,
multicast_group_type char(1) not null,
multicast_class_c_scheduling_type varchar(20) not null,
multicast_dr smallint not null,
@ -54,9 +56,13 @@ create table fuota_deployment_job (
max_retry_count smallint not null,
attempt_count smallint not null,
scheduler_run_after datetime not null,
return_msg text not null,
primary key (fuota_deployment_id, job)
);
create index idx_fuota_deployment_job_completed_at on fuota_deployment_job(completed_at);
create index idx_fuota_deployment_job_scheduler_run_after on fuota_deployment_job(scheduler_run_after);
alter table device_keys
add column gen_app_key blob not null default x'00000000000000000000000000000000';

View File

@ -354,11 +354,8 @@ impl DeviceService for Device {
let dk = device_keys::DeviceKeys {
dev_eui,
nwk_key: AES128Key::from_str(&req_dk.nwk_key).map_err(|e| e.status())?,
app_key: if !req_dk.app_key.is_empty() {
AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())?
} else {
AES128Key::null()
},
app_key: AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())?,
gen_app_key: AES128Key::from_str(&req_dk.gen_app_key).map_err(|e| e.status())?,
..Default::default()
};
@ -392,6 +389,7 @@ impl DeviceService for Device {
dev_eui: dk.dev_eui.to_string(),
nwk_key: dk.nwk_key.to_string(),
app_key: dk.app_key.to_string(),
gen_app_key: dk.gen_app_key.to_string(),
}),
created_at: Some(helpers::datetime_to_prost_timestamp(&dk.created_at)),
updated_at: Some(helpers::datetime_to_prost_timestamp(&dk.updated_at)),
@ -428,11 +426,8 @@ impl DeviceService for Device {
dev_nonces: dk.dev_nonces,
join_nonce: dk.join_nonce,
nwk_key: AES128Key::from_str(&req_dk.nwk_key).map_err(|e| e.status())?,
app_key: if !req_dk.app_key.is_empty() {
AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())?
} else {
AES128Key::null()
},
app_key: AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())?,
gen_app_key: AES128Key::from_str(&req_dk.gen_app_key).map_err(|e| e.status())?,
..Default::default()
};
let _ = device_keys::update(dk).await.map_err(|e| e.status())?;
@ -1393,6 +1388,7 @@ pub mod test {
dev_eui: "0102030405060708".into(),
nwk_key: "01020304050607080102030405060708".into(),
app_key: "02020304050607080202030405060708".into(),
gen_app_key: "03020304050607080202030405060708".into(),
}),
},
);
@ -1411,6 +1407,7 @@ pub mod test {
dev_eui: "0102030405060708".into(),
nwk_key: "01020304050607080102030405060708".into(),
app_key: "02020304050607080202030405060708".into(),
gen_app_key: "03020304050607080202030405060708".into(),
}),
get_keys_resp.get_ref().device_keys
);
@ -1423,6 +1420,7 @@ pub mod test {
dev_eui: "0102030405060708".into(),
nwk_key: "01020304050607080102030405060708".into(),
app_key: "03020304050607080302030405060708".into(),
gen_app_key: "03020304050607080202030405060708".into(),
}),
},
);
@ -1441,6 +1439,7 @@ pub mod test {
dev_eui: "0102030405060708".into(),
nwk_key: "01020304050607080102030405060708".into(),
app_key: "03020304050607080302030405060708".into(),
gen_app_key: "03020304050607080202030405060708".into(),
}),
get_keys_resp.get_ref().device_keys
);

View File

@ -8,9 +8,11 @@ use chirpstack_api::api;
use chirpstack_api::api::fuota_service_server::FuotaService;
use lrwn::EUI64;
use crate::aeskey::get_random_aes_key;
use crate::api::auth::validator;
use crate::api::error::ToStatus;
use crate::api::helpers::{self, FromProto, ToProto};
use crate::devaddr::get_random_dev_addr;
use crate::storage::{fields, fuota};
pub struct Fuota {
@ -50,6 +52,8 @@ impl FuotaService for Fuota {
name: req_dp.name.clone(),
application_id: app_id.into(),
device_profile_id: dp_id.into(),
multicast_addr: get_random_dev_addr(),
multicast_key: get_random_aes_key(),
multicast_group_type: match req_dp.multicast_group_type() {
api::MulticastGroupType::ClassB => "B",
api::MulticastGroupType::ClassC => "C",
@ -284,8 +288,7 @@ impl FuotaService for Fuota {
fuota::create_job(fuota::FuotaDeploymentJob {
fuota_deployment_id: d.id,
job: fields::FuotaJob::McGroupSetup,
max_retry_count: d.unicast_max_retry_count,
job: fields::FuotaJob::CreateMcGroup,
..Default::default()
})
.await
@ -733,7 +736,7 @@ mod test {
.unwrap();
assert_eq!(1, jobs.len());
assert_eq!(create_resp.id, jobs[0].fuota_deployment_id.to_string());
assert_eq!(fields::FuotaJob::McGroupSetup, jobs[0].job);
assert_eq!(fields::FuotaJob::CreateMcGroup, jobs[0].job);
// add device
let add_dev_req = get_request(

View File

@ -19,7 +19,9 @@ use tracing_subscriber::{filter, prelude::*};
use lrwn::EUI64;
mod adr;
mod aeskey;
mod api;
mod applayer;
mod backend;
mod certificate;
mod cmd;

View File

@ -1140,6 +1140,8 @@ pub mod test {
count: 1,
limit: 10,
offset: 0,
order: OrderBy::Name,
order_by_desc: false,
},
FilterTest {
name: "filter by tags - 1.2.0".into(),
@ -1154,6 +1156,8 @@ pub mod test {
count: 1,
limit: 10,
offset: 0,
order: OrderBy::Name,
order_by_desc: false,
},
];

View File

@ -20,6 +20,7 @@ pub struct DeviceKeys {
pub app_key: AES128Key,
pub dev_nonces: fields::DevNonces,
pub join_nonce: i32,
pub gen_app_key: AES128Key,
}
impl Default for DeviceKeys {
@ -27,19 +28,14 @@ impl Default for DeviceKeys {
let now = Utc::now();
DeviceKeys {
dev_eui: EUI64::from_be_bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
dev_eui: Default::default(),
created_at: now,
updated_at: now,
nwk_key: AES128Key::from_bytes([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]),
app_key: AES128Key::from_bytes([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]),
dev_nonces: fields::DevNonces::default(),
nwk_key: Default::default(),
app_key: Default::default(),
dev_nonces: Default::default(),
join_nonce: 0,
gen_app_key: Default::default(),
}
}
}

View File

@ -83,6 +83,9 @@ impl serialize::ToSql<Text, Sqlite> for RequestFragmentationSessionStatus {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, AsExpression, FromSqlRow)]
#[diesel(sql_type = Text)]
pub enum FuotaJob {
CreateMcGroup,
AddDevsToMcGroup,
AddGwsToMcGroup,
McGroupSetup,
McSession,
FragSessionSetup,
@ -99,6 +102,9 @@ impl fmt::Display for FuotaJob {
impl From<&FuotaJob> for String {
fn from(value: &FuotaJob) -> Self {
match value {
FuotaJob::CreateMcGroup => "CREATE_MC_GROUP",
FuotaJob::AddDevsToMcGroup => "ADD_DEVS_TO_MC_GROUP",
FuotaJob::AddGwsToMcGroup => "ADD_GWS_TO_MC_GROUP",
FuotaJob::McGroupSetup => "MC_GROUP_SETUP",
FuotaJob::McSession => "MC_SESSION",
FuotaJob::FragSessionSetup => "FRAG_SESSION_SETUP",
@ -114,6 +120,9 @@ impl TryFrom<&str> for FuotaJob {
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"CREATE_MC_GROUP" => Self::CreateMcGroup,
"ADD_DEVS_TO_MC_GROUP" => Self::AddDevsToMcGroup,
"ADD_GWS_TO_MC_GROUP" => Self::AddGwsToMcGroup,
"MC_GROUP_SETUP" => Self::McGroupSetup,
"MC_SESSION" => Self::McSession,
"FRAG_SESSION_SETUP" => Self::FragSessionSetup,

View File

@ -13,7 +13,7 @@ use crate::storage::schema::{
fuota_deployment_job, gateway, tenant,
};
use crate::storage::{self, db_transaction, device_profile, fields, get_async_db_conn};
use lrwn::EUI64;
use lrwn::{AES128Key, DevAddr, EUI64};
#[derive(Clone, Queryable, Insertable, Debug, PartialEq, Eq, Validate)]
#[diesel(table_name = fuota_deployment)]
@ -26,6 +26,8 @@ pub struct FuotaDeployment {
pub name: String,
pub application_id: fields::Uuid,
pub device_profile_id: fields::Uuid,
pub multicast_addr: DevAddr,
pub multicast_key: AES128Key,
pub multicast_group_type: String,
pub multicast_class_c_scheduling_type: fields::MulticastGroupSchedulingType,
pub multicast_dr: i16,
@ -56,6 +58,8 @@ impl Default for FuotaDeployment {
name: "".into(),
application_id: Uuid::nil().into(),
device_profile_id: Uuid::nil().into(),
multicast_addr: Default::default(),
multicast_key: Default::default(),
multicast_group_type: "".into(),
multicast_class_c_scheduling_type: fields::MulticastGroupSchedulingType::DELAY,
multicast_dr: 0,
@ -144,6 +148,7 @@ pub struct FuotaDeploymentJob {
pub max_retry_count: i16,
pub attempt_count: i16,
pub scheduler_run_after: DateTime<Utc>,
pub return_msg: String,
}
impl Default for FuotaDeploymentJob {
@ -158,6 +163,7 @@ impl Default for FuotaDeploymentJob {
max_retry_count: 0,
attempt_count: 0,
scheduler_run_after: now,
return_msg: "".into(),
}
}
}
@ -324,15 +330,21 @@ pub async fn get_devices(
limit: i64,
offset: i64,
) -> Result<Vec<FuotaDeploymentDevice>, Error> {
fuota_deployment_device::dsl::fuota_deployment_device
let mut q = 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?)
.into_boxed();
if limit != -1 {
q = q
.order_by(fuota_deployment_device::dsl::dev_eui)
.limit(limit)
.offset(offset);
}
q.load(&mut get_async_db_conn().await?)
.await
.map_err(|e| Error::from_diesel(e, "".into()))
}
@ -449,15 +461,21 @@ pub async fn get_gateways(
limit: i64,
offset: i64,
) -> Result<Vec<FuotaDeploymentGateway>, Error> {
fuota_deployment_gateway::dsl::fuota_deployment_gateway
let mut q = 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?)
.into_boxed();
if limit != -1 {
q = q
.order_by(fuota_deployment_gateway::dsl::gateway_id)
.limit(limit)
.offset(offset);
}
q.load(&mut get_async_db_conn().await?)
.await
.map_err(|e| Error::from_diesel(e, "".into()))
}
@ -501,6 +519,7 @@ pub async fn update_job(j: FuotaDeploymentJob) -> Result<FuotaDeploymentJob, Err
fuota_deployment_job::completed_at.eq(&j.completed_at),
fuota_deployment_job::attempt_count.eq(&j.attempt_count),
fuota_deployment_job::scheduler_run_after.eq(&j.scheduler_run_after),
fuota_deployment_job::return_msg.eq(&j.return_msg),
))
.get_result(&mut get_async_db_conn().await?)
.await
@ -636,7 +655,7 @@ pub fn get_multicast_timeout(d: &FuotaDeployment) -> Result<usize> {
// Multiply by the number of fragments (+1 for additional margin).
let mc_class_c_duration_secs = mc_class_c_margin_secs * (total_fragments + 1 as usize);
// Calculate the timeout value. In case of Class-B, timeout is defined as seconds,
// Calculate the timeout value. In case of Class-C, timeout is defined as seconds,
// where the number of seconds is 2^timeout.
for i in 0..16 {
// i = 0-15
@ -1115,7 +1134,7 @@ mod test {
payload: vec![0; 10],
..Default::default()
},
expected_timeout: 3,
expected_timeout: 4,
expected_error: None,
},
];

View File

@ -77,6 +77,7 @@ diesel::table! {
app_key -> Bytea,
dev_nonces -> Jsonb,
join_nonce -> Int4,
gen_app_key -> Bytea,
}
}
@ -192,6 +193,8 @@ diesel::table! {
name -> Varchar,
application_id -> Uuid,
device_profile_id -> Uuid,
multicast_addr -> Bytea,
multicast_key -> Bytea,
#[max_length = 1]
multicast_group_type -> Bpchar,
#[max_length = 20]
@ -244,6 +247,7 @@ diesel::table! {
max_retry_count -> Int2,
attempt_count -> Int2,
scheduler_run_after -> Timestamptz,
return_msg -> Text,
}
}

View File

@ -72,6 +72,7 @@ diesel::table! {
app_key -> Binary,
dev_nonces -> Text,
join_nonce -> Integer,
gen_app_key -> Binary,
}
}
@ -171,6 +172,8 @@ diesel::table! {
name -> Text,
application_id -> Text,
device_profile_id -> Text,
multicast_addr -> Binary,
multicast_key -> Binary,
multicast_group_type -> Text,
multicast_class_c_scheduling_type -> Text,
multicast_dr -> SmallInt,
@ -219,6 +222,7 @@ diesel::table! {
max_retry_count -> SmallInt,
attempt_count -> SmallInt,
scheduler_run_after -> TimestamptzSqlite,
return_msg -> Text,
}
}

View File

@ -37,6 +37,7 @@ function LW10DeviceKeysForm(props: FormProps) {
// NOTE: this is not an error! In the LoRaWAN 1.1 specs, the what was previously
// the AppKey has been renamed to the NwkKey and a new value AppKey was added.
dk.setNwkKey(v.nwkKey);
dk.setGenAppKey(v.genAppKey);
props.onFinish(dk);
};
@ -56,6 +57,12 @@ function LW10DeviceKeysForm(props: FormProps) {
value={props.initialValues.getNwkKey()}
required
/>
<AesKeyInput
label="Gen App Key (for Remote Multicast Setup)"
name="genAppKey"
tooltip="For LoRaWAN 1.0 devices. In case your device supports LoRaWAN 1.1, update the device-profile first."
value={props.initialValues.getGenAppKey()}
/>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit