UNPKG

hana-cli

Version:
317 lines (284 loc) 9.67 kB
// @ts-check import * as baseLite from '../utils/base-lite.js' import { buildDocEpilogue } from '../utils/doc-linker.js' export const command = 'backupStatus' export const aliases = ['bstatus', 'backupstate', 'bkpstatus'] export const describe = baseLite.bundle.getText("backupStatus") export const builder = (yargs) => yargs.options(baseLite.getBuilder({ catalogOnly: { alias: ['co'], type: 'boolean', default: false, desc: baseLite.bundle.getText("backupStatusCatalogOnly") }, limit: { alias: ['l'], type: 'number', default: 20, desc: baseLite.bundle.getText("limit") }, backupType: { alias: ['type'], choices: ["complete", "data", "log", "incremental", "differential", "all"], default: "all", type: 'string', desc: baseLite.bundle.getText("backupStatusType") }, status: { alias: ['st'], choices: ["successful", "running", "failed", "canceled", "all"], default: "all", type: 'string', desc: baseLite.bundle.getText("backupStatusState") }, days: { type: 'number', default: 7, desc: baseLite.bundle.getText("backupStatusDays") } })).wrap(160).example('hana-cli backupStatus --backupId 12345', baseLite.bundle.getText("backupStatusExample")).wrap(160).epilog(buildDocEpilogue('backupStatus', 'backup-recovery', ['backup', 'backupList', 'replicationStatus'])) export let inputPrompts = { catalogOnly: { description: baseLite.bundle.getText("backupStatusCatalogOnly"), type: 'boolean', required: false }, limit: { description: baseLite.bundle.getText("limit"), type: 'number', required: false }, backupType: { description: baseLite.bundle.getText("backupStatusType"), type: 'string', required: false }, status: { description: baseLite.bundle.getText("backupStatusState"), type: 'string', required: false }, days: { description: baseLite.bundle.getText("backupStatusDays"), type: 'number', required: false } } /** * Command handler function * @param {object} argv - Command line arguments from yargs * @returns {Promise<void>} */ export async function handler(argv) { const base = await import('../utils/base.js') base.promptHandler(argv, getBackupStatus, inputPrompts) } /** * Get backup and recovery status from database * @param {object} prompts - Input prompts with status query configuration * @returns {Promise<object>} - Backup status information */ export async function getBackupStatus(prompts) { const base = await import('../utils/base.js') const dbClientModule = await import("../utils/database/index.js") const dbClientClass = dbClientModule.default let dbClient = null try { base.debug('getBackupStatus') dbClient = await dbClientClass.getNewClient(prompts) await dbClient.connect() const db = dbClient.getDB() console.log(base.bundle.getText("backupStatusChecking")) // Query backup catalog let backupCatalogQuery = ` SELECT BACKUP_ID, ENTRY_TYPE_NAME, SYS_START_TIME, SYS_END_TIME, STATE_NAME, COMMENT, MESSAGE FROM SYS.M_BACKUP_CATALOG WHERE SYS_START_TIME >= ADD_DAYS(CURRENT_TIMESTAMP, -${prompts.days}) ` // Add type filter if (prompts.backupType && prompts.backupType !== 'all') { const typeMap = { 'complete': 'complete data backup', 'data': 'data backup', 'log': 'log backup', 'incremental': 'incremental', 'differential': 'differential' } const typeFilter = typeMap[prompts.backupType] || prompts.backupType backupCatalogQuery += ` AND ENTRY_TYPE_NAME LIKE '%${typeFilter}%'` } // Add status filter if (prompts.status && prompts.status !== 'all') { backupCatalogQuery += ` AND STATE_NAME = '${prompts.status}'` } backupCatalogQuery += ` ORDER BY SYS_START_TIME DESC` if (prompts.limit) { backupCatalogQuery += ` LIMIT ${prompts.limit}` } const backupCatalog = await db.statementExecPromisified( await db.preparePromisified(backupCatalogQuery), [] ) // Get backup progress for running backups const progressQuery = ` SELECT HOST, PORT, BACKUP_ID, SERVICE_NAME, STATE_NAME, START_TIME, COMMENT, PROGRESS_PERCENTAGE, ACTIVE_PHASE, CURRENT_SIZE, EXPECTED_SIZE FROM SYS.M_BACKUP_PROGRESS WHERE STATE_NAME = 'running' ` let backupProgress = [] try { backupProgress = await db.statementExecPromisified( await db.preparePromisified(progressQuery), [] ) } catch (error) { // M_BACKUP_PROGRESS might not be accessible console.warn(base.bundle.getText("backupStatusProgressUnavailable")) } // Get backup configuration let backupConfig = [] try { const configQuery = ` SELECT KEY, VALUE, LAYER_NAME, SECTION FROM SYS.M_INIFILE_CONTENTS WHERE FILE_NAME = 'global.ini' AND SECTION = 'backup' ` backupConfig = await db.statementExecPromisified( await db.preparePromisified(configQuery), [] ) } catch (error) { console.warn(base.bundle.getText("backupStatusConfigUnavailable")) } // Get last successful backup const lastSuccessfulQuery = ` SELECT MAX(SYS_END_TIME) as LAST_SUCCESSFUL_BACKUP, ENTRY_TYPE_NAME FROM SYS.M_BACKUP_CATALOG WHERE STATE_NAME = 'successful' GROUP BY ENTRY_TYPE_NAME ` let lastSuccessful = [] try { lastSuccessful = await db.statementExecPromisified( await db.preparePromisified(lastSuccessfulQuery), [] ) } catch (error) { console.warn(base.bundle.getText("backupStatusLastSuccessfulUnavailable")) } // Format and display results console.log(`\n${base.bundle.getText("backupStatusSummary")}`) console.log('='.repeat(80)) // Display last successful backups if (lastSuccessful.length > 0) { console.log(`\n${base.bundle.getText("backupStatusLastSuccessful")}:`) const lastSuccessfulFormatted = lastSuccessful.map(b => ({ TYPE: b.ENTRY_TYPE_NAME, LAST_BACKUP: b.LAST_SUCCESSFUL_BACKUP ? new Date(b.LAST_SUCCESSFUL_BACKUP).toLocaleString() : 'Never' })) base.outputTableFancy(lastSuccessfulFormatted) } // Display running backups if (backupProgress.length > 0) { console.log(`\n${base.bundle.getText("backupStatusRunning", [backupProgress.length])}:`) const progressFormatted = backupProgress.map(p => ({ BACKUP_ID: p.BACKUP_ID, SERVICE: p.SERVICE_NAME, PHASE: p.ACTIVE_PHASE, PROGRESS: `${p.PROGRESS_PERCENTAGE}%`, SIZE: formatBytes(p.CURRENT_SIZE || 0), EXPECTED: formatBytes(p.EXPECTED_SIZE || 0), STARTED: p.START_TIME ? new Date(p.START_TIME).toLocaleString() : '-' })) base.outputTableFancy(progressFormatted) } else { console.log(`\n${base.bundle.getText("backupStatusNoRunning")}`) } // Display recent backups if (backupCatalog.length > 0) { console.log(`\n${base.bundle.getText("backupStatusRecent", [backupCatalog.length])}:`) const catalogFormatted = backupCatalog.map(b => ({ BACKUP_ID: b.BACKUP_ID, TYPE: b.ENTRY_TYPE_NAME, START_TIME: b.SYS_START_TIME ? new Date(b.SYS_START_TIME).toLocaleString() : '-', END_TIME: b.SYS_END_TIME ? new Date(b.SYS_END_TIME).toLocaleString() : '-', STATUS: b.STATE_NAME, MESSAGE: b.MESSAGE ? (b.MESSAGE.length > 50 ? b.MESSAGE.substring(0, 50) + '...' : b.MESSAGE) : '-' })) base.outputTableFancy(catalogFormatted) } else { console.log(`\n${base.bundle.getText("backupStatusNoRecent")}`) } // Display backup configuration if not catalog-only if (!prompts.catalogOnly && backupConfig.length > 0) { console.log(`\n${base.bundle.getText("backupStatusConfiguration")}:`) const configFormatted = backupConfig.map(c => ({ KEY: c.KEY, VALUE: c.VALUE, LAYER: c.LAYER_NAME })) base.outputTableFancy(configFormatted) } console.log('\n' + '='.repeat(80)) // Return summary const summary = { lastSuccessful: lastSuccessful, runningBackups: backupProgress.length, recentBackups: backupCatalog.length, successfulBackups: backupCatalog.filter(b => b.STATE_NAME === 'successful').length, failedBackups: backupCatalog.filter(b => b.STATE_NAME === 'failed').length, configuration: backupConfig } await dbClient.disconnect() return summary } catch (error) { console.error(base.bundle.getText("backupStatusFailed", [error.message])) // Provide helpful message if user lacks privileges if (error.message.includes('insufficient privilege')) { console.log('\n' + base.bundle.getText("backupStatusPrivilegeNote")) } if (dbClient) { await dbClient.disconnect() } throw error } } /** * Format bytes to human-readable string * @param {number} bytes - Number of bytes * @param {number} decimals - Number of decimal places * @returns {string} - Formatted string */ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes' const k = 1024 const dm = decimals < 0 ? 0 : decimals const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] }