use std::{collections::BTreeMap, fmt};
use chrono::{DateTime, SecondsFormat};
use rpki::ca::idexchange::{
ChildHandle, MyHandle, ParentHandle, PublisherHandle, ServiceUri,
};
use rpki::ca::provisioning::ResourceClassName;
use rpki::crypto::KeyIdentifier;
use rpki::repository::resources::ResourceSet;
use rpki::repository::x509::Time;
use rpki::rrdp::Hash;
use serde::{Deserialize, Serialize};
use crate::commons::eventsourcing::{
Event, InitEvent, StoredEffect,
};
use super::admin::StorableParentContact;
use super::ca::ResourceSetSummary;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CommandHistory {
pub offset: usize,
pub total: usize,
pub commands: Vec<CommandHistoryRecord>,
}
impl fmt::Display for CommandHistory {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "time::command::version::success")?;
for command in &self.commands {
let success_string = match &command.effect {
CommandHistoryResult::Init() => "INIT".to_string(),
CommandHistoryResult::Ok() => "OK".to_string(),
CommandHistoryResult::Error(msg) => {
format!("ERROR -> {msg}")
}
};
writeln!(
f,
"{}::{}::{}::{}",
command.time().to_rfc3339_opts(SecondsFormat::Secs, true),
command.summary.msg,
command.version,
success_string
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CommandHistoryRecord {
pub actor: String,
pub timestamp: i64,
pub handle: MyHandle,
pub version: u64,
pub summary: CommandSummary,
pub effect: CommandHistoryResult,
}
impl CommandHistoryRecord {
pub fn matches(&self, crit: &CommandHistoryCriteria) -> bool {
crit.matches_timestamp(self.timestamp)
&& crit.matches_version(self.version)
&& crit.matches_label(&self.summary.label)
}
pub fn time(&self) -> Time {
DateTime::from_timestamp(
self.timestamp / 1000, 0
).expect("timestamp out-of-range").into()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum CommandHistoryResult {
Init(),
Ok(),
Error(String),
}
impl<E, I> From<StoredEffect<E, I>> for CommandHistoryResult
where E: Event, I: InitEvent {
fn from(effect: StoredEffect<E, I>) -> Self {
match effect {
StoredEffect::Error { msg, .. } => {
CommandHistoryResult::Error(msg)
}
StoredEffect::Success { .. } => CommandHistoryResult::Ok(),
StoredEffect::Init { .. } => CommandHistoryResult::Init(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CommandSummary {
pub msg: String,
pub label: String,
pub args: BTreeMap<String, String>,
}
impl CommandSummary {
pub fn new(label: &str, msg: impl fmt::Display) -> Self {
CommandSummary {
msg: msg.to_string(),
label: label.to_string(),
args: BTreeMap::new(),
}
}
pub fn arg(mut self, key: &str, val: impl fmt::Display) -> Self {
self.args.insert(key.to_string(), val.to_string());
self
}
pub fn child(self, child: &ChildHandle) -> Self {
self.arg("child", child)
}
pub fn parent(self, parent: &ParentHandle) -> Self {
self.arg("parent", parent)
}
pub fn publisher(self, publisher: &PublisherHandle) -> Self {
self.arg("publisher", publisher)
}
pub fn id_key(self, id: &str) -> Self {
self.arg("id_key", id)
}
pub fn resources(self, resources: &ResourceSet) -> Self {
let summary = ResourceSetSummary::from(resources);
self.arg("resources", resources)
.arg("asn_blocks", summary.asn_blocks)
.arg("ipv4_blocks", summary.ipv4_blocks)
.arg("ipv6_blocks", summary.ipv6_blocks)
}
pub fn rcn(self, rcn: &ResourceClassName) -> Self {
self.arg("class_name", rcn)
}
pub fn key(self, ki: KeyIdentifier) -> Self {
self.arg("key", ki)
}
pub fn id_cert_hash(self, hash: &Hash) -> Self {
self.arg("id_cert_hash", hash)
}
pub fn parent_contact(
self,
contact: &StorableParentContact,
) -> Self {
self.arg("parent_contact", contact)
}
pub fn seconds(self, seconds: i64) -> Self {
self.arg("seconds", seconds)
}
pub fn added(self, nr: usize) -> Self {
self.arg("added", nr)
}
pub fn removed(self, nr: usize) -> Self {
self.arg("removed", nr)
}
pub fn service_uri(self, service_uri: &ServiceUri) -> Self {
self.arg("service_uri", service_uri)
}
pub fn rta_name(self, name: &str) -> Self {
self.arg("rta_name", name)
}
}
#[derive(Clone, Debug, Deserialize, Default, Eq, PartialEq, Serialize)]
pub struct CommandHistoryCriteria {
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_includes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label_excludes: Option<Vec<String>>,
pub offset: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows_limit: Option<usize>,
}
impl CommandHistoryCriteria {
fn matches_timestamp(&self, stamp: i64) -> bool {
if let Some(before) = self.before && stamp > before {
return false;
}
if let Some(after) = self.after && stamp < after {
return false;
}
true
}
fn matches_version(&self, version: u64) -> bool {
match self.after_version {
None => true,
Some(seq_crit) => version > seq_crit,
}
}
fn matches_label(&self, label: &String) -> bool {
if
let Some(includes) = &self.label_includes
&& !includes.contains(label)
{
return false;
}
if
let Some(excludes) = &self.label_excludes
&& excludes.contains(label)
{
return false;
}
true
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CommandDetails {
pub actor: String,
pub time: Time,
pub handle: MyHandle,
pub version: u64,
pub msg: String,
pub details: serde_json::Value,
pub effect: CommandEffect
}
impl fmt::Display for CommandDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"Time: {}",
self.time.to_rfc3339_opts(SecondsFormat::Secs, true)
)?;
writeln!(f, "Actor: {}", self.actor)?;
writeln!(f, "Action: {}", self.msg)?;
match &self.effect {
CommandEffect::Error { msg, .. } => {
writeln!(f, "Error: {msg}")?
}
CommandEffect::Success { events } => {
writeln!(f, "Changes:")?;
for evt in events {
writeln!(f, " {}", evt.msg)?;
}
}
CommandEffect::Init { init } => {
writeln!(f, "{}", init.msg)?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(
rename_all = "snake_case",
tag = "result",
)]
pub enum CommandEffect {
Error { msg: String },
Success { events: Vec<CommandEffectEvent> },
Init { init: CommandEffectEvent },
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CommandEffectEvent {
pub msg: String,
pub details: serde_json::Value,
}