use std::{error, fmt, io};
use std::io::Write;
use std::str::FromStr;
use std::sync::Arc;
use chrono::Utc;
use chrono::format::{Item, Numeric, Pad};
use log::{error, info};
use routecore::addr;
use rpki::repository::resources::Asn;
use rpki::rtr::payload::{RouteOrigin, RouterKey};
use crate::error::Failed;
use crate::http::ContentType;
use crate::payload::{
PayloadInfo, PayloadSnapshot, SnapshotArcOriginsIter,
SnapshotArcRouterKeysIter,
};
use crate::metrics::Metrics;
use crate::utils::date::format_iso_date;
use crate::utils::json::json_str;
#[derive(Clone, Copy, Debug)]
pub enum OutputFormat {
Csv,
CompatCsv,
ExtendedCsv,
Json,
ExtendedJson,
Slurm,
Openbgpd,
Bird1,
Bird2,
Rpsl,
Summary,
None,
}
impl OutputFormat {
const VALUES: &'static [(&'static str, Self)] = &[
("csv", OutputFormat::Csv),
("csvcompat", OutputFormat::CompatCsv),
("csvext", OutputFormat::ExtendedCsv),
("json", OutputFormat::Json),
("jsonext", OutputFormat::ExtendedJson),
("slurm", OutputFormat::Slurm),
("openbgpd", OutputFormat::Openbgpd),
("bird1", OutputFormat::Bird1),
("bird2", OutputFormat::Bird2),
("rpsl", OutputFormat::Rpsl),
("summary", OutputFormat::Summary),
("none", OutputFormat::None),
];
pub const DEFAULT_VALUE: &'static str = "csv";
}
impl OutputFormat {
pub fn from_path(path: &str) -> Option<Self> {
if !path.starts_with('/') {
return None
}
Self::try_from_str(&path[1..])
}
fn try_from_str(value: &str) -> Option<Self> {
for &(name, res) in Self::VALUES {
if name == value {
return Some(res)
}
}
None
}
pub fn content_type(self) -> ContentType {
match self {
OutputFormat::Csv | OutputFormat::CompatCsv |
OutputFormat::ExtendedCsv
=> ContentType::CSV,
OutputFormat::Json | OutputFormat::ExtendedJson |
OutputFormat::Slurm
=> ContentType::JSON,
_ => ContentType::TEXT,
}
}
pub fn output_snapshot<W: io::Write>(
self,
snapshot: &PayloadSnapshot,
selection: Option<&Selection>,
metrics: &Metrics,
target: &mut W,
) -> Result<(), io::Error> {
let formatter = self.formatter();
let mut first = true;
formatter.header(snapshot, metrics, target)?;
for (origin, info) in snapshot.origins() {
if let Some(selection) = selection {
if !selection.include_origin(origin) {
continue
}
}
if first {
first = false;
}
else {
formatter.delimiter(target)?;
}
formatter.origin(origin, info, target)?;
}
formatter.intermission(target)?;
let mut first = true;
for (key, info) in snapshot.router_keys() {
if let Some(selection) = selection {
if !selection.include_router_key(key) {
continue
}
}
if first {
first = false;
}
else {
formatter.delimiter(target)?;
}
formatter.router_key(key, info, target)?;
}
formatter.footer(metrics, target)
}
pub fn stream(
self,
snapshot: Arc<PayloadSnapshot>,
selection: Option<Selection>,
metrics: Arc<Metrics>,
) -> impl Iterator<Item = Vec<u8>> {
OutputStream::new(self, snapshot, selection, metrics)
}
fn formatter<W: io::Write>(self) -> Box<dyn Formatter<W> + Send> {
match self {
OutputFormat::Csv => Box::new(Csv),
OutputFormat::CompatCsv => Box::new(CompatCsv),
OutputFormat::ExtendedCsv => Box::new(ExtendedCsv),
OutputFormat::Json => Box::new(Json),
OutputFormat::ExtendedJson => Box::new(ExtendedJson),
OutputFormat::Slurm => Box::new(Slurm),
OutputFormat::Openbgpd => Box::new(Openbgpd),
OutputFormat::Bird1 => Box::new(Bird1),
OutputFormat::Bird2 => Box::new(Bird2),
OutputFormat::Rpsl => Box::new(Rpsl),
OutputFormat::Summary => Box::new(Summary),
OutputFormat::None => Box::new(NoOutput),
}
}
}
impl FromStr for OutputFormat {
type Err = Failed;
fn from_str(value: &str) -> Result<Self, Failed> {
Self::try_from_str(value).ok_or_else(|| {
error!("Unknown output format: {}", value);
Failed
})
}
}
#[derive(Clone, Debug, Default)]
pub struct Selection {
origins: Vec<SelectOrigin>,
more_specifics: bool,
}
impl Selection {
pub fn new() -> Self {
Selection::default()
}
pub fn set_more_specifics(&mut self, more_specifics: bool) {
self.more_specifics = more_specifics
}
pub fn from_query(query: Option<&str>) -> Result<Option<Self>, QueryError> {
let query = match query {
Some(query) => query,
None => return Ok(None)
};
let mut res = Self::default();
for (key, value) in form_urlencoded::parse(query.as_ref()) {
if key == "select-prefix" || key == "filter-prefix" {
res.origins.push(
SelectOrigin::Prefix(addr::Prefix::from_str(&value)?)
);
}
else if key == "select-asn" || key == "filter-asn" {
res.origins.push(
SelectOrigin::Asn(
Asn::from_str(&value).map_err(|_| QueryError)?
)
);
}
else if key == "include" {
for value in value.split(',') {
#[allow(clippy::single_match)]
match value {
"more-specifics" => res.more_specifics = true,
_ => { }
}
}
}
else {
return Err(QueryError)
}
}
Ok(Some(res))
}
pub fn push_origin_asn(&mut self, asn: Asn) {
self.origins.push(SelectOrigin::Asn(asn))
}
pub fn push_origin_prefix(&mut self, prefix: addr::Prefix) {
self.origins.push(SelectOrigin::Prefix(prefix))
}
pub fn include_origin(&self, origin: RouteOrigin) -> bool {
for select in &self.origins {
if select.include_origin(origin, self.more_specifics) {
return true
}
}
false
}
pub fn include_router_key(&self, key: &RouterKey) -> bool {
for select in &self.origins {
if select.include_router_key(key) {
return true
}
}
false
}
}
impl AsRef<Selection> for Selection {
fn as_ref(&self) -> &Self {
self
}
}
#[derive(Clone, Copy, Debug)]
enum SelectOrigin {
Asn(Asn),
Prefix(addr::Prefix),
}
impl SelectOrigin {
fn include_origin(
self, origin: RouteOrigin, more_specifics: bool
) -> bool {
match self {
SelectOrigin::Asn(asn) => origin.asn == asn,
SelectOrigin::Prefix(prefix) => {
origin.prefix.prefix().covers(prefix)
|| (more_specifics && prefix.covers(origin.prefix.prefix()))
}
}
}
fn include_router_key(self, key: &RouterKey) -> bool {
match self {
SelectOrigin::Asn(asn) => key.asn == asn,
_ => false
}
}
}
struct OutputStream<Target> {
snapshot: Arc<PayloadSnapshot>,
state: StreamState,
formatter: Box<dyn Formatter<Target> + Send>,
selection: Option<Selection>,
metrics: Arc<Metrics>,
}
enum StreamState {
Header,
Origin { iter: SnapshotArcOriginsIter, first: bool },
Key { iter: SnapshotArcRouterKeysIter, first: bool },
Done,
}
impl<Target: io::Write> OutputStream<Target> {
fn new(
format: OutputFormat,
snapshot: Arc<PayloadSnapshot>,
selection: Option<Selection>,
metrics: Arc<Metrics>,
) -> Self {
OutputStream {
snapshot,
state: StreamState::Header,
formatter: format.formatter(),
selection,
metrics,
}
}
fn write_next(&mut self, target: &mut Target) -> Result<bool, io::Error> {
let next = match self.state {
StreamState::Header => {
self.formatter.header(
&self.snapshot, &self.metrics, target
)?;
StreamState::Origin {
iter: self.snapshot.clone().arc_origins_iter(),
first: true,
}
}
StreamState::Origin { ref mut iter, ref mut first } => {
loop {
let (origin, info) = match iter.next_with_info() {
Some((origin, info)) => (origin, info),
None => {
self.formatter.intermission(target)?;
break
}
};
if let Some(selection) = self.selection.as_ref() {
if !selection.include_origin(origin) {
continue
}
}
if *first {
*first = false;
}
else {
self.formatter.delimiter(target)?;
}
self.formatter.origin(origin, info, target)?;
return Ok(true)
}
StreamState::Key {
iter: self.snapshot.clone().arc_router_keys_iter(),
first: true
}
}
StreamState::Key { ref mut iter, ref mut first } => {
loop {
let (key, info) = match iter.next_with_info() {
Some((key, info)) => (key, info),
None => {
self.formatter.footer(
self.metrics.as_ref(), target
)?;
break
}
};
if let Some(selection) = self.selection.as_ref() {
if !selection.include_router_key(key) {
continue
}
}
if *first {
*first = false;
}
else {
self.formatter.delimiter(target)?;
}
self.formatter.router_key(key, info, target)?;
return Ok(true)
}
StreamState::Done
}
StreamState::Done => return Ok(false)
};
self.state = next;
Ok(true)
}
}
impl Iterator for OutputStream<Vec<u8>> {
type Item = Vec<u8>;
fn next(&mut self) -> Option<Self::Item> {
let mut res = Vec::new();
while self.write_next(&mut res).expect("write to vec failed") {
if res.len() > 64000 {
return Some(res)
}
}
if res.is_empty() {
None
}
else {
Some(res)
}
}
}
#[derive(Debug)]
pub struct QueryError;
impl From<addr::ParsePrefixError> for QueryError {
fn from(_: addr::ParsePrefixError) -> Self {
QueryError
}
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("invalid query")
}
}
impl error::Error for QueryError { }
trait Formatter<W> {
fn header(
&self, snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
let _ = (snapshot, metrics, target);
Ok(())
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error>;
fn intermission(
&self, _target: &mut W
) -> Result<(), io::Error> {
Ok(())
}
fn router_key(
&self, _key: &RouterKey, _info: &PayloadInfo, _target: &mut W
) -> Result<(), io::Error> {
Ok(())
}
fn footer(
&self, metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
let _ = (metrics, target);
Ok(())
}
fn delimiter(&self, target: &mut W) -> Result<(), io::Error> {
let _ = target;
Ok(())
}
}
struct Csv;
impl<W: io::Write> Formatter<W> for Csv {
fn header(
&self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "ASN,IP Prefix,Max Length,Trust Anchor")
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "{},{}/{},{},{}",
origin.asn,
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
info.tal_name().unwrap_or("N/A"),
)
}
}
struct CompatCsv;
impl<W: io::Write> Formatter<W> for CompatCsv {
fn header(
&self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(
target, "\"ASN\",\"IP Prefix\",\"Max Length\",\"Trust Anchor\""
)
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "\"{}\",\"{}/{}\",\"{}\",\"{}\"",
origin.asn,
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
info.tal_name().unwrap_or("N/A"),
)
}
}
struct ExtendedCsv;
impl ExtendedCsv {
const TIME_ITEMS: &'static [Item<'static>] = &[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Literal("-"),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Literal("-"),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Literal(" "),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Second, Pad::Zero),
];
}
impl<W: io::Write> Formatter<W> for ExtendedCsv {
fn header(
&self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "URI,ASN,IP Prefix,Max Length,Not Before,Not After")
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
write!(target, "{},{},{}/{},{},",
info.uri().map(|uri| uri.as_str()).unwrap_or("N/A"),
origin.asn,
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
)?;
match info.validity() {
Some(validity) => {
writeln!(target, "{},{}",
validity.not_before().format_with_items(
Self::TIME_ITEMS.iter().cloned()
),
validity.not_after().format_with_items(
Self::TIME_ITEMS.iter().cloned()
)
)
}
None => writeln!(target, "N/A,N/A"),
}
}
}
struct Json;
impl<W: io::Write> Formatter<W> for Json {
fn header(
&self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target,
"{{\
\n \"metadata\": {{\
\n \"generated\": {},\
\n \"generatedTime\": \"{}\"\
\n }},\
\n \"roas\": [",
metrics.time.timestamp(),
format_iso_date(metrics.time)
)
}
fn footer(
&self, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "\n ]\n}}")
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
write!(target,
" {{ \"asn\": \"{}\", \"prefix\": \"{}/{}\", \
\"maxLength\": {}, \"ta\": \"{}\" }}",
origin.asn,
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
info.tal_name().unwrap_or("N/A"),
)
}
fn delimiter(&self, target: &mut W) -> Result<(), io::Error> {
writeln!(target, ",")
}
}
struct ExtendedJson;
impl ExtendedJson {
fn payload_info(
info: &PayloadInfo, rpki_type: &str, target: &mut impl io::Write
) -> Result<(), io::Error> {
let mut first = true;
for item in info {
if let Some(roa) = item.publish_info() {
if !first {
write!(target, ", ")?;
}
else {
first = false;
}
write!(target,
" {{ \"type\": \"{}\", \"uri\": ",
rpki_type,
)?;
match roa.uri.as_ref() {
Some(uri) => write!(target, "\"{}\"", uri)?,
None => write!(target, "null")?
}
write!(target,
", \"tal\": \"{}\", \
\"validity\": {{ \"notBefore\": \"{}\", \
\"notAfter\": \"{}\" }}, \
\"chainValidity\": {{ \"notBefore\": \"{}\", \
\"notAfter\": \"{}\" }} \
}}",
json_str(roa.tal.name()),
format_iso_date(roa.roa_validity.not_before().into()),
format_iso_date(roa.roa_validity.not_after().into()),
format_iso_date(roa.chain_validity.not_before().into()),
format_iso_date(roa.chain_validity.not_after().into()),
)?;
}
if let Some(exc) = item.exception_info() {
if !first {
write!(target, ", ")?;
}
else {
first = false;
}
write!(target, " {{ \"type\": \"exception\", \"path\": ")?;
match exc.path.as_ref() {
Some(path) => {
write!(target, "\"{}\"", json_str(path.display()))?
}
None => write!(target, "null")?,
}
if let Some(comment) = exc.comment.as_ref() {
write!(
target, ", \"comment\": \"{}\"", json_str(comment)
)?
}
write!(target, " }}")?;
}
}
Ok(())
}
}
impl<W: io::Write> Formatter<W> for ExtendedJson {
fn header(
&self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target,
"{{\
\n \"metadata\": {{\
\n \"generated\": {},\
\n \"generatedTime\": \"{}\"\
\n }},\
\n \"roas\": [",
metrics.time.timestamp(),
format_iso_date(metrics.time)
)
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
write!(target,
" {{ \"asn\": \"{}\", \"prefix\": \"{}/{}\", \
\"maxLength\": {}, \"source\": [",
origin.asn,
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
)?;
Self::payload_info(info, "roa", target)?;
write!(target, "] }}")
}
fn intermission(&self, target: &mut W) -> Result<(), io::Error> {
writeln!(target, "\n ],\n \"routerKeys\": [")
}
fn router_key(
&self, key: &RouterKey, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
write!(target,
" {{ \"asn\": \"{}\", \"SKI\": \"{}\", \
\"routerPublicKey\": \"{}\", \"source\": [",
key.asn,
key.key_identifier,
key.key_info,
)?;
Self::payload_info(info, "cer", target)?;
write!(target, "] }}")
}
fn footer(
&self, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "\n ]\n}}")
}
fn delimiter(&self, target: &mut W) -> Result<(), io::Error> {
writeln!(target, ",")
}
}
struct Slurm;
impl<W: io::Write> Formatter<W> for Slurm {
fn header(
&self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target,
"{{\
\n \"slurmVersion\": 1,\
\n \"validationOutputFilters\": {{\
\n \"prefixFilters\": [ ],\
\n \"bgpsecFilters\": [ ]\
\n }},\
\n \"locallyAddedAssertions\": {{\
\n \"prefixAssertions\": ["
)
}
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
writeln!(target,
" {{\
\n \"asn\": {},\
\n \"prefix\": \"{}/{}\",",
origin.asn.into_u32(),
origin.prefix.addr(), origin.prefix.prefix_len()
)?;
if let Some(max_len) = origin.prefix.max_len() {
writeln!(target, " \"maxPrefixLength\": {},", max_len)?;
}
write!(target,
" \"comment\": \"{}\"\
\n }}",
info.tal_name().unwrap_or("N/A")
)
}
fn intermission(&self, target: &mut W) -> Result<(), io::Error> {
writeln!(target,
"\n ],\
\n \"bgpsecAssertions\": ["
)
}
fn router_key(
&self, key: &RouterKey, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
write!(target,
" {{\
\n \"asn\": {},\
\n \"SKI\": \"",
key.asn.into_u32(),
)?;
let mut enc = base64::write::EncoderWriter::new(
target, base64::URL_SAFE_NO_PAD
);
enc.write_all(key.key_identifier.as_slice())?;
let target = enc.finish()?;
write!(target, "\",\
\n \"routerPublicKey\": \""
)?;
let mut enc = base64::write::EncoderWriter::new(
target, base64::URL_SAFE_NO_PAD
);
enc.write_all(key.key_identifier.as_slice())?;
let target = enc.finish()?;
write!(target, "\",\
\n \"comment\": \"{}\"
\n }}",
info.tal_name().unwrap_or("N/A")
)
}
fn footer(
&self, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target,
"\n ]\
\n }}\
\n}}"
)
}
fn delimiter(&self, target: &mut W) -> Result<(), io::Error> {
writeln!(target, ",")
}
}
struct Openbgpd;
impl<W: io::Write> Formatter<W> for Openbgpd {
fn header(
&self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "roa-set {{")
}
fn footer(
&self, _metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "}}")
}
fn origin(
&self, origin: RouteOrigin, _info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
write!(
target, " {}/{}",
origin.prefix.addr(), origin.prefix.prefix_len(),
)?;
let max_len = origin.prefix.resolved_max_len();
if origin.prefix.prefix_len() < max_len {
write!(target, " maxlen {}", max_len)?;
}
writeln!(target, " source-as {}", u32::from(origin.asn))
}
}
struct Bird1;
impl<W: io::Write> Formatter<W> for Bird1 {
fn origin(
&self, origin: RouteOrigin, _info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "roa {}/{} max {} as {};",
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
u32::from(origin.asn)
)
}
}
struct Bird2;
impl<W: io::Write> Formatter<W> for Bird2 {
fn origin(
&self, origin: RouteOrigin, _info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
writeln!(target, "route {}/{} max {} as {};",
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.prefix.resolved_max_len(),
u32::from(origin.asn)
)
}
}
struct Rpsl;
impl Rpsl {
const TIME_ITEMS: &'static [Item<'static>] = &[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Literal("-"),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Literal("-"),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Literal("T"),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Literal(":"),
Item::Numeric(Numeric::Second, Pad::Zero),
Item::Literal("Z"),
];
}
impl<W: io::Write> Formatter<W> for Rpsl {
fn origin(
&self, origin: RouteOrigin, info: &PayloadInfo, target: &mut W
) -> Result<(), io::Error> {
let now = Utc::now().format_with_items(
Self::TIME_ITEMS.iter().cloned()
);
writeln!(target,
"\n{}: {}/{}\norigin: {}\n\
descr: RPKI attestation\nmnt-by: NA\ncreated: {}\n\
last-modified: {}\nsource: ROA-{}-RPKI-ROOT\n",
if origin.prefix.addr().is_ipv4() { "route" }
else { "route6" },
origin.prefix.addr(), origin.prefix.prefix_len(),
origin.asn, now, now,
info.tal_name().map(|name| {
name.to_uppercase()
}).unwrap_or_else(|| "N/A".into())
)
}
}
pub struct Summary;
impl Summary {
fn produce_header(
metrics: &Metrics,
mut line: impl FnMut(fmt::Arguments) -> Result<(), io::Error>
) -> Result<(), io::Error> {
line(format_args!("Summary at {}", metrics.time))?;
for tal in &metrics.tals {
line(format_args!("{}: ", tal.name()))?;
line(format_args!(
" ROAs: {:7} verified;",
tal.publication.valid_roas
))?;
line(format_args!(
" VRPs: {:7} verified, {:7} final;",
tal.payload.vrps().valid,
tal.payload.vrps().contributed
))?;
line(format_args!(
" router certs: {:7} verified;",
tal.publication.valid_ee_certs,
))?;
line(format_args!(
" router keys: {:7} verified, {:7} final.",
tal.payload.router_keys.valid,
tal.payload.router_keys.contributed
))?;
}
line(format_args!("total: "))?;
line(format_args!(
" ROAs: {:7} verified;",
metrics.publication.valid_roas
))?;
line(format_args!(
" VRPs: {:7} verified, {:7} final;",
metrics.payload.vrps().valid,
metrics.payload.vrps().contributed
))?;
line(format_args!(
" router certs: {:7} verified;",
metrics.publication.valid_ee_certs,
))?;
line(format_args!(
" router keys: {:7} verified, {:7} final.",
metrics.payload.router_keys.valid,
metrics.payload.router_keys.contributed
))
}
pub fn log(metrics: &Metrics) {
Self::produce_header(metrics, |args| {
info!("{}", args);
Ok(())
}).unwrap()
}
}
impl<W: io::Write> Formatter<W> for Summary {
fn header(
&self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W
) -> Result<(), io::Error> {
Self::produce_header(metrics, |args| {
writeln!(target, "{}", args)
})
}
fn origin(
&self, _origin: RouteOrigin, _info: &PayloadInfo, _target: &mut W
) -> Result<(), io::Error> {
Ok(())
}
}
struct NoOutput;
impl<W: io::Write> Formatter<W> for NoOutput {
fn origin(
&self, _origin: RouteOrigin, _info: &PayloadInfo, _target: &mut W
) -> Result<(), io::Error> {
Ok(())
}
}