Sashimono CLI implementation (#38)

This commit is contained in:
Chalith Desaman
2021-07-22 09:51:28 +05:30
committed by GitHub
parent 66b0c86beb
commit adbb1c8839
9 changed files with 480 additions and 168 deletions

View File

@@ -18,6 +18,18 @@ add_executable(bootstrap_contract
bootstrap-contract/bootstrap_contract.cpp
)
#-------Sashi CLI-------
add_executable(sashi
sashi-cli/cli-manager.cpp
sashi-cli/main.cpp
)
target_link_libraries(sashi
libboost_stacktrace_backtrace.a
${CMAKE_DL_LIBS} # Needed for stacktrace support
)
#-------Sashimono Agent-------
add_subdirectory(src/killswitch)
@@ -47,6 +59,7 @@ target_link_libraries(sagent
add_dependencies(sagent
bootstrap_contract
sashi
)
add_custom_command(TARGET sagent POST_BUILD
@@ -63,7 +76,7 @@ target_precompile_headers(sagent PUBLIC src/pchheader.hpp)
# Add target to generate the installer setup.
add_custom_target(installer
COMMAND mkdir -p ./build/sashimono-installer
COMMAND bash -c "cp -r ./build/{sagent,hpfs,user-install.sh,user-uninstall.sh,contract_template} ./build/sashimono-installer/"
COMMAND bash -c "cp -r ./build/{sagent,sashi,hpfs,user-install.sh,user-uninstall.sh,contract_template} ./build/sashimono-installer/"
COMMAND bash -c "cp -r ./installer/{docker-install.sh,registry-install.sh,registry-uninstall.sh,sashimono-install.sh,sashimono-uninstall.sh} ./build/sashimono-installer/"
COMMAND bash -c "cp -r ./dependencies/{user-cgcreate.sh,libblake3.so} ./build/sashimono-installer/"
COMMAND tar cfz ./build/sashimono-installer.tar.gz --directory=./build/ sashimono-installer

View File

@@ -1,4 +1,3 @@
const WebSocket = require('ws');
const https = require('https');
const http = require('http');
const fs = require('fs');
@@ -7,39 +6,30 @@ const { v4: uuidv4 } = require('uuid');
const { execSync } = require("child_process");
const express = require('express');
// Generate tls keys if not found.
if (!fs.existsSync('./tlskey.pem')) {
console.log("TLS key files not detected. Generating..");
execSync("openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout tlskey.pem -out tlscert.pem -subj \"/C=SA/ST=SA/L=SA/O=SA/CN=SA\"");
console.log("New tls key files generated.")
const cliDevPath = "../../build/sashi";
const cliProdPath = "/usr/bin/sashi";
let cliPath;
let args = process.argv;
if (args.length == 3 && args[2] == 'prod')
cliPath = cliProdPath;
else if (args.length == 2 || (args.length == 3 && args[2] == 'dev'))
cliPath = cliDevPath;
else {
console.log("Arguments mismatch.\n Usage: node message-board (optional)<dev|prod>");
process.exit(0);
}
let restServer, websocketServer;
const reqMap = {}; // Store response object vs message id to deliver response back when the SA responses.
if (!fs.existsSync(cliPath)) {
console.error(`Sashi CLI does not exist in ${cliPath}.`)
process.exit(0);
}
let restServer;
/**
* Interactive interface to get message from the command line and sent it to all the connected agents.
*/
const interatctiveInterface = async () => {
const server = https.createServer({
cert: fs.readFileSync('./tlscert.pem'),
key: fs.readFileSync('./tlskey.pem')
});
websocketServer = new WebSocket.Server({ server });
websocketServer.on('connection', (ws) => {
ws.on('message', (msg) => {
try {
const message = JSON.parse(Buffer.from(msg).toString());
console.log('Received: ', message);
reqMap[message.reply_for] && reqMap[message.reply_for].status(message.type == "error" ? 500 : 200).send(message);
} catch (error) {
console.error("Error occured in json parsing." + error);
}
});
});
// start listening for stdin
const rl = readLine.createInterface({
input: process.stdin,
@@ -49,9 +39,7 @@ const interatctiveInterface = async () => {
// On ctrl + c we should close SA connection gracefully.
rl.on('SIGINT', () => {
console.log('SIGINT received...');
websocketServer.close();
rl.close();
server.close();
restServer && restServer.close();
});
@@ -63,135 +51,149 @@ const interatctiveInterface = async () => {
})
}
server.listen(5000, () => {
console.log(`wss://localhost:${server.address().port}`)
console.log("Ready to accept inputs.");
console.log("Ready to accept inputs.");
const inputPump = () => {
rl.question('', async (inp) => {
if (inp.length > 0) {
if (websocketServer.clients.size == 0) {
console.log('No Sashimano agents connected yet.')
}
else {
switch (inp) {
case 'create':
contractId = await askForInput('Contract ID (default:uuidv4)', uuidv4());
image = await askForInput('Image: 1=ubuntu(default) | 2=nodejs', "1");
if (image != "1" && image != "2") {
console.error('Invalid image. (Should be "1" or "2").')
break;
}
sendToAllAgents(JSON.stringify({
id: uuidv4(),
type: 'create',
owner_pubkey: 'ed5cb83404120ac759609819591ef839b7d222c84f1f08b3012f490586159d2b50',
contract_id: contractId,
image: (image == "1" ? "ubt.20.04" : "ubt.20.04-njs.14")
}));
break;
case 'initiate':
containerName = await askForInput('Container Name');
role = await askForInput('Role: validator(default) | observer', "validator");
if (role != 'validator' && role != 'observer') {
console.error('Invalid role. (Should be "validator" or "observer").')
break;
}
history = await askForInput('History <{full|custom},max_primary_shards,max_raw_shards> (custom,1,1)', "custom,1,1");
split = [];
if (history) {
split = history.split(',');
if (split.length == 0 || split.length == 0 > 3) {
console.error('Invalid history.')
break;
}
else if (split[0] != 'full' && split[0] != 'custom') {
console.error('Invalid history. (Should be "full" or "custom").')
break;
}
}
peers = await askForInput('Comma seperated Peer List <host1:port1>,<host2:port2>,...');
unl = await askForInput('Comma seperated UNL <pubkey1>,<pubkey2>,...');
sendToAllAgents(JSON.stringify({
id: uuidv4(),
type: 'initiate',
container_name: containerName,
peers: peers ? peers.split(',') : [],
unl: unl ? unl.split(',') : [],
role: role,
history: split.length > 0 ? split[0] : '',
max_primary_shards: split.length > 1 ? parseInt(split[1]) : '',
max_raw_shards: split.length > 2 ? parseInt(split[2]) : ''
}));
break;
case 'destroy':
containerName = await askForInput('Container Name');
sendToAllAgents(JSON.stringify({
id: uuidv4(),
type: 'destroy',
container_name: containerName
}))
break;
case 'start':
containerName = await askForInput('Container Name');
sendToAllAgents(JSON.stringify({
id: uuidv4(),
type: 'start',
container_name: containerName
}))
break;
case 'stop':
containerName = await askForInput('Container Name');
sendToAllAgents(JSON.stringify({
id: uuidv4(),
type: 'stop',
container_name: containerName
}))
break;
default:
console.error('Invalid command. Only valid [create, initiate, destroy, start and stop]');
break;
const inputPump = () => {
rl.question('', async (inp) => {
if (inp.length > 0) {
switch (inp) {
case 'status':
checkAgentStatus();
break;
case 'create':
contractId = await askForInput('Contract ID (default:uuidv4)', uuidv4());
image = await askForInput('Image: 1=ubuntu(default) | 2=nodejs', "1");
if (image != "1" && image != "2") {
console.error('Invalid image. (Should be "1" or "2").')
break;
}
}
sendToAgent(JSON.stringify({
id: uuidv4(),
type: 'create',
owner_pubkey: 'ed5cb83404120ac759609819591ef839b7d222c84f1f08b3012f490586159d2b50',
contract_id: contractId,
image: (image == "1" ? "ubt.20.04" : "ubt.20.04-njs.14")
}));
break;
case 'initiate':
containerName = await askForInput('Container Name');
role = await askForInput('Role: validator(default) | observer', "validator");
if (role != 'validator' && role != 'observer') {
console.error('Invalid role. (Should be "validator" or "observer").')
break;
}
history = await askForInput('History <{full|custom},max_primary_shards,max_raw_shards> (custom,1,1)', "custom,1,1");
split = [];
if (history) {
split = history.split(',');
if (split.length == 0 || split.length == 0 > 3) {
console.error('Invalid history.')
break;
}
else if (split[0] != 'full' && split[0] != 'custom') {
console.error('Invalid history. (Should be "full" or "custom").')
break;
}
}
peers = await askForInput('Comma seperated Peer List <host1:port1>,<host2:port2>,...');
unl = await askForInput('Comma seperated UNL <pubkey1>,<pubkey2>,...');
sendToAgent(JSON.stringify({
id: uuidv4(),
type: 'initiate',
container_name: containerName,
peers: peers ? peers.split(',') : [],
unl: unl ? unl.split(',') : [],
role: role,
history: split.length > 0 ? split[0] : '',
max_primary_shards: split.length > 1 ? parseInt(split[1]) : '',
max_raw_shards: split.length > 2 ? parseInt(split[2]) : ''
}));
break;
case 'destroy':
containerName = await askForInput('Container Name');
sendToAgent(JSON.stringify({
id: uuidv4(),
type: 'destroy',
container_name: containerName
}))
break;
case 'start':
containerName = await askForInput('Container Name');
sendToAgent(JSON.stringify({
id: uuidv4(),
type: 'start',
container_name: containerName
}))
break;
case 'stop':
containerName = await askForInput('Container Name');
sendToAgent(JSON.stringify({
id: uuidv4(),
type: 'stop',
container_name: containerName
}))
break;
default:
console.error('Invalid command. Only valid [create, initiate, destroy, start and stop]');
break;
}
inputPump();
})
}
inputPump();
});
}
inputPump();
})
}
inputPump();
}
const sendToAllAgents = (msg) => {
websocketServer && websocketServer.clients.forEach(ws => {
ws.send(msg);
});
const sendToAgent = (msg, res = null) => {
try {
let output = execSync(`${cliPath} json '${msg}'`, { stdio: 'pipe' });
let message = Buffer.from(output).toString();
message = JSON.parse(message.substring(0, message.length - 2)); // Skipping the \n from the result.
console.log('Received: ', message);
res && res.status((message.content && typeof message.content == 'string' && message.content.endsWith("error")) ? 500 : 200).send(message);
}
catch (e) {
console.error(`Message sending error. ${e}`);
res && res.status(500).send(`Message sending error. ${e}`);
}
}
const checkAgentStatus = (res = null) => {
try {
let output = execSync(`${cliPath} status`, { stdio: 'pipe' });
let message = Buffer.from(output).toString();
message = message.substring(0, message.length - 1); // Skipping the \n from the result.
console.log(`Socket ${message} is online.`);
res && res.status(200).send(message);
return true;
}
catch (e) {
console.error(`Socket is offline. ${e}`);
res && res.status(500).send(`Socket is offline. ${e}`);
return false;
}
}
const webServerProtocol = (process.env.SSL === "true") ? "https" : "http";
const webServerPort = 5001;
const restApi = async () => {
// Generate tls keys if not found.
if (webServerProtocol == "https" && !fs.existsSync('./tlskey.pem')) {
console.log("TLS key files not detected. Generating..");
execSync("openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout tlskey.pem -out tlscert.pem -subj \"/C=SA/ST=SA/L=SA/O=SA/CN=SA\"");
console.log("New tls key files generated.")
}
const app = express();
app.use(express.json());
// Handle errors before forward to processing.
app.use((req, res, next) => {
if (websocketServer.clients.size == 0) {
console.log('No Sashimano agents connected yet.')
res.status(404).send('No Sashimano agents connected yet.');
}
else {
next();
}
});
app.post("/status", (req, res) => {
res.send("Message board running...");
checkAgentStatus(res);
});
app.post("/create", (req, res) => {
const id = uuidv4();
@@ -202,58 +204,49 @@ const restApi = async () => {
contract_id: (req.body.contract_id === "") ? uuidv4() : req.body.contract_id,
image: req.body.image ? req.body.image : "ubt.20.04"
};
reqMap[id] = res;
sendToAllAgents(JSON.stringify(msg));
sendToAgent(JSON.stringify(msg), res);
});
app.post("/initiate", (req, res) => {
const id = uuidv4();
const msg = {
id,
type: 'initiate',
owner_pubkey: req.body.owner_pubkey,
container_name: req.body.container_name,
peers: req.body.peers ? req.body.peers: [],
unl: req.body.unl ? req.body.unl: [],
role: req.body.role? req.body.role: 'validator',
history: req.body.history? req.body.history: 'custom',
max_primary_shards: req.body.max_primary_shards? req.body.max_primary_shards: 1,
max_raw_shards: req.body.max_raw_shards? req.body.max_raw_shards: 1
peers: req.body.peers ? req.body.peers : [],
unl: req.body.unl ? req.body.unl : [],
role: req.body.role ? req.body.role : 'validator',
history: req.body.history ? req.body.history : 'custom',
max_primary_shards: req.body.max_primary_shards ? req.body.max_primary_shards : 1,
max_raw_shards: req.body.max_raw_shards ? req.body.max_raw_shards : 1
};
reqMap[id] = res;
sendToAllAgents(JSON.stringify(msg));
sendToAgent(JSON.stringify(msg), res);
});
app.post("/start", (req, res) => {
const id = uuidv4();
const msg = {
id,
type: 'start',
owner_pubkey: req.body.owner_pubkey,
container_name: req.body.container_name
};
reqMap[id] = res;
sendToAllAgents(JSON.stringify(msg));
sendToAgent(JSON.stringify(msg), res);
});
app.post("/stop", (req, res) => {
const id = uuidv4();
const msg = {
id,
type: 'stop',
owner_pubkey: req.body.owner_pubkey,
container_name: req.body.container_name
};
reqMap[id] = res;
sendToAllAgents(JSON.stringify(msg));
sendToAgent(JSON.stringify(msg), res);
});
app.post("/destroy", (req, res) => {
const id = uuidv4();
const msg = {
id,
type: 'destroy',
owner_pubkey: req.body.owner_pubkey,
container_name: req.body.container_name
};
reqMap[id] = res;
sendToAllAgents(JSON.stringify(msg));
sendToAgent(JSON.stringify(msg), res);
});
restServer = (webServerProtocol == "https") ?
https.createServer({
@@ -266,6 +259,8 @@ const restApi = async () => {
}
(async () => {
await restApi();
await interatctiveInterface();
if (checkAgentStatus()) {
await restApi();
await interatctiveInterface();
}
})();

View File

@@ -2,6 +2,7 @@
# Sashimono agent installation script.
# This must be executed with root privileges.
user_bin=/usr/bin
sashimono_bin=/usr/bin/sashimono-agent
docker_bin=/usr/bin/sashimono-agent/dockerbin
sashimono_data=/etc/sashimono
@@ -62,6 +63,9 @@ function rollback() {
cp "$script_dir"/{sagent,hpfs,user-cgcreate.sh,user-install.sh,user-uninstall.sh} $sashimono_bin
chmod -R +x $sashimono_bin
# Install Sashimono CLI binaries into user bin dir.
cp "$script_dir"/sashi $user_bin
# Download and install rootless dockerd.
"$script_dir"/docker-install.sh $docker_bin

View File

@@ -2,6 +2,7 @@
# Sashimono agent uninstall script.
# -q for non-interactive (quiet) mode
user_bin=/usr/bin
sashimono_bin=/usr/bin/sashimono-agent
docker_bin=/usr/bin/sashimono-agent/dockerbin
sashimono_data=/etc/sashimono
@@ -74,6 +75,9 @@ rm /etc/systemd/system/$sashimono_service.service
echo "Deleting binaries..."
rm -r $sashimono_bin
echo "Deleting Sashimono CLI..."
rm $user_bin/sashi
echo "Deleting data folder..."
rm -r $sashimono_data

143
sashi-cli/cli-manager.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "pchheader.hpp"
#include "cli-manager.hpp"
namespace cli
{
constexpr const char *SOCKET_NAME = "sa.sock"; // Name of the sashimono socket.
constexpr const char *DATA_DIR = "/etc/sashimono"; // Sashimono data directory.
constexpr const int BUFFER_SIZE = 1024; // Max read buffer size.
cli_context ctx;
bool init_success = false;
/**
* Initialize the socket and connect.
* @return 0 on success, -1 on error.
*/
int init(std::string_view sashi_dir)
{
ctx.sashi_dir = sashi_dir;
// Get the socket path from available location.
if (get_socket_path(ctx.socket_path) == -1)
return -1;
// Create the seq paket socket.
ctx.socket_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
if (ctx.socket_fd == -1)
{
std::cerr << errno << " :Error while creating the sashimono socket.\n";
return -1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, ctx.socket_path.data(), sizeof(addr.sun_path) - 1);
if (connect(ctx.socket_fd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_un)) == -1)
{
// If permission denied, show a custom error.
if (errno == EACCES)
std::cerr << "Permission denied: Only root or users in 'sashiadmin' group can access the sashimono socket.\n";
else
std::cerr << errno << " :Error while connecting to the sashimono socket.\n";
close(ctx.socket_fd);
return -1;
}
init_success = true;
return 0;
}
/**
* Locate and return the sashimono agent socket path according predefined rules.
* If sa.sock found on the same path as the cli binary, use that. (to support dev testing)
* Else sa.sock found on /etc/sashimono, use that.
* Else show error.
* @param socket_path Socket path to be populated.
* @return 0 on success, -1 on error.
*/
int get_socket_path(std::string &socket_path)
{
// Check whether socket exists in exec path.
std::string path = ctx.sashi_dir + std::string("/") + SOCKET_NAME;
struct stat st;
if (stat(path.data(), &st) == 0 && S_ISSOCK(st.st_mode))
{
socket_path = path;
return 0;
}
// Otherwise check in the data dir.
path = DATA_DIR + std::string("/") + SOCKET_NAME;
memset(&st, 0, sizeof(struct stat));
if (stat(path.data(), &st) == 0 && S_ISSOCK(st.st_mode))
{
socket_path = path;
return 0;
}
std::cerr << SOCKET_NAME << " is not found.\n";
return -1;
}
/**
* Write a given message into the sashimono socket.
* @param message Message to be write.
* @return 0 on success, -1 on error.
*/
int write_to_socket(std::string_view message)
{
if (!init_success)
{
std::cerr << "Sashimono socket is not initialized.\n";
return -1;
}
if (write(ctx.socket_fd, message.data(), message.size()) == -1)
{
std::cerr << errno << " :Error while wrting to the sashimono socket.\n";
return -1;
}
return 0;
}
/**
* Read message from the sashimono socket.
* @param message Message to be read.
* @return Read message length on success, -1 on error.
*/
int read_from_socket(std::string &message)
{
if (!init_success)
{
std::cerr << "Sashimono socket is not initialized.\n";
return -1;
}
// Resize the message to max length and resize to original read length after reading.
message.resize(BUFFER_SIZE);
const int res = read(ctx.socket_fd, message.data(), message.length());
if (res == -1)
{
std::cerr << errno << " :Error while reading from the sashimono socket.\n";
return -1;
}
message.resize(res);
return res;
}
/**
* Close the socket and deinitialize.
*/
void deinit()
{
if (init_success)
close(ctx.socket_fd);
}
}

26
sashi-cli/cli-manager.hpp Normal file
View File

@@ -0,0 +1,26 @@
#ifndef _CLI_MANAGER_
#define _CLI_MANAGER_
namespace cli
{
struct cli_context
{
std::string sashi_dir; // Path of the Sashi CLI executable.
std::string socket_path; // Path of the sashimono socket.
int socket_fd = -1; // File descriptor of the socket.
};
extern cli_context ctx;
int init(std::string_view sashi_dir);
int get_socket_path(std::string &socket_path);
int write_to_socket(std::string_view message);
int read_from_socket(std::string &message);
void deinit();
}
#endif

113
sashi-cli/main.cpp Normal file
View File

@@ -0,0 +1,113 @@
/**
Entry point for Sashi-cli
**/
#include "pchheader.hpp"
#include "cli-manager.hpp"
#define PARSE_ERROR \
{ \
std::cerr << "Arguments mismatch.\n"; \
std::cerr << "Usage:\n"; \
std::cerr << "sashi status\n"; \
std::cerr << "sashi json <json message>\n"; \
std::cerr << "Example: sashi json '{\"container_name\":\"<container name>\", ...}'\n"; \
return -1; \
}
/**
* Performs any cleanup on graceful application termination.
*/
void deinit()
{
cli::deinit();
}
void segfault_handler(int signum)
{
std::cout << boost::stacktrace::stacktrace() << std::endl;
exit(SIGABRT);
}
/**
* Global exception handler for std exceptions.
*/
void std_terminate() noexcept
{
const std::exception_ptr exptr = std::current_exception();
if (exptr != 0)
{
try
{
std::rethrow_exception(exptr);
}
catch (std::exception &ex)
{
std::cerr << "std error: " << ex.what() << std::endl;
}
catch (...)
{
std::cerr << "std error: Terminated due to unknown exception\n";
}
}
else
{
std::cerr << "std error: Terminated due to unknown reason\n";
}
std::cerr << boost::stacktrace::stacktrace() << std::endl;
exit(1);
}
int main(int argc, char **argv)
{
// Register exception and segfault handlers.
std::set_terminate(&std_terminate);
signal(SIGSEGV, &segfault_handler);
signal(SIGABRT, &segfault_handler);
// Disable SIGPIPE to avoid crashing on broken pipe IO.
{
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGPIPE);
// sigprocmask is used instead of pthread_sigmask since this is single threaded.
sigprocmask(SIG_BLOCK, &mask, NULL);
}
if (argc > 1)
{
// Take the realpath of sash exec path.
std::array<char, PATH_MAX> buffer;
::realpath(argv[0], buffer.data());
buffer[PATH_MAX] = '\0';
const std::string exec_dir = dirname(buffer.data());
const std::string command = argv[1];
if (command == "status")
{
if (cli::init(exec_dir) == -1)
return -1;
std::cout << cli::ctx.socket_path << std::endl;
cli::deinit();
return 0;
}
else if (command == "json" && argc == 3)
{
std::string output;
if (cli::init(exec_dir) == -1 || cli::write_to_socket(argv[2]) == -1 || cli::read_from_socket(output) == -1)
{
cli::deinit();
return -1;
}
std::cout << output << std::endl;
cli::deinit();
return 0;
}
}
PARSE_ERROR
}

13
sashi-cli/pchheader.hpp Normal file
View File

@@ -0,0 +1,13 @@
#ifndef _CLI_PCHHEADER_
#define _CLI_PCHHEADER_
#include <boost/stacktrace.hpp>
#include <csignal>
#include <iostream>
#include <libgen.h>
#include <string>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif

View File

@@ -260,14 +260,15 @@ namespace comm
**/
int read_socket(std::string &message)
{
char buffer[BUFFER_SIZE];
const int ret = read(ctx.data_socket, buffer, BUFFER_SIZE);
// Resize the message to max length and resize to original read length after reading.
message.resize(BUFFER_SIZE);
const int ret = read(ctx.data_socket, message.data(), message.length());
if (ret == -1)
{
LOG_ERROR << errno << ": Error receiving data.";
return -1;
}
message = std::string(buffer);
message.resize(ret);
return ret;
}
} // namespace comm