All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.yamcs.cli.BackupCli Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs.cli;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import org.rocksdb.BackupEngine;
import org.rocksdb.BackupEngineOptions;
import org.rocksdb.BackupInfo;
import org.rocksdb.ColumnFamilyDescriptor;
import org.rocksdb.ColumnFamilyHandle;
import org.rocksdb.ColumnFamilyOptions;
import org.rocksdb.DBOptions;
import org.rocksdb.Env;
import org.rocksdb.Options;
import org.rocksdb.RestoreOptions;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.yamcs.YamcsServer;
import org.yamcs.yarch.BackupControlMBean;
import org.yamcs.yarch.BackupUtils;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.converters.PathConverter;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

/**
 * Command line backup utility for yamcs.
 * 
 * Taking a backup can be done while Yamcs server is running.
 * 
 * Restoring it has to be done while Yamcs is offline.
 * 
 * @author nm
 *
 */
@Parameters(commandDescription = "Perform and restore backups")
public class BackupCli extends Command {
    public BackupCli(YamcsAdminCli yamcsCli) {
        super("backup", yamcsCli);
        addSubCommand(new BackupCreate());
        addSubCommand(new BackupDelete());
        addSubCommand(new BackupList());
        addSubCommand(new BackupRestore());
        addSubCommand(new BackupPurge());
    }

    @Override
    void execute() throws Exception {
        RocksDB.loadLibrary();
        super.execute();
    }

    private abstract class BackupCommand extends Command {
        public BackupCommand(String name, Command parent) {
            super(name, parent);
        }

        @Parameter(names = "--backup-dir", description = "Directory containing backups", required = true)
        String backupDir;

    }

    @Parameters(commandDescription = "Create a new backup.")
    private class BackupCreate extends BackupCommand {

        @Parameter(names = "--data-dir", description = "Yamcs Data directory", converter = PathConverter.class)
        Path dataDir;

        @Parameter(names = "--host", description = "Trigger a hot backup over JMX")
        String jmxAddress;

        @Parameter(names = "--pid", description = "Process identifier of a Yamcs server")
        String pid;

        @Parameter(description = "TABLESPACE", required = true)
        List mainParameter;

        public BackupCreate() {
            super("create", BackupCli.this);
        }

        @Override
        void execute() throws Exception {
            if (mainParameter.size() > 1) {
                throw new IllegalArgumentException("Only one tablespace can be backed up at a time");
            }
            String tablespace = mainParameter.get(0);
            if (dataDir != null) {
                Path tablespaceDir = dataDir.resolve(tablespace + ".rdb");
                Path current = tablespaceDir.resolve("CURRENT");
                if (!Files.exists(current)) {
                    throw new IllegalArgumentException("'" + tablespaceDir
                            + "' does not look like a tablespace, the CURRENT file does not exist inside");
                }
                BackupUtils.verifyBackupDirectory(backupDir, false);
                try (Options opt = new Options();
                        BackupEngineOptions bopt = new BackupEngineOptions(backupDir);
                        ColumnFamilyOptions cfOptions = new ColumnFamilyOptions();
                        DBOptions dbOptions = new DBOptions();
                        BackupEngine backupEngine = BackupEngine.open(Env.getDefault(), bopt);) {

                    List cfl = RocksDB.listColumnFamilies(opt, tablespaceDir.toString());
                    List cfdList = new ArrayList<>(cfl.size());

                    for (byte[] b : cfl) {
                        cfdList.add(new ColumnFamilyDescriptor(b, cfOptions));
                    }
                    List cfhList = new ArrayList<>(cfl.size());

                    try (RocksDB db = RocksDB.open(dbOptions, tablespaceDir.toString(), cfdList, cfhList)) {
                        backupEngine.createNewBackup(db);
                        console.println("Backup performed successfully");
                    } finally {
                        for (final ColumnFamilyHandle cfh : cfhList) {
                            cfh.close();
                        }
                    }
                } catch (RocksDBException e) {
                    throw new IOException(
                            "Error when backing up tablespace '" + tablespace + "' to '" + backupDir + "': "
                                    + e.toString());
                }
            } else if (jmxAddress != null) {
                JMXServiceURL jmxServiceUrl = new JMXServiceURL(String.format(
                        "service:jmx:rmi:///jndi/rmi://%s/jmxrmi", jmxAddress));
                JMXConnector jmxc = JMXConnectorFactory.connect(jmxServiceUrl);
                MBeanServerConnection conn = jmxc.getMBeanServerConnection();
                ObjectName mbeanName = new ObjectName("org.yamcs:name=Backup");
                BackupControlMBean control = JMX.newMBeanProxy(conn, mbeanName, BackupControlMBean.class);
                control.createBackup(tablespace, backupDir);
            } else {
                String jvmIdentifier = pid;
                if (jvmIdentifier == null) {
                    // Find a single local runnning Yamcs
                    for (VirtualMachineDescriptor vmDescriptor : VirtualMachine.list()) {
                        // Something of the form:
                        // org.yamcs.YamcsServer --data-dir /storage/yamcs-data
                        var displayName = vmDescriptor.displayName();
                        if (displayName.startsWith(YamcsServer.class.getName())) {
                            if (jvmIdentifier != null) {
                                throw new YamcsAdminException(
                                        "More than one Yamcs server is running. Specify --pid");
                            }
                            jvmIdentifier = vmDescriptor.id();
                        }
                    }
                }

                if (jvmIdentifier == null) {
                    throw new YamcsAdminException("Cannot connect to Yamcs. "
                            + "Use --data-dir if you want to perform an offline backup");
                }

                VirtualMachine vm = VirtualMachine.attach(jvmIdentifier);
                try {
                    String jmxAddress = vm.startLocalManagementAgent();
                    JMXServiceURL url = new JMXServiceURL(jmxAddress);
                    JMXConnector jmxc = JMXConnectorFactory.connect(url);
                    MBeanServerConnection conn = jmxc.getMBeanServerConnection();
                    ObjectName mbeanName = new ObjectName("org.yamcs:name=Backup");
                    BackupControlMBean control = JMX.newMBeanProxy(conn, mbeanName, BackupControlMBean.class);
                    control.createBackup(tablespace, backupDir);
                } finally {
                    vm.detach();
                }
            }
        }
    }

    @Parameters(commandDescription = "Restore a backup. This can only be done when Yamcs is not running.")
    private class BackupRestore extends BackupCommand {

        @Parameter(names = "--restore-dir", description = "Directory where to restore the backup", required = true)
        String restoreDir;

        @Parameter(description = "ID", required = true)
        List mainParameters;

        public BackupRestore() {
            super("restore", BackupCli.this);
        }

        @Override
        void execute() throws Exception {
            BackupUtils.verifyBackupDirectory(backupDir, true);
            try (BackupEngineOptions opt = new BackupEngineOptions(backupDir);
                    BackupEngine backupEngine = BackupEngine.open(Env.getDefault(), opt);
                    RestoreOptions restoreOpt = new RestoreOptions(true);) {
                if (mainParameters.size() > 1) {
                    throw new IllegalArgumentException("Too many arguments. Only one backup can be restored");
                } else if (mainParameters.size() == 1) {
                    int backupId = Integer.parseInt(mainParameters.get(0));
                    backupEngine.restoreDbFromBackup(backupId, restoreDir, restoreDir, restoreOpt);
                } else {
                    backupEngine.restoreDbFromLatestBackup(restoreDir, restoreDir, restoreOpt);
                }
            }
            console.println("Backup restored successfully to " + restoreDir);
        }
    }

    @Parameters(commandDescription = "List the existing backups")
    private class BackupList extends BackupCommand {
        public BackupList() {
            super("list", BackupCli.this);
        }

        @Override
        void execute() throws Exception {
            BackupUtils.verifyBackupDirectory(backupDir, true);

            try (BackupEngineOptions opt = new BackupEngineOptions(backupDir);
                    BackupEngine backupEngine = BackupEngine.open(Env.getDefault(), opt)) {
                TableStringBuilder b = new TableStringBuilder("backup id", "size (bytes)", "num files", "time");
                final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneId.of("UTC"));
                for (BackupInfo bi : backupEngine.getBackupInfo()) {
                    b.addLine(bi.backupId(), bi.size(), bi.numberFiles(),
                            formatter.format(Instant.ofEpochMilli(1000 * bi.timestamp())));
                }
                console.println(b.toString());
            }
        }
    }

    @Parameters(commandDescription = "Delete a backup")
    public class BackupDelete extends BackupCommand {

        @Parameter(description = "ID ...", required = true)
        List mainParameters;

        public BackupDelete() {
            super("delete", BackupCli.this);
        }

        @Override
        void execute() throws Exception {
            BackupUtils.verifyBackupDirectory(backupDir, true);
            try (BackupEngineOptions opt = new BackupEngineOptions(backupDir);
                    BackupEngine backupEngine = BackupEngine.open(Env.getDefault(), opt);) {

                for (String mainParameter : mainParameters) {
                    int backupId = Integer.parseInt(mainParameter);
                    backupEngine.deleteBackup(backupId);
                    console.println("Deleted backup " + backupId);
                }
            }
        }
    }

    @Parameters(commandDescription = "Purge old backups")
    public class BackupPurge extends BackupCommand {

        @Parameter(names = "--keep", description = "Number of backups to keep", required = true)
        Integer backupsToKeep;

        public BackupPurge() {
            super("purge", BackupCli.this);
        }

        @Override
        void execute() throws Exception {
            BackupUtils.verifyBackupDirectory(backupDir, true);
            try (BackupEngineOptions opt = new BackupEngineOptions(backupDir);
                    BackupEngine backupEngine = BackupEngine.open(Env.getDefault(), opt);) {
                backupEngine.purgeOldBackups(backupsToKeep);
                int n = backupEngine.getBackupInfo().size();
                console.println("Purged operation successful; " + n + " backups remaining.");
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy