Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Node CLI wrapper and configuration tool #959

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"./crates/context/config",
"./crates/meroctl",
"./crates/merod",
"./crates/merow",
"./crates/network",
"./crates/node",
"./crates/node-primitives",
Expand Down
19 changes: 19 additions & 0 deletions crates/merow/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "merow"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
clap = { workspace = true, features = ["env", "derive"] }
const_format.workspace = true
eyre.workspace = true

tokio = { workspace = true, features = ["full"] }
toml = "0.5.2"
serde = { workspace = true, features = ["derive"] }

[lints]
workspace = true
90 changes: 90 additions & 0 deletions crates/merow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Core Cli Wrapper (crate::merow)

A CLI wrapper for the Calimero Node that provides a default node configuration file for initializing a node, and quickly running a development environment for testing P2P Calimero Apps.

## Features

- Custom Node Configuration File
- Simple Commands to Initialize and Run a Calimero Node
- Creates a Node Home Directory (if it doesn't already exist)

## Prerequisites
- Rust: [Official Rust Installation](https://www.rust-lang.org/tools/install)

## Setting up
Clone the project

```bash
git clone https://github.com/kevinjaypatel/core.git
```

Change to repo

```bash
cd core
```

Check out cli-wrapper
```bash
git branch cli-wrapper
```

## Usage

Setup the Default Configuration: `./crates/merow/config/default.toml`

```javascript
[coordinator]
name = "coordinator"
server_port = 2427
swarm_port = 2527
home = "data"

[admin]
name = "node1"
server_port = 2428
swarm_port = 2528
home = "data"
```

Initialize a coordinator
`$ merow -- init-coordinator`

Initialize a node
`$ merow -- init-node`

Start a running coordinator
`$ merow -- start-coordinator`

Start a running node
`$ merow -- start-node`


## How to Run (from project root)

### Build the Rust Package
```bash
cargo build
```

### Starting up a Coordinator (same steps apply for Node Configuration)
E.g. Initializes Coordinator (with defaults)
```bash
cargo run -p merow -- init-coordinator
```

Start a running coordinator
```bash
cargo run -p merow -- start-coordinator
```

### Accessing the coordinator via Admin Dashboard
```bash
http://localhost:<coordinator.server_port>/admin-dashboard/
```

## Roadmap

- Additional commands for `Dev Context` creation and `Peer Invitation`
- Add a boolean flag to the Configuration File for Deploying the Admin Dashboard
- Multi-node deployment (e.g. node1, node2, ... nodeN)
11 changes: 11 additions & 0 deletions crates/merow/config/default.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[coordinator]
name = "coordinator"
server_port = 2427
swarm_port = 2527
home = "data"

[admin]
name = "node1"
server_port = 2428
swarm_port = 2528
home = "data"
209 changes: 209 additions & 0 deletions crates/merow/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
use clap::Parser;
use const_format::concatcp;
use eyre::Result as EyreResult;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::process::exit;
use std::process::{Command, Output, Stdio};
use toml;

pub const EXAMPLES: &str = r"

# Initialize a coordinator
$ merow -- init-coordinator

# Initialize a node
$ merow -- init-node

# Start a running coordinator
$ merow -- start-coordinator

# Start a running node
$ merow -- start-node
";

// Points to the Node Cofiguration Filepath relative to the working directory
const CONFIG_FILE_PATH: &str = "crates/merow/config/default.toml";

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
#[command(after_help = concatcp!(
"Examples:",
EXAMPLES
))]
pub struct RootCommand {
/// Name of the command
pub action: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct NodeData {
coordinator: NodeConfig,
admin: NodeConfig,
}

#[derive(Serialize, Deserialize, Debug)]
struct NodeConfig {
name: String,
server_port: u16,
swarm_port: u16,
home: String,
}

fn build_command(
name: &str,
home: &str,
server: Option<&str>,
swarm: Option<&str>,
run_node: bool,
) -> Command {
let mut command: Command = Command::new("cargo");

// Sets the default CLI arguments
command.args([
"run",
"-p",
"merod",
"--",
"--node-name",
name,
"--home",
home,
]);

// Sets the custom CLI arguments
if !run_node {
command.args([
"init",
"--server-port",
server.unwrap(),
"--swarm-port",
swarm.unwrap(),
]);
} else {
command.arg("run");
}

command.stdout(Stdio::piped()); // Capture stdout
command.stderr(Stdio::piped()); // Capture stderr

return command;
}

fn display_command_output(output: Output) {
println!("Status: {}", output.status);
println!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
}

fn make_direcory(node_home: &str) {
match fs::create_dir(node_home) {
Ok(()) => println!("Created Home Directory: ./{}\n", node_home),
Err(error) => panic!("Problem creating the Node Home directory: {error:?}"),
};
}

fn init_node(config: &NodeConfig) -> EyreResult<()> {
// Sets the default configuration for the node
let node_name: &str = config.name.as_str();
let node_home: &str = config.home.as_str();

let server_port: &str = &config.server_port.to_string();
let swarm_port: &str = &config.swarm_port.to_string();

// create the home directory if it doesnt exist
if !Path::new(node_home).is_dir() {
// Make the Node home directory
make_direcory(node_home);
}

let mut command: Command = build_command(
node_name,
node_home,
Some(server_port),
Some(swarm_port),
false,
);

let child: Output = command.output()?; // Execute the command and get the output

display_command_output(child);

Ok(()) // Return the output (stdout, stderr, and exit status)
}

async fn start_node(node_name: &str, node_home: &str) -> EyreResult<()> {
let mut command: Command = build_command(node_name, node_home, None, None, true);
let child: Output = command.output()?;

display_command_output(child);
Ok(())
}

impl RootCommand {
pub async fn run(self) -> EyreResult<()> {
// Fetch the nodes configuration
let data = NodeData::get_node_data();

let coordinator = data.coordinator;
let admin = data.admin;

match self.action.as_str() {
"init-coordinator" => {
println!("Initializing coordinator...\n");
init_node(&coordinator)
}
"init-node" => {
println!("Initializing node...\n");
init_node(&admin)
}
"start-coordinator" => {
println!("Running coordinator...\n");

let name: &str = coordinator.name.as_str();
let home: &str = coordinator.home.as_str();

start_node(name, home).await
}
"start-node" => {
println!("Running node...\n");

let name: &str = admin.name.as_str();
let home: &str = admin.home.as_str();

start_node(name, home).await
}
_ => {
println!("Unknown command...");
Ok(())
}
}
}
}

impl NodeData {
fn get_node_data() -> NodeData {
// Sets the contents of the configuration file to a String
let contents = match fs::read_to_string(CONFIG_FILE_PATH) {
Ok(c) => c,
Err(_) => {
eprintln!("Could not read file `{}`", CONFIG_FILE_PATH);
exit(1);
}
};

// Deserializes the String into a type (NodeData)
let node_data: NodeData = match toml::from_str(&contents) {
Ok(nd) => nd,
Err(_) => {
// Write `msg` to `stderr`.
eprintln!("Unable to load data from `{}`", CONFIG_FILE_PATH);
// Exit the program with exit code `1`.
exit(1);
}
};

return node_data;
}
}
Loading