Automation scripts for Evernode cluster deployment. (#67)

* Message board install automation with faucet host account generation.
* Deregister Evernode host on uninstall.
* Used Evernode client lib in message board.
* Removed file logging by default in Sashimono services.
* Evernode cluster spin-up script.
* Cleaned up hp.cfg template.
This commit is contained in:
Ravin Perera
2021-10-10 22:21:16 +05:30
committed by GitHub
parent 307466fee9
commit 4102c119a4
19 changed files with 3123 additions and 2225 deletions

View File

@@ -69,7 +69,7 @@ add_custom_command(TARGET sagent POST_BUILD
COMMAND cp ./bootstrap-contract/bootstrap_upgrade.sh ./build/contract_template/contract_fs/seed/state/
COMMAND mv ./build/bootstrap_contract ./build/contract_template/contract_fs/seed/state/
COMMAND ./installer/docker-install.sh ./build/dockerbin
COMMAND npm run --prefix ./mb-xrpl build
COMMAND npm --prefix ./mb-xrpl install && npm run --prefix ./mb-xrpl build
)
target_precompile_headers(sagent PUBLIC src/pchheader.hpp)
@@ -80,7 +80,7 @@ add_custom_target(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 bash -c "cp -r ./mb-xrpl/dist/mb-xrpl ./build/sashimono-installer/"
COMMAND bash -c "cp -r ./mb-xrpl/dist ./build/sashimono-installer/mb-xrpl"
COMMAND tar cfz ./build/sashimono-installer.tar.gz --directory=./build/ sashimono-installer
COMMAND rm -r ./build/sashimono-installer
)

18
dependencies/hp.cfg vendored
View File

@@ -1,17 +1,17 @@
{
"hp_version": "1.0.0",
"node": {
"public_key": "ed903ba38b1fda781e048aee8cf805afa3e8dfb39dac4cd97ee5d00976d3a4808b",
"private_key": "ed433d9ad4d6294254e4e307081508fc37327f62a35d6bd0c765187c77cff978e2903ba38b1fda781e048aee8cf805afa3e8dfb39dac4cd97ee5d00976d3a4808b",
"public_key": "",
"private_key": "",
"role": "validator",
"history": "custom",
"history_config": {
"max_primary_shards": 1,
"max_raw_shards": 1
"max_primary_shards": 2,
"max_raw_shards": 2
}
},
"contract": {
"id": "f15abd89-ea82-4d8b-8d11-29fbc665dcf6",
"id": "",
"execute": true,
"run_as": "",
"log": {
@@ -20,11 +20,9 @@
"max_file_count": 10
},
"version": "1.0",
"unl": [
"ed903ba38b1fda781e048aee8cf805afa3e8dfb39dac4cd97ee5d00976d3a4808b"
],
"bin_path": "/usr/bin/node",
"bin_args": "/contract/echo_contract.js",
"unl": [],
"bin_path": "",
"bin_args": "",
"roundtime": 2000,
"stage_slice": 25,
"consensus": "public",

View File

@@ -81,6 +81,7 @@ sudo apt-get install -y \
sudo cp $scriptdir/dependencies/libblake3.so /usr/local/lib/
# NodeJs
sudo apt-get install -y ca-certificates # In case nodejs package certitficates are renewed.
curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs

View File

@@ -5,14 +5,15 @@
user_bin=/usr/bin
sashimono_bin=/usr/bin/sashimono-agent
docker_bin=/usr/bin/sashimono-agent/dockerbin
mb_xrpl_bin=$sashimono_bin/mb-xrpl
docker_bin=$sashimono_bin/dockerbin
sashimono_data=/etc/sashimono
mb_xrpl_data=$sashimono_data/mb-xrpl
mb_xrpl_conf=$mb_xrpl_data/mb-xrpl.cfg
sashimono_service="sashimono-agent"
cgcreate_service="sashimono-cgcreate"
mb_xrpl_service="sashimono-mb-xrpl"
mb_xrpl_dir="$sashimono_bin"/mb-xrpl
mb_xrpl_conf="$mb_xrpl_dir"/mb-xrpl.cfg
hook_xrpl_addr="rb4H5w7H1QA2qKjHCRSuUey2fnMBGbN2c"
hook_address="rwGLw5uSGYm2couHZnrbCDKaQZQByvamj8"
group="sashimonousers"
admin_group="sashiadmin"
cgroupsuffix="-cg"
@@ -21,9 +22,6 @@ registryport=4444
script_dir=$(dirname "$(realpath "$0")")
quiet=$1
xrpl_server_url="wss://hooks-testnet.xrpl-labs.com"
xrpl_fauset_url="https://hooks-testnet.xrpl-labs.com/newcreds"
[ -d $sashimono_bin ] && [ -n "$(ls -A $sashimono_bin)" ] &&
echo "Aborting installation. Previous Sashimono installation detected at $sashimono_bin" && exit 1
@@ -37,6 +35,8 @@ mkdir -p $docker_bin
[ "$?" == "1" ] && echo "Could not create '$docker_bin'. Make sure you are running as sudo." && exit 1
mkdir -p $sashimono_data
[ "$?" == "1" ] && echo "Could not create '$sashimono_data'. Make sure you are running as sudo." && exit 1
mkdir -p $mb_xrpl_data
[ "$?" == "1" ] && echo "Could not create '$mb_xrpl_data'. Make sure you are running as sudo." && exit 1
echo "Installing Sashimono..."
@@ -140,12 +140,43 @@ systemctl enable $sashimono_service
systemctl start $sashimono_service
# Both of these services needed to be restarted if sa.cfg max instance resources are manually changed.
if [ "$quiet" != "-q" ]; then
# Setup xrpl message board.
cp -r "$script_dir"/mb-xrpl $sashimono_bin
chmod -R +x "$sashimono_bin"/mb-xrpl
# Setup xrpl message board.
echo "Installing Evernode xrpl message board..."
if [ "$quiet"=="-q" ]; then
echo "Please answer following questions to setup xrpl message board.."
# We are in the quiet mode. Hence we auto-generate an XRPL test account and token details for the host.
# (This is done for testing purposes during development)
xrpl_faucet_url="https://hooks-testnet.xrpl-labs.com/newcreds"
hook_secret="shaXJCUZeE37nCe5VpiT8xiVFjFmY"
func_url="https://func-hotpocket.azurewebsites.net/api/evrfaucet?code=pPUyV1q838ryrihA5NVlobVXj8ZGgn9HsQjGGjl6Vhgxlfha4/xCgQ=="
# Generate new fauset account.
echo "Generating XRP faucet account..."
new_acc=$(curl -X POST $xrpl_faucet_url)
# If result is not a json, account generation failed.
[[ ! "$new_acc" =~ \{.+\} ]] && echo "Xrpl faucet account generation failed." && rollback
xrp_address=$(echo $new_acc | jq -r '.address')
xrp_secret=$(echo $new_acc | jq -r '.secret')
([ "$xrp_address" == "" ] || [ "$xrp_address" == "null" ] ||
[ "$xrp_secret" == "" ] || [ "$xrp_secret" == "null" ]) && echo "Invalid generated xrpl account details: $new_acc" && rollback
# Setup the host xrpl account with an EVR balance and default rippling flag.
echo "Setting up host XRP account..."
acc_setup_func="$func_url&action=setuphost&hookaddr=$hook_address&hooksecret=$hook_secret&addr=$xrp_address&secret=$xrp_secret"
func_code=$(curl -o /dev/null -s -w "%{http_code}\n" -d "" -X POST $acc_setup_func)
[ "$func_code" != "200" ] && echo "Host XRP account setup failed. code:$func_code" && rollback
# Generate random details for instance size, location and token.
instance_size="AUTO InstSize "$(tr -dc A-Z </dev/urandom | head -c 10)
location="AUTO Loc "$(tr -dc A-Z </dev/urandom | head -c 5)
token=$(tr -dc A-Z </dev/urandom | head -c 3)
echo "Auto-generated host information."
else
echo "Please answer following questions to setup Evernode xrpl message board."
# Ask for input until a correct value is given
while [ -z "$instance_size" ] || [[ "$instance_size" =~ .*\;.* ]]; do
read -p "Instance size? " instance_size </dev/tty
@@ -159,41 +190,40 @@ if [ "$quiet" != "-q" ]; then
read -p "Token name? " token </dev/tty
[[ ! "$token" =~ ^[A-Z]{3}$ ]] && echo "Token name should be 3 UPPERCASE letters."
done
# Generate new fauset account.
new_acc=$(curl -X POST $xrpl_fauset_url)
# If result is not a json, account generation failed.
[[ ! "$new_acc" =~ \{.+\} ]] && echo "Xrpl fauset account generation failed." && rollback
address=$(echo $new_acc | jq -r '.address')
secret=$(echo $new_acc | jq -r '.secret')
([ "$address" == "" ] || [ "$address" == "null" ] ||
[ "$secret" == "" ] || [ "$secret" == "null" ]) && echo "Invalid xrpl account details: $new_acc" && rollback
(! echo "{\"host\":{\"name\":\"\",\"location\":\"$location\",\"instanceSize\":\"$instance_size\"},\"xrpl\":{\"address\":\"$address\",\"secret\":\"$secret\",\"token\":\"$token\",\"hookAddress\":\"$hook_xrpl_addr\",\"regTrustHash\":\"\",\"regFeeHash\":\"\"}}" | jq . >$mb_xrpl_conf) && rollback
# Install xrpl message board systemd service.
# StartLimitIntervalSec=0 to make unlimited retries. RestartSec=5 is to keep 5 second gap between restarts.
echo "[Unit]
Description=Running and monitoring evernode xrpl transactions.
After=network.target
StartLimitIntervalSec=0
[Service]
User=root
Group=root
Type=simple
WorkingDirectory=$mb_xrpl_dir
ExecStart=node $mb_xrpl_dir $xrpl_server_url --enable-logging
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target" >/etc/systemd/system/$mb_xrpl_service.service
# This service needed to be restarted when mb-xrpl.cfg is changed.
systemctl enable $mb_xrpl_service
systemctl start $mb_xrpl_service
while [[ -z "$xrp_address" ]]; do
read -p "XRPL account address? " xrp_address </dev/tty
done
while [[ -z "$xrp_secret" ]]; do
read -p "XRPL account secret? " xrp_secret </dev/tty
done
fi
cp -r "$script_dir"/mb-xrpl $sashimono_bin
(! echo "{\"host\":{\"location\":\"$location\",\"instanceSize\":\"$instance_size\"},\"xrpl\":{\"address\":\"$xrp_address\",\"secret\":\"$xrp_secret\",\"token\":\"$token\",\"hookAddress\":\"$hook_address\",\"regFeeHash\":\"\"}}" | jq . >$mb_xrpl_conf) && rollback
# Install xrpl message board systemd service.
# StartLimitIntervalSec=0 to make unlimited retries. RestartSec=5 is to keep 5 second gap between restarts.
echo "[Unit]
Description=Running and monitoring evernode xrpl transactions.
After=network.target
StartLimitIntervalSec=0
[Service]
User=root
Group=root
Type=simple
WorkingDirectory=$mb_xrpl_bin
Environment=\"MB_DATA_DIR=$mb_xrpl_data\"
ExecStart=node $mb_xrpl_bin
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target" >/etc/systemd/system/$mb_xrpl_service.service
# This service needs to be restarted when mb-xrpl.cfg is changed.
systemctl enable $mb_xrpl_service
systemctl start $mb_xrpl_service
echo "Installed Evernode xrpl message board."
echo "Sashimono installed successfully."
echo "Please restart your cgroup rule generator service or reboot your server for changes to apply."
exit 0

View File

@@ -4,8 +4,10 @@
user_bin=/usr/bin
sashimono_bin=/usr/bin/sashimono-agent
docker_bin=/usr/bin/sashimono-agent/dockerbin
mb_xrpl_bin=$sashimono_bin/mb-xrpl
docker_bin=$sashimono_bin/dockerbin
sashimono_data=/etc/sashimono
mb_xrpl_data=$sashimono_data/mb-xrpl
sashimono_service="sashimono-agent"
cgcreate_service="sashimono-cgcreate"
mb_xrpl_service="sashimono-mb-xrpl"
@@ -23,6 +25,35 @@ if [ "$quiet" != "-q" ]; then
[ "$confirmation" != "yes" ] && echo "Uninstall cancelled." && exit 0
fi
# Remove xrpl message board service if exists.
if [ -f /etc/systemd/system/$mb_xrpl_service.service ]; then
systemctl stop $mb_xrpl_service
systemctl disable $mb_xrpl_service
rm /etc/systemd/system/$mb_xrpl_service.service
fi
if [ "$quiet" == "-q" ]; then
# We only perform this for our testing setup during development.
echo "Cleaning up host XRP account..."
hook_address="rwGLw5uSGYm2couHZnrbCDKaQZQByvamj8"
func_url="https://func-hotpocket.azurewebsites.net/api/evrfaucet?code=pPUyV1q838ryrihA5NVlobVXj8ZGgn9HsQjGGjl6Vhgxlfha4/xCgQ=="
mb_xrpl_conf=$mb_xrpl_data/mb-xrpl.cfg
xrp_address=$(jq -r '.xrpl.address' $mb_xrpl_conf)
xrp_secret=$(jq -r '.xrpl.secret' $mb_xrpl_conf)
if [ "$xrp_address" != "" ] && [ "$xrp_secret" != "" ]; then
acc_clean_func="$func_url&action=cleanhost&hookaddr=$hook_address&addr=$xrp_address&secret=$xrp_secret"
func_code=$(curl -o /dev/null -s -w "%{http_code}\n" -d "" -X POST $acc_clean_func)
[ "$func_code" != "200" ] && echo "Host XRP account cleanup failed. code:$func_code"
[ "$func_code" == "200" ] && echo "Cleaned up host XRP account."
fi
fi
# Deregister evernode message board host registration.
echo "Attempting Evernode xrpl message board host deregistration..."
MB_DATA_DIR=$mb_xrpl_data MB_LOG=1 MB_DEREGISTER=1 node $mb_xrpl_bin
# Uninstall all contract instance users
prefix="sashi"
users=$(cut -d: -f1 /etc/passwd | grep "^$prefix" | sort)
@@ -60,13 +91,6 @@ if [ $ucount -gt 0 ]; then
fi
fi
# Remove xrpl message board service if exists.
if [ -f /etc/systemd/system/$mb_xrpl_service.service ]; then
systemctl stop $mb_xrpl_service
systemctl disable $mb_xrpl_service
rm /etc/systemd/system/$mb_xrpl_service.service
fi
echo "Removing Sashimono cgroup creation service..."
systemctl stop $cgcreate_service
systemctl disable $cgcreate_service

View File

@@ -6,26 +6,35 @@ const formatText = (text, logType = 'dbg') => {
const date = new Date().toISOString().
replace(/T/, ' '). // Replace T with a space.
replace(/\..+/, ''). // Delete the dot and everything after.
replace(/-/, ''); // Delete the dashes.
replace(/-/g, ''); // Delete the dashes.
return `${date} [${logType}] ${text}\n`;
}
exports.init = (logPath) => {
const dirname = path.dirname(logPath);
if (!fs.existsSync(dirname))
fs.mkdirSync(dirname, { recursive: true });
exports.init = (logPath, fileLogEnabled) => {
let flog = null;
if (fileLogEnabled) {
const dirname = path.dirname(logPath);
if (!fs.existsSync(dirname))
fs.mkdirSync(dirname, { recursive: true });
flog = fs.createWriteStream(logPath, { flags: 'a' });
}
// Formating logs and printing.
const ws = fs.createWriteStream(logPath, { flags: 'a' });
console.log = function () {
const text = formatText(util.format.apply(this, arguments));
ws.write(text);
process.stdout.write(text);
if (flog)
flog.write(text);
};
console.error = function () {
const text = formatText(util.format.apply(this, arguments), 'err');
ws.write(text);
process.stderr.write(text);
if (flog)
flog.write(text);
};
}

View File

@@ -1,358 +0,0 @@
const eccrypto = require("eccrypto");
const RippleAPI = require('ripple-lib').RippleAPI;
const CONNECTION_RETRY_THREASHOLD = 60;
const CONNECTION_RETRY_INTERVAL = 1000;
const DISABLE_MASTERKEY = 0x100000;
const maxLedgerOffset = 10;
const MemoTypes = {
REDEEM: 'evnRedeem',
REDEEM_REF: 'evnRedeemRef',
REDEEM_RESP: 'evnRedeemResp',
HOST_REG: 'evnHostReg'
}
const MemoFormats = {
TEXT: 'text/plain',
JSON: 'text/json',
BINARY: 'binary'
}
const Events = {
RECONNECTED: 'reconnected',
LEDGER: 'ledger',
PAYMENT: 'payment'
}
const ErrorCodes = {
REDEEM_ERR: 'REDEEM_ERR'
}
const hexToASCII = (hex) => {
let str = "";
for (let n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substr(n, 2), 16));
}
return str;
}
class EventEmitter {
constructor() {
this.handlers = {};
}
on(event, handler) {
if (!this.handlers[event])
this.handlers[event] = [];
this.handlers[event].push(handler);
}
emit(event, value, error = null) {
if (this.handlers[event])
this.handlers[event].forEach(handler => handler(value, error));
}
}
class RippleAPIWarpper {
constructor(rippleServer) {
this.connectionRetryCount = 0;
this.connected = false;
this.rippleServer = rippleServer;
this.events = new EventEmitter();
this.api = new RippleAPI({ server: this.rippleServer });
this.api.on('error', (errorCode, errorMessage) => {
console.log(errorCode + ': ' + errorMessage);
});
this.api.on('connected', () => {
console.log(`Connected to ${this.rippleServer}`);
this.connectionRetryCount = 0;
this.connected = true;
});
this.api.on('disconnected', async (code) => {
if (!this.connected)
return;
this.connected = false;
console.log(`Disconnected from ${this.rippleServer} code:`, code);
try {
await this.connect();
this.events.emit(Events.RECONNECTED, `Reconnected to ${this.rippleServer}`);
}
catch (e) { console.error(e); };
});
this.api.on('ledger', (ledger) => {
this.events.emit(Events.LEDGER, ledger);
});
}
async connect() {
if (this.connected)
return;
let retryInterval = CONNECTION_RETRY_INTERVAL;
// If failed, Keep retrying and increasing the retry timeout.
while (true) {
try {
this.connectionRetryCount++;
console.log(`Trying to connect ${this.rippleServer}`);
await this.api.connect();
return;
}
catch (e) {
console.log(`Couldn't connect ${this.rippleServer} : `, e);
// If threashold reaches, increase the retry interval.
if (this.connectionRetryCount % CONNECTION_RETRY_THREASHOLD === 0)
retryInterval += CONNECTION_RETRY_INTERVAL;
// Wait before retry.
await new Promise(resolve => setTimeout(resolve, retryInterval));
}
}
}
deriveAddress(publicKey) {
return this.api.deriveAddress(publicKey);
}
async getAccountInfo(address) {
return (await this.api.request('account_info', { account: address }));
}
async getLedgerVersion() {
return (await this.api.getLedgerVersion());
}
async isValidAddress(publicKey, address) {
const derivedAddress = this.deriveAddress(publicKey);
const info = await this.getAccountInfo(address);
const accountFlags = info.account_data.Flags;
const regularKey = info.account_data.RegularKey;
const masterKeyDisabled = (DISABLE_MASTERKEY & accountFlags) === DISABLE_MASTERKEY;
// If the master key is disabled the derived address should be the regular key.
// Otherwise it could be master key or the regular key
if (masterKeyDisabled)
return regularKey && (derivedAddress === regularKey);
else
return derivedAddress === address || (regularKey && derivedAddress === regularKey);
}
}
class XrplAccount {
constructor(rippleAPI, address, secret = null) {
this.rippleAPI = rippleAPI;
this.address = address;
this.secret = secret;
this.events = new EventEmitter();
this.subscribed = false;
}
deriveKeypair() {
if (!this.secret)
throw 'Cannot derive key pair: Account secret is empty.';
return this.rippleAPI.api.deriveKeypair(this.secret);
}
async makePayment(toAddr, amount, currency, issuer, memos = null) {
// Get current ledger.
const ledger = await (await this.rippleAPI.api.getLedger()).ledgerVersion;
const maxLedger = ledger + maxLedgerOffset;
const amountObj = {
currency: currency,
counterparty: issuer,
value: amount.toString()
}
// Delete counterparty key if issuer is empty.
if (!amountObj.counterparty)
delete amountObj.counterparty;
const prepared = await this.rippleAPI.api.preparePayment(this.address, {
source: {
address: this.address,
maxAmount: amountObj
},
destination: {
address: toAddr,
amount: amountObj
},
memos: this.getMemoCollection(memos)
}, {
maxLedgerVersion: maxLedger
})
const signed = this.rippleAPI.api.sign(prepared.txJSON, this.secret);
await this.rippleAPI.api.submit(signed.signedTransaction);
const verified = await this.verifyTransaction(signed.id, ledger, maxLedger);
return verified ? verified : false;
}
async createTrustline(currency, issuer, limit, allowRippling, memos = null) {
const res = await this.createTrustlines([{
issuer: issuer,
limit: limit,
currency: currency,
allowRippling: allowRippling,
memos: this.getMemoCollection(memos)
}]);
return res[0];
}
getMemoCollection(memos) {
return memos ? memos.filter(m => m.data).map(m => {
return {
type: m.type,
format: m.format,
data: (typeof m.data === "object") ? JSON.stringify(m.data) : m.data
}
}) : [];
}
async createTrustlines(lines) {
// Get current ledger.
const ledger = await (await this.rippleAPI.api.getLedger()).ledgerVersion;
const maxLedger = ledger + maxLedgerOffset;
// Create and verify multiple trust lines in parallel.
const tasks = [];
for (const line of lines) {
tasks.push(new Promise(async (resolve) => {
const prepared = await this.rippleAPI.api.prepareTrustline(this.address, {
counterparty: line.issuer,
currency: line.currency,
limit: line.limit.toString(),
memos: line.memos,
ripplingDisabled: !line.allowRippling
}, {
maxLedgerVersion: maxLedger
})
const signed = this.rippleAPI.api.sign(prepared.txJSON, this.secret);
await this.rippleAPI.api.submit(signed.signedTransaction);
console.log("Submitted trust line.");
const verified = await this.verifyTransaction(signed.id, ledger, maxLedger);
verified ? resolve(verified) : resolve(false);
}));
}
const results = await Promise.all(tasks);
return results;
}
verifyTransaction(txHash, minLedger, maxLedger) {
return new Promise(resolve => {
this.rippleAPI.api.getTransaction(txHash, {
minLedgerVersion: minLedger,
maxLedgerVersion: maxLedger
}).then(data => {
console.log(data.outcome.result);
if (data.outcome.result !== 'tesSUCCESS')
console.log("Transaction verification failed. Result: " + data.outcome.result);
resolve(data.outcome.result === 'tesSUCCESS' ? { txHash: data.id, ledgerVersion: data.outcome.ledgerVersion } : false);
}).catch(error => {
// If transaction not in latest validated ledger, try again until max ledger is hit.
if (error instanceof this.rippleAPI.api.errors.PendingLedgerVersionError || error instanceof this.rippleAPI.api.errors.NotFoundError) {
console.log("Waiting for verification...");
setTimeout(() => {
this.verifyTransaction(txHash, minLedger, maxLedger).then(result => resolve(result));
}, 1000);
}
else {
console.log(error);
console.log("Transaction verification failed.");
resolve(false); // give up.
}
})
})
}
deserializeMemo(memo) {
return {
type: memo.MemoType ? hexToASCII(memo.MemoType) : null,
format: memo.MemoFormat ? hexToASCII(memo.MemoFormat) : null,
data: memo.MemoData ? hexToASCII(memo.MemoData) : null
};
}
subscribe() {
// Subscribe only once. Otherwise event handlers will be duplicated.
if (this.subscribed)
return;
this.rippleAPI.api.connection.on("transaction", (data) => {
const eventName = data.transaction.TransactionType.toLowerCase();
// Emit the event only for successful transactions, Otherwise emit error.
if (data.engine_result === "tesSUCCESS") {
// Convert memo fields to ASCII before emitting the event.
if (data.transaction.Memos)
data.transaction.Memos = data.transaction.Memos.filter(m => m.Memo).map(m => this.deserializeMemo(m.Memo));
this.events.emit(eventName, data.transaction);
}
else
this.events.emit(eventName, null, data.engine_result_message);
});
const request = {
command: 'subscribe',
accounts: [this.address]
}
const message = `Subscribed to transactions on ${this.address}`;
// Subscribe to transactions when api is reconnected.
// Because API will be automatically reconnected if it's disconnected.
this.rippleAPI.events.on(Events.RECONNECTED, (e) => {
this.rippleAPI.api.connection.request(request);
console.log(message);
});
this.rippleAPI.api.connection.request(request);
console.log(message);
this.subscribed = true;
}
}
class EncryptionHelper {
// Offsets of the properties in the encrypted buffer.
static ivOffset = 65;
static macOffset = this.ivOffset + 16;
static ciphertextOffset = this.macOffset + 32;
static contentFormat = 'base64';
static keyFormat = 'hex';
static async encrypt(publicKey, json) {
// For the encryption library, both keys and data should be buffers.
const encrypted = await eccrypto.encrypt(Buffer.from(publicKey, this.keyFormat), Buffer.from(JSON.stringify(json)));
// Concat all the properties of the encrypted object to a single buffer.
return Buffer.concat([encrypted.ephemPublicKey, encrypted.iv, encrypted.mac, encrypted.ciphertext]).toString(this.contentFormat);
}
static async decrypt(privateKey, encrypted) {
// Extract the buffer from the string and prepare encrypt object from buffer offsets for decryption.
const encryptedBuf = Buffer.from(encrypted, this.contentFormat);
const encryptedObj = {
ephemPublicKey: encryptedBuf.slice(0, this.ivOffset),
iv: encryptedBuf.slice(this.ivOffset, this.macOffset),
mac: encryptedBuf.slice(this.macOffset, this.ciphertextOffset),
ciphertext: encryptedBuf.slice(this.ciphertextOffset)
}
return JSON.parse((await eccrypto.decrypt(Buffer.from(privateKey, this.keyFormat).slice(1), encryptedObj)).toString());
}
}
module.exports = {
XrplAccount,
RippleAPIWarpper,
EncryptionHelper,
MemoFormats,
MemoTypes,
Events,
ErrorCodes
}

View File

@@ -1,21 +1,28 @@
const fs = require('fs');
const { exec } = require("child_process");
const logger = require('./lib/logger');
const { XrplAccount, RippleAPIWarpper, Events, MemoFormats, MemoTypes, ErrorCodes, EncryptionHelper } = require('./lib/ripple-handler');
const { XrplAccount, RippleAPIWrapper, RippleAPIEvents, RippleConstants, MemoFormats, MemoTypes, ErrorCodes, EncryptionHelper } = require('evernode-js-client');
const { SqliteDatabase, DataTypes } = require('./lib/sqlite-handler');
const CONFIG_PATH = 'mb-xrpl.cfg';
const DB_PATH = 'mb-xrpl.sqlite';
// Environment variables.
const IS_DEV_MODE = process.env.MB_DEV === "1";
const FILE_LOG_ENABLED = process.env.MB_FILE_LOG === "1";
const IS_DEREGISTER = process.env.MB_DEREGISTER === "1";
const RIPPLED_URL = process.env.MB_RIPPLED_URL || "wss://hooks-testnet.xrpl-labs.com";
const DATA_DIR = process.env.MB_DATA_DIR || ".";
const CONFIG_PATH = DATA_DIR + '/mb-xrpl.cfg';
const LOG_PATH = DATA_DIR + '/log/mb-xrpl.log';
const DB_PATH = DATA_DIR + '/mb-xrpl.sqlite';
const DB_TABLE_NAME = 'redeem_ops';
const DB_UTIL_TABLE_NAME = 'util_data';
const LAST_WATCHED_LEDGER = 'last_watched_ledger';
const EVR_CUR_CODE = 'EVR';
const EVR_LIMIT = 99999999;
const REG_FEE = 5;
const RES_FEE = 0.000001;
const REG_FEE = '5';
const LEDGERS_PER_MOMENT = 72;
const REDEEM_TIMEOUT_WINDOW = 24; // Max no. of ledgers within which a redeem operation has to be served.
const REDEEM_TIMEOUT_THRESHOLD = 0.8;
const SASHI_CLI_PATH = IS_DEV_MODE ? "../build/sashi" : "/usr/bin/sashi";
const RedeemStatus = {
REDEEMING: 'Redeeming',
@@ -25,11 +32,9 @@ const RedeemStatus = {
SASHI_TIMEOUT: 'SashiTimeout',
}
const SASHI_CLI_PATH_DEV = "../build/sashi";
const SASHI_CLI_PATH_PROD = "/usr/bin/sashi";
class MessageBoard {
constructor(configPath, dbPath, sashiCliPath, rippleServer) {
constructor(configPath, dbPath, sashiCliPath, rippledServer) {
this.configPath = configPath;
this.redeemTable = DB_TABLE_NAME;
this.utilTable = DB_UTIL_TABLE_NAME;
@@ -41,7 +46,7 @@ class MessageBoard {
throw `Sashi CLI does not exist in ${sashiCliPath}.`;
this.sashiCli = new SashiCLI(sashiCliPath);
this.ripplAPI = new RippleAPIWarpper(rippleServer);
this.rippleAPI = new RippleAPIWrapper(rippledServer);
this.db = new SqliteDatabase(dbPath);
}
@@ -50,15 +55,23 @@ class MessageBoard {
if (!this.cfg.xrpl.address || !this.cfg.xrpl.secret || !this.cfg.xrpl.token || !this.cfg.xrpl.hookAddress)
throw "Required cfg fields cannot be empty.";
try { await this.ripplAPI.connect(); }
try { await this.rippleAPI.connect(); }
catch (e) { throw e; }
this.xrplAcc = new XrplAccount(this.rippleAPI, this.cfg.xrpl.address, this.cfg.xrpl.secret);
if (IS_DEREGISTER) {
await this.deregisterHost();
this.rippleAPI.disconnect();
return;
}
this.db.open();
// Create redeem table if not exist.
await this.createRedeemTableIfNotExists();
await this.createUtilDataTableIfNotExists();
this.lastValidatedLedgerSequence = await this.ripplAPI.getLedgerVersion();
this.lastValidatedLedgerSequence = await this.rippleAPI.ledgerVersion;
const redeems = await this.getRedeemedRecords();
for (const redeem of redeems)
@@ -67,7 +80,7 @@ class MessageBoard {
this.db.close();
// Check for instance expiry.
this.ripplAPI.events.on(Events.LEDGER, async (e) => {
this.rippleAPI.events.on(RippleAPIEvents.LEDGER, async (e) => {
this.lastValidatedLedgerSequence = e.ledgerVersion;
// Filter out instances which needed to be expired and destroy them.
@@ -91,15 +104,13 @@ class MessageBoard {
}
});
this.xrplAcc = new XrplAccount(this.ripplAPI, this.cfg.xrpl.address, this.cfg.xrpl.secret);
// Check whether registration fee is already payed and trustline is made.
await this.checkForRegistration();
this.evernodeXrplAcc = new XrplAccount(this.ripplAPI, this.cfg.xrpl.hookAddress);
this.evernodeXrplAcc = new XrplAccount(this.rippleAPI, this.cfg.xrpl.hookAddress);
// Handle the transactions on evernode account and filter out redeem operations.
this.evernodeXrplAcc.events.on(Events.PAYMENT, async (data, error) => {
this.evernodeXrplAcc.events.on(RippleAPIEvents.PAYMENT, async (data, error) => {
if (error)
console.error(error);
else if (!data)
@@ -179,20 +190,14 @@ class MessageBoard {
}
async checkForRegistration() {
// Create trustline with evernode account.
if (!this.cfg.xrpl.regTrustHash) {
const res = await this.xrplAcc.createTrustline(EVR_CUR_CODE, this.cfg.xrpl.hookAddress, EVR_LIMIT, false);
if (res) {
this.cfg.xrpl.regTrustHash = res.txHash;
this.persistConfig();
console.log(`Created ${EVR_CUR_CODE} trustline with evernode account.`);
}
}
// We assume host account already posseses some EVR balance along with EVR trust line.
// Make registration fee evernode account.
if (!this.cfg.xrpl.regFeeHash) {
const memoData = `${this.cfg.xrpl.token};${this.cfg.host.instanceSize};${this.cfg.host.location}`
// For now we comment EVR reg fee transaction and make XRP transaction instead.
console.log(`Making Evernode host registration payment of ${REG_FEE} ${EVR_CUR_CODE}...`)
const res = await this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
REG_FEE,
EVR_CUR_CODE,
@@ -206,9 +211,21 @@ class MessageBoard {
}
}
async deregisterHost() {
// Sends evernode host de-registration transaction.
console.log(`Performing Evernode host deregistration...`);
const res = await this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
RippleConstants.MIN_XRP_AMOUNT,
"XRP",
null,
[{ type: MemoTypes.HOST_DEREG, format: MemoFormats.TEXT, data: "" }]);
if (res)
console.log('Deregistration complete.');
}
async sendRedeemResponse(txHash, txPubkey, txAccount, response, encrypt = true) {
// Verifying the pubkey.
if (!(await this.ripplAPI.isValidAddress(txPubkey, txAccount)))
if (!(await this.rippleAPI.isValidAddress(txPubkey, txAccount)))
throw 'Invalid public key for encryption';
// Encrypt response with user pubkey.
@@ -216,7 +233,7 @@ class MessageBoard {
response = await EncryptionHelper.encrypt(txPubkey, response);
return (await this.xrplAcc.makePayment(this.cfg.xrpl.hookAddress,
RES_FEE,
RippleConstants.MIN_XRP_AMOUNT,
"XRP",
null,
[{ type: MemoTypes.REDEEM_REF, format: MemoFormats.BINARY, data: txHash },
@@ -305,10 +322,10 @@ class MessageBoard {
}
async getMissedPaymentTransactions(lastWatchedLedger) {
return await this.ripplAPI.api.getTransactions(this.cfg.xrpl.hookAddress, {
return await this.rippleAPI.api.getTransactions(this.cfg.xrpl.hookAddress, {
excludeFailures: true,
minLedgerVersion: lastWatchedLedger,
types: [Events.PAYMENT]
types: [RippleAPIEvents.PAYMENT]
});
}
}
@@ -373,26 +390,16 @@ class SashiCLI {
}
async function main() {
const args = process.argv;
// This is used for logging purposes.
// Logs are formatted with the timestamp and a log file will be created inside log directory.
if (args.includes('--enable-logging'))
logger.init('log/mb-xrpl.log');
logger.init(LOG_PATH, FILE_LOG_ENABLED);
if (args.length < 3)
throw "Arguments mismatch.\n Usage: node mb-xrpl <ripple server url>";
console.log('Starting the Evernode xrpl message board.' + (IS_DEV_MODE ? ' (in dev mode)' : ''));
console.log('Data dir: ' + DATA_DIR);
console.log('Rippled server: ' + RIPPLED_URL);
console.log('Using Sashimono cli: ' + SASHI_CLI_PATH);
let sashiCliPath = SASHI_CLI_PATH_PROD;
// Use sashi CLI in the build folder for dev environment.
if (args.includes('--dev'))
sashiCliPath = SASHI_CLI_PATH_DEV;
console.log('Starting the xrpl message board' + (args[3] == '--dev' ? ' (in dev mode)' : ''));
// Read Ripple Server Url.
const rippleServer = args[2];
const mb = new MessageBoard(CONFIG_PATH, DB_PATH, sashiCliPath, rippleServer);
const mb = new MessageBoard(CONFIG_PATH, DB_PATH, SASHI_CLI_PATH, RIPPLED_URL);
await mb.init();
}

3434
mb-xrpl/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
{
"name": "mb-xrpl",
"scripts": {
"build": "ncc build mb-xrpl.js -o dist/mb-xrpl"
},
"dependencies": {
"eccrypto": "^1.1.6",
"ripple-lib": "^1.9.8",
"sqlite3": "^5.0.2"
}
"name": "mb-xrpl",
"scripts": {
"build": "ncc build mb-xrpl.js -o dist"
},
"dependencies": {
"evernode-js-client": "0.0.19-alpha",
"sqlite3": "5.0.2"
}
}

View File

@@ -16,13 +16,13 @@ cp $originalfstab "$tmpfstab"
backup=$originalfstab.sashi.bk
apt-get update
apt install uidmap -y
apt-get install uidmap -y
# Install node 14 if not exists.
# Install nodejs 14 if not exists.
if ! command -v node &>/dev/null; then
apt update
apt-get -y install ca-certificates # In case nodejs package certitficates are renewed.
curl -sL https://deb.nodesource.com/setup_14.x | bash -
apt -y install nodejs
apt-get -y install nodejs
else
version=$(node -v)
if [[ ! $version =~ v14\..* ]]; then
@@ -32,7 +32,7 @@ fi
# Install slirp4netns if not exists (required for high performance rootless networking).
if ! command -v slirp4netns &>/dev/null; then
apt -y install slirp4netns
apt-get -y install slirp4netns
fi
# Check for pattern <Not starting with a comment><Not whitespace(Device)><Whitespace></><Whitespace><Not whitespace(FS type)><Whitespace><No whitespace(Options)><Whitespace><Number(Dump)><Whitespace><Number(Pass)>

View File

@@ -62,11 +62,11 @@ namespace conf
cfg.hp.init_peer_port = 22861;
cfg.hp.init_user_port = 8081;
cfg.system.max_instance_count = 5;
cfg.system.max_instance_count = 3;
cfg.system.max_mem_kbytes = 1048576; // Total 1GB RAM
cfg.system.max_swap_kbytes = 3145728; // Total 3GB swap.
cfg.system.max_cpu_us = 5000000; // CPU cfs period cannot be less than 1ms (i.e. 1000) or larger than 1s (i.e. 1000000) per instance.
cfg.system.max_storage_kbytes = 5242880; // Total 5GB
cfg.system.max_storage_kbytes = 5242880; // Total 5GB disk storage.
const std::string img_prefix = registry_addr.empty() ? "hotpocketdev" : std::string(registry_addr);
cfg.docker.images["hp.latest-ubt.20.04"] = img_prefix + "/sashimono:hp.latest-ubt.20.04";
@@ -76,7 +76,10 @@ namespace conf
cfg.log.max_mbytes_per_file = 10;
cfg.log.log_level = "inf";
cfg.log.loggers.emplace("console");
cfg.log.loggers.emplace("file");
// We don't enable file logging by default because Sashimono running as a systemd service
// would automatically log console output to journal log.
// cfg.log.loggers.emplace("file");
// Save the default settings into the config file.
if (write_config(cfg) != 0)

View File

@@ -496,14 +496,6 @@ namespace hp
const std::string pubkey_hex = util::to_hex(pubkey);
// Default hp.cfg configs.
d["node"]["history_config"]["max_primary_shards"] = 2;
d["node"]["history_config"]["max_raw_shards"] = 2;
d["hpfs"]["log"]["log_level"] = "err";
d["log"]["log_level"] = "inf";
d["log"]["max_mbytes_per_file"] = 5;
d["log"]["max_file_count"] = 10;
d["node"]["public_key"] = pubkey_hex;
d["node"]["private_key"] = util::to_hex(seckey);
d["contract"]["id"] = contract_id;

View File

@@ -1,4 +1,5 @@
config.json
hp.cfg
contract_fs
ledger_fs
ledger_fs
node_modules

View File

@@ -221,6 +221,12 @@ if [ $mode == "reconfig" ]; then
if [ $nodeid = -1 ]; then
for i in "${!hostaddrs[@]}"; do
reconfig $i &
# In reinstall mode, leave a time gap between reinstall initiation to avoid host faucet wallet generation
# overload on XRPL testnet.
if [ ! -z $reinstall ] && [ $reinstall == "R" ]; then
sleep 2
fi
done
wait
else
@@ -612,17 +618,36 @@ if [ $mode == "sshu" ]; then
function sshwithuser() {
hostaddr=${hostaddrs[$1]}
execute_command=$2
nodeno=$(expr $1 + 1)
containername=$(echo $continfo | jq -r ".hosts.\"$hostaddr\".name")
username=$(sshskp $sshuser@$hostaddr "sashi list | grep $containername | awk '{ print \$2 }'")
ssh_command="cd /home/$username/$containername ; sudo -u $username bash"
sshskp -t $sshuser@$hostaddr $ssh_command
user_shell="cd /home/$username/$containername ; sudo -u $username bash"
if [ "$execute_command" == "" ]; then
sshskp -t $sshuser@$hostaddr $user_shell
else
echo "node"$n":" $(sshskp $sshuser@$hostaddr $user_shell -c "'$execute_command'")
fi
}
if [ $nodeid = -1 ]; then
echo "Must specify node no."
exit 1
if [ -n "$2" ]; then
# Interpret second arg as a command to execute on all nodes.
command=${*:2}
echo "Executing '$command' on user shell in all nodes..."
for i in "${!hostaddrs[@]}"; do
hostaddr=${hostaddrs[i]}
let n=$i+1
sshwithuser $n $command &
done
wait
exit 0
else
echo "Please specify node no. or command to execute on all nodes."
exit 1
fi
else
sshwithuser $nodeid
fi

View File

@@ -0,0 +1,281 @@
import fetch from "node-fetch";
import { exec } from "child_process";
import fs from "fs";
import evernode from "evernode-js-client";
const { EvernodeClient, XrplAccount, RippleAPIWrapper } = evernode;
const REDEEM_AMOUNT = 18000; // 18000 Moments ~ 60days
const PEER_SUBSET_SIZE = 5;
const configFile = "config.json";
const config = JSON.parse(fs.readFileSync(configFile));
const currentContract = config.contracts.filter(c => c.name === config.selected)[0];
if (!currentContract)
throw "Invalid contract selected.";
const userAddr = config.xrpl.userAddress;
const userSecret = config.xrpl.userSecret;
let rippleAPI = null;
let evernodeClient = null;
let userAcc = null;
let shouldAbort = false;
const hostAccounts = {};
async function issueRedeem(host, hostId, elem, peers, unl) {
if (Object.keys(elem).length > 0) {
console.log(`Instance in host ${hostId} (${host}) already created.`)
return [true];
}
// Take a subset of peers based on host id. (Take last subset up to previous host)
const subsetPeers = peers ? peers.slice(0, hostId - 1).slice(-PEER_SUBSET_SIZE) : null;
// Copy defined config from config.json to instance requirements config.
const config = JSON.parse(JSON.stringify(currentContract.config)) || {};
if (unl)
config.contract = { ...config.contract, unl: unl };
if (subsetPeers)
config.mesh = { ...config.mesh, known_peers: subsetPeers };
// Redeem
const acc = hostAccounts[host].hostAccount;
console.log(`------Host ${hostId} (${host}): Redeeming ${acc.token}-${acc.address}...`);
const res = await evernodeClient.redeemSubmit(acc.token, acc.address, REDEEM_AMOUNT, {
image: currentContract.docker.image,
contract_id: currentContract.contract_id,
owner_pubkey: currentContract.owner_pubkey,
config: config
}).catch(err => console.log(err));
if (!res) {
console.log(`Redeem issuing failued for host ${hostId}.`);
return [false]
}
else {
return [true, evernodeClient.watchRedeemResponse(res)]
}
}
async function processRedeemResponse(hostId, redeemOp, elem) {
const instanceInfo = await redeemOp;
if (instanceInfo) {
for (var k in instanceInfo)
elem[k] = instanceInfo[k];
console.log(`Created instance in host ${hostId}.`);
saveConfig();
return true;
}
else {
console.log(`Instance creation failed in host ${hostId}.`);
return false;
}
}
async function createInstancesSequentially() {
await createEvernodeConnections();
await initHosts();
let peers = null, unl = null;
let idx = 1;
for (const [host, elem] of Object.entries(currentContract.hosts)) {
if (shouldAbort)
return;
const hostId = idx++;
const [success, redeemOp] = await issueRedeem(host, hostId, elem, peers, unl);
if (!success)
return;
if (redeemOp && !await processRedeemResponse(hostId, redeemOp, elem))
return;
if (!unl)
unl = [elem.pubkey]; // Insert first instance's pubkey into all other instance's unl.
if (!peers)
peers = [];
peers.push(`${host}:${elem.peer_port}`);
}
}
async function createInstancesParallely(peerPort) {
await createEvernodeConnections();
await initHosts();
// Create first instance and then create all other instances parallely assuming they all have the same peer port.
let unl = null;
const peers = Object.keys(currentContract.hosts).map(h => `${h}:${peerPort}`);
const tasks = [];
let idx = 1;
for (const [host, elem] of Object.entries(currentContract.hosts)) {
if (shouldAbort)
return;
const hostId = idx++;
if (!unl) {
const [success, redeemOp] = await issueRedeem(host, hostId, elem, peers, null);
if (!success)
return;
if (redeemOp && !await processRedeemResponse(hostId, redeemOp, elem))
return;
unl = [elem.pubkey]; // Insert first instance's pubkey into all other instance's unl.
}
else {
const [success, redeemOp] = await issueRedeem(host, hostId, elem, peers, unl);
if (redeemOp)
tasks.push(processRedeemResponse(hostId, redeemOp, elem)); // Add to parallel response watcher tasks.
}
}
await Promise.all(tasks);
}
async function initHosts() {
if (Object.keys(currentContract.hosts).length == 0) {
const ips = await getVultrHosts(currentContract.vultr_group);
ips.forEach(ip => currentContract.hosts[ip] = {});
saveConfig();
}
const hosts = Object.keys(currentContract.hosts);
console.log(`${hosts.length} hosts loaded.`);
await Promise.all(hosts.map(host => initHostAccountData(host)))
for (const host of hosts) {
const { ownsTokens, acc } = hostAccounts[host];
if (!ownsTokens)
await transferHostingTokens(acc.token, acc.address, acc.secret);
}
console.log(`${Object.keys(hostAccounts).length} host accounts data initialized.`)
}
async function initHostAccountData(host) {
const output = await execSsh(host, "cat /etc/sashimono/mb-xrpl/mb-xrpl.cfg");
if (!output || output.trim() === "") {
console.log("ERROR: No output from mb-xrpl config read.")
return;
}
const conf = JSON.parse(output);
const acc = {
address: conf.xrpl.address,
secret: conf.xrpl.secret,
token: conf.xrpl.token
}
// Check whether user owns hosting tokens already.
console.log(`Checking user's ${acc.token} balance...`);
const lines = await userAcc.getTrustLines(acc.token, acc.address);
hostAccounts[host] = { ownsTokens: lines.length > 0, hostAccount: acc };
}
// Get hosting tokens from host account to user account.
async function transferHostingTokens(token, hostAddr, hostSecret) {
console.log(`Transfering ${token} to user...`);
const trustRes = await userAcc.createTrustLine(token, hostAddr, 9999999);
if (!trustRes)
return false;
const hostAcc = new XrplAccount(rippleAPI, hostAddr, hostSecret);
const payRes = await hostAcc.makePayment(userAddr, 9999999, token, hostAddr);
if (!payRes)
return false;
console.log(`Transfering of ${token} complete.`);
return true;
}
function getVultrHosts(group) {
return new Promise(async (resolve) => {
if (!group || group.trim().length === 0)
resolve([]);
const resp = await fetch(`https://api.vultr.com/v2/instances?tag=${group}`, {
method: 'GET',
headers: { "Authorization": `Bearer ${config.vultr.api_key}` }
});
const vms = (await resp.json()).instances;
if (!vms) {
console.log("Failed to get vultr instances.");
resolve([]);
return;
}
const ips = vms.sort((a, b) => (a.label < b.label) ? -1 : 1).map(i => i.main_ip);
console.log(`${ips.length} ips retrieved from Vultr.`)
resolve(ips);
})
}
async function createEvernodeConnections() {
rippleAPI = new RippleAPIWrapper();
await rippleAPI.connect();
evernodeClient = new EvernodeClient(userAddr, userSecret, { rippleAPI: rippleAPI });
await evernodeClient.connect();
userAcc = new XrplAccount(rippleAPI, userAddr, userSecret);
}
function saveConfig() {
fs.writeFileSync(configFile, JSON.stringify(config, null, 4));
}
function execSsh(host, command) {
return new Promise(resolve => {
const cmd = `ssh -o StrictHostKeychecking=no root@${host} ${command}`;
exec(cmd, (err, stdout, stderr) => {
resolve(stdout);
});
})
}
async function main() {
var args = process.argv.slice(2);
process.on('SIGINT', () => {
console.log('Received SIGINT. Aborting...');
shouldAbort = true;
});
const mode = args[0];
if (mode === "create") {
if (args.length === 1) {
await createInstancesSequentially();
}
else {
console.log("Invalid args for 'create'.");
}
}
else if (mode === "createall") {
const peerPort = parseInt(args[1]);
if (!isNaN(peerPort))
await createInstancesParallely(peerPort);
else
console.log("Specify peer port for 'createall'.");
}
else {
console.log("Specifiy args: create | createall <peerport>")
}
if (rippleAPI)
await rippleAPI.disconnect();
}
main();

849
test/vm-cluster/package-lock.json generated Normal file
View File

@@ -0,0 +1,849 @@
{
"name": "evcluster",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@types/lodash": {
"version": "4.14.175",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz",
"integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw=="
},
"@types/node": {
"version": "16.10.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz",
"integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ=="
},
"@types/ws": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
"requires": {
"@types/node": "*"
}
},
"acorn": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg=="
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"requires": {
"debug": "4"
}
},
"assert": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz",
"integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==",
"requires": {
"es6-object-assign": "^1.1.0",
"is-nan": "^1.2.1",
"object-is": "^1.0.1",
"util": "^0.12.0"
}
},
"available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
},
"base-x": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz",
"integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"big-integer": {
"version": "1.6.49",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz",
"integrity": "sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw=="
},
"bignumber.js": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA=="
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bip66": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
"integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=",
"optional": true,
"requires": {
"safe-buffer": "^5.0.1"
}
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"optional": true,
"requires": {
"buffer-xor": "^1.0.3",
"cipher-base": "^1.0.0",
"create-hash": "^1.1.0",
"evp_bytestokey": "^1.0.3",
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"optional": true
},
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"requires": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
"md5.js": "^1.3.4",
"ripemd160": "^2.0.1",
"sha.js": "^2.4.0"
}
},
"create-hmac": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"optional": true,
"requires": {
"cipher-base": "^1.0.3",
"create-hash": "^1.1.0",
"inherits": "^2.0.1",
"ripemd160": "^2.0.0",
"safe-buffer": "^5.0.1",
"sha.js": "^2.4.8"
}
},
"data-uri-to-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="
},
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"decimal.js": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
"integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ=="
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "^1.0.12"
}
},
"drbg.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
"integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=",
"optional": true,
"requires": {
"browserify-aes": "^1.0.6",
"create-hash": "^1.1.2",
"create-hmac": "^1.1.4"
}
},
"eccrypto": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/eccrypto/-/eccrypto-1.1.6.tgz",
"integrity": "sha512-d78ivVEzu7Tn0ZphUUaL43+jVPKTMPFGtmgtz1D0LrFn7cY3K8CdrvibuLz2AAkHBLKZtR8DMbB2ukRYFk987A==",
"requires": {
"acorn": "7.1.1",
"elliptic": "6.5.4",
"es6-promise": "4.2.8",
"nan": "2.14.0",
"secp256k1": "3.7.1"
}
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"es-abstract": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
"integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.1.1",
"get-symbol-description": "^1.0.0",
"has": "^1.0.3",
"has-symbols": "^1.0.2",
"internal-slot": "^1.0.3",
"is-callable": "^1.2.4",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.1",
"is-string": "^1.0.7",
"is-weakref": "^1.0.1",
"object-inspect": "^1.11.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.4",
"string.prototype.trimstart": "^1.0.4",
"unbox-primitive": "^1.0.1"
}
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
"es6-object-assign": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
"integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw="
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"evernode-js-client": {
"version": "0.0.19-alpha",
"resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.0.19-alpha.tgz",
"integrity": "sha512-0ErIuURPkYdt3eOhy8RV2H5+OrLtcrWS0+ARhSadQvkM6svQRd4bsS9Scvp6gikRjcZ6qPPSDP1IUVTnJkFkSw==",
"requires": {
"eccrypto": "1.1.6",
"ripple-lib": "1.9.8"
}
},
"evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"optional": true,
"requires": {
"md5.js": "^1.3.4",
"safe-buffer": "^5.1.1"
}
},
"fetch-blob": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz",
"integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==",
"requires": {
"web-streams-polyfill": "^3.0.3"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"optional": true
},
"foreach": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
}
},
"get-symbol-description": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
"integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
"requires": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.1"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-bigints": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
"integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
},
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"requires": {
"has-symbols": "^1.0.2"
}
},
"hash-base": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
"integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
"requires": {
"inherits": "^2.0.4",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
}
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"requires": {
"agent-base": "6",
"debug": "4"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"internal-slot": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
"integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
"requires": {
"get-intrinsic": "^1.1.0",
"has": "^1.0.3",
"side-channel": "^1.0.4"
}
},
"is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"is-bigint": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
"requires": {
"has-bigints": "^1.0.1"
}
},
"is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
"integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"is-callable": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
},
"is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-nan": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
}
},
"is-negative-zero": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
"integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w=="
},
"is-number-object": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
"integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"requires": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
}
},
"is-shared-array-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
"integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA=="
},
"is-string": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
"integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
"requires": {
"has-tostringtag": "^1.0.0"
}
},
"is-symbol": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
"integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
"requires": {
"has-symbols": "^1.0.2"
}
},
"is-typed-array": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz",
"integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==",
"requires": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"es-abstract": "^1.18.5",
"foreach": "^2.0.5",
"has-tostringtag": "^1.0.0"
}
},
"is-weakref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz",
"integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==",
"requires": {
"call-bind": "^1.0.0"
}
},
"jsonschema": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.2.tgz",
"integrity": "sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA=="
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},
"node-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz",
"integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==",
"requires": {
"data-uri-to-buffer": "^3.0.1",
"fetch-blob": "^3.1.2"
}
},
"object-inspect": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
"integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg=="
},
"object-is": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
}
},
"ripple-address-codec": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-4.1.3.tgz",
"integrity": "sha512-9mymOhfCUyLZGwotGPg3I2wMfrwHof0W8ygjhW46UdNgFW6J+OvDB/VS9dbHlgED/41mzECp41IXvTrkc1fTwA==",
"requires": {
"base-x": "3.0.8",
"create-hash": "^1.1.2"
}
},
"ripple-binary-codec": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.1.3.tgz",
"integrity": "sha512-NnFNZZ+225BxdDdHtcEn4GiGzup+V0DGAbtKygZIwbqA5116oZBt6uY3g43gYpdDMISsEbM7NewBij8+7jdlvA==",
"requires": {
"assert": "^2.0.0",
"big-integer": "^1.6.48",
"buffer": "5.6.0",
"create-hash": "^1.2.0",
"decimal.js": "^10.2.0",
"ripple-address-codec": "^4.1.1"
}
},
"ripple-keypairs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-1.0.3.tgz",
"integrity": "sha512-Na5q8sUdxjd5DXBM88ocJgL2Ig0I1USyO3bvI0SMxJPp7F9DHvqLdPX45PVXs7HUq0Dj691Z9Uz9NeD/K7/eOA==",
"requires": {
"bn.js": "^5.1.1",
"brorand": "^1.0.5",
"elliptic": "^6.5.2",
"hash.js": "^1.0.3",
"ripple-address-codec": "^4.0.0"
},
"dependencies": {
"bn.js": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
}
}
},
"ripple-lib": {
"version": "1.9.8",
"resolved": "https://registry.npmjs.org/ripple-lib/-/ripple-lib-1.9.8.tgz",
"integrity": "sha512-86wAP4GD6ZVMHIZeqrzn9m8BZSWSpMr06ULaAKNZbN6mxvh7SwcabJjcINRZNpI73Sb3oibWoOsCQ+Vf64WSTA==",
"requires": {
"@types/lodash": "^4.14.136",
"@types/ws": "^7.2.0",
"bignumber.js": "^9.0.0",
"https-proxy-agent": "^5.0.0",
"jsonschema": "1.2.2",
"lodash": "^4.17.4",
"ripple-address-codec": "^4.1.1",
"ripple-binary-codec": "^1.1.3",
"ripple-keypairs": "^1.0.3",
"ripple-lib-transactionparser": "0.8.2",
"ws": "^7.2.0"
}
},
"ripple-lib-transactionparser": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/ripple-lib-transactionparser/-/ripple-lib-transactionparser-0.8.2.tgz",
"integrity": "sha512-1teosQLjYHLyOQrKUQfYyMjDR3MAq/Ga+MJuLUfpBMypl4LZB4bEoMcmG99/+WVTEiZOezJmH9iCSvm/MyxD+g==",
"requires": {
"bignumber.js": "^9.0.0",
"lodash": "^4.17.15"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"secp256k1": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz",
"integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==",
"optional": true,
"requires": {
"bindings": "^1.5.0",
"bip66": "^1.1.5",
"bn.js": "^4.11.8",
"create-hash": "^1.2.0",
"drbg.js": "^1.0.1",
"elliptic": "^6.4.1",
"nan": "^2.14.0",
"safe-buffer": "^5.1.2"
}
},
"sha.js": {
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"requires": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",
"object-inspect": "^1.9.0"
}
},
"string.prototype.trimend": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"string.prototype.trimstart": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
},
"unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
"integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
"requires": {
"function-bind": "^1.1.1",
"has-bigints": "^1.0.1",
"has-symbols": "^1.0.2",
"which-boxed-primitive": "^1.0.2"
}
},
"util": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
"integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==",
"requires": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"safe-buffer": "^5.1.2",
"which-typed-array": "^1.1.2"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"web-streams-polyfill": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
"integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q=="
},
"which-boxed-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
"integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
"requires": {
"is-bigint": "^1.0.1",
"is-boolean-object": "^1.1.0",
"is-number-object": "^1.0.4",
"is-string": "^1.0.5",
"is-symbol": "^1.0.3"
}
},
"which-typed-array": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz",
"integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==",
"requires": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2",
"es-abstract": "^1.18.5",
"foreach": "^2.0.5",
"has-tostringtag": "^1.0.0",
"is-typed-array": "^1.1.7"
}
},
"ws": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w=="
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "evcluster",
"type": "module",
"dependencies": {
"evernode-js-client": "0.0.19-alpha",
"node-fetch": "3.0.0"
}
}

View File

@@ -3,39 +3,44 @@
# This must be executed with root privileges.
# This script performs following activities.
# 1. Install Sashimono prerequisites and reboots.
# 2. Install Sashimono after reboot.
# 3. Restarts cgroup engine service after installing Sashimono.
# 1. Install debugging tools.
# 2. Install Sashimono prerequisites and reboots.
# 3. Install Sashimono after reboot.
# 4. Reboot after installing Sashimono.
# Install debugging tools
# sqlite is used to fetch lcl from cluster script
# smem is a memory reporting tool used to analyze instance users memory usage.
apt-get -y install sqlite3 smem
cloudstorage="https://sthotpocket.blob.core.windows.net/sashimono"
bootscript="/usr/local/bin/sashimono-install.sh"
cgrulesengd_service="cgrulesengdsvc"
boot_service="sashimonoboot"
# Create boot script.
echo "#!/bin/bash
# Install Sashimono and restart cgrules service.
curl -fsSL $cloudstorage/install.sh | bash
echo Restarting $cgrulesengd_service
systemctl restart $cgrulesengd_service
# Remove ourselves.
rm $bootscript
systemctl disable $boot_service
rm /etc/systemd/system/$boot_service.service
systemctl daemon-reload" >$bootscript
systemctl daemon-reload
# Install Sashimono and reboot.
curl -fsSL $cloudstorage/install.sh | bash -s -- -q
reboot" >$bootscript
chmod +x $bootscript
echo "[Unit]
Description=Sashimono test vm setup one-time boot script
ConditionPathExists=$bootscript
[Service]
ExecStart=$bootscript
ExecStart=$bootscript -q
[Install]
WantedBy=multi-user.target" >/etc/systemd/system/$boot_service.service
# Schedule the boot script to run at startup.
systemctl enable $boot_service
# Install Sashimono prerequisites and reboot.
curl -fsSL $cloudstorage/prereq.sh | bash -s -- -q
curl -fsSL $cloudstorage/prereq.sh | bash -s -- -q