#!/usr/bin/node // Simple systemd unit generator for etcd // Copyright (c) Vitaliy Filippov, 2019+ // License: MIT // USAGE: // 1) Put the same etcd_address into /etc/vitastor/vitastor.conf on all monitor nodes // 2) Run ./make-etcd.js. It will create the etcd service on one of specified IPs const child_process = require('child_process'); const fs = require('fs'); const os = require('os'); run().catch(e => { console.error(e); process.exit(1); }); async function run() { const config_path = process.argv[2] || '/etc/vitastor/vitastor.conf'; if (config_path == '-h' || config_path == '--help') { console.log( 'Initialize systemd etcd service for Vitastor\n'+ '(c) Vitaliy Filippov, 2019+ (MIT)\n'+ '\n'+ 'USAGE:\n'+ '1) Put the same etcd_address into /etc/vitastor/vitastor.conf on all monitor nodes\n'+ '2) Run '+process.argv[1]+' [config_path]\n' ); process.exit(0); } if (!fs.existsSync(config_path)) { console.log(config_path+' is missing'); process.exit(1); } const in_docker = fs.existsSync("/etc/vitastor/etcd.conf") && fs.existsSync("/etc/vitastor/docker.conf"); if (!in_docker && fs.existsSync("/etc/systemd/system/vitastor-etcd.service")) { console.log("/etc/systemd/system/vitastor-etcd.service already exists"); process.exit(1); } if (!in_docker && fs.existsSync("/etc/systemd/system/etcd.service")) { console.log("/etc/systemd/system/etcd.service already exists"); process.exit(1); } const config = JSON.parse(fs.readFileSync(config_path, { encoding: 'utf-8' })); if (!config.etcd_address) { console.log("etcd_address is missing in "+config_path); process.exit(1); } const etcds = (config.etcd_address instanceof Array ? config.etcd_address : (''+config.etcd_address).split(/,/)) .map(s => (''+s).replace(/^https?:\/\/|(:\d+)?(\/.*)?$/g, '').replace(/^\[(.*)\]$/, '$1').toLowerCase()); const num = select_local_etcd(etcds); if (num < 0) { console.log('No matching IPs in etcd_address from '+config_path); process.exit(0); } const etcd_url = 'http://' + (etcds[num].indexOf(':') >= 0 ? '['+etcds[num]+']' : etcds[num]); const etcd_name = 'etcd'+etcds[num].replace(/[^0-9a-z_]/ig, '_'); const etcd_cluster = etcds.map(e => `etcd${e.replace(/[^0-9a-z_]/ig, '_')}=http://${e.indexOf(':') >= 0 ? '['+e+']' : e}:2380`).join(','); if (in_docker) { let etcd_conf = fs.readFileSync("/etc/vitastor/etcd.conf", { encoding: 'utf-8' }); etcd_conf = replace_env(etcd_conf, 'ETCD_NAME', etcd_name); etcd_conf = replace_env(etcd_conf, 'ETCD_IP', etcds[num]); etcd_conf = replace_env(etcd_conf, 'ETCD_INITIAL_CLUSTER', etcd_cluster); fs.writeFileSync("/etc/vitastor/etcd.conf", etcd_conf); console.log('etcd for Vitastor configured. Run `systemctl enable --now vitastor-etcd` to start etcd'); process.exit(0); } await system(`mkdir -p /var/lib/etcd/vitastor`); fs.writeFileSync( "/etc/systemd/system/vitastor-etcd.service", `[Unit] Description=etcd for vitastor After=network-online.target local-fs.target time-sync.target Wants=network-online.target local-fs.target time-sync.target [Service] Restart=always Environment=GOGC=50 ExecStart=etcd --name ${etcd_name} --data-dir /var/lib/etcd/vitastor \\ --snapshot-count 10000 --advertise-client-urls ${etcd_url}:2379 --listen-client-urls ${etcd_url}:2379 \\ --initial-advertise-peer-urls ${etcd_url}:2380 --listen-peer-urls ${etcd_url}:2380 \\ --initial-cluster-token vitastor-etcd-1 --initial-cluster ${etcd_cluster} \\ --initial-cluster-state new --max-txn-ops=100000 --max-request-bytes=104857600 \\ --auto-compaction-retention=10 --auto-compaction-mode=revision WorkingDirectory=/var/lib/etcd/vitastor ExecStartPre=+chown -R etcd /var/lib/etcd/vitastor User=etcd PrivateTmp=false TasksMax=infinity Restart=always StartLimitInterval=0 RestartSec=10 [Install] WantedBy=multi-user.target `); await system(`useradd etcd`); await system(`systemctl daemon-reload`); // Disable distribution etcd unit and enable our one await system(`systemctl disable --now etcd`); await system(`systemctl enable --now vitastor-etcd`); process.exit(0); } function replace_env(text, key, value) { let found = false; text = text.replace(new RegExp('^'+key+'\\s*=.*', 'm'), () => { found = true; return key+'='+value; }); return found ? text : text.replace(/\s*$/, '\n')+key+'='+value+'\n'; } function select_local_etcd(etcds) { const ifaces = os.networkInterfaces(); for (const ifname in ifaces) for (const iface of ifaces[ifname]) for (let i = 0; i < etcds.length; i++) if (etcds[i] == iface.address.toLowerCase()) return i; return -1; } async function system(cmd) { const cp = child_process.spawn(cmd, { shell: true, stdio: [ 0, 1, 2 ] }); let finish_cb; cp.on('exit', () => finish_cb && finish_cb()); if (cp.exitCode == null) await new Promise(ok => finish_cb = ok); return cp.exitCode; }