main.cpp 28.7 KB
Newer Older
1
/**
2 3 4
 *  @file
 *  @copyright defined in eos/LICENSE.txt
 *  @brief launch testnet nodes
P
Phil Mesnier 已提交
5 6 7 8 9
 **/
#include <string>
#include <vector>
#include <math.h>

10
#include <boost/algorithm/string.hpp>
11 12
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ip/host_name.hpp>
P
Phil Mesnier 已提交
13 14 15 16
#include <boost/program_options.hpp>
#include <boost/process/child.hpp>
#include <boost/process/system.hpp>
#include <boost/process/io.hpp>
17
#include <boost/lexical_cast.hpp>
P
Phil Mesnier 已提交
18 19 20
#include <boost/filesystem.hpp>
#include <boost/filesystem/path.hpp>
#include <fc/io/json.hpp>
21
#include <fc/network/ip.hpp>
22
#include <fc/reflect/variant.hpp>
23 24 25 26
#include <ifaddrs.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <net/if.h>
P
Phil Mesnier 已提交
27

28 29
#include "config.hpp"

P
Phil Mesnier 已提交
30 31 32 33
using namespace std;
namespace bf = boost::filesystem;
namespace bp = boost::process;
namespace bpo = boost::program_options;
34 35
using boost::asio::ip::tcp;
using boost::asio::ip::host_name;
P
Phil Mesnier 已提交
36 37 38
using bpo::options_description;
using bpo::variables_map;

39
struct local_identity {
40 41 42 43 44
  vector <fc::ip::address> addrs;
  vector <string> names;

  void initialize () {
    names.push_back ("localhost");
45 46
    names.push_back ("127.0.0.1");

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
    boost::system::error_code ec;
    string hn = host_name (ec);
    if (ec.value() != boost::system::errc::success) {
      cerr << "unable to retrieve host name: " << ec.message() << endl;
    }
    else {
      names.push_back (hn);
      if (hn.find ('.') != string::npos) {
        names.push_back (hn.substr (0,hn.find('.')));
      }
    }

    ifaddrs *ifap = 0;
    if (::getifaddrs (&ifap) == 0) {
      for (ifaddrs *p_if = ifap; p_if != 0; p_if = p_if->ifa_next) {
        if (p_if->ifa_addr != 0 &&
            p_if->ifa_addr->sa_family == AF_INET &&
            (p_if->ifa_flags & IFF_UP) == IFF_UP) {
          sockaddr_in *ifaddr = reinterpret_cast<sockaddr_in *>(p_if->ifa_addr);
          int32_t in_addr = ntohl(ifaddr->sin_addr.s_addr);

          if (in_addr != 0) {
            fc::ip::address ifa(in_addr);
            addrs.push_back (ifa);
          }
        }
      }
      ::freeifaddrs (ifap);
    }
    else {
      cerr << "unable to query local ip interfaces" << endl;
      addrs.push_back (fc::ip::address("127.0.0.1"));
    }
  }

  bool contains (const string &name) const {
    try {
      fc::ip::address test(name);
      for (const auto &a : addrs) {
        if (a == test)
          return true;
      }
    }
    catch (...) {
      // not an ip address
      for (const auto n : names) {
        if (n == name)
          return true;
      }
    }
    return false;
  }

} local_id;

P
Phil Mesnier 已提交
102

103 104 105
struct keypair {
  string public_key;
  string wif_private_key;
P
Phil Mesnier 已提交
106

107 108 109 110 111
  keypair ()
    : public_key("EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV"),
      wif_private_key("5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3")
  {}
};
P
Phil Mesnier 已提交
112

113 114 115 116 117 118
class eosd_def;

class host_def {
public:
  host_def ()
    : genesis("./genesis.json"),
119 120
      ssh_identity (""),
      ssh_args (""),
121
      eos_root_dir(),
122
      host_name("127.0.0.1"),
123
      public_name("localhost"),
124 125 126 127 128 129 130 131
      listen_addr("0.0.0.0"),
      base_p2p_port(9876),
      base_http_port(8888),
      def_file_size(8192),
      instances(),
      p2p_count(0),
      http_count(0),
      dot_label_str()
132 133
  {}

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
  string           genesis;
  string           ssh_identity;
  string           ssh_args;
  string           eos_root_dir;
  string           host_name;
  string           public_name;
  string           listen_addr;
  uint16_t         base_p2p_port;
  uint16_t         base_http_port;
  uint16_t         def_file_size;
  vector<eosd_def> instances;

  uint16_t p2p_port() {
    return base_p2p_port + p2p_count++;
  }

  uint16_t http_port() {
    return base_http_port + http_count++;
  }

  bool is_local( ) {
    return local_id.contains( host_name );
156 157
  }

158 159 160
  const string &dot_label () {
    if (dot_label_str.empty() ) {
      mk_dot_label();
P
Phil Mesnier 已提交
161
    }
162
    return dot_label_str;
163
  }
P
Phil Mesnier 已提交
164

165 166 167 168 169 170 171 172 173 174

private:
  uint16_t p2p_count;
  uint16_t http_count;
  string   dot_label_str;

protected:
  void mk_dot_label() {
    if (public_name.empty()) {
      dot_label_str = host_name;
175
    }
176 177
    else if (boost::iequals(public_name,host_name)) {
      dot_label_str = public_name;
178
    }
179 180
    else
      dot_label_str = public_name + "/" + host_name;
181
  }
182
};
183

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
class tn_node_def;

class eosd_def {
public:
  string       data_dir;
  uint16_t     p2p_port;
  uint16_t     http_port;
  uint16_t     file_size;
  bool         has_db;
  string       name;
  tn_node_def* node;
  string       host;
  string       p2p_endpoint;

  void set_host (host_def* h);
  void mk_dot_label ();
  const string &dot_label () {
    if (dot_label_str.empty() ) {
      mk_dot_label();
    }
    return dot_label_str;
  }
206 207

private:
208
  string dot_label_str;
209
};
P
Phil Mesnier 已提交
210

211 212 213 214 215 216 217
class tn_node_def {
public:
  string          name;
  vector<keypair> keys;
  vector<string>  peers;
  vector<string>  producers;
  eosd_def*       instance;
218
};
P
Phil Mesnier 已提交
219

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
void
eosd_def::mk_dot_label () {
  dot_label_str = name + "\\nprod=";
  if (node == 0 || node->producers.empty()) {
    dot_label_str += "<none>";
  }
  else {
    bool docomma = false;
    for (auto &prod: node->producers) {
      if (docomma)
        dot_label_str += ",";
      else
        docomma = true;
      dot_label_str += prod;
    }
  }
}

void
eosd_def::set_host( host_def* h ) {
  host = h->host_name;
  has_db = false;
  p2p_port = h->p2p_port();
  http_port = h->http_port();
  file_size = h->def_file_size;
  p2p_endpoint = h->public_name + ":" + boost::lexical_cast<string, uint16_t>(p2p_port);
}

248 249 250 251 252 253 254
struct remote_deploy {
  string ssh_cmd = "/usr/bin/ssh";
  string scp_cmd = "/usr/bin/scp";
  string ssh_identity;
  string ssh_args;
  string local_config_file = "temp_config";
};
P
Phil Mesnier 已提交
255

256 257 258 259
struct host_map_def {
  map <string, host_def> bindings;
};

260 261
struct testnet_def {
  remote_deploy ssh_helper;
262
  map <string,tn_node_def> nodes;
263
};
P
Phil Mesnier 已提交
264 265


266 267 268 269 270
struct node_rt_info {
  bool remote;
  string pid_file;
  string kill_cmd;
};
P
Phil Mesnier 已提交
271

272 273 274
struct last_run_def {
  vector <node_rt_info> running_nodes;
};
P
Phil Mesnier 已提交
275 276


277 278 279 280
enum launch_modes {
  LM_NONE,
  LM_LOCAL,
  LM_REMOTE,
281 282 283
  LM_NAMED,
  LM_ALL,
  LM_VERIFY
284
};
P
Phil Mesnier 已提交
285

286 287 288 289 290 291 292
enum allowed_connection : char {
  PC_NONE = 0,
  PC_PRODUCERS = 1 << 0,
  PC_SPECIFIED = 1 << 1,
  PC_ANY = 1 << 2
};

293
struct launcher_def {
294 295 296
  size_t producers;
  size_t total_nodes;
  size_t prod_nodes;
297
  size_t next_node;
298
  string shape;
299
  allowed_connection allowed_connections = PC_NONE;
300 301
  bf::path genesis;
  bf::path output;
302 303
  bf::path host_map_file;
  string data_dir_base;
304
  bool skip_transaction_signatures = false;
305
  string eosd_extra_args;
306
  testnet_def network;
307
  string alias_base;
308
  vector <string> aliases;
309
  host_map_def host_map;
310
  last_run_def last_run;
311
  int start_delay;
312 313 314 315
  bool nogen;
  string launch_name;

  void assign_name (eosd_def &node);
316 317 318 319

  void set_options (bpo::options_description &cli);
  void initialize (const variables_map &vmap);
  bool generate ();
320 321 322
  void define_local ();
  void bind_nodes ();
  void write_config_file (tn_node_def &node);
323 324 325 326
  void make_ring ();
  void make_star ();
  void make_mesh ();
  void make_custom ();
327
  void write_dot_file ();
328 329
  void format_ssh (const string &cmd, const string &host_name, string &ssh_cmd_line);
  bool do_ssh (const string &cmd, const string &host_name);
330 331
  void prep_remote_config_dir (eosd_def &node);
  void launch (eosd_def &node, string &gts);
332
  void kill (launch_modes mode, string sig_opt);
333 334
  void start_all (string &gts, launch_modes mode);
};
P
Phil Mesnier 已提交
335

336 337 338
void
launcher_def::set_options (bpo::options_description &cli) {
  cli.add_options()
339 340 341
    ("nodes,n",bpo::value<size_t>(&total_nodes)->default_value(1),"total number of nodes to configure and launch")
    ("pnodes,p",bpo::value<size_t>(&prod_nodes)->default_value(1),"number of nodes that are producers")
    ("mode,m",bpo::value<vector<string>>()->multitoken()->default_value({"any"}, "any"),"connection mode, combination of \"any\", \"producers\", \"specified\", \"none\"")
342
    ("shape,s",bpo::value<string>(&shape)->default_value("star"),"network topology, use \"star\" \"mesh\" or give a filename for custom")
343 344 345 346 347
    ("genesis,g",bpo::value<bf::path>(&genesis)->default_value("./genesis.json"),"set the path to genesis.json")
    ("output,o",bpo::value<bf::path>(&output),"save a copy of the generated topology in this file")
    ("skip-signature", bpo::bool_switch(&skip_transaction_signatures)->default_value(false), "EOSD does not require transaction signatures.")
    ("eosd", bpo::value<string>(&eosd_extra_args), "forward eosd command line argument(s) to each instance of eosd, enclose arg in quotes")
    ("delay,d",bpo::value<int>(&start_delay)->default_value(0),"seconds delay before starting each node after the first")
348
    ("nogen",bpo::bool_switch(&nogen)->default_value(false),"launch nodes without writing new config files")
349
    ("host-map",bpo::value<bf::path>(&host_map_file)->default_value(""),"a file containing mapping specific nodes to hosts. Used to enhance the custom shape argument")
350
        ;
351
}
352

353 354 355 356 357 358 359
template<class enum_type, class=typename std::enable_if<std::is_enum<enum_type>::value>::type>
inline enum_type& operator|=(enum_type&lhs, const enum_type& rhs)
{
  using T = std::underlying_type_t <enum_type>;
  return lhs = static_cast<enum_type>(static_cast<T>(lhs) | static_cast<T>(rhs));
}

360 361
void
launcher_def::initialize (const variables_map &vmap) {
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
  if (vmap.count("mode")) {
    const vector<string> modes = vmap["mode"].as<vector<string>>();
    for(const string&m : modes)
    {
      if (boost::iequals(m, "any"))
        allowed_connections |= PC_ANY;
      else if (boost::iequals(m, "producers"))
        allowed_connections |= PC_PRODUCERS;
      else if (boost::iequals(m, "specified"))
        allowed_connections |= PC_SPECIFIED;
      else {
        cerr << "unrecognized connection mode: " << m << endl;
        exit (-1);
      }
    }
  }
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398

  if ( ! (shape.empty() ||
          boost::iequals( shape, "ring" ) ||
          boost::iequals( shape, "star" ) ||
          boost::iequals( shape, "mesh" )) &&
       host_map_file.empty()) {
    bf::path src = shape;
    host_map_file = src.stem().string() + "_hosts.json";
  }

  if( !host_map_file.empty() ) {
    try {
      fc::json::from_file(host_map_file).as<host_map_def>(host_map);
      for (auto &binding : host_map.bindings) {
        for (auto &eosd : binding.second.instances) {
          aliases.push_back (eosd.name);
        }
      }
    } catch (...) { // this is an optional feature, so an exception is OK
    }
  }
399 400 401 402

  producers = 21;
  data_dir_base = "tn_data_";
  alias_base = "testnet_";
403
  next_node = 0;
404 405 406 407 408

  if (prod_nodes > producers)
    prod_nodes = producers;
  if (prod_nodes > total_nodes)
    total_nodes = prod_nodes;
409 410 411 412 413 414 415 416 417 418 419

  if (host_map.bindings.empty()) {
    define_local ();
  }
}

void
launcher_def::assign_name (eosd_def &node) {
  string dex = boost::lexical_cast<string,int>(next_node++);
  node.name = alias_base + dex;
  node.data_dir = data_dir_base + dex;
420
}
421

422 423
bool
launcher_def::generate () {
424

425 426 427 428 429 430 431 432 433 434 435 436
  if (boost::iequals (shape,"ring")) {
    make_ring ();
  }
  else if (boost::iequals (shape, "star")) {
    make_star ();
  }
  else if (boost::iequals (shape, "mesh")) {
    make_mesh ();
  }
  else {
    make_custom ();
  }
437 438 439

  if( !nogen ) {
    for (auto &node : network.nodes) {
440
      write_config_file(node.second);
441 442
    }
    write_dot_file ();
P
Phil Mesnier 已提交
443
  }
444

445 446
  if (!output.empty()) {
    bf::path savefile = output;
447 448 449 450 451 452
    {
      bf::ofstream sf (savefile);

      sf << fc::json::to_pretty_string (network) << endl;
      sf.close();
    }
453 454 455 456 457 458
    if (host_map_file.empty()) {
      savefile = bf::path (output.stem().string() + "_hosts.json");
    }
    else {
      savefile = bf::path (host_map_file);
    }
459 460 461 462 463 464 465 466

    {

      bf::ofstream sf (savefile);

      sf << fc::json::to_pretty_string (host_map) << endl;
      sf.close();
    }
P
Phil Mesnier 已提交
467

468 469 470 471
    return false;
  }
  return true;
}
472

473 474 475
void
launcher_def::write_dot_file () {
  bf::ofstream df ("testnet.dot");
476
  df << "digraph G\n{\nlayout=\"circo\";\n";
477 478
  for (auto &node : network.nodes) {
    for (const auto &p : node.second.peers) {
479 480
      string pname=network.nodes.find(p)->second.instance->dot_label();
      df << "\"" << node.second.instance->dot_label ()
481
         << "\"->\"" << pname
482
         << "\" [dir=\"forward\"];" << std::endl;
483 484
    }
  }
485 486 487 488 489 490 491 492 493 494 495 496
  #if 0
  // this is an experiment. I was thinking of adding a "notes" box but I
  // can't figure out hot to force it to the lower left corner of the diagram
  df << " { rank = sink;\n"
     << "   Notes  [shape=none, margin=0, label=<\n"
     << "    <TABLE BORDER=\"1\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"1\">\n"
     << "     <TR> <TD align=\"left\">Data flow between nodes is bidirectional</TD> </TR>\n"
     << "     <TR> <TD align=\"left\">Arrows point from client to server</TD> </TR>\n"
     << "    </TABLE>\n"
     << "   >];"
     << " }\n";
#endif
497 498 499
  df << "}\n";
}

500
void
501 502
launcher_def::define_local () {
  host_def local_host;
503 504 505 506 507 508
  char * erd = getenv ("EOS_ROOT_DIR");
  if (erd == 0) {
    cerr << "environment variable \"EOS_ROOT_DIR\" unset. defaulting to current dir" << endl;
    erd = getenv ("PWD");
  }
  local_host.eos_root_dir = erd;
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
  local_host.genesis = genesis.string();

  for (size_t i = 0; i < total_nodes; i++) {
    eosd_def eosd;

    assign_name(eosd);
    aliases.push_back(eosd.name);
    eosd.set_host (&local_host);
    local_host.instances.emplace_back(move(eosd));
  }
  host_map.bindings[local_host.host_name] = move(local_host);
}

void
launcher_def::bind_nodes () {
524 525
  int per_node = producers / prod_nodes;
  int extra = producers % prod_nodes;
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
  int i = 0;
  for (auto &h : host_map.bindings) {
    for (auto &inst : h.second.instances) {
      tn_node_def node;
      node.name = inst.name;
      node.instance = &inst;
      keypair kp;
      node.keys.emplace_back (move(kp));
      if (i < prod_nodes) {
        int count = per_node;
        if (extra) {
          ++count;
          --extra;
        }
        char ext = 'a' + i;
        string pname = "init";
        while (count--) {
          node.producers.push_back(pname + ext);
          ext += prod_nodes;
        }
P
Phil Mesnier 已提交
546
      }
547
      network.nodes[node.name] = move(node);
548
      inst.node = &network.nodes[inst.name];
549
      i++;
P
Phil Mesnier 已提交
550
    }
551 552
  }
}
P
Phil Mesnier 已提交
553

554
void
555
launcher_def::write_config_file (tn_node_def &node) {
556 557
  bf::path filename;
  boost::system::error_code ec;
558 559 560 561
  eosd_def &instance = *node.instance;
  host_def *host = &host_map.bindings[instance.host];
  if (host->is_local()) {
    bf::path dd = bf::path(host->eos_root_dir) / instance.data_dir;
562 563 564 565 566 567 568
    filename = dd / "config.ini";
    if (bf::exists (dd)) {
      int64_t count =  bf::remove_all (dd, ec);
      if (ec.value() != 0) {
        cerr << "count = " << count << " could not remove old directory: " << dd
             << " " << strerror(ec.value()) << endl;
        exit (-1);
P
Phil Mesnier 已提交
569 570
      }
    }
571 572
    if (!bf::create_directory (instance.data_dir, ec) && ec.value()) {
      cerr << "could not create new directory: " << instance.data_dir
573 574
           << " errno " << ec.value() << " " << strerror(ec.value()) << endl;
      exit (-1);
575
    }
P
Phil Mesnier 已提交
576
  }
577 578 579
  else {
    filename = network.ssh_helper.local_config_file;
  }
P
Phil Mesnier 已提交
580

581 582 583 584 585 586
  bf::ofstream cfg(filename);
  if (!cfg.good()) {
    cerr << "unable to open " << filename << " " << strerror(errno) << "\n";
    exit (-1);
  }

587
  cfg << "genesis-json = " << host->genesis << "\n"
588 589
      << "block-log-dir = blocks\n"
      << "readonly = 0\n"
590
      << "send-whole-blocks = true\n"
591
      << "shared-file-dir = blockchain\n"
592 593 594 595
      << "shared-file-size = " << instance.file_size << "\n"
      << "http-server-endpoint = " << host->host_name << ":" << instance.http_port << "\n"
      << "listen-endpoint = " << host->listen_addr << ":" << instance.p2p_port << "\n"
      << "public-endpoint = " << host->public_name << ":" << instance.p2p_port << "\n";
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
  if (allowed_connections & PC_ANY) {
    cfg << "allowed-connection = any\n";
  }
  else
  {
    if (allowed_connections & PC_PRODUCERS) {
      cfg << "allowed-connection = producers\n";
    }
    if (allowed_connections & PC_SPECIFIED) {
      cfg << "allowed-connection = specified\n";
      cfg << "peer-key = \"" << node.keys.begin()->public_key << "\"\n";
      cfg << "peer-private-key = [\"" << node.keys.begin()->public_key
          << "\",\"" << node.keys.begin()->wif_private_key << "\"]\n";
    }
  }
611
  for (const auto &p : node.peers) {
612
    cfg << "remote-endpoint = " << network.nodes.find(p)->second.instance->p2p_endpoint << "\n";
613 614
  }
  if (node.producers.size()) {
615
    cfg << "enable-stale-production = false\n"
616 617 618 619
        << "required-participation = true\n";
    for (const auto &kp : node.keys ) {
      cfg << "private-key = [\"" << kp.public_key
          << "\",\"" << kp.wif_private_key << "\"]\n";
P
Phil Mesnier 已提交
620
    }
P
Pravin 已提交
621 622 623 624 625 626
    cfg << "plugin = eosio::producer_plugin\n"
        << "plugin = eosio::chain_api_plugin\n"
        << "plugin = eosio::wallet_api_plugin\n"
        << "plugin = eosio::db_plugin\n"
        << "plugin = eosio::account_history_plugin\n"
        << "plugin = eosio::account_history_api_plugin\n";
627 628
    for (auto &p : node.producers) {
      cfg << "producer-name = " << p << "\n";
P
Phil Mesnier 已提交
629 630
    }
  }
631 632 633 634 635 636
  if( instance.has_db ) {
    if( !node.producers.size() ) {
      cfg << "plugin = eosio::producer_plugin\n";
    }
    cfg << "plugin = eosio::db_plugin\n";
  }
637 638 639 640
  cfg << "plugin = eosio::chain_api_plugin\n"
      << "plugin = eosio::wallet_api_plugin\n"
      << "plugin = eosio::account_history_plugin\n"
      << "plugin = eosio::account_history_api_plugin\n";
641
  cfg.close();
642 643
  if (!host->is_local()) {
    prep_remote_config_dir (instance);
644
    string scp_cmd_line = network.ssh_helper.scp_cmd + " ";
645
    const string &args = host->ssh_args.length() ? host->ssh_args : network.ssh_helper.ssh_args;
646 647 648 649
    if (args.length()) {
      scp_cmd_line += args + " ";
    }
    scp_cmd_line += filename.string() + " ";
P
Phil Mesnier 已提交
650

651
    const string &uid = host->ssh_identity.length() ? host->ssh_identity : network.ssh_helper.ssh_identity;
652 653
    if (uid.length()) {
      scp_cmd_line += uid + "@";
P
Phil Mesnier 已提交
654 655
    }

656 657
    bf::path dpath = bf::path (host->eos_root_dir) / instance.data_dir / "config.ini";
    scp_cmd_line += host->host_name + ":" + dpath.string();
658 659 660 661

    cerr << "cmdline = " << scp_cmd_line << endl;
    int res = boost::process::system (scp_cmd_line);
    if (res != 0) {
662
      cerr << "unable to scp config file to host " << host->host_name << endl;
663
      exit(-1);
664 665
    }
  }
666 667 668 669
}

void
launcher_def::make_ring () {
670
  bind_nodes();
671
  if (total_nodes > 2) {
672
    for (size_t i = 0; i < total_nodes; i++) {
673 674
      size_t front = (i + 1) % total_nodes;
      network.nodes.find(aliases[i])->second.peers.push_back (aliases[front]);
675 676
    }
  }
677 678 679
  else if (total_nodes == 2) {
    network.nodes.find(aliases[0])->second.peers.push_back (aliases[1]);
    network.nodes.find(aliases[1])->second.peers.push_back (aliases[0]);
680
  }
681
}
682

683 684
void
launcher_def::make_star () {
685
  bind_nodes();
686 687 688 689
  if (total_nodes < 4) {
    make_ring ();
    return;
  }
690

691 692 693 694
  size_t links = 3;
  if (total_nodes > 12) {
    links = (size_t)sqrt(total_nodes);
  }
695 696 697 698
  size_t gap = total_nodes > 6 ? 3 : (total_nodes - links)/2 +1;
  while (total_nodes % gap == 0) {
    ++gap;
  }
699 700
  // use to prevent duplicates since all connections are bidirectional
  std::map <string, std::set<string>> peers_to_from;
701
  for (size_t i = 0; i < total_nodes; i++) {
702 703 704
    const auto& iter = network.nodes.find(aliases[i]);
    auto &current = iter->second;
    const auto& current_name = iter->first;
705 706 707 708
    for (size_t l = 1; l <= links; l++) {
      size_t ndx = (i + l * gap) % total_nodes;
      if (i == ndx) {
        ++ndx;
709 710 711
        if (ndx == total_nodes) {
          ndx = 0;
        }
712
      }
713 714 715 716 717
      auto &peer = aliases[ndx];
      for (bool found = true; found; ) {
        found = false;
        for (auto &p : current.peers) {
          if (p == peer) {
718 719 720 721
            ++ndx;
            if (ndx == total_nodes) {
              ndx = 0;
            }
722

723 724
            peer = aliases[ndx];

725 726 727 728
            found = true;
            break;
          }
        }
729
      }
730 731 732 733 734 735
      // if already established, don't add to list
      if (peers_to_from[peer].count(current_name) == 0) {
        current.peers.push_back(peer); // current_name -> peer
        // keep track of bidirectional relationships to prevent duplicates
        peers_to_from[current_name].insert(peer);
      }
736
    }
737 738
  }
}
739

740 741
void
launcher_def::make_mesh () {
742
  bind_nodes();
743 744
  // use to prevent duplicates since all connections are bidirectional
  std::map <string, std::set<string>> peers_to_from;
745
  for (size_t i = 0; i < total_nodes; i++) {
746 747 748
    const auto& iter = network.nodes.find(aliases[i]);
    auto &current = iter->second;
    const auto& current_name = iter->first;
749 750
    for (size_t j = 1; j < total_nodes; j++) {
      size_t ndx = (i + j) % total_nodes;
751 752 753 754 755 756 757
      const auto& peer = aliases[ndx];
      // if already established, don't add to list
      if (peers_to_from[peer].count(current_name) == 0) {
        current.peers.push_back (peer);
        // keep track of bidirectional relationships to prevent duplicates
        peers_to_from[current_name].insert(peer);
      }
758 759 760
    }
  }
}
761

762 763 764 765
void
launcher_def::make_custom () {
  bf::path source = shape;
  fc::json::from_file(source).as<testnet_def>(network);
766 767 768 769 770 771 772
  for (auto &h : host_map.bindings) {
    for (auto &inst : h.second.instances) {
      tn_node_def *node = &network.nodes[inst.name];
      node->instance = &inst;
      inst.node = node;
    }
  }
773
}
774

775 776
void
launcher_def::format_ssh (const string &cmd,
777
                          const string &host_name,
778
                          string & ssh_cmd_line) {
779

780 781 782 783 784 785
  ssh_cmd_line = network.ssh_helper.ssh_cmd + " ";
  if (network.ssh_helper.ssh_args.length()) {
    ssh_cmd_line += network.ssh_helper.ssh_args + " ";
  }
  if (network.ssh_helper.ssh_identity.length()) {
    ssh_cmd_line += network.ssh_helper.ssh_identity + "@";
786
  }
787
  ssh_cmd_line += host_name + " \"" + cmd + "\"";
788 789 790 791
  cerr << "cmdline = " << ssh_cmd_line << endl;
}

bool
792
launcher_def::do_ssh (const string &cmd, const string &host_name) {
793
  string ssh_cmd_line;
794
  format_ssh (cmd, host_name, ssh_cmd_line);
795 796 797
  int res = boost::process::system (ssh_cmd_line);
  return (res == 0);
}
798

799 800
void
launcher_def::prep_remote_config_dir (eosd_def &node) {
801 802
  host_def* host = &host_map.bindings[node.host];
  bf::path abs_data_dir = bf::path(host->eos_root_dir) / node.data_dir;
803
  string add = abs_data_dir.string();
804 805 806 807
  string cmd = "cd " + host->eos_root_dir;
  if (!do_ssh(cmd, host->host_name)) {
    cerr << "Unable to switch to path " << host->eos_root_dir
         << " on host " <<  host->host_name << endl;
808 809 810
    exit (-1);
  }
  cmd = "cd " + add;
811
  if (do_ssh(cmd,host->host_name)) {
812
    cmd = "rm -rf " + add + "/block*";
813
    if (!do_ssh (cmd, host->host_name)) {
814
      cerr << "Unable to remove old data directories on host "
815
           << host->host_name << endl;
816 817
      exit (-1);
    }
818 819 820
  }
  else {
    cmd = "mkdir " + add;
821
    if (!do_ssh (cmd, host->host_name)) {
822
      cerr << "Unable to invoke " << cmd << " on host "
823
           << host->host_name << endl;
824
      exit (-1);
P
Phil Mesnier 已提交
825 826
    }
  }
827
}
P
Phil Mesnier 已提交
828

829 830 831 832 833 834
void
launcher_def::launch (eosd_def &node, string &gts) {
  bf::path dd = node.data_dir;
  bf::path reout = dd / "stdout.txt";
  bf::path reerr = dd / "stderr.txt";
  bf::path pidf  = dd / "eosd.pid";
835

836 837
  host_def* host = &host_map.bindings[node.host];

838
  node_rt_info info;
839
  info.remote = !host->is_local();
840

841 842 843 844
  string eosdcmd = "programs/eosd/eosd ";
  if (skip_transaction_signatures) {
    eosdcmd += "--skip-transaction-signatures ";
  }
845
  eosdcmd += eosd_extra_args + " ";
846
  eosdcmd += "--data-dir " + node.data_dir;
847 848 849
  if (gts.length()) {
    eosdcmd += " --genesis-timestamp " + gts;
  }
850

851
  if (info.remote) {
852
    string cmdl ("cd ");
853
    cmdl += host->eos_root_dir + "; nohup " + eosdcmd + " > "
854
      + reout.string() + " 2> " + reerr.string() + "& echo $! > " + pidf.string();
855
    if (!do_ssh (cmdl, host->host_name)){
856
      cerr << "Unable to invoke " << cmdl
857
           << " on host " << host->host_name << endl;
858
      exit (-1);
859 860
    }

861 862
    string cmd = "cd " + host->eos_root_dir + "; kill -9 `cat " + pidf.string() + "`";
    format_ssh (cmd, host->host_name, info.kill_cmd);
863 864
  }
  else {
865 866
    cerr << "spawning child, " << eosdcmd << endl;

867 868 869 870 871 872
    bp::child c(eosdcmd, bp::std_out > reout, bp::std_err > reerr );

    bf::ofstream pidout (pidf);
    pidout << c.id() << flush;
    pidout.close();

873 874 875
    info.pid_file = pidf.string();
    info.kill_cmd = "";

876
    if(!c.running()) {
877
      cerr << "child not running after spawn " << eosdcmd << endl;
878 879 880 881 882
      for (int i = 0; i > 0; i++) {
        if (c.running () ) break;
      }
    }
    c.detach();
P
Phil Mesnier 已提交
883
  }
884
  last_run.running_nodes.emplace_back (move(info));
885 886 887
}

void
888
launcher_def::kill (launch_modes mode, string sig_opt) {
889 890 891 892 893 894 895 896 897 898 899
  if (mode == LM_NONE) {
    return;
  }
  bf::path source = "last_run.json";
  fc::json::from_file(source).as<last_run_def>(last_run);
  for (auto &info : last_run.running_nodes) {
    if (mode == LM_ALL || (info.remote && mode == LM_REMOTE) ||
        (!info.remote && mode == LM_LOCAL)) {
      if (info.pid_file.length()) {
        string pid;
        fc::json::from_file(info.pid_file).as<string>(pid);
900
        string kill_cmd = "kill " + sig_opt + " " + pid;
901 902 903 904 905 906 907 908
        boost::process::system (kill_cmd);
      }
      else {
        boost::process::system (info.kill_cmd);
      }
    }
  }
}
P
Phil Mesnier 已提交
909

910 911
void
launcher_def::start_all (string &gts, launch_modes mode) {
912 913
  switch (mode) {
  case LM_NONE:
914
    return;
915 916 917 918 919 920 921 922
  case LM_VERIFY:
    //validate configuration, report findings, exit
    return;
  case LM_NAMED : {
    try {
      auto node = network.nodes.find(launch_name);
      launch(*node->second.instance, gts);
    } catch (...) {
923
      cerr << "Unable to launch " << launch_name << endl;
K
Kevin Heifner 已提交
924
      exit (-1);
925 926 927 928 929 930 931 932 933 934
    }
    break;
  }
  case LM_ALL:
  case LM_REMOTE:
  case LM_LOCAL: {
    for (auto &h : host_map.bindings ) {
      if (mode == LM_ALL ||
          (h.second.is_local() ? mode == LM_LOCAL : mode == LM_REMOTE)) {
        for (auto &inst : h.second.instances) {
935 936 937 938 939
          try {
            launch (inst, gts);
          } catch (...) {
            cerr << "unable to launch " << inst.name << endl;
          }
940
          sleep (start_delay);
941
        }
942
      }
943
    }
944 945
    break;
  }
946
  }
947 948 949 950 951 952 953 954
  bf::path savefile = "last_run.json";
  bf::ofstream sf (savefile);

  sf << fc::json::to_pretty_string (last_run) << endl;
  sf.close();
}

//------------------------------------------------------------
P
Phil Mesnier 已提交
955 956 957 958 959

int main (int argc, char *argv[]) {

  variables_map vmap;
  options_description opts ("Testnet launcher options");
960
  launcher_def top;
P
Phil Mesnier 已提交
961
  string gts;
962
  launch_modes mode;
963
  string kill_arg;
964 965

  local_id.initialize();
966 967
  top.set_options(opts);

P
Phil Mesnier 已提交
968
  opts.add_options()
969
    ("timestamp,i",bpo::value<string>(&gts),"set the timestamp for the first block. Use \"now\" to indicate the current time")
970
    ("launch,l",bpo::value<string>(), "select a subset of nodes to launch. Currently may be \"all\", \"none\", or \"local\". If not set, the default is to launch all unless an output file is named, in which case it starts none.")
971
    ("kill,k", bpo::value<string>(&kill_arg),"The launcher retrieves the previously started process ids and issue a kill signal to each.")
972
    ("version,v", "print version information")
973 974
    ("help,h","print this list");

P
Phil Mesnier 已提交
975

976 977
  try {
    bpo::store(bpo::parse_command_line(argc, argv, opts), vmap);
978
    bpo::notify(vmap);
P
Phil Mesnier 已提交
979

980
    top.initialize(vmap);
P
Phil Mesnier 已提交
981

982 983 984 985
    if (vmap.count("help") > 0) {
      opts.print(cerr);
      return 0;
    }
986 987 988 989
    if (vmap.count("version") > 0) {
      cout << eosio::launcher::config::version_str << endl;
      return 0;
    }
990 991 992 993 994 995 996 997 998 999 1000

    if (vmap.count("launch")) {
      string l = vmap["launch"].as<string>();
      if (boost::iequals(l,"all"))
        mode = LM_ALL;
      else if (boost::iequals(l,"local"))
        mode = LM_LOCAL;
      else if (boost::iequals(l,"remote"))
        mode = LM_REMOTE;
      else if (boost::iequals(l,"none"))
        mode = LM_NONE;
1001 1002
      else if (boost::iequals(l,"verify"))
        mode = LM_VERIFY;
1003
      else {
1004 1005
        mode = LM_NAMED;
        top.launch_name = l;
1006 1007 1008 1009 1010
        cerr << "unrecognized launch mode: " << l << endl;
        exit (-1);
      }
    }
    else {
1011
      mode = !kill_arg.empty() || top.output.empty() ? LM_ALL : LM_NONE;
1012 1013
    }

1014
    if (!kill_arg.empty()) {
P
Phil Mesnier 已提交
1015
      cout << "killing" << std::endl;
1016 1017 1018 1019
      if (kill_arg[0] != '-') {
        kill_arg = "-" + kill_arg;
      }
      top.kill (mode, kill_arg);
1020 1021 1022 1023 1024 1025 1026 1027 1028
    }
    else {
      top.generate();
      top.start_all(gts, mode);
    }
  } catch (bpo::unknown_option &ex) {
    cerr << ex.what() << endl;
    opts.print (cerr);
  }
P
Phil Mesnier 已提交
1029 1030
  return 0;
}
1031 1032 1033 1034 1035 1036 1037 1038 1039 1040


//-------------------------------------------------------------


FC_REFLECT( keypair, (public_key)(wif_private_key) )

FC_REFLECT( remote_deploy,
            (ssh_cmd)(scp_cmd)(ssh_identity)(ssh_args) )

1041 1042 1043 1044 1045 1046
FC_REFLECT( host_def,
            (genesis)(ssh_identity)(ssh_args)(eos_root_dir)
            (host_name)(public_name)
            (base_p2p_port)(base_http_port)(def_file_size)
            (instances) )

1047
FC_REFLECT( eosd_def,
1048 1049 1050 1051 1052 1053
            (name)(data_dir)(has_db)(host)(p2p_endpoint)
            (p2p_port)(http_port)(file_size) )

FC_REFLECT( tn_node_def, (name)(keys)(peers)(producers) )

FC_REFLECT( testnet_def, (ssh_helper)(nodes) )
1054

1055
FC_REFLECT( host_map_def, (bindings) )
1056

1057
FC_REFLECT( node_rt_info, (remote)(pid_file)(kill_cmd) )
1058

1059
FC_REFLECT( last_run_def, (running_nodes) )