Skip to content

Commit

Permalink
northbound: generate structs with typed fields
Browse files Browse the repository at this point in the history
Using typed fields enhances safety by enabling compile-time checks
for correct types. Additionally, it provides a more user-friendly API.

These changes will be particularly important later as we refactor the
northbound framework for operational state to use the auto-generated
YANG structures as well.

Upcoming plans involve adding support for custom protocol-specific
types and resolving leafrefs to their actual types.

Signed-off-by: Renato Westphal <renato@opensourcerouting.org>
  • Loading branch information
rwestphal committed May 15, 2024
1 parent 56a1833 commit efa1053
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 52 deletions.
34 changes: 13 additions & 21 deletions holo-bfd/src/northbound/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,20 @@ fn state_change_singlehop(
use yang::singlehop_notification::{self, SinglehopNotification};

let data = SinglehopNotification {
local_discr: Some(sess.state.local_discr.to_string().into()),
remote_discr: sess
.state
.remote
.as_ref()
.map(|remote| remote.discr.to_string().into()),
local_discr: Some(sess.state.local_discr),
remote_discr: sess.state.remote.as_ref().map(|remote| remote.discr),
new_state: Some(sess.state.local_state.to_yang()),
state_change_reason: Some(sess.state.local_diag.to_yang()),
time_of_last_state_change: sess
.statistics
.last_state_change_time
.map(|time| time.to_rfc3339().into()),
dest_addr: Some(dst.to_string().into()),
source_addr: sess.config.src.map(|src| src.to_string().into()),
session_index: Some(sess.id.to_string().into()),
.as_ref(),
dest_addr: Some(dst),
source_addr: sess.config.src.as_ref(),
session_index: Some(sess.id as u32),
path_type: Some(sess.key.path_type().to_yang()),
interface: Some(ifname.into()),
echo_enabled: Some("false".into()),
echo_enabled: Some(false),
};
notification::send(nb_tx, singlehop_notification::PATH, data);
}
Expand All @@ -67,21 +63,17 @@ fn state_change_multihop(
use yang::multihop_notification::{self, MultihopNotification};

let data = MultihopNotification {
local_discr: Some(sess.state.local_discr.to_string().into()),
remote_discr: sess
.state
.remote
.as_ref()
.map(|remote| remote.discr.to_string().into()),
local_discr: Some(sess.state.local_discr),
remote_discr: sess.state.remote.as_ref().map(|remote| remote.discr),
new_state: Some(sess.state.local_state.to_yang()),
state_change_reason: Some(sess.state.local_diag.to_yang()),
time_of_last_state_change: sess
.statistics
.last_state_change_time
.map(|time| time.to_rfc3339().into()),
dest_addr: Some(dst.to_string().into()),
source_addr: Some(src.to_string().into()),
session_index: Some(sess.id.to_string().into()),
.as_ref(),
dest_addr: Some(dst),
source_addr: Some(src),
session_index: Some(sess.id as u32),
path_type: Some(sess.key.path_type().to_yang()),
};
notification::send(nb_tx, multihop_notification::PATH, data);
Expand Down
16 changes: 6 additions & 10 deletions holo-bgp/src/northbound/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,18 @@ pub(crate) fn backward_transition(
remote_addr: Some(nbr.remote_addr.to_string().into()),
notification_received: nbr.notification_rcvd.as_ref().map(
|(time, notif)| NotificationReceived {
last_notification: Some(time.to_rfc3339().into()),
last_notification: Some(time),
last_error: Some(notif.to_yang()),
last_error_code: Some(notif.error_code.to_string().into()),
last_error_subcode: Some(
notif.error_subcode.to_string().into(),
),
last_error_code: Some(notif.error_code),
last_error_subcode: Some(notif.error_subcode),
},
),
notification_sent: nbr.notification_sent.as_ref().map(
|(time, notif)| NotificationSent {
last_notification: Some(time.to_rfc3339().into()),
last_notification: Some(time),
last_error: Some(notif.to_yang()),
last_error_code: Some(notif.error_code.to_string().into()),
last_error_subcode: Some(
notif.error_subcode.to_string().into(),
),
last_error_code: Some(notif.error_code),
last_error_subcode: Some(notif.error_subcode),
},
),
};
Expand Down
6 changes: 3 additions & 3 deletions holo-ldp/src/northbound/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ pub(crate) fn mpls_ldp_hello_adjacency_event(
protocol_name: Some(instance_name.into()),
event_type: Some(event_type.into()),
targeted: ifname.is_none().then_some(Targeted {
target_address: Some(addr.to_string().into()),
target_address: Some(addr),
}),
link: ifname.map(|ifname| Link {
next_hop_interface: Some(ifname.into()),
next_hop_address: Some(addr.to_string().into()),
next_hop_address: Some(addr),
}),
};
notification::send(nb_tx, mpls_ldp_hello_adjacency_event::PATH, data);
Expand All @@ -72,7 +72,7 @@ pub(crate) fn mpls_ldp_fec_event(
let data = MplsLdpFecEvent {
event_type: Some(event_type.into()),
protocol_name: Some(instance_name.into()),
fec: Some(fec.inner.prefix.to_string().into()),
fec: Some(&fec.inner.prefix),
};
notification::send(nb_tx, mpls_ldp_fec_event::PATH, data);
}
Expand Down
207 changes: 197 additions & 10 deletions holo-northbound/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,58 @@ use check_keyword::CheckKeyword;
use convert_case::{Boundary, Case, Casing};
use holo_yang as yang;
use holo_yang::YANG_IMPLEMENTED_MODULES;
use yang2::schema::{DataValue, SchemaNode, SchemaNodeKind, SchemaPathFormat};
use yang2::schema::{
DataValue, DataValueType, SchemaNode, SchemaNodeKind, SchemaPathFormat,
};

const HEADER: &str = r#"
use std::borrow::Cow;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::{Duration, Instant};
use chrono::{DateTime, Utc};
use holo_yang::{YangObject, YangPath, YANG_CTX};
use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
use yang2::data::DataNodeRef;
use yang2::schema::SchemaModule;
fn timer_secs16_to_yang(timer: &Duration) -> String {
let remaining = timer.as_secs();
// Round up the remaining time to 1 in case it's less than one second.
let remaining = if remaining == 0 { 1 } else { remaining };
let remaining = u16::try_from(remaining).unwrap_or(u16::MAX);
remaining.to_string()
}
#[allow(dead_code)]
fn timer_secs32_to_yang(timer: &Duration) -> String {
let remaining = timer.as_secs();
// Round up the remaining time to 1 in case it's less than one second.
let remaining = if remaining == 0 { 1 } else { remaining };
let remaining = u32::try_from(remaining).unwrap_or(u32::MAX);
remaining.to_string()
}
fn timer_millis_to_yang(timer: &Duration) -> String {
let remaining = timer.as_millis();
// Round up the remaining time to 1 in case it's less than one millisecond.
let remaining = if remaining == 0 { 1 } else { remaining };
let remaining = u32::try_from(remaining).unwrap_or(u32::MAX);
remaining.to_string()
}
fn timeticks_to_yang(timeticks: &Instant) -> String {
let uptime = Instant::now() - *timeticks;
let uptime = u32::try_from(uptime.as_millis() / 10).unwrap_or(u32::MAX);
uptime.to_string()
}
fn timeticks64_to_yang(timeticks: &Instant) -> String {
let uptime = Instant::now() - *timeticks;
let uptime = u64::try_from(uptime.as_millis() / 10).unwrap_or(u64::MAX);
uptime.to_string()
}
"#;

struct StructBuilder<'a> {
level: usize,
Expand Down Expand Up @@ -88,7 +139,7 @@ impl<'a> StructBuilder<'a> {
snode_normalized_name(snode, Case::Pascal)
)
} else {
"Cow<'a, str>".to_owned()
snode_type_map(snode).to_owned()
};

writeln!(
Expand All @@ -98,6 +149,14 @@ impl<'a> StructBuilder<'a> {
)
.unwrap();
}
if self.fields.iter().all(|snode| snode_is_base_type(snode)) {
writeln!(
output,
"{}_marker: std::marker::PhantomData<&'a str>,",
indent2
)
.unwrap();
}
writeln!(output, "{}}}", indent1).unwrap();

// YangObject trait implementation.
Expand All @@ -114,6 +173,12 @@ impl<'a> StructBuilder<'a> {
indent2
)
.unwrap();
writeln!(
output,
"{}let module: Option<&SchemaModule<'_>> = None;",
indent3
)
.unwrap();
for snode in &self.fields {
let field_name = snode_normalized_name(snode, Case::Snake);
let module = snode.module();
Expand All @@ -137,8 +202,6 @@ impl<'a> StructBuilder<'a> {
.unwrap();
writeln!(output, "{}let module = Some(&module);", indent4)
.unwrap();
} else {
writeln!(output, "{}let module = None;", indent4).unwrap();
}

if snode.kind() == SchemaNodeKind::Container {
Expand All @@ -156,12 +219,13 @@ impl<'a> StructBuilder<'a> {
)
.unwrap();
} else {
let value = snode_type_value(snode, &field_name);
writeln!(
output,
"{}dnode.new_term(module, \"{}\", {}).unwrap();",
indent4,
snode.name(),
field_name
value
)
.unwrap();
}
Expand Down Expand Up @@ -208,6 +272,133 @@ fn snode_normalized_name(snode: &SchemaNode<'_>, case: Case) -> String {
name
}

fn snode_is_base_type(snode: &SchemaNode<'_>) -> bool {
matches!(
snode.base_type(),
Some(
DataValueType::Uint8
| DataValueType::Uint16
| DataValueType::Uint32
| DataValueType::Uint64
| DataValueType::Int8
| DataValueType::Int16
| DataValueType::Int32
| DataValueType::Int64
| DataValueType::Bool
| DataValueType::Empty
)
)
}

fn snode_typedef_map(snode: &SchemaNode<'_>) -> Option<&'static str> {
match snode.typedef_name().as_deref() {
Some("ip-address") => Some("&'a IpAddr"),
Some("ipv4-address") => Some("&'a Ipv4Addr"),
Some("ipv6-address") => Some("&'a Ipv6Addr"),
Some("ip-prefix") => Some("&'a IpNetwork"),
Some("ipv4-prefix") => Some("&'a Ipv4Network"),
Some("ipv6-prefix") => Some("&'a Ipv6Network"),
Some("date-and-time") => Some("&'a DateTime<Utc>"),
Some("timer-value-seconds16") => Some("&'a Duration"),
Some("timer-value-seconds32") => Some("&'a Duration"),
Some("timer-value-milliseconds") => Some("&'a Duration"),
Some("timeticks") => Some("&'a Instant"),
Some("timeticks64") => Some("&'a Instant"),
_ => None,
}
}

fn snode_typedef_value(
snode: &SchemaNode<'_>,
field_name: &str,
) -> Option<String> {
match snode.typedef_name().as_deref() {
Some("ip-address") | Some("ipv4-address") | Some("ipv6-address")
| Some("ip-prefix") | Some("ipv4-prefix") | Some("ipv6-prefix") => {
Some(format!("Some(&{}.to_string())", field_name))
}
Some("date-and-time") => {
Some(format!("Some(&{}.to_rfc3339())", field_name))
}
Some("timer-value-seconds16") => {
Some(format!("Some(&timer_secs16_to_yang({}))", field_name))
}
Some("timer-value-seconds32") => {
Some(format!("Some(&timer_secs32_to_yang({}))", field_name))
}
Some("timer-value-milliseconds") => {
Some(format!("Some(&timer_millis_to_yang({}))", field_name))
}
Some("timeticks") => {
Some(format!("Some(&timeticks_to_yang({}))", field_name))
}
Some("timeticks64") => {
Some(format!("Some(&timeticks64_to_yang({}))", field_name))
}
_ => None,
}
}

fn snode_type_map(snode: &SchemaNode<'_>) -> &'static str {
if let Some(typedef) = snode_typedef_map(snode) {
return typedef;
}

match snode.base_type().unwrap() {
DataValueType::Unknown => panic!("Unknown leaf type"),
DataValueType::Uint8 => "u8",
DataValueType::Uint16 => "u16",
DataValueType::Uint32 => "u32",
DataValueType::Uint64 => "u64",
DataValueType::Int8 => "i8",
DataValueType::Int16 => "i16",
DataValueType::Int32 => "i32",
DataValueType::Int64 => "i64",
DataValueType::Bool => "bool",
DataValueType::Empty => "()",
DataValueType::String
| DataValueType::Union
| DataValueType::Dec64
| DataValueType::Enum
| DataValueType::IdentityRef
| DataValueType::InstanceId
| DataValueType::LeafRef
| DataValueType::Binary
| DataValueType::Bits => "Cow<'a, str>",
}
}

fn snode_type_value(snode: &SchemaNode<'_>, field_name: &str) -> String {
if let Some(typedef_value) = snode_typedef_value(snode, field_name) {
return typedef_value;
}

match snode.base_type().unwrap() {
DataValueType::Unknown => panic!("Unknown leaf type"),
DataValueType::Uint8
| DataValueType::Uint16
| DataValueType::Uint32
| DataValueType::Uint64
| DataValueType::Int8
| DataValueType::Int16
| DataValueType::Int32
| DataValueType::Int64
| DataValueType::Bool => {
format!("Some(&{}.to_string())", field_name)
}
DataValueType::Empty => "None".to_owned(),
DataValueType::String
| DataValueType::Union
| DataValueType::Dec64
| DataValueType::Enum
| DataValueType::IdentityRef
| DataValueType::InstanceId
| DataValueType::LeafRef
| DataValueType::Binary
| DataValueType::Bits => format!("Some({})", field_name),
}
}

fn generate_module(output: &mut String, snode: &SchemaNode<'_>, level: usize) {
let indent = " ".repeat(level * 2);

Expand Down Expand Up @@ -393,11 +584,7 @@ fn main() {

// Generate file header.
let mut output = String::new();
writeln!(output, "use std::borrow::Cow;").unwrap();
writeln!(output, "use holo_yang::{{YangObject, YangPath, YANG_CTX}};")
.unwrap();
writeln!(output, "use yang2::data::DataNodeRef;").unwrap();
writeln!(output).unwrap();
writeln!(output, "{}", HEADER).unwrap();

// Generate modules.
for snode in yang_ctx
Expand Down

0 comments on commit efa1053

Please sign in to comment.