Skip to content

Commit

Permalink
Merge pull request #654 from input-output-hk/rest_leaders_list
Browse files Browse the repository at this point in the history
Add leadership management REST API
  • Loading branch information
vincenthz committed Jul 31, 2019
2 parents 356553b + 3df4dba commit aafae45
Show file tree
Hide file tree
Showing 13 changed files with 249 additions and 23 deletions.
61 changes: 60 additions & 1 deletion doc/jcli/rest.md
Expand Up @@ -212,7 +212,7 @@ value: 990
Fetches node settings

```
jcli rest v0 node settings get <options>
jcli rest v0 settings get <options>
```

The options are
Expand Down Expand Up @@ -249,3 +249,62 @@ The options are

- -h <node_addr> - see [conventions](#conventions)
- --debug - see [conventions](#conventions)

## Get leaders

Fetches list of leader IDs

```
jcli rest v0 leaders get <options>
```

The options are

- -h <node_addr> - see [conventions](#conventions)
- --debug - see [conventions](#conventions)
- --output-format <format> - see [conventions](#conventions)


YAML printed on success

```yaml
---
- 1 # list of leader IDs
- 2
```

## Register leader

Register new leader and get its ID

```
jcli rest v0 leaders post <options>
```

The options are

- -h <node_addr> - see [conventions](#conventions)
- --debug - see [conventions](#conventions)
- --output-format <format> - see [conventions](#conventions)
-f, --file <file> - File containing YAML with leader secret. It must have the same format as secret YAML passed to Jormungandr as --secret. If not provided, YAML will be read from stdin.

On success created leader ID is printed

```
3
```

## Delete leader

Delete leader with given ID

```
jcli rest v0 leaders delete <id> <options>
```

<id> - ID of deleted leader

The options are

- -h <node_addr> - see [conventions](#conventions)
- --debug - see [conventions](#conventions)
19 changes: 15 additions & 4 deletions jcli/src/jcli_app/rest/mod.rs
@@ -1,6 +1,6 @@
mod v0;

use jcli_app::utils::{host_addr, output_format};
use jcli_app::utils::{host_addr, io::ReadYamlError, output_format, CustomErrorFiller};
use structopt::StructOpt;

/// Send request to node REST API
Expand All @@ -11,24 +11,35 @@ pub enum Rest {
V0(v0::V0),
}

const SERIALIZATION_ERROR_MSG: &'static str = "node returned malformed data";
const DESERIALIZATION_ERROR_MSG: &'static str = "node returned malformed data";

custom_error! {pub Error
ReqwestError { source: reqwest::Error } = @{ reqwest_error_msg(source) },
HostAddrError { source: host_addr::Error } = "invalid host address",
SerializationError { source: serde_json::Error } = @{{ let _ = source; SERIALIZATION_ERROR_MSG }},
DeserializationError { source: serde_json::Error } = @{{ let _ = source; DESERIALIZATION_ERROR_MSG }},
OutputFormatFailed { source: output_format::Error } = "formatting output failed",
InputFileInvalid { source: std::io::Error } = "could not read input file",
InputFileYamlMalformed { source: serde_yaml::Error } = "input yaml is not valid",
InputSerializationFailed { source: serde_json::Error, filler: CustomErrorFiller } = "failed to serialize input",
InputHexMalformed { source: hex::Error } = "input hex encoding is not valid",
}

impl From<ReadYamlError> for Error {
fn from(error: ReadYamlError) -> Self {
match error {
ReadYamlError::Io { source } => Error::InputFileInvalid { source },
ReadYamlError::Yaml { source } => Error::InputFileYamlMalformed { source },
}
}
}

fn reqwest_error_msg(err: &reqwest::Error) -> &'static str {
if err.is_timeout() {
"connection with node timed out"
} else if err.is_http() {
"could not connect with node"
} else if err.is_serialization() {
SERIALIZATION_ERROR_MSG
DESERIALIZATION_ERROR_MSG
} else if err.is_redirect() {
"redirecting error while connecting with node"
} else if err.is_client_error() {
Expand Down
87 changes: 87 additions & 0 deletions jcli/src/jcli_app/rest/v0/leaders/mod.rs
@@ -0,0 +1,87 @@
use jcli_app::rest::Error;
use jcli_app::utils::{io, DebugFlag, HostAddr, OutputFormat, RestApiSender};
use std::path::PathBuf;
use structopt::StructOpt;

#[derive(StructOpt)]
#[structopt(rename_all = "kebab-case")]
pub enum Leaders {
/// Get list of leader IDs
Get {
#[structopt(flatten)]
addr: HostAddr,
#[structopt(flatten)]
debug: DebugFlag,
#[structopt(flatten)]
output_format: OutputFormat,
},
/// Register new leader and get its ID
Post {
#[structopt(flatten)]
addr: HostAddr,
#[structopt(flatten)]
debug: DebugFlag,
/// File containing YAML with leader secret.
/// It must have the same format as secret YAML passed to Jormungandr as --secret.
/// If not provided, YAML will be read from stdin.
#[structopt(short, long)]
file: Option<PathBuf>,
},
/// Delete leader
Delete {
#[structopt(flatten)]
addr: HostAddr,
#[structopt(flatten)]
debug: DebugFlag,
/// ID of deleted leader
id: u32,
},
}

impl Leaders {
pub fn exec(self) -> Result<(), Error> {
match self {
Leaders::Get {
addr,
debug,
output_format,
} => get(addr, debug, output_format),
Leaders::Post { addr, debug, file } => post(addr, debug, file),
Leaders::Delete { id, addr, debug } => delete(addr, debug, id),
}
}
}

fn get(addr: HostAddr, debug: DebugFlag, output_format: OutputFormat) -> Result<(), Error> {
let url = addr.with_segments(&["v0", "leaders"])?.into_url();
let builder = reqwest::Client::new().get(url);
let response = RestApiSender::new(builder, &debug).send()?;
response.response().error_for_status_ref()?;
let leaders = response.body().json_value()?;
let formatted = output_format.format_json(leaders)?;
println!("{}", formatted);
Ok(())
}

fn post(addr: HostAddr, debug: DebugFlag, file: Option<PathBuf>) -> Result<(), Error> {
let url = addr.with_segments(&["v0", "leaders"])?.into_url();
let builder = reqwest::Client::new().post(url);
let input: serde_json::Value = io::read_yaml(&file)?;
let response = RestApiSender::new(builder, &debug)
.with_json_body(&input)?
.send()?;
response.response().error_for_status_ref()?;
println!("{}", response.body().text().as_ref());
Ok(())
}

fn delete(addr: HostAddr, debug: DebugFlag, id: u32) -> Result<(), Error> {
let url = addr
.with_segments(&["v0", "leaders", &id.to_string()])?
.into_url();
let builder = reqwest::Client::new().delete(url);
let response = RestApiSender::new(builder, &debug).send()?;
response.response().error_for_status_ref()?;
println!("Success");
Ok(())
}
4 changes: 4 additions & 0 deletions jcli/src/jcli_app/rest/v0/mod.rs
@@ -1,5 +1,6 @@
mod account;
mod block;
mod leaders;
mod message;
mod node;
mod settings;
Expand All @@ -17,6 +18,8 @@ pub enum V0 {
Account(account::Account),
/// Block operations
Block(block::Block),
/// Node leaders operations
Leaders(leaders::Leaders),
/// Message sending
Message(message::Message),
/// Node information
Expand All @@ -36,6 +39,7 @@ impl V0 {
match self {
V0::Account(account) => account.exec(),
V0::Block(block) => block.exec(),
V0::Leaders(leaders) => leaders.exec(),
V0::Message(message) => message.exec(),
V0::Node(node) => node.exec(),
V0::Settings(settings) => settings.exec(),
Expand Down
12 changes: 12 additions & 0 deletions jcli/src/jcli_app/utils/io.rs
@@ -1,3 +1,4 @@
use serde::de::DeserializeOwned;
use std::io::{stdin, stdout, BufRead, BufReader, Error, Write};
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -48,3 +49,14 @@ pub fn read_line<P: AsRef<Path>>(path: &Option<P>) -> Result<String, Error> {
open_file_read(path)?.read_line(&mut line)?;
Ok(line.trim_end().to_string())
}

custom_error! { pub ReadYamlError
Io { source: Error } = "could not read input",
Yaml { source: serde_yaml::Error } = "input contains malformed yaml",
}

pub fn read_yaml<D: DeserializeOwned>(path: &Option<impl AsRef<Path>>) -> Result<D, ReadYamlError> {
let reader = open_file_read(path)?;
let yaml = serde_yaml::from_reader(reader)?;
Ok(yaml)
}
1 change: 1 addition & 0 deletions jcli/src/jcli_app/utils/mod.rs
Expand Up @@ -10,6 +10,7 @@ pub mod output_format;

pub use self::account_id::AccountId;
pub use self::debug_flag::DebugFlag;
pub use self::error::CustomErrorFiller;
pub use self::host_addr::HostAddr;
pub use self::output_format::OutputFormat;
pub use self::rest_api::{RestApiResponse, RestApiResponseBody, RestApiSender};
Expand Down
16 changes: 16 additions & 0 deletions jcli/src/jcli_app/utils/rest_api.rs
@@ -1,6 +1,7 @@
use hex;
use jcli_app::utils::DebugFlag;
use reqwest::{Error, RequestBuilder, Response};
use serde::Serialize;
use std::{fmt, io::Write};

pub struct RestApiSender<'a> {
Expand Down Expand Up @@ -42,6 +43,21 @@ impl<'a> RestApiSender<'a> {
self
}

pub fn with_json_body(mut self, body: &impl Serialize) -> Result<Self, serde_json::Error> {
let json = serde_json::to_string(body)?;
if self.debug_flag.debug_writer().is_some() {
self.request_body_debug = Some(json.clone());
}
self.builder = self
.builder
.header(
reqwest::header::CONTENT_TYPE,
mime::APPLICATION_JSON.as_ref(),
)
.body(json.into_bytes());
Ok(self)
}

pub fn send(self) -> Result<RestApiResponse, Error> {
let request = self.builder.build()?;
if let Some(mut writer) = self.debug_flag.debug_writer() {
Expand Down
9 changes: 5 additions & 4 deletions jormungandr/src/leadership/task.rs
Expand Up @@ -139,11 +139,12 @@ fn handle_leadership(
blockchain_tip.hash().unwrap(),
);

let block = enclave.create_block(block, scheduled_event.leader_output);
if let Some(block) = enclave.create_block(block, scheduled_event.leader_output) {
block_message
.try_send(BlockMsg::LeadershipBlock(block))
.unwrap()
}

block_message
.try_send(BlockMsg::LeadershipBlock(block))
.unwrap();
stats_counter.set_slot_start_time(scheduled_event.expected_time.into());
future::ok(())
})
Expand Down
1 change: 1 addition & 0 deletions jormungandr/src/main.rs
Expand Up @@ -229,6 +229,7 @@ fn start_services(bootstrapped_node: BootstrappedNode) -> Result<(), start_up::E
transaction_task: Arc::new(Mutex::new(fragment_msgbox)),
logs: Arc::new(Mutex::new(pool_logs)),
server: Arc::default(),
enclave,
};
Some(rest::start_rest_server(&rest, context)?)
}
Expand Down
2 changes: 2 additions & 0 deletions jormungandr/src/rest/mod.rs
Expand Up @@ -10,6 +10,7 @@ use std::sync::{Arc, Mutex, RwLock};

use crate::blockchain::BlockchainR;
use crate::fragment::Logs;
use crate::secure::enclave::Enclave;
use crate::settings::start::{Error as ConfigError, Rest};
use crate::stats_counter::StatsCounter;

Expand All @@ -23,6 +24,7 @@ pub struct Context {
pub transaction_task: Arc<Mutex<MessageBox<TransactionMsg>>>,
pub logs: Arc<Mutex<Logs>>,
pub server: Arc<RwLock<Option<Server>>>,
pub enclave: Enclave,
}

pub fn start_rest_server(config: &Rest, context: Context) -> Result<Server, ConfigError> {
Expand Down
32 changes: 29 additions & 3 deletions jormungandr/src/rest/v0/handlers.rs
Expand Up @@ -9,16 +9,17 @@ use chain_crypto::{Blake2b256, PublicKey};
use chain_impl_mockchain::account::{AccountAlg, Identifier};
use chain_impl_mockchain::fragment::Fragment;
use chain_impl_mockchain::key::Hash;
use chain_impl_mockchain::leadership::LeadershipConsensus;
use chain_impl_mockchain::ledger::Ledger;
use chain_impl_mockchain::leadership::{Leader, LeadershipConsensus};
use chain_impl_mockchain::value::{Value, ValueError};
use chain_storage::store;

use crate::intercom::TransactionMsg;
use crate::secure::enclave::LeaderId;
use crate::secure::NodeSecret;
use bytes::{Bytes, IntoBuf};
use futures::Future;
use std::str::FromStr;

use crate::intercom::TransactionMsg;
pub use crate::rest::Context;

pub fn get_utxos(context: State<Context>) -> impl Responder {
Expand Down Expand Up @@ -255,3 +256,28 @@ pub fn get_shutdown(context: State<Context>) -> Result<impl Responder, Error> {
.stop();
Ok(HttpResponse::Ok().finish())
}

pub fn get_leaders(context: State<Context>) -> impl Responder {
Json(json! {
context.enclave.get_leaderids()
})
}

pub fn post_leaders(secret: Json<NodeSecret>, context: State<Context>) -> impl Responder {
let leader = Leader {
bft_leader: secret.bft(),
genesis_leader: secret.genesis(),
};
let leader_id = context.enclave.add_leader(leader);
Json(leader_id)
}

pub fn delete_leaders(
context: State<Context>,
leader_id: Path<LeaderId>,
) -> Result<impl Responder, Error> {
match context.enclave.remove_leader(*leader_id) {
true => Ok(HttpResponse::Ok().finish()),
false => Err(ErrorNotFound("Leader with given ID not found")),
}
}
7 changes: 7 additions & 0 deletions jormungandr/src/rest/v0/mod.rs
Expand Up @@ -17,6 +17,13 @@ pub fn app(context: handlers::Context) -> App<handlers::Context> {
.resource("/fragment/logs", |r| {
r.get().with(handlers::get_message_logs)
})
.resource("/leaders", |r| {
r.get().with(handlers::get_leaders);
r.post().with(handlers::post_leaders);
})
.resource("/leaders/{leader_id}", |r| {
r.delete().with(handlers::delete_leaders)
})
.resource("/settings", |r| r.get().with(handlers::get_settings))
.resource("/stake", |r| r.get().with(handlers::get_stake_distribution))
.resource("/shutdown", |r| r.get().with(handlers::get_shutdown))
Expand Down

0 comments on commit aafae45

Please sign in to comment.