UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

934 lines 78.7 kB
import { Zippy } from '../../core/zippy.js'; import * as constants from '../../core/constants.js'; import { DEFAULT_NETWORK_NODE_NAME, FREEZE_ADMIN_ACCOUNT, HEDERA_NODE_DEFAULT_STAKE_AMOUNT, IGNORED_NODE_ACCOUNT_ID, TREASURY_ACCOUNT_ID, } from '../../core/constants.js'; import { Templates } from '../../core/templates.js'; import { Task } from '../../core/task.js'; import { AccountBalanceQuery, AccountId, AccountUpdateTransaction, FileAppendTransaction, FileUpdateTransaction, FreezeTransaction, FreezeType, Long, NodeCreateTransaction, NodeDeleteTransaction, NodeUpdateTransaction, PrivateKey, Timestamp, } from '@hashgraph/sdk'; import { IllegalArgumentError, MissingArgumentError, SoloError } from '../../core/errors.js'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; import * as helpers from '../../core/helpers.js'; import { addDebugOptions, getNodeAccountMap, prepareEndpoints, renameAndCopyFile, sleep, splitFlagInput, } from '../../core/helpers.js'; import chalk from 'chalk'; import { Flags as flags } from '../flags.js'; import { PodName } from '../../core/kube/resources/pod/pod_name.js'; import { NodeStatusCodes, NodeStatusEnums, NodeSubcommandType } from '../../core/enumerations.js'; import { ListrLease } from '../../core/lease/listr_lease.js'; import { Duration } from '../../core/time/duration.js'; import { GenesisNetworkDataConstructor } from '../../core/genesis_network_models/genesis_network_data_constructor.js'; import { NodeOverridesModel } from '../../core/node_overrides_model.js'; import { PodRef } from '../../core/kube/resources/pod/pod_ref.js'; import { ContainerRef } from '../../core/kube/resources/container/container_ref.js'; import { NetworkNodes } from '../../core/network_nodes.js'; import { container } from 'tsyringe-neo'; import { ConsensusNode } from '../../core/model/consensus_node.js'; import { Base64 } from 'js-base64'; export class NodeCommandTasks { accountManager; configManager; keyManager; profileManager; platformInstaller; logger; k8Factory; parent; chartManager; certificateManager; prepareValuesFiles; constructor(opts) { if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager); if (!opts || !opts.configManager) throw new Error('An instance of core/ConfigManager is required'); if (!opts || !opts.logger) throw new Error('An instance of core/Logger is required'); if (!opts || !opts.k8Factory) throw new Error('An instance of core/K8Factory is required'); if (!opts || !opts.platformInstaller) throw new IllegalArgumentError('An instance of core/PlatformInstaller is required', opts.platformInstaller); if (!opts || !opts.keyManager) throw new IllegalArgumentError('An instance of core/KeyManager is required', opts.keyManager); if (!opts || !opts.profileManager) throw new IllegalArgumentError('An instance of ProfileManager is required', opts.profileManager); if (!opts || !opts.certificateManager) throw new IllegalArgumentError('An instance of CertificateManager is required', opts.certificateManager); if (!opts || !opts.parent) throw new IllegalArgumentError('An instance of parents as BaseCommand is required', opts.parent); this.accountManager = opts.accountManager; this.configManager = opts.configManager; this.logger = opts.logger; this.k8Factory = opts.k8Factory; this.platformInstaller = opts.platformInstaller; this.profileManager = opts.profileManager; this.keyManager = opts.keyManager; this.chartManager = opts.chartManager; this.certificateManager = opts.certificateManager; this.prepareValuesFiles = opts.parent.prepareValuesFiles.bind(opts.parent); this.parent = opts.parent; } async _prepareUpgradeZip(stagingDir) { // we build a mock upgrade.zip file as we really don't need to upgrade the network // also the platform zip file is ~80Mb in size requiring a lot of transactions since the max // transaction size is 6Kb and in practice we need to send the file as 4Kb chunks. // Note however that in DAB phase-2, we won't need to trigger this fake upgrade process const zipper = new Zippy(this.logger); const upgradeConfigDir = path.join(stagingDir, 'mock-upgrade', 'data', 'config'); if (!fs.existsSync(upgradeConfigDir)) { fs.mkdirSync(upgradeConfigDir, { recursive: true }); } // bump field hedera.config.version const fileBytes = fs.readFileSync(path.join(stagingDir, 'templates', 'application.properties')); const lines = fileBytes.toString().split('\n'); const newLines = []; for (let line of lines) { line = line.trim(); const parts = line.split('='); if (parts.length === 2) { if (parts[0] === 'hedera.config.version') { let version = parseInt(parts[1]); line = `hedera.config.version=${++version}`; } newLines.push(line); } } fs.writeFileSync(path.join(upgradeConfigDir, 'application.properties'), newLines.join('\n')); return await zipper.zip(path.join(stagingDir, 'mock-upgrade'), path.join(stagingDir, 'mock-upgrade.zip')); } async _uploadUpgradeZip(upgradeZipFile, nodeClient) { // get byte value of the zip file const zipBytes = fs.readFileSync(upgradeZipFile); // @ts-ignore const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex'); this.logger.debug(`loaded upgrade zip file [ zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}, zipPath = ${upgradeZipFile}]`); // create a file upload transaction to upload file to the network try { let start = 0; while (start < zipBytes.length) { const zipBytesChunk = new Uint8Array(zipBytes.subarray(start, start + constants.UPGRADE_FILE_CHUNK_SIZE)); let fileTransaction = null; if (start === 0) { fileTransaction = new FileUpdateTransaction().setFileId(constants.UPGRADE_FILE_ID).setContents(zipBytesChunk); } else { fileTransaction = new FileAppendTransaction().setFileId(constants.UPGRADE_FILE_ID).setContents(zipBytesChunk); } const resp = await fileTransaction.execute(nodeClient); const receipt = await resp.getReceipt(nodeClient); this.logger.debug(`updated file ${constants.UPGRADE_FILE_ID} [chunkSize= ${zipBytesChunk.length}, txReceipt = ${receipt.toString()}]`); start += constants.UPGRADE_FILE_CHUNK_SIZE; this.logger.debug(`uploaded ${start} bytes of ${zipBytes.length} bytes`); } return zipHash; } catch (e) { throw new SoloError(`failed to upload build.zip file: ${e.message}`, e); } } async copyLocalBuildPathToNode(k8, podRef, configManager, localDataLibBuildPath) { const filterFunction = (path, stat) => { return !(path.includes('data/keys') || path.includes('data/config')); }; await k8 .containers() .readByRef(ContainerRef.of(podRef, constants.ROOT_CONTAINER)) .copyTo(localDataLibBuildPath, `${constants.HEDERA_HAPI_PATH}`, filterFunction); if (configManager.getFlag(flags.appConfig)) { const testJsonFiles = configManager.getFlag(flags.appConfig).split(','); for (const jsonFile of testJsonFiles) { if (fs.existsSync(jsonFile)) { await k8 .containers() .readByRef(ContainerRef.of(podRef, constants.ROOT_CONTAINER)) .copyTo(jsonFile, `${constants.HEDERA_HAPI_PATH}`); } } } } _uploadPlatformSoftware(nodeAliases, podRefs, task, localBuildPath, consensusNodes) { const subTasks = []; this.logger.debug('no need to fetch, use local build jar files'); const buildPathMap = new Map(); let defaultDataLibBuildPath; const parameterPairs = localBuildPath.split(','); for (const parameterPair of parameterPairs) { if (parameterPair.includes('=')) { const [nodeAlias, localDataLibBuildPath] = parameterPair.split('='); buildPathMap.set(nodeAlias, localDataLibBuildPath); } else { defaultDataLibBuildPath = parameterPair; } } let localDataLibBuildPath; for (const nodeAlias of nodeAliases) { const podRef = podRefs[nodeAlias]; const context = helpers.extractContextFromConsensusNodes(nodeAlias, consensusNodes); if (buildPathMap.has(nodeAlias)) { localDataLibBuildPath = buildPathMap.get(nodeAlias); } else { localDataLibBuildPath = defaultDataLibBuildPath; } if (!fs.existsSync(localDataLibBuildPath)) { throw new SoloError(`local build path does not exist: ${localDataLibBuildPath}`); } const self = this; const k8 = self.k8Factory.getK8(context); subTasks.push({ title: `Copy local build to Node: ${chalk.yellow(nodeAlias)} from ${localDataLibBuildPath}`, task: async () => { // retry copying the build to the node to handle edge cases during performance testing let error = null; let i = 0; for (; i < constants.LOCAL_BUILD_COPY_RETRY; i++) { error = null; try { // filter the data/config and data/keys to avoid failures due to config and secret mounts await self.copyLocalBuildPathToNode(k8, podRef, self.configManager, localDataLibBuildPath); } catch (e) { error = e; } } if (error) { throw new SoloError(`Error in copying local build to node: ${error.message}`, error); } }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: constants.NODE_COPY_CONCURRENT, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); } _fetchPlatformSoftware(nodeAliases, podRefs, releaseTag, task, platformInstaller, consensusNodes) { const subTasks = []; for (const nodeAlias of nodeAliases) { const context = helpers.extractContextFromConsensusNodes(nodeAlias, consensusNodes); const podRef = podRefs[nodeAlias]; subTasks.push({ title: `Update node: ${chalk.yellow(nodeAlias)} [ platformVersion = ${releaseTag}, context = ${context} ]`, task: async () => await platformInstaller.fetchPlatform(podRef, releaseTag, context), }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, // since we download in the container directly, we want this to be in parallel across all nodes rendererOptions: { collapseSubtasks: false, }, }); } _checkNodeActivenessTask(ctx, task, nodeAliases, status = NodeStatusCodes.ACTIVE) { const { config: { namespace }, } = ctx; const enableDebugger = ctx.config.debugNodeAlias && status !== NodeStatusCodes.FREEZE_COMPLETE; const subTasks = nodeAliases.map((nodeAlias, i) => { const reminder = 'debugNodeAlias' in ctx.config && ctx.config.debugNodeAlias === nodeAlias && status !== NodeStatusCodes.FREEZE_COMPLETE ? 'Please attach JVM debugger now. Sleeping for 1 hour, hit ctrl-c once debugging is complete.' : ''; const title = `Check network pod: ${chalk.yellow(nodeAlias)} ${chalk.red(reminder)}`; const context = helpers.extractContextFromConsensusNodes(nodeAlias, ctx.config.consensusNodes); const subTask = async (ctx, task) => { if (enableDebugger) { await sleep(Duration.ofHours(1)); } ctx.config.podRefs[nodeAlias] = await this._checkNetworkNodeActiveness(namespace, nodeAlias, task, title, i, status, undefined, undefined, undefined, context); }; return { title, task: subTask }; }); return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, }, }); } async _checkNetworkNodeActiveness(namespace, nodeAlias, task, title, index, status = NodeStatusCodes.ACTIVE, maxAttempts = constants.NETWORK_NODE_ACTIVE_MAX_ATTEMPTS, delay = constants.NETWORK_NODE_ACTIVE_DELAY, timeout = constants.NETWORK_NODE_ACTIVE_TIMEOUT, context) { nodeAlias = nodeAlias.trim(); const podName = Templates.renderNetworkPodName(nodeAlias); const podRef = PodRef.of(namespace, podName); task.title = `${title} - status ${chalk.yellow('STARTING')}, attempt ${chalk.blueBright(`0/${maxAttempts}`)}`; let attempt = 0; let success = false; while (attempt < maxAttempts) { const controller = new AbortController(); const timeoutId = setTimeout(() => { task.title = `${title} - status ${chalk.yellow('TIMEOUT')}, attempt ${chalk.blueBright(`${attempt}/${maxAttempts}`)}`; controller.abort(); }, timeout); try { const response = await this.k8Factory .getK8(context) .containers() .readByRef(ContainerRef.of(podRef, constants.ROOT_CONTAINER)) .execContainer([ 'bash', '-c', 'curl -s http://localhost:9999/metrics | grep platform_PlatformStatus | grep -v \\#', ]); if (!response) { task.title = `${title} - status ${chalk.yellow('UNKNOWN')}, attempt ${chalk.blueBright(`${attempt}/${maxAttempts}`)}`; clearTimeout(timeoutId); throw new Error('empty response'); // Guard } const statusLine = response.split('\n').find(line => line.startsWith('platform_PlatformStatus')); if (!statusLine) { task.title = `${title} - status ${chalk.yellow('STARTING')}, attempt: ${chalk.blueBright(`${attempt}/${maxAttempts}`)}`; clearTimeout(timeoutId); throw new Error('missing status line'); // Guard } const statusNumber = parseInt(statusLine.split(' ').pop()); if (statusNumber === status) { task.title = `${title} - status ${chalk.green(NodeStatusEnums[status])}, attempt: ${chalk.blueBright(`${attempt}/${maxAttempts}`)}`; success = true; clearTimeout(timeoutId); break; } else if (statusNumber === NodeStatusCodes.CATASTROPHIC_FAILURE) { task.title = `${title} - status ${chalk.red('CATASTROPHIC_FAILURE')}, attempt: ${chalk.blueBright(`${attempt}/${maxAttempts}`)}`; break; } else if (statusNumber) { task.title = `${title} - status ${chalk.yellow(NodeStatusEnums[statusNumber])}, attempt: ${chalk.blueBright(`${attempt}/${maxAttempts}`)}`; } clearTimeout(timeoutId); } catch (e) { this.logger.debug(`${title} : Error in checking node activeness: attempt: ${attempt}/${maxAttempts}: ${JSON.stringify(e)}`); } attempt++; clearTimeout(timeoutId); await sleep(Duration.ofMillis(delay)); } if (!success) { throw new SoloError(`node '${nodeAlias}' is not ${NodeStatusEnums[status]}` + `[ attempt = ${chalk.blueBright(`${attempt}/${maxAttempts}`)} ]`); } await sleep(Duration.ofSeconds(2)); // delaying prevents - gRPC service error return podRef; } /** Return task for check if node proxies are ready */ _checkNodesProxiesTask(ctx, task, nodeAliases) { const subTasks = []; for (const nodeAlias of nodeAliases) { subTasks.push({ title: `Check proxy for node: ${chalk.yellow(nodeAlias)}`, task: async (ctx) => { const context = helpers.extractContextFromConsensusNodes(nodeAlias, ctx.config.consensusNodes); const k8 = this.k8Factory.getK8(context); await k8 .pods() .waitForReadyStatus(ctx.config.namespace, [`app=haproxy-${nodeAlias}`, 'solo.hedera.com/type=haproxy'], constants.NETWORK_PROXY_MAX_ATTEMPTS, constants.NETWORK_PROXY_DELAY); }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: false, rendererOptions: { collapseSubtasks: false, }, }); } /** * When generating multiple all aliases are read from config.nodeAliases, * When generating a single key the alias in config.nodeAlias is used */ _generateGossipKeys(generateMultiple) { const self = this; return new Task('Generate gossip keys', (ctx, task) => { const config = ctx.config; const nodeAliases = generateMultiple ? config.nodeAliases : [config.nodeAlias]; const subTasks = self.keyManager.taskGenerateGossipKeys(nodeAliases, config.keysDir, config.curDate); // set up the sub-tasks return task.newListr(subTasks, { concurrent: false, rendererOptions: { collapseSubtasks: false, timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION, }, }); }, (ctx) => !ctx.config.generateGossipKeys); } /** * When generating multiple all aliases are read from config.nodeAliases, * When generating a single key the alias in config.nodeAlias is used */ _generateGrpcTlsKeys(generateMultiple) { const self = this; return new Task('Generate gRPC TLS Keys', (ctx, task) => { const config = ctx.config; const nodeAliases = generateMultiple ? config.nodeAliases : [config.nodeAlias]; const subTasks = self.keyManager.taskGenerateTLSKeys(nodeAliases, config.keysDir, config.curDate); // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION, }, }); }, (ctx) => !ctx.config.generateTlsKeys); } copyGrpcTlsCertificates() { const self = this; return new Task('Copy gRPC TLS Certificates', (ctx, parentTask) => self.certificateManager.buildCopyTlsCertificatesTasks(parentTask, ctx.config.grpcTlsCertificatePath, ctx.config.grpcWebTlsCertificatePath, ctx.config.grpcTlsKeyPath, ctx.config.grpcWebTlsKeyPath), (ctx) => !ctx.config.grpcTlsCertificatePath && !ctx.config.grpcWebTlsCertificatePath); } async _addStake(namespace, accountId, nodeAlias, stakeAmount = HEDERA_NODE_DEFAULT_STAKE_AMOUNT, context) { try { const deploymentName = this.configManager.getFlag(flags.deployment); await this.accountManager.loadNodeClient(namespace, this.parent.getClusterRefs(), deploymentName, this.configManager.getFlag(flags.forcePortForward), context); const client = this.accountManager._nodeClient; const treasuryKey = await this.accountManager.getTreasuryAccountKeys(namespace); const treasuryPrivateKey = PrivateKey.fromStringED25519(treasuryKey.privateKey); client.setOperator(TREASURY_ACCOUNT_ID, treasuryPrivateKey); // check balance const treasuryBalance = await new AccountBalanceQuery().setAccountId(TREASURY_ACCOUNT_ID).execute(client); this.logger.debug(`Account ${TREASURY_ACCOUNT_ID} balance: ${treasuryBalance.hbars}`); // get some initial balance await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, stakeAmount); // check balance const balance = await new AccountBalanceQuery().setAccountId(accountId).execute(client); this.logger.debug(`Account ${accountId} balance: ${balance.hbars}`); // Create the transaction const transaction = new AccountUpdateTransaction() .setAccountId(accountId) .setStakedNodeId(Templates.nodeIdFromNodeAlias(nodeAlias)) .freezeWith(client); // Sign the transaction with the account's private key const signTx = await transaction.sign(treasuryPrivateKey); // Submit the transaction to a Hedera network const txResponse = await signTx.execute(client); // Request the receipt of the transaction const receipt = await txResponse.getReceipt(client); // Get the transaction status const transactionStatus = receipt.status; this.logger.debug(`The transaction consensus status is ${transactionStatus.toString()}`); } catch (e) { throw new SoloError(`Error in adding stake: ${e.message}`, e); } } prepareUpgradeZip() { const self = this; return new Task('Prepare upgrade zip file for node upgrade process', async (ctx, task) => { const config = ctx.config; const { upgradeZipFile } = ctx.config; if (upgradeZipFile) { this.logger.debug(`Using upgrade zip file: ${ctx.upgradeZipFile}`); ctx.upgradeZipFile = upgradeZipFile; } else { ctx.upgradeZipFile = await self._prepareUpgradeZip(config.stagingDir); } ctx.upgradeZipHash = await self._uploadUpgradeZip(ctx.upgradeZipFile, config.nodeClient); }); } loadAdminKey() { return new Task('Load node admin key', async (ctx, task) => { const config = ctx.config; if (ctx.config.nodeAlias) { try { // load nodeAdminKey form k8s if exist const keyFromK8 = await this.k8Factory .default() .secrets() .read(config.namespace, Templates.renderNodeAdminKeyName(config.nodeAlias)); const privateKey = Base64.decode(keyFromK8.data.privateKey); config.adminKey = PrivateKey.fromStringED25519(privateKey); } catch (e) { this.logger.debug(`Error in loading node admin key: ${e.message}, use default key`); config.adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY); } } else { config.adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY); } }); } checkExistingNodesStakedAmount() { const self = this; return new Task('Check existing nodes staked amount', async (ctx, task) => { const config = ctx.config; // Transfer some hbar to the node for staking purpose const accountMap = getNodeAccountMap(config.existingNodeAliases); for (const nodeAlias of config.existingNodeAliases) { const accountId = accountMap.get(nodeAlias); await self.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1); } }); } sendPrepareUpgradeTransaction() { const self = this; return new Task('Send prepare upgrade transaction', async (ctx, task) => { const { upgradeZipHash } = ctx; const { nodeClient, freezeAdminPrivateKey } = ctx.config; try { // query the balance const balance = await new AccountBalanceQuery().setAccountId(FREEZE_ADMIN_ACCOUNT).execute(nodeClient); self.logger.debug(`Freeze admin account balance: ${balance.hbars}`); // transfer some tiny amount to the freeze admin account await self.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 100000); // set operator of freeze transaction as freeze admin account nodeClient.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey); const prepareUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.PrepareUpgrade) .setFileId(constants.UPGRADE_FILE_ID) .setFileHash(upgradeZipHash) .freezeWith(nodeClient) .execute(nodeClient); const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(nodeClient); self.logger.debug(`sent prepare upgrade transaction [id: ${prepareUpgradeTx.transactionId.toString()}]`, prepareUpgradeReceipt.status.toString()); } catch (e) { self.logger.error(`Error in prepare upgrade: ${e.message}`, e); throw new SoloError(`Error in prepare upgrade: ${e.message}`, e); } }); } sendFreezeUpgradeTransaction() { const self = this; return new Task('Send freeze upgrade transaction', async (ctx, task) => { const { upgradeZipHash } = ctx; const { freezeAdminPrivateKey, nodeClient } = ctx.config; try { const futureDate = new Date(); self.logger.debug(`Current time: ${futureDate}`); futureDate.setTime(futureDate.getTime() + 5000); // 5 seconds in the future self.logger.debug(`Freeze time: ${futureDate}`); // query the balance const balance = await new AccountBalanceQuery().setAccountId(FREEZE_ADMIN_ACCOUNT).execute(nodeClient); self.logger.debug(`Freeze admin account balance: ${balance.hbars}`); nodeClient.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey); const freezeUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.FreezeUpgrade) .setStartTimestamp(Timestamp.fromDate(futureDate)) .setFileId(constants.UPGRADE_FILE_ID) .setFileHash(upgradeZipHash) .freezeWith(nodeClient) .execute(nodeClient); const freezeUpgradeReceipt = await freezeUpgradeTx.getReceipt(nodeClient); self.logger.debug(`Upgrade frozen with transaction id: ${freezeUpgradeTx.transactionId.toString()}`, freezeUpgradeReceipt.status.toString()); } catch (e) { self.logger.error(`Error in freeze upgrade: ${e.message}`, e); throw new SoloError(`Error in freeze upgrade: ${e.message}`, e); } }); } /** Download generated config files and key files from the network node */ downloadNodeGeneratedFiles() { const self = this; return new Task('Download generated files from an existing node', async (ctx, task) => { const config = ctx.config; // don't try to download from the same node we are deleting, it won't work const nodeAlias = ctx.config.nodeAlias === config.existingNodeAliases[0] && config.existingNodeAliases.length > 1 ? config.existingNodeAliases[1] : config.existingNodeAliases[0]; const nodeFullyQualifiedPodName = Templates.renderNetworkPodName(nodeAlias); const podRef = PodRef.of(config.namespace, nodeFullyQualifiedPodName); const containerRef = ContainerRef.of(podRef, constants.ROOT_CONTAINER); // copy the config.txt file from the node1 upgrade directory await self.k8Factory .default() .containers() .readByRef(containerRef) .copyFrom(`${constants.HEDERA_HAPI_PATH}/data/upgrade/current/config.txt`, config.stagingDir); // if directory data/upgrade/current/data/keys does not exist, then use data/upgrade/current let keyDir = `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/data/keys`; if (!(await self.k8Factory.default().containers().readByRef(containerRef).hasDir(keyDir))) { keyDir = `${constants.HEDERA_HAPI_PATH}/data/upgrade/current`; } const signedKeyFiles = (await self.k8Factory.default().containers().readByRef(containerRef).listDir(keyDir)).filter(file => file.name.startsWith(constants.SIGNING_KEY_PREFIX)); await self.k8Factory .default() .containers() .readByRef(containerRef) .execContainer([ 'bash', '-c', `mkdir -p ${constants.HEDERA_HAPI_PATH}/data/keys_backup && cp -r ${keyDir} ${constants.HEDERA_HAPI_PATH}/data/keys_backup/`, ]); for (const signedKeyFile of signedKeyFiles) { await self.k8Factory .default() .containers() .readByRef(containerRef) .copyFrom(`${keyDir}/${signedKeyFile.name}`, `${config.keysDir}`); } if (await self.k8Factory .default() .containers() .readByRef(containerRef) .hasFile(`${constants.HEDERA_HAPI_PATH}/data/upgrade/current/application.properties`)) { await self.k8Factory .default() .containers() .readByRef(containerRef) .copyFrom(`${constants.HEDERA_HAPI_PATH}/data/upgrade/current/application.properties`, `${config.stagingDir}/templates`); } }); } downloadNodeUpgradeFiles() { const self = this; return new Task('Download upgrade files from an existing node', async (ctx, task) => { const config = ctx.config; const nodeAlias = ctx.config.nodeAliases[0]; const nodeFullyQualifiedPodName = Templates.renderNetworkPodName(nodeAlias); const podRef = PodRef.of(config.namespace, nodeFullyQualifiedPodName); // found all files under ${constants.HEDERA_HAPI_PATH}/data/upgrade/current/ const upgradeDirectories = [ `${constants.HEDERA_HAPI_PATH}/data/upgrade/current`, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/data/apps`, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/data/libs`, ]; const containerRef = ContainerRef.of(podRef, constants.ROOT_CONTAINER); for (const upgradeDir of upgradeDirectories) { // check if directory upgradeDir exist in root container if (!(await self.k8Factory.default().containers().readByRef(containerRef).hasDir(upgradeDir))) { continue; } const files = await self.k8Factory.default().containers().readByRef(containerRef).listDir(upgradeDir); // iterate all files and copy them to the staging directory for (const file of files) { if (file.name.endsWith('.mf')) { continue; } if (file.directory) { continue; } this.logger.debug(`Copying file: ${file.name}`); await self.k8Factory .default() .containers() .readByRef(containerRef) .copyFrom(`${upgradeDir}/${file.name}`, `${config.stagingDir}`); } } }); } taskCheckNetworkNodePods(ctx, task, nodeAliases, maxAttempts = undefined) { if (!ctx.config) ctx.config = {}; ctx.config.podRefs = {}; const consensusNodes = ctx.config.consensusNodes; const subTasks = []; const self = this; for (const nodeAlias of nodeAliases) { const context = helpers.extractContextFromConsensusNodes(nodeAlias, consensusNodes); subTasks.push({ title: `Check network pod: ${chalk.yellow(nodeAlias)}`, task: async (ctx) => { try { ctx.config.podRefs[nodeAlias] = await self.checkNetworkNodePod(ctx.config.namespace, nodeAlias, maxAttempts, undefined, context); } catch (_) { ctx.config.skipStop = true; } }, }); } // setup the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, }, }); } /** Check if the network node pod is running */ async checkNetworkNodePod(namespace, nodeAlias, maxAttempts = constants.PODS_RUNNING_MAX_ATTEMPTS, delay = constants.PODS_RUNNING_DELAY, context) { nodeAlias = nodeAlias.trim(); const podName = Templates.renderNetworkPodName(nodeAlias); const podRef = PodRef.of(namespace, podName); try { const k8 = this.k8Factory.getK8(context); await k8 .pods() .waitForRunningPhase(namespace, [`solo.hedera.com/node-name=${nodeAlias}`, 'solo.hedera.com/type=network-node'], maxAttempts, delay); return podRef; } catch (e) { throw new SoloError(`no pod found for nodeAlias: ${nodeAlias}`, e); } } identifyExistingNodes() { const self = this; return new Task('Identify existing network nodes', async (ctx, task) => { const config = ctx.config; config.existingNodeAliases = []; const clusterRefs = this.parent.getClusterRefs(); config.serviceMap = await self.accountManager.getNodeServiceMap(config.namespace, clusterRefs, config.deployment); for (const networkNodeServices of config.serviceMap.values()) { config.existingNodeAliases.push(networkNodeServices.nodeAlias); } config.allNodeAliases = [...config.existingNodeAliases]; return self.taskCheckNetworkNodePods(ctx, task, config.existingNodeAliases); }); } uploadStateFiles(skip) { const self = this; return new Task('Upload state files network nodes', async (ctx, task) => { const config = ctx.config; const zipFile = config.stateFile; self.logger.debug(`zip file: ${zipFile}`); for (const nodeAlias of ctx.config.nodeAliases) { const context = helpers.extractContextFromConsensusNodes(nodeAlias, config.consensusNodes); const k8 = this.k8Factory.getK8(context); const podRef = ctx.config.podRefs[nodeAlias]; const containerRef = ContainerRef.of(podRef, constants.ROOT_CONTAINER); self.logger.debug(`Uploading state files to pod ${podRef.name}`); await k8.containers().readByRef(containerRef).copyTo(zipFile, `${constants.HEDERA_HAPI_PATH}/data`); self.logger.info(`Deleting the previous state files in pod ${podRef.name} directory ${constants.HEDERA_HAPI_PATH}/data/saved`); await k8 .containers() .readByRef(containerRef) .execContainer(['rm', '-rf', `${constants.HEDERA_HAPI_PATH}/data/saved/*`]); await k8 .containers() .readByRef(containerRef) .execContainer([ 'tar', '-xvf', `${constants.HEDERA_HAPI_PATH}/data/${path.basename(zipFile)}`, '-C', `${constants.HEDERA_HAPI_PATH}/data/saved`, ]); } }, skip); } identifyNetworkPods(maxAttempts) { const self = this; return new Task('Identify network pods', (ctx, task) => { return self.taskCheckNetworkNodePods(ctx, task, ctx.config.nodeAliases, maxAttempts); }); } fetchPlatformSoftware(aliasesField) { const self = this; return new Task('Fetch platform software into network nodes', (ctx, task) => { const { podRefs, releaseTag, localBuildPath } = ctx.config; return localBuildPath !== '' ? self._uploadPlatformSoftware(ctx.config[aliasesField], podRefs, task, localBuildPath, ctx.config.consensusNodes) : self._fetchPlatformSoftware(ctx.config[aliasesField], podRefs, releaseTag, task, this.platformInstaller, ctx.config.consensusNodes); }); } populateServiceMap() { return new Task('Populate serviceMap', async (ctx, task) => { ctx.config.serviceMap = await this.accountManager.getNodeServiceMap(ctx.config.namespace, this.parent.getClusterRefs(), ctx.config.deployment); ctx.config.podRefs[ctx.config.nodeAlias] = PodRef.of(ctx.config.namespace, ctx.config.serviceMap.get(ctx.config.nodeAlias).nodePodName); }); } setupNetworkNodes(nodeAliasesProperty, isGenesis) { return new Task('Setup network nodes', async (ctx, task) => { ctx.config.nodeAliases = helpers.parseNodeAliases(ctx.config.nodeAliasesUnparsed); if (isGenesis) { await this.generateGenesisNetworkJson(ctx.config.namespace, ctx.config.consensusNodes, ctx.config.keysDir, ctx.config.stagingDir); } await this.generateNodeOverridesJson(ctx.config.namespace, ctx.config.nodeAliases, ctx.config.stagingDir); const consensusNodes = ctx.config.consensusNodes; const subTasks = []; for (const nodeAlias of ctx.config[nodeAliasesProperty]) { const podRef = ctx.config.podRefs[nodeAlias]; let context = helpers.extractContextFromConsensusNodes(nodeAlias, consensusNodes); // temporary, bridge for node add if (!context) { context = this.k8Factory.default().contexts().readCurrent(); } subTasks.push({ title: `Node: ${chalk.yellow(nodeAlias)}`, task: () => this.platformInstaller.taskSetup(podRef, ctx.config.stagingDir, isGenesis, context), }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); }); } async generateNodeOverridesJson(namespace, nodeAliases, stagingDir) { const deploymentName = this.configManager.getFlag(flags.deployment); const networkNodeServiceMap = await this.accountManager.getNodeServiceMap(namespace, this.parent.getClusterRefs(), deploymentName); const nodeOverridesModel = new NodeOverridesModel(nodeAliases, networkNodeServiceMap); const nodeOverridesJson = path.join(stagingDir, constants.NODE_OVERRIDE_FILE); fs.writeFileSync(nodeOverridesJson, nodeOverridesModel.toYAML()); } /** * Generate genesis network json file * @private * @param namespace - namespace * @param consensusNodes - consensus nodes * @param keysDir - keys directory * @param stagingDir - staging directory */ async generateGenesisNetworkJson(namespace, consensusNodes, keysDir, stagingDir) { const deploymentName = this.configManager.getFlag(flags.deployment); const networkNodeServiceMap = await this.accountManager.getNodeServiceMap(namespace, this.parent.getClusterRefs(), deploymentName); const adminPublicKeys = splitFlagInput(this.configManager.getFlag(flags.adminPublicKeys)); const genesisNetworkData = await GenesisNetworkDataConstructor.initialize(consensusNodes, this.keyManager, this.accountManager, keysDir, networkNodeServiceMap, adminPublicKeys); const genesisNetworkJson = path.join(stagingDir, 'genesis-network.json'); fs.writeFileSync(genesisNetworkJson, genesisNetworkData.toJSON()); } prepareStagingDirectory(nodeAliasesProperty) { return new Task('Prepare staging directory', (ctx, task) => { const config = ctx.config; const nodeAliases = config[nodeAliasesProperty]; const subTasks = [ { title: 'Copy Gossip keys to staging', task: async () => { this.keyManager.copyGossipKeysToStaging(config.keysDir, config.stagingKeysDir, nodeAliases); }, }, { title: 'Copy gRPC TLS keys to staging', task: async () => { for (const nodeAlias of nodeAliases) { const tlsKeyFiles = this.keyManager.prepareTLSKeyFilePaths(nodeAlias, config.keysDir); this.keyManager.copyNodeKeysToStaging(tlsKeyFiles, config.stagingKeysDir); } }, }, ]; return task.newListr(subTasks, { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); }); } startNodes(nodeAliasesProperty) { return new Task('Starting nodes', (ctx, task) => { const config = ctx.config; const nodeAliases = config[nodeAliasesProperty]; const subTasks = []; for (const nodeAlias of nodeAliases) { const podRef = config.podRefs[nodeAlias]; const containerRef = ContainerRef.of(podRef, constants.ROOT_CONTAINER); subTasks.push({ title: `Start node: ${chalk.yellow(nodeAlias)}`, task: async () => { const context = helpers.extractContextFromConsensusNodes(nodeAlias, config.consensusNodes); const k8 = this.k8Factory.getK8(context); await k8.containers().readByRef(containerRef).execContainer(['systemctl', 'restart', 'network-node']); }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION, }, }); }); } enablePortForwarding() { return new Task('Enable port forwarding for JVM debugger', async (ctx, task) => { const podRef = PodRef.of(ctx.config.namespace, PodName.of(`network-${ctx.config.debugNodeAlias}-0`)); this.logger.debug(`Enable port forwarding for JVM debugger on pod ${podRef.name}`); await this.k8Factory .default() .pods() .readByRef(podRef) .portForward(constants.JVM_DEBUG_PORT, constants.JVM_DEBUG_PORT); }, (ctx) => !ctx.config.debugNodeAlias); } checkAllNodesAreActive(nodeAliasesProperty) { return new Task('Check all nodes are ACTIVE', (ctx, task) => { return this._checkNodeActivenessTask(ctx, task, ctx.config[nodeAliasesProperty]); }); } checkAllNodesAreFrozen(nodeAliasesProperty) { return new Task('Check all nodes are FROZEN', (ctx, task) => { return this._checkNodeActivenessTask(ctx, task, ctx.config[nodeAliasesProperty], NodeStatusCodes.FREEZE_COMPLETE); }); } checkNodeProxiesAreActive() { return new Task('Check node proxies are ACTIVE', (ctx, task) => { // this is more reliable than checking the nodes logs for ACTIVE, as the // logs will have a lot of white noise from being behind return this._checkNodesProxiesTask(ctx, task, ctx.config.nodeAliases); }, async (ctx) => ctx.config.app !== '' && ctx.config.app !== constants.HEDERA_APP_NAME); } checkAllNodeProxiesAreActive() { return new Task('Check all node proxies are ACTIVE', (ctx, task) => { // this is more reliable than checking the nodes logs for ACTIVE, as the // logs will have a lot of white noise from being behind return this._checkNodesProxiesTask(ctx, task, ctx.config.allNodeAliases); }); } // Update account manager and transfer hbar for staking purpose triggerStakeWeightCalculate(transactionType) { const self = this; return new Task('Trigger stake weight calculate', async (ctx, task) => { const config = ctx.config; self.logger.info('sleep 60 seconds for the handler to be able to trigger the network node stake weight recalculate'); await sleep(Duration.ofSeconds(60)); const accountMap = getNodeAccountMap(config.allNodeAliases); let skipNodeAlias; switch (transactionType) { case NodeSubcommandType.ADD: break; case NodeSubcommandType.UPDATE: if (config.newAccountNumber) { // update map with current account ids accountMap.set(config.nodeAlias, config.newAccountNumber); skipNodeAlias = config.nodeAlias; } break; case NodeSubcommandType.DELETE: if (config.nodeAlias) { accountMap.delete(config.nodeAlias); skipNodeAlias = config.nodeAlias; } } config.nodeClient = await self.accountManager.refreshNodeClient(config.namespace, skipNodeAlias, this.parent.getClusterRefs(), this.configManager.getFlag(flags.deployment)); // send some write transactions to invoke the handler that will trigger the stake weight recalculate for (const nodeAlias of accountMap.keys()) { const accountId = accountMap.get(nodeAlias); config.nodeClient.setOperator(TREASURY_ACCOUNT_ID, config.treasuryKey); await self.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1); } }); } addNodeStakes() { const self = this; // @ts-ignore return new Task('Add node stakes', (ctx, task) => { if (ctx.config.app === '' || ctx.config.app === constants.HEDERA_APP_NAME) { const subTasks = []; const accountMap = getNodeAccountMap(ctx.config.nodeAliases); const stakeAmountParsed = ctx.config.stakeAmount ? splitFlagInput(ctx.config.stakeAmount) : []; let nodeIndex = 0; for (const nodeAlias of ctx.config.nodeAliases) { const accountId = accountMap.get(nodeAlias); const context = helpers.extractContextFromConsensusNodes(nodeAlias, ctx.config.consensusNodes); const stakeAmount = stakeAmountParsed.length > 0 ? stakeAmountParsed[nodeIndex] : HEDERA_NODE_DEFAULT_STAKE_AMOUNT; subTasks.push({ title: `Adding stake for node: ${chalk.yellow(nodeAlias)}`, task: async () => await self._addStake(ctx.config.namespace, accountId, nodeAlias, +stakeAmount, context), }); nodeIndex++; } // set up the sub-tasks return task.newListr(subTasks, { concurrent: false, rendererOptions: { collapseSubtasks: false, }, }); } }); } stakeNewNode() { const self = this; return new Task('Stake new node', async (ctx, task) => { const context = helpers.extractContextFromConsensusNodes(ctx.config.nodeAlias, ctx.config.consensusNodes); await self.accountManager.refreshNodeClient(ctx.config.namespace, ctx.config.nodeAlias, this.parent.getClusterRefs(), this.configManager.getFlag(flags.deployment), context, this.configManager.getFlag(flags.forcePortForward)); await this._addStake(ctx.config.namespace, ctx.newNode