diff --git a/packages/deployment/Dockerfile b/packages/deployment/Dockerfile index ffdb55420e4..b0aa0463a88 100644 --- a/packages/deployment/Dockerfile +++ b/packages/deployment/Dockerfile @@ -27,7 +27,7 @@ RUN echo 'deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main' >> /e # COPY --from=go-build /go/bin/journalbeat /usr/local/bin/ WORKDIR /usr/src/agoric-sdk/packages/deployment -RUN ln -sf $PWD/ag-setup-cosmos /usr/local/bin/ +RUN ln -sf $PWD/src/entrypoint.cjs /usr/local/bin/ag-setup-cosmos WORKDIR /data/chains diff --git a/packages/deployment/Dockerfile.sdk b/packages/deployment/Dockerfile.sdk index 5a25ccd67d2..385afc580a0 100644 --- a/packages/deployment/Dockerfile.sdk +++ b/packages/deployment/Dockerfile.sdk @@ -13,7 +13,7 @@ RUN make GIT_REVISION="$GIT_REVISION" MOD_READONLY= compile-go ############################### # The js build container -FROM node:12-buster AS build-js +FROM node:14.15-buster AS build-js ARG MODDABLE_COMMIT_HASH ARG MODDABLE_URL @@ -39,7 +39,7 @@ RUN yarn install --frozen-lockfile --production ############################### # The install container. -FROM node:12-buster AS install +FROM node:14.15-buster AS install # Install some conveniences. RUN apt-get update && apt-get install -y vim jq less && apt-get clean -y diff --git a/packages/deployment/ag-setup-cosmos b/packages/deployment/ag-setup-cosmos deleted file mode 100755 index 27845d45bba..00000000000 --- a/packages/deployment/ag-setup-cosmos +++ /dev/null @@ -1,36 +0,0 @@ -#! /usr/bin/env node -/* global require, module, setInterval */ - -const esmRequire = require('esm')(module); - -esmRequire('@agoric/install-ses'); - -const fs = require('fs'); -const path = require('path'); -const temp = require('temp'); -const process = require('process'); -const { exec, spawn } = require('child_process'); -const inquirer = require('inquirer'); -const fetch = require('node-fetch'); - -const { running } = esmRequire('./run'); -const { setup } = esmRequire('./setup'); -const files = esmRequire('./files'); -const deploy = esmRequire(`./main.js`).default; - -process.on('SIGINT', () => process.exit(-1)); -deploy(process.argv[1], process.argv.splice(2), { - env: process.env, - rd: files.reading(fs, path), - wr: files.writing(fs, path, temp), - setup: setup({ resolve: path.resolve, env: process.env, setInterval }), - running: running(process, { exec, process, spawn }), - inquirer, - fetch, -}).then( - res => process.exit(res || 0), - rej => { - console.error(`error running ag-setup-cosmos:`, rej); - process.exit(1); - }, -); diff --git a/packages/deployment/ansible/prepare-machine.yml b/packages/deployment/ansible/prepare-machine.yml index 20d22e044af..f055324a821 100644 --- a/packages/deployment/ansible/prepare-machine.yml +++ b/packages/deployment/ansible/prepare-machine.yml @@ -6,6 +6,6 @@ gather_facts: yes strategy: free vars: - - NODEJS_VERSION: 12 + - NODEJS_VERSION: 14 roles: - prereq diff --git a/packages/deployment/ansible/roles/copy/tasks/main.yml b/packages/deployment/ansible/roles/copy/tasks/main.yml index 7205d8f8d02..9f45cc0bced 100644 --- a/packages/deployment/ansible/roles/copy/tasks/main.yml +++ b/packages/deployment/ansible/roles/copy/tasks/main.yml @@ -9,7 +9,7 @@ - name: Synchronize Agoric SDK synchronize: - src: "/usr/src/agoric-sdk/" + src: "{{ AGORIC_SDK }}/" dest: "/usr/src/agoric-sdk/" dirs: yes delete: yes diff --git a/packages/deployment/ansible/update_known_hosts.yml b/packages/deployment/ansible/update_known_hosts.yml index 7db300a5844..62c34c8fdec 100644 --- a/packages/deployment/ansible/update_known_hosts.yml +++ b/packages/deployment/ansible/update_known_hosts.yml @@ -29,3 +29,9 @@ state: present line: "{{ item }}" with_items: "{{ ssh_keys.splitlines() }}" + + - name: Set master authorized SSH key + authorized_key: + user: root + state: present + key: "{{ lookup('file', SETUP_HOME + '/id_ecdsa.pub') }}" diff --git a/packages/deployment/package.json b/packages/deployment/package.json index 9c9febbabd5..035d97d95e5 100644 --- a/packages/deployment/package.json +++ b/packages/deployment/package.json @@ -6,7 +6,10 @@ "js": "mjs" }, "private": true, - "main": "main.js", + "main": "src/main.js", + "bin": { + "ag-setup-cosmos": "src/entrypoint.cjs" + }, "scripts": { "test": "exit 0", "test:xs": "exit 0", diff --git a/packages/deployment/src/entrypoint.js b/packages/deployment/src/entrypoint.js new file mode 100755 index 00000000000..009b01c0885 --- /dev/null +++ b/packages/deployment/src/entrypoint.js @@ -0,0 +1,34 @@ +#! /usr/bin/env node +/* global setInterval */ + +import '@agoric/install-ses'; + +import fs from 'fs'; +import path from 'path'; +import temp from 'temp'; +import process from 'process'; +import { exec, spawn } from 'child_process'; +import inquirer from 'inquirer'; +import fetch from 'node-fetch'; + +import { running } from './run'; +import { setup } from './setup'; +import * as files from './files'; +import deploy from './main.js'; + +process.on('SIGINT', () => process.exit(-1)); +deploy(process.argv[1], process.argv.splice(2), { + env: process.env, + rd: files.reading(fs, path), + wr: files.writing(fs, path, temp), + setup: setup({ resolve: path.resolve, env: process.env, setInterval }), + running: running(process, { exec, process, spawn }), + inquirer, + fetch, +}).then( + res => process.exit(res || 0), + rej => { + console.error(`error running ag-setup-cosmos:`, rej); + process.exit(1); + }, +); diff --git a/packages/deployment/files.js b/packages/deployment/src/files.js similarity index 100% rename from packages/deployment/files.js rename to packages/deployment/src/files.js diff --git a/packages/deployment/init.js b/packages/deployment/src/init.js similarity index 93% rename from packages/deployment/init.js rename to packages/deployment/src/init.js index 331dbb09469..53e1ba482b1 100644 --- a/packages/deployment/init.js +++ b/packages/deployment/src/init.js @@ -25,14 +25,16 @@ const nodeCount = (count, force) => { const tfStringify = obj => { let ret = ''; if (Array.isArray(obj)) { - let sep = '['; + ret += '['; + let sep = ''; for (const el of obj) { ret += sep + tfStringify(el); sep = ','; } ret += ']'; } else if (Object(obj) === obj) { - let sep = '{'; + ret += '{'; + let sep = ''; for (const key of Object.keys(obj).sort()) { ret += `${sep}${JSON.stringify(key)}=${tfStringify(obj[key])}`; sep = ','; @@ -149,7 +151,7 @@ module "${PLACEMENT}" { source = "${setup.SETUP_DIR}/terraform/${provider.value}" CLUSTER_NAME = "${PREFIX}\${var.NETWORK_NAME}-${PLACEMENT}" OFFSET = "\${var.OFFSETS["${PLACEMENT}"]}" - SSH_KEY_FILE = "\${var.SSH_KEY_FILE}" + SSH_KEY_FILE = "${PLACEMENT}-\${var.SSH_KEY_FILE}" ROLE = "\${var.ROLES["${PLACEMENT}"]}" SERVERS = "\${length(var.DATACENTERS["${PLACEMENT}"])}" VOLUMES = "\${var.VOLUMES["${PLACEMENT}"]}" @@ -187,7 +189,8 @@ module "${PLACEMENT}" { OFFSET = "\${var.OFFSETS["${PLACEMENT}"]}" REGIONS = "\${var.DATACENTERS["${PLACEMENT}"]}" ROLE = "\${var.ROLES["${PLACEMENT}"]}" - SSH_KEY_FILE = "\${var.SSH_KEY_FILE}" + # TODO: DigitalOcean provider module doesn't allow reuse of SSH public keys. + SSH_KEY_FILE = "${PLACEMENT}-\${var.SSH_KEY_FILE}" DO_API_TOKEN = "\${var.API_KEYS["${PLACEMENT}"]}" SERVERS = "\${length(var.DATACENTERS["${PLACEMENT}"])}" } @@ -292,16 +295,23 @@ const doInit = ({ env, rd, wr, running, setup, inquirer, fetch }) => async ( const deploymentJson = `deployment.json`; const config = (await rd.exists(deploymentJson)) ? JSON.parse(await rd.readFile(deploymentJson, 'utf-8')) - : { - PLACEMENTS: [], - PLACEMENT_PROVIDER: {}, - SSH_PRIVATE_KEY_FILE: `id_${SSH_TYPE}`, - DETAILS: {}, - OFFSETS: {}, - ROLES: {}, - DATACENTERS: {}, - PROVIDER_NEXT_INDEX: {}, - }; + : {}; + + const defaultConfigs = { + PLACEMENTS: [], + PLACEMENT_PROVIDER: {}, + SSH_PRIVATE_KEY_FILE: `id_${SSH_TYPE}`, + DETAILS: {}, + OFFSETS: {}, + ROLES: {}, + DATACENTERS: {}, + PROVIDER_NEXT_INDEX: {}, + }; + Object.entries(defaultConfigs).forEach(([key, dflt]) => { + if (!(key in config)) { + config[key] = dflt; + } + }); config.NETWORK_NAME = overrideNetworkName; // eslint-disable-next-line no-constant-condition @@ -514,6 +524,16 @@ variable ${JSON.stringify(vname)} { for (const PLACEMENT of Object.keys(config.PLACEMENT_PROVIDER).sort()) { const PROVIDER = config.PLACEMENT_PROVIDER[PLACEMENT]; const provider = PROVIDERS[PROVIDER]; + + // Create a placement-specific key file. + const keyFile = `${PLACEMENT}-${config.SSH_PRIVATE_KEY_FILE}`; + // eslint-disable-next-line no-await-in-loop + if (!(await rd.exists(keyFile))) { + // Set empty password. + // eslint-disable-next-line no-await-in-loop + await needDoRun(['ssh-keygen', '-N', '', '-t', SSH_TYPE, '-f', keyFile]); + } + // eslint-disable-next-line no-await-in-loop await provider.createPlacementFiles(provider, PLACEMENT, clusterPrefix); } @@ -552,6 +572,7 @@ output "offsets" { #! /bin/sh exec ansible-playbook -f10 \\ -eSETUP_HOME=${shellEscape(cwd())} \\ + -eAGORIC_SDK=${shellEscape(setup.AGORIC_SDK)} \\ -eNETWORK_NAME=\`cat ${shellEscape(rd.resolve('network.txt'))}\` \\ \${1+"$@"} `, diff --git a/packages/deployment/main.js b/packages/deployment/src/main.js similarity index 93% rename from packages/deployment/main.js rename to packages/deployment/src/main.js index 16d54873c0b..a5a80d74c3f 100644 --- a/packages/deployment/main.js +++ b/packages/deployment/src/main.js @@ -5,6 +5,7 @@ import { createHash } from 'crypto'; import chalk from 'chalk'; import parseArgs from 'minimist'; import { assert, details as X } from '@agoric/assert'; +import { dirname, basename } from 'path'; import { doInit } from './init'; import { shellMetaRegexp, shellEscape } from './run'; import { streamFromString } from './files'; @@ -200,6 +201,7 @@ show-config display the client connection parameters default: { 'boot-tokens': DEFAULT_BOOT_TOKENS, }, + string: ['bump', 'import-from', 'genesis'], stopEarly: true, }, ); @@ -246,7 +248,7 @@ show-config display the client connection parameters case undefined: { await wr.createFile('boot-tokens.txt', bootTokens); const bootOpts = []; - for (const propagate of ['bump', 'import-from']) { + for (const propagate of ['bump', 'import-from', 'genesis']) { const val = subOpts[propagate]; if (val !== undefined) { bootOpts.push(`--${propagate}=${val}`); @@ -309,7 +311,7 @@ show-config display the client connection parameters await inited(); // eslint-disable-next-line no-unused-vars const { _: subArgs, ...subOpts } = parseArgs(args.slice(1), { - string: ['bump', 'import-from'], + string: ['bump', 'import-from', 'genesis'], stopEarly: true, }); @@ -339,9 +341,18 @@ show-config display the client connection parameters await guardFile(`chain-version.txt`, makeFile => makeFile('1')); // Assign the chain name. - const networkName = await trimReadFile('network.txt'); - const chainVersion = await trimReadFile('chain-version.txt'); - const chainName = `${networkName}-${chainVersion}`; + let chainName; + let genJSON; + if (subOpts.genesis) { + // Fetch the specified genesis, don't generate it. + genJSON = await trimReadFile(subOpts.genesis); + const genesis = JSON.parse(genJSON); + chainName = genesis.chain_id; + } else { + const networkName = await trimReadFile('network.txt'); + const chainVersion = await trimReadFile('chain-version.txt'); + chainName = `${networkName}-${chainVersion}`; + } const currentChainName = await trimReadFile( `${COSMOS_DIR}/chain-name.txt`, ).catch(_ => undefined); @@ -363,14 +374,25 @@ show-config display the client connection parameters await guardFile(`${COSMOS_DIR}/prepare.stamp`, () => needReMain(['play', 'prepare-cosmos']), ); - await guardFile(`${COSMOS_DIR}/genesis.stamp`, () => - needReMain(['play', 'cosmos-genesis']), - ); + + // If the canonical genesis exists, use it. + await guardFile(`${COSMOS_DIR}/genesis.stamp`, async () => { + await wr.mkdir(`${COSMOS_DIR}/data`, { recursive: true }); + if (genJSON) { + await wr.createFile(`${COSMOS_DIR}/data/genesis.json`, genJSON); + } else { + await guardFile(`${COSMOS_DIR}/data/genesis.json`, async () => { + await needReMain(['play', 'cosmos-genesis']); + // Don't overwrite the data/genesis.json. + return true; + }); + } + }); await guardFile(`${COSMOS_DIR}/set-defaults.stamp`, async () => { await needReMain(['play', 'cosmos-clone-config']); - const agoricCli = rd.resolve(__dirname, `../agoric-cli/bin/agoric`); + const agoricCli = rd.resolve(__dirname, `../../agoric-cli/bin/agoric`); // Apply the Agoric set-defaults to all the .dst dirs. const files = await rd.readdir(`${COSMOS_DIR}/data`); @@ -397,13 +419,19 @@ show-config display the client connection parameters ...importFlags, `${COSMOS_DIR}/data/${dst}`, ]); - if (i === 0) { - // Make a canonical copy of the genesis.json. - const data = await rd.readFile( - `${COSMOS_DIR}/data/${dst}/genesis.json`, - ); - await wr.createFile(`${COSMOS_DIR}/data/genesis.json`, data); + if (i !== 0) { + return; } + await guardFile( + `${COSMOS_DIR}/data/genesis.json`, + async makeGenesis => { + // Make a canonical copy of the genesis.json if there isn't one. + const data = await rd.readFile( + `${COSMOS_DIR}/data/${dst}/genesis.json`, + ); + await makeGenesis(data); + }, + ); }), ); }); @@ -862,6 +890,10 @@ ${name}: if (!addRole[role]) { addRole[role] = makeGroup(role, 4); } + const keyFile = rd.resolve( + dirname(SSH_PRIVATE_KEY_FILE), + `${provider}-${basename(SSH_PRIVATE_KEY_FILE)}`, + ); for (let instance = 0; instance < ips.length; instance += 1) { const ip = ips[instance]; const node = `${role}${offset + instance}`; @@ -877,7 +909,7 @@ ${name}: ${node}:${roleParams} ansible_host: ${ip} ansible_ssh_user: root - ansible_ssh_private_key_file: '${SSH_PRIVATE_KEY_FILE}' + ansible_ssh_private_key_file: '${keyFile}' ansible_python_interpreter: /usr/bin/python`; addProvider(host); diff --git a/packages/deployment/run.js b/packages/deployment/src/run.js similarity index 100% rename from packages/deployment/run.js rename to packages/deployment/src/run.js diff --git a/packages/deployment/setup.js b/packages/deployment/src/setup.js similarity index 90% rename from packages/deployment/setup.js rename to packages/deployment/src/setup.js index 78749e1c65d..73d3bfc6081 100644 --- a/packages/deployment/setup.js +++ b/packages/deployment/src/setup.js @@ -8,7 +8,8 @@ export const SSH_TYPE = 'ecdsa'; export const setup = ({ resolve, env, setInterval }) => { const it = harden({ - SETUP_DIR: __dirname, + AGORIC_SDK: resolve(__dirname, '../../..'), + SETUP_DIR: resolve(__dirname, '..'), SETUP_HOME: env.AG_SETUP_COSMOS_HOME ? resolve(env.AG_SETUP_COSMOS_HOME) : resolve('.'),