Files
NLnetLabs-krill/src/cli/client.rs
T

432 lines
14 KiB
Rust

use std::{env, fmt, io};
use serde::de::DeserializeOwned;
use serde::Serialize;
use rpki::uri;
use crate::cli::options::{
BulkCaCommand, CaCommand, Command, KrillInitDetails, Options, PublishersCommand,
};
use crate::cli::report::{ApiResponse, ReportError};
use crate::commons::api::{
CaRepoDetails, ChildCaInfo, CurrentRepoState, ParentCaContact, PublisherDetails, PublisherList,
Token,
};
use crate::commons::remote::rfc8183;
use crate::commons::util::httpclient;
use crate::constants::KRILL_CLI_API_ENV;
use crate::daemon::config::Config;
/// Command line tool for Krill admin tasks
pub struct KrillClient {
server: uri::Https,
token: Token,
}
impl KrillClient {
/// Delegates the options to be processed, and reports the response
/// back to the user. Note that error reporting is handled by CLI.
pub fn report(options: Options) -> Result<(), Error> {
let format = options.format;
let res = Self::process(options)?;
if let Some(string) = res.report(format)? {
println!("{}", string)
}
Ok(())
}
/// Processes the options, and returns a response ready for formatting.
/// Note that this function is public to help integration testing the API
/// and client.
pub fn process(options: Options) -> Result<ApiResponse, Error> {
let client = KrillClient {
server: options.server,
token: options.token,
};
if options.api {
// passing the api option in the env, so that the call
// to the back-end will just print and exit.
env::set_var(KRILL_CLI_API_ENV, "1")
}
match options.command {
Command::Health => client.health(),
Command::Bulk(cmd) => client.bulk(cmd),
Command::CertAuth(cmd) => client.certauth(cmd),
Command::Publishers(cmd) => client.publishers(cmd),
Command::Init(details) => client.init(details),
Command::NotSet => Err(Error::MissingCommand),
}
}
fn health(&self) -> Result<ApiResponse, Error> {
httpclient::get_ok(&self.resolve_uri("api/v1/authorized"), Some(&self.token))?;
Ok(ApiResponse::Health)
}
fn bulk(&self, command: BulkCaCommand) -> Result<ApiResponse, Error> {
match command {
BulkCaCommand::Refresh => {
self.post_empty("api/v1/cas/refresh_all")?;
Ok(ApiResponse::Empty)
}
BulkCaCommand::Publish => {
self.post_empty("api/v1/cas/republish_all")?;
Ok(ApiResponse::Empty)
}
BulkCaCommand::Sync => {
self.post_empty("api/v1/cas/resync_all")?;
Ok(ApiResponse::Empty)
}
}
}
fn certauth(&self, command: CaCommand) -> Result<ApiResponse, Error> {
match command {
CaCommand::Init(init) => {
self.post_json("api/v1/cas", init)?;
Ok(ApiResponse::Empty)
}
CaCommand::UpdateId(handle) => {
let uri = format!("api/v1/cas/{}/id", handle);
self.post_empty(&uri)?;
Ok(ApiResponse::Empty)
}
CaCommand::ParentResponse(handle, child) => {
let uri = format!("api/v1/cas/{}/children/{}/contact", handle, child);
let info: ParentCaContact = self.get_json(&uri)?;
Ok(ApiResponse::ParentCaContact(info))
}
CaCommand::ChildRequest(handle) => {
let uri = format!("api/v1/cas/{}/child_request.json", handle);
let req = self.get_json(&uri)?;
Ok(ApiResponse::Rfc8183ChildRequest(req))
}
CaCommand::RepoPublisherRequest(handle) => {
let uri = format!("api/v1/cas/{}/repo/request.json", handle);
let req: rfc8183::PublisherRequest = self.get_json(&uri)?;
Ok(ApiResponse::Rfc8183PublisherRequest(req))
}
CaCommand::RepoDetails(handle) => {
let uri = format!("api/v1/cas/{}/repo", handle);
let details: CaRepoDetails = self.get_json(&uri)?;
Ok(ApiResponse::RepoDetails(details))
}
CaCommand::RepoState(handle) => {
let uri = format!("api/v1/cas/{}/repo/state", handle);
let state: CurrentRepoState = self.get_json(&uri)?;
Ok(ApiResponse::RepoState(state))
}
CaCommand::RepoUpdate(handle, update) => {
let uri = format!("api/v1/cas/{}/repo", handle);
self.post_json(&uri, update)?;
Ok(ApiResponse::Empty)
}
CaCommand::AddParent(handle, parent) => {
let uri = format!("api/v1/cas/{}/parents", handle);
self.post_json(&uri, parent)?;
Ok(ApiResponse::Empty)
}
CaCommand::UpdateParentContact(handle, parent, contact) => {
let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
self.post_json(&uri, contact)?;
Ok(ApiResponse::Empty)
}
CaCommand::RemoveParent(handle, parent) => {
let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
self.delete(&uri)?;
Ok(ApiResponse::Empty)
}
CaCommand::MyParentCaContact(handle, parent) => {
let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
let parent: ParentCaContact = self.get_json(&uri)?;
Ok(ApiResponse::ParentCaContact(parent))
}
CaCommand::ChildInfo(handle, child) => {
let uri = format!("api/v1/cas/{}/children/{}", handle, child);
let info: ChildCaInfo = self.get_json(&uri)?;
Ok(ApiResponse::ChildInfo(info))
}
CaCommand::ChildAdd(handle, req) => {
let uri = format!("api/v1/cas/{}/children", handle);
let info: ParentCaContact = self.post_json_with_response(&uri, req)?;
Ok(ApiResponse::ParentCaContact(info))
}
CaCommand::ChildUpdate(handle, child, req) => {
let uri = format!("api/v1/cas/{}/children/{}", handle, child);
self.post_json(&uri, req)?;
Ok(ApiResponse::Empty)
}
CaCommand::ChildDelete(handle, child) => {
let uri = format!("api/v1/cas/{}/children/{}", handle, child);
self.delete(&uri)?;
Ok(ApiResponse::Empty)
}
CaCommand::KeyRollInit(handle) => {
let uri = format!("api/v1/cas/{}/keys/roll_init", handle);
self.post_empty(&uri)?;
Ok(ApiResponse::Empty)
}
CaCommand::KeyRollActivate(handle) => {
let uri = format!("api/v1/cas/{}/keys/roll_activate", handle);
self.post_empty(&uri)?;
Ok(ApiResponse::Empty)
}
CaCommand::RouteAuthorizationsList(handle) => {
let uri = format!("api/v1/cas/{}/routes", handle);
let roas = self.get_json(&uri)?;
Ok(ApiResponse::RouteAuthorizations(roas))
}
CaCommand::RouteAuthorizationsUpdate(handle, updates) => {
let uri = format!("api/v1/cas/{}/routes", handle);
self.post_json(&uri, updates)?;
Ok(ApiResponse::Empty)
}
CaCommand::Show(handle) => {
let uri = format!("api/v1/cas/{}", handle);
let ca_info = self.get_json(&uri)?;
Ok(ApiResponse::CertAuthInfo(ca_info))
}
CaCommand::ShowHistory(handle) => {
let uri = format!("api/v1/cas/{}/history", handle);
let history = self.get_json(&uri)?;
Ok(ApiResponse::CertAuthHistory(history))
}
CaCommand::List => {
let cas = self.get_json("api/v1/cas")?;
Ok(ApiResponse::CertAuths(cas))
}
}
}
fn publishers(&self, command: PublishersCommand) -> Result<ApiResponse, Error> {
match command {
PublishersCommand::PublisherList => {
let list: PublisherList = self.get_json("api/v1/publishers")?;
Ok(ApiResponse::PublisherList(list))
}
PublishersCommand::AddPublisher(req) => {
let res = self.post_json_with_response("api/v1/publishers", req)?;
Ok(ApiResponse::Rfc8183RepositoryResponse(res))
}
PublishersCommand::RemovePublisher(handle) => {
let uri = format!("api/v1/publishers/{}", handle);
self.delete(&uri)?;
Ok(ApiResponse::Empty)
}
PublishersCommand::ShowPublisher(handle) => {
let uri = format!("api/v1/publishers/{}", handle);
let details: PublisherDetails = self.get_json(&uri)?;
Ok(ApiResponse::PublisherDetails(details))
}
PublishersCommand::RepositoryResponse(handle) => {
let uri = format!("api/v1/publishers/{}/response.json", handle);
let res = self.get_json(&uri)?;
Ok(ApiResponse::Rfc8183RepositoryResponse(res))
}
}
}
fn resolve_uri(&self, path: &str) -> String {
format!("{}{}", &self.server, path)
}
fn init(&self, details: KrillInitDetails) -> Result<ApiResponse, Error> {
let defaults = include_str!("../../defaults/krill.conf");
let mut config = defaults.to_string();
config = config.replace(
"### auth_token =",
&format!("auth_token = \"{}\"", self.token),
);
config = config.replace(
"### service_uri = \"https://localhost:3000/\"",
&format!("service_uri = \"{}\"", self.server),
);
if let Some(rsync_base) = details.rsync_base() {
config = config.replace("### repo_enabled = false", "repo_enabled = true");
config = config.replace(
"### rsync_base = \"rsync://localhost/repo/\"",
&format!("rsync_base = \"{}\"", rsync_base),
)
}
if let Some(rrdp_service_uri) = details.rrdp_service_uri() {
config = config.replace(
"### rrdp_service_uri = \"$service_uri/rrdp/\"",
&format!("rrdp_service_uri = \"{}\"", rrdp_service_uri),
)
}
if let Some(data_dir) = details.data_dir() {
config = config.replace(
"### data_dir = \"./data\"",
&format!("data_dir = \"{}\"", data_dir),
)
}
if let Some(log_file) = details.log_file() {
config = config.replace(
"### log_file = \"./krill.log\"",
&format!("log_file = \"{}\"", log_file),
)
}
let c: Config = toml::from_slice(config.as_ref()).map_err(Error::init)?;
c.verify().map_err(Error::init)?;
Ok(ApiResponse::GenericBody(config.to_string()))
}
fn get_json<T: DeserializeOwned>(&self, uri: &str) -> Result<T, Error> {
let uri = self.resolve_uri(uri);
httpclient::get_json(&uri, Some(&self.token)).map_err(Error::HttpClientError)
}
fn post_empty(&self, uri: &str) -> Result<(), Error> {
let uri = self.resolve_uri(uri);
httpclient::post_empty(&uri, Some(&self.token)).map_err(Error::HttpClientError)
}
fn post_json(&self, uri: &str, data: impl Serialize) -> Result<(), Error> {
let uri = self.resolve_uri(uri);
httpclient::post_json(&uri, data, Some(&self.token)).map_err(Error::HttpClientError)
}
fn post_json_with_response<T: DeserializeOwned>(
&self,
uri: &str,
data: impl Serialize,
) -> Result<T, Error> {
let uri = self.resolve_uri(uri);
httpclient::post_json_with_response(&uri, data, Some(&self.token))
.map_err(Error::HttpClientError)
}
fn delete(&self, uri: &str) -> Result<(), Error> {
let uri = self.resolve_uri(uri);
httpclient::delete(&uri, Some(&self.token)).map_err(Error::HttpClientError)
}
}
//------------ Error ---------------------------------------------------------
#[derive(Debug, Display)]
pub enum Error {
#[display(fmt = "No valid command given, see --help")]
MissingCommand,
#[display(fmt = "Server is not available.")]
ServerDown,
#[display(fmt = "{}", _0)]
HttpClientError(httpclient::Error),
#[display(fmt = "{}", _0)]
ReportError(ReportError),
#[display(fmt = "Can't read file: {}", _0)]
IoError(io::Error),
#[display(fmt = "Empty response received from server")]
EmptyResponse,
#[display(fmt = "{}", _0)]
Rfc8183(rfc8183::Error),
#[display(fmt = "{}", _0)]
InitError(String),
}
impl Error {
fn init(msg: impl fmt::Display) -> Self {
Error::InitError(msg.to_string())
}
}
impl From<httpclient::Error> for Error {
fn from(e: httpclient::Error) -> Self {
Error::HttpClientError(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::IoError(e)
}
}
impl From<ReportError> for Error {
fn from(e: ReportError) -> Self {
Error::ReportError(e)
}
}
impl From<rfc8183::Error> for Error {
fn from(e: rfc8183::Error) -> Error {
Error::Rfc8183(e)
}
}
//------------ Tests ---------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::options::KrillInitDetails;
use crate::commons::util::test;
#[test]
fn init_config_file() {
let mut details = KrillInitDetails::default();
details.with_rsync_base(test::rsync("rsync://myhost/repo/"));
details.with_rrdp_service_uri(test::https("https://myhost/rrdp/"));
details.with_data_dir("/var/lib/krill/data/");
details.with_log_file("/var/log/krill/krill.log");
let client = KrillClient {
server: test::https("https://localhost:3001/"),
token: Token::from("secret"),
};
let res = client.init(details).unwrap();
match res {
ApiResponse::GenericBody(body) => {
let expected = include_str!("../../test-resources/krill-init.conf");
assert_eq!(expected, &body)
}
_ => panic!("Expected body"),
}
}
}