001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.backup.impl;
019
020import static org.apache.hadoop.hbase.backup.BackupInfo.withState;
021import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BACKUP_LIST_DESC;
022import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH;
023import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_BANDWIDTH_DESC;
024import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG;
025import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_DEBUG_DESC;
026import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_IGNORECHECKSUM;
027import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_IGNORECHECKSUM_DESC;
028import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP;
029import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_KEEP_DESC;
030import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_LIST;
031import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_PATH;
032import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_PATH_DESC;
033import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_RECORD_NUMBER;
034import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_RECORD_NUMBER_DESC;
035import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET;
036import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET_BACKUP_DESC;
037import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_SET_DESC;
038import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE;
039import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_DESC;
040import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_TABLE_LIST_DESC;
041import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_WORKERS;
042import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_WORKERS_DESC;
043import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME;
044import static org.apache.hadoop.hbase.backup.BackupRestoreConstants.OPTION_YARN_QUEUE_NAME_DESC;
045import static org.apache.hadoop.hbase.backup.impl.BackupSystemTable.Order.NEW_TO_OLD;
046
047import java.io.IOException;
048import java.net.URI;
049import java.util.List;
050import org.apache.commons.lang3.StringUtils;
051import org.apache.hadoop.conf.Configuration;
052import org.apache.hadoop.conf.Configured;
053import org.apache.hadoop.fs.FileSystem;
054import org.apache.hadoop.fs.Path;
055import org.apache.hadoop.hbase.HBaseConfiguration;
056import org.apache.hadoop.hbase.TableName;
057import org.apache.hadoop.hbase.backup.BackupAdmin;
058import org.apache.hadoop.hbase.backup.BackupInfo;
059import org.apache.hadoop.hbase.backup.BackupInfo.BackupState;
060import org.apache.hadoop.hbase.backup.BackupRequest;
061import org.apache.hadoop.hbase.backup.BackupRestoreConstants;
062import org.apache.hadoop.hbase.backup.BackupRestoreConstants.BackupCommand;
063import org.apache.hadoop.hbase.backup.BackupType;
064import org.apache.hadoop.hbase.backup.HBackupFileSystem;
065import org.apache.hadoop.hbase.backup.util.BackupSet;
066import org.apache.hadoop.hbase.backup.util.BackupUtils;
067import org.apache.hadoop.hbase.client.Connection;
068import org.apache.hadoop.hbase.client.ConnectionFactory;
069import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
070import org.apache.yetus.audience.InterfaceAudience;
071
072import org.apache.hbase.thirdparty.com.google.common.base.Splitter;
073import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
074import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
075import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter;
076import org.apache.hbase.thirdparty.org.apache.commons.cli.Options;
077
078/**
079 * General backup commands, options and usage messages
080 */
081@InterfaceAudience.Private
082public final class BackupCommands {
083  public final static String INCORRECT_USAGE = "Incorrect usage";
084
085  public final static String TOP_LEVEL_NOT_ALLOWED =
086    "Top level (root) folder is not allowed to be a backup destination";
087
088  public static final String USAGE = "Usage: hbase backup COMMAND [command-specific arguments]\n"
089    + "where COMMAND is one of:\n" + "  create     create a new backup image\n"
090    + "  delete     delete an existing backup image\n"
091    + "  describe   show the detailed information of a backup image\n"
092    + "  history    show history of all successful backups\n"
093    + "  progress   show the progress of the latest backup request\n"
094    + "  set        backup set management\n" + "  repair     repair backup system table\n"
095    + "  merge      merge backup images\n"
096    + "Run \'hbase backup COMMAND -h\' to see help message for each command\n";
097
098  public static final String CREATE_CMD_USAGE =
099    "Usage: hbase backup create <type> <backup_path> [options]\n"
100      + "  type           \"full\" to create a full backup image\n"
101      + "                 \"incremental\" to create an incremental backup image\n"
102      + "  backup_path     Full path to store the backup image\n";
103
104  public static final String PROGRESS_CMD_USAGE = "Usage: hbase backup progress <backup_id>\n"
105    + "  backup_id       Backup image id (optional). If no id specified, the command will show\n"
106    + "                  progress for currently running backup session.";
107  public static final String NO_INFO_FOUND = "No info was found for backup id: ";
108  public static final String NO_ACTIVE_SESSION_FOUND = "No active backup sessions found.";
109
110  public static final String DESCRIBE_CMD_USAGE =
111    "Usage: hbase backup describe <backup_id>\n" + "  backup_id       Backup image id\n";
112
113  public static final String HISTORY_CMD_USAGE = "Usage: hbase backup history [options]";
114
115  public static final String DELETE_CMD_USAGE = "Usage: hbase backup delete [options]";
116
117  public static final String REPAIR_CMD_USAGE = "Usage: hbase backup repair\n";
118
119  public static final String SET_CMD_USAGE = "Usage: hbase backup set COMMAND [name] [tables]\n"
120    + "  name            Backup set name\n" + "  tables          Comma separated list of tables.\n"
121    + "COMMAND is one of:\n" + "  add             add tables to a set, create a set if needed\n"
122    + "  remove          remove tables from a set\n"
123    + "  list            list all backup sets in the system\n" + "  describe        describe set\n"
124    + "  delete          delete backup set\n";
125  public static final String MERGE_CMD_USAGE = "Usage: hbase backup merge [backup_ids]\n"
126    + "  backup_ids      Comma separated list of backup image ids.\n";
127
128  public static final String USAGE_FOOTER = "";
129
130  public static abstract class Command extends Configured {
131    CommandLine cmdline;
132    Connection conn;
133
134    Command(Configuration conf) {
135      if (conf == null) {
136        conf = HBaseConfiguration.create();
137      }
138      setConf(conf);
139    }
140
141    public void execute() throws IOException {
142      if (cmdline.hasOption("h") || cmdline.hasOption("help")) {
143        printUsage();
144        throw new IOException(INCORRECT_USAGE);
145      }
146
147      if (cmdline.hasOption(OPTION_YARN_QUEUE_NAME)) {
148        String queueName = cmdline.getOptionValue(OPTION_YARN_QUEUE_NAME);
149        // Set MR job queuename to configuration
150        getConf().set("mapreduce.job.queuename", queueName);
151      }
152
153      // Create connection
154      conn = ConnectionFactory.createConnection(getConf());
155      if (requiresNoActiveSession()) {
156        // Check active session
157        try (BackupSystemTable table = new BackupSystemTable(conn)) {
158          List<BackupInfo> sessions =
159            table.getBackupHistory(NEW_TO_OLD, 1, withState(BackupState.RUNNING));
160
161          if (sessions.size() > 0) {
162            System.err.println("Found backup session in a RUNNING state: ");
163            System.err.println(sessions.get(0));
164            System.err.println("This may indicate that a previous session has failed abnormally.");
165            System.err.println("In this case, backup recovery is recommended.");
166            throw new IOException("Active session found, aborted command execution");
167          }
168        }
169      }
170      if (requiresConsistentState()) {
171        // Check failed delete
172        try (BackupSystemTable table = new BackupSystemTable(conn)) {
173          String[] ids = table.getListOfBackupIdsFromDeleteOperation();
174
175          if (ids != null && ids.length > 0) {
176            System.err.println("Found failed backup DELETE coommand. ");
177            System.err.println("Backup system recovery is required.");
178            throw new IOException("Failed backup DELETE found, aborted command execution");
179          }
180
181          ids = table.getListOfBackupIdsFromMergeOperation();
182          if (ids != null && ids.length > 0) {
183            System.err.println("Found failed backup MERGE coommand. ");
184            System.err.println("Backup system recovery is required.");
185            throw new IOException("Failed backup MERGE found, aborted command execution");
186          }
187        }
188      }
189    }
190
191    public void finish() throws IOException {
192      if (conn != null) {
193        conn.close();
194      }
195    }
196
197    protected abstract void printUsage();
198
199    /**
200     * The command can't be run if active backup session is in progress
201     * @return true if no active sessions are in progress
202     */
203    protected boolean requiresNoActiveSession() {
204      return false;
205    }
206
207    /**
208     * Command requires consistent state of a backup system Backup system may become inconsistent
209     * because of an abnormal termination of a backup session or delete command
210     * @return true, if yes
211     */
212    protected boolean requiresConsistentState() {
213      return false;
214    }
215  }
216
217  private BackupCommands() {
218    throw new AssertionError("Instantiating utility class...");
219  }
220
221  public static Command createCommand(Configuration conf, BackupCommand type, CommandLine cmdline) {
222    Command cmd;
223    switch (type) {
224      case CREATE:
225        cmd = new CreateCommand(conf, cmdline);
226        break;
227      case DESCRIBE:
228        cmd = new DescribeCommand(conf, cmdline);
229        break;
230      case PROGRESS:
231        cmd = new ProgressCommand(conf, cmdline);
232        break;
233      case DELETE:
234        cmd = new DeleteCommand(conf, cmdline);
235        break;
236      case HISTORY:
237        cmd = new HistoryCommand(conf, cmdline);
238        break;
239      case SET:
240        cmd = new BackupSetCommand(conf, cmdline);
241        break;
242      case REPAIR:
243        cmd = new RepairCommand(conf, cmdline);
244        break;
245      case MERGE:
246        cmd = new MergeCommand(conf, cmdline);
247        break;
248      case HELP:
249      default:
250        cmd = new HelpCommand(conf, cmdline);
251        break;
252    }
253    return cmd;
254  }
255
256  static int numOfArgs(String[] args) {
257    if (args == null) {
258      return 0;
259    }
260
261    return args.length;
262  }
263
264  public static class CreateCommand extends Command {
265    CreateCommand(Configuration conf, CommandLine cmdline) {
266      super(conf);
267      this.cmdline = cmdline;
268    }
269
270    @Override
271    protected boolean requiresNoActiveSession() {
272      return true;
273    }
274
275    @Override
276    protected boolean requiresConsistentState() {
277      return true;
278    }
279
280    @Override
281    public void execute() throws IOException {
282      if (cmdline == null || cmdline.getArgs() == null) {
283        printUsage();
284        throw new IOException(INCORRECT_USAGE);
285      }
286      String[] args = cmdline.getArgs();
287      if (args.length != 3) {
288        printUsage();
289        throw new IOException(INCORRECT_USAGE);
290      }
291
292      if (
293        !BackupType.FULL.toString().equalsIgnoreCase(args[1])
294          && !BackupType.INCREMENTAL.toString().equalsIgnoreCase(args[1])
295      ) {
296        System.out.println("ERROR: invalid backup type: " + args[1]);
297        printUsage();
298        throw new IOException(INCORRECT_USAGE);
299      }
300      if (!verifyPath(args[2])) {
301        System.out.println("ERROR: invalid backup destination: " + args[2]);
302        printUsage();
303        throw new IOException(INCORRECT_USAGE);
304      }
305      String targetBackupDir = args[2];
306      // Check if backup destination is top level (root) folder - not allowed
307      if (isRootFolder(targetBackupDir)) {
308        throw new IOException(TOP_LEVEL_NOT_ALLOWED);
309      }
310      String tables;
311
312      // Check if we have both: backup set and list of tables
313      if (cmdline.hasOption(OPTION_TABLE) && cmdline.hasOption(OPTION_SET)) {
314        System.out
315          .println("ERROR: You can specify either backup set or list" + " of tables, but not both");
316        printUsage();
317        throw new IOException(INCORRECT_USAGE);
318      }
319      // Creates connection
320      super.execute();
321      // Check backup set
322      String setName = null;
323      if (cmdline.hasOption(OPTION_SET)) {
324        setName = cmdline.getOptionValue(OPTION_SET);
325        tables = getTablesForSet(setName);
326
327        if (tables == null) {
328          System.out
329            .println("ERROR: Backup set '" + setName + "' is either empty or does not exist");
330          printUsage();
331          throw new IOException(INCORRECT_USAGE);
332        }
333      } else {
334        tables = cmdline.getOptionValue(OPTION_TABLE);
335      }
336      int bandwidth = cmdline.hasOption(OPTION_BANDWIDTH)
337        ? Integer.parseInt(cmdline.getOptionValue(OPTION_BANDWIDTH))
338        : -1;
339      int workers = cmdline.hasOption(OPTION_WORKERS)
340        ? Integer.parseInt(cmdline.getOptionValue(OPTION_WORKERS))
341        : -1;
342
343      boolean ignoreChecksum = cmdline.hasOption(OPTION_IGNORECHECKSUM);
344
345      try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
346        BackupRequest.Builder builder = new BackupRequest.Builder();
347        BackupRequest request = builder.withBackupType(BackupType.valueOf(args[1].toUpperCase()))
348          .withTableList(
349            tables != null ? Lists.newArrayList(BackupUtils.parseTableNames(tables)) : null)
350          .withTargetRootDir(targetBackupDir).withTotalTasks(workers)
351          .withBandwidthPerTasks(bandwidth).withNoChecksumVerify(ignoreChecksum)
352          .withBackupSetName(setName).build();
353        String backupId = admin.backupTables(request);
354        System.out.println("Backup session " + backupId + " finished. Status: SUCCESS");
355      } catch (IOException e) {
356        System.out.println("Backup session finished. Status: FAILURE");
357        throw e;
358      }
359    }
360
361    private boolean isRootFolder(String targetBackupDir) {
362      Path p = new Path(targetBackupDir);
363      return p.isRoot();
364    }
365
366    private boolean verifyPath(String path) {
367      try {
368        Path p = new Path(path);
369        Configuration conf = getConf() != null ? getConf() : HBaseConfiguration.create();
370        URI uri = p.toUri();
371
372        if (uri.getScheme() == null) {
373          return false;
374        }
375
376        FileSystem.get(uri, conf);
377        return true;
378      } catch (Exception e) {
379        return false;
380      }
381    }
382
383    private String getTablesForSet(String name) throws IOException {
384      try (final BackupSystemTable table = new BackupSystemTable(conn)) {
385        List<TableName> tables = table.describeBackupSet(name);
386
387        if (tables == null) {
388          return null;
389        }
390
391        return StringUtils.join(tables, BackupRestoreConstants.TABLENAME_DELIMITER_IN_COMMAND);
392      }
393    }
394
395    @Override
396    protected void printUsage() {
397      System.out.println(CREATE_CMD_USAGE);
398      Options options = new Options();
399      options.addOption(OPTION_WORKERS, true, OPTION_WORKERS_DESC);
400      options.addOption(OPTION_BANDWIDTH, true, OPTION_BANDWIDTH_DESC);
401      options.addOption(OPTION_SET, true, OPTION_SET_BACKUP_DESC);
402      options.addOption(OPTION_TABLE, true, OPTION_TABLE_LIST_DESC);
403      options.addOption(OPTION_YARN_QUEUE_NAME, true, OPTION_YARN_QUEUE_NAME_DESC);
404      options.addOption(OPTION_DEBUG, false, OPTION_DEBUG_DESC);
405      options.addOption(OPTION_IGNORECHECKSUM, false, OPTION_IGNORECHECKSUM_DESC);
406
407      HelpFormatter helpFormatter = new HelpFormatter();
408      helpFormatter.setLeftPadding(2);
409      helpFormatter.setDescPadding(8);
410      helpFormatter.setWidth(100);
411      helpFormatter.setSyntaxPrefix("Options:");
412      helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
413    }
414  }
415
416  public static class HelpCommand extends Command {
417    HelpCommand(Configuration conf, CommandLine cmdline) {
418      super(conf);
419      this.cmdline = cmdline;
420    }
421
422    @Override
423    public void execute() throws IOException {
424      if (cmdline == null) {
425        printUsage();
426        throw new IOException(INCORRECT_USAGE);
427      }
428
429      String[] args = cmdline.getArgs();
430      if (args == null || args.length == 0) {
431        printUsage();
432        throw new IOException(INCORRECT_USAGE);
433      }
434
435      if (args.length != 2) {
436        System.out.println("ERROR: Only supports help message of a single command type");
437        printUsage();
438        throw new IOException(INCORRECT_USAGE);
439      }
440
441      String type = args[1];
442
443      if (BackupCommand.CREATE.name().equalsIgnoreCase(type)) {
444        System.out.println(CREATE_CMD_USAGE);
445      } else if (BackupCommand.DESCRIBE.name().equalsIgnoreCase(type)) {
446        System.out.println(DESCRIBE_CMD_USAGE);
447      } else if (BackupCommand.HISTORY.name().equalsIgnoreCase(type)) {
448        System.out.println(HISTORY_CMD_USAGE);
449      } else if (BackupCommand.PROGRESS.name().equalsIgnoreCase(type)) {
450        System.out.println(PROGRESS_CMD_USAGE);
451      } else if (BackupCommand.DELETE.name().equalsIgnoreCase(type)) {
452        System.out.println(DELETE_CMD_USAGE);
453      } else if (BackupCommand.SET.name().equalsIgnoreCase(type)) {
454        System.out.println(SET_CMD_USAGE);
455      } else {
456        System.out.println("Unknown command : " + type);
457        printUsage();
458      }
459    }
460
461    @Override
462    protected void printUsage() {
463      System.out.println(USAGE);
464    }
465  }
466
467  public static class DescribeCommand extends Command {
468    DescribeCommand(Configuration conf, CommandLine cmdline) {
469      super(conf);
470      this.cmdline = cmdline;
471    }
472
473    @Override
474    public void execute() throws IOException {
475      if (cmdline == null || cmdline.getArgs() == null) {
476        printUsage();
477        throw new IOException(INCORRECT_USAGE);
478      }
479      String[] args = cmdline.getArgs();
480      if (args.length != 2) {
481        printUsage();
482        throw new IOException(INCORRECT_USAGE);
483      }
484
485      super.execute();
486
487      String backupId = args[1];
488      try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
489        BackupInfo info = sysTable.readBackupInfo(backupId);
490        if (info == null) {
491          System.out.println("ERROR: " + backupId + " does not exist");
492          printUsage();
493          throw new IOException(INCORRECT_USAGE);
494        }
495        System.out.println(info.getShortDescription());
496      }
497    }
498
499    @Override
500    protected void printUsage() {
501      System.out.println(DESCRIBE_CMD_USAGE);
502    }
503  }
504
505  public static class ProgressCommand extends Command {
506    ProgressCommand(Configuration conf, CommandLine cmdline) {
507      super(conf);
508      this.cmdline = cmdline;
509    }
510
511    @Override
512    public void execute() throws IOException {
513
514      if (cmdline == null || cmdline.getArgs() == null || cmdline.getArgs().length == 1) {
515        System.out.println(
516          "No backup id was specified, " + "will retrieve the most recent (ongoing) session");
517      }
518      String[] args = cmdline == null ? null : cmdline.getArgs();
519      if (args != null && args.length > 2) {
520        System.err.println("ERROR: wrong number of arguments: " + args.length);
521        printUsage();
522        throw new IOException(INCORRECT_USAGE);
523      }
524
525      super.execute();
526
527      String backupId = (args == null || args.length <= 1) ? null : args[1];
528      try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
529        BackupInfo info = null;
530
531        if (backupId != null) {
532          info = sysTable.readBackupInfo(backupId);
533        } else {
534          List<BackupInfo> infos =
535            sysTable.getBackupHistory(NEW_TO_OLD, 1, withState(BackupState.RUNNING));
536          if (infos != null && infos.size() > 0) {
537            info = infos.get(0);
538            backupId = info.getBackupId();
539            System.out.println("Found ongoing session with backupId=" + backupId);
540          }
541        }
542        int progress = info == null ? -1 : info.getProgress();
543        if (progress < 0) {
544          if (backupId != null) {
545            System.out.println(NO_INFO_FOUND + backupId);
546          } else {
547            System.err.println(NO_ACTIVE_SESSION_FOUND);
548          }
549        } else {
550          System.out.println(backupId + " progress=" + progress + "%");
551        }
552      }
553    }
554
555    @Override
556    protected void printUsage() {
557      System.out.println(PROGRESS_CMD_USAGE);
558    }
559  }
560
561  public static class DeleteCommand extends Command {
562    DeleteCommand(Configuration conf, CommandLine cmdline) {
563      super(conf);
564      this.cmdline = cmdline;
565    }
566
567    @Override
568    protected boolean requiresNoActiveSession() {
569      return true;
570    }
571
572    @Override
573    public void execute() throws IOException {
574
575      if (cmdline == null || cmdline.getArgs() == null || cmdline.getArgs().length < 1) {
576        printUsage();
577        throw new IOException(INCORRECT_USAGE);
578      }
579
580      if (!cmdline.hasOption(OPTION_KEEP) && !cmdline.hasOption(OPTION_LIST)) {
581        printUsage();
582        throw new IOException(INCORRECT_USAGE);
583      }
584      super.execute();
585      if (cmdline.hasOption(OPTION_KEEP)) {
586        executeDeleteOlderThan(cmdline);
587      } else if (cmdline.hasOption(OPTION_LIST)) {
588        executeDeleteListOfBackups(cmdline);
589      }
590    }
591
592    private void executeDeleteOlderThan(CommandLine cmdline) throws IOException {
593      String value = cmdline.getOptionValue(OPTION_KEEP);
594      int days = 0;
595      try {
596        days = Integer.parseInt(value);
597      } catch (NumberFormatException e) {
598        throw new IOException(value + " is not an integer number");
599      }
600      final long fdays = days;
601      BackupInfo.Filter dateFilter = info -> {
602        long currentTime = EnvironmentEdgeManager.currentTime();
603        long maxTsToDelete = currentTime - fdays * 24 * 3600 * 1000;
604        return info.getCompleteTs() <= maxTsToDelete;
605      };
606      List<BackupInfo> history = null;
607      try (final BackupSystemTable sysTable = new BackupSystemTable(conn);
608        BackupAdminImpl admin = new BackupAdminImpl(conn)) {
609        history = sysTable.getBackupHistory(dateFilter);
610        String[] backupIds = convertToBackupIds(history);
611        int deleted = admin.deleteBackups(backupIds);
612        System.out.println("Deleted " + deleted + " backups. Total older than " + days + " days: "
613          + backupIds.length);
614      } catch (IOException e) {
615        System.err.println("Delete command FAILED. Please run backup repair tool to restore backup "
616          + "system integrity");
617        throw e;
618      }
619    }
620
621    private String[] convertToBackupIds(List<BackupInfo> history) {
622      String[] ids = new String[history.size()];
623      for (int i = 0; i < ids.length; i++) {
624        ids[i] = history.get(i).getBackupId();
625      }
626      return ids;
627    }
628
629    private void executeDeleteListOfBackups(CommandLine cmdline) throws IOException {
630      String value = cmdline.getOptionValue(OPTION_LIST);
631      String[] backupIds = value.split(",");
632
633      try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
634        int deleted = admin.deleteBackups(backupIds);
635        System.out.println("Deleted " + deleted + " backups. Total requested: " + backupIds.length);
636      } catch (IOException e) {
637        System.err.println("Delete command FAILED. Please run backup repair tool to restore backup "
638          + "system integrity");
639        throw e;
640      }
641
642    }
643
644    @Override
645    protected void printUsage() {
646      System.out.println(DELETE_CMD_USAGE);
647      Options options = new Options();
648      options.addOption(OPTION_KEEP, true, OPTION_KEEP_DESC);
649      options.addOption(OPTION_LIST, true, OPTION_BACKUP_LIST_DESC);
650
651      HelpFormatter helpFormatter = new HelpFormatter();
652      helpFormatter.setLeftPadding(2);
653      helpFormatter.setDescPadding(8);
654      helpFormatter.setWidth(100);
655      helpFormatter.setSyntaxPrefix("Options:");
656      helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
657
658    }
659  }
660
661  public static class RepairCommand extends Command {
662    RepairCommand(Configuration conf, CommandLine cmdline) {
663      super(conf);
664      this.cmdline = cmdline;
665    }
666
667    @Override
668    public void execute() throws IOException {
669      super.execute();
670
671      String[] args = cmdline == null ? null : cmdline.getArgs();
672      if (args != null && args.length > 1) {
673        System.err.println("ERROR: wrong number of arguments: " + args.length);
674        printUsage();
675        throw new IOException(INCORRECT_USAGE);
676      }
677
678      Configuration conf = getConf() != null ? getConf() : HBaseConfiguration.create();
679      try (final Connection conn = ConnectionFactory.createConnection(conf);
680        final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
681        // Failed backup
682        BackupInfo backupInfo;
683        List<BackupInfo> list =
684          sysTable.getBackupHistory(NEW_TO_OLD, 1, withState(BackupState.RUNNING));
685        if (list.size() == 0) {
686          // No failed sessions found
687          System.out.println("REPAIR status: no failed sessions found."
688            + " Checking failed delete backup operation ...");
689          repairFailedBackupDeletionIfAny(conn, sysTable);
690          repairFailedBackupMergeIfAny(conn, sysTable);
691          return;
692        }
693        backupInfo = list.get(0);
694        // If this is a cancel exception, then we've already cleaned.
695        // set the failure timestamp of the overall backup
696        backupInfo.setCompleteTs(EnvironmentEdgeManager.currentTime());
697        // set failure message
698        backupInfo.setFailedMsg("REPAIR status: repaired after failure:\n" + backupInfo);
699        // set overall backup status: failed
700        backupInfo.setState(BackupState.FAILED);
701        // compose the backup failed data
702        String backupFailedData = "BackupId=" + backupInfo.getBackupId() + ",startts="
703          + backupInfo.getStartTs() + ",failedts=" + backupInfo.getCompleteTs() + ",failedphase="
704          + backupInfo.getPhase() + ",failedmessage=" + backupInfo.getFailedMsg();
705        System.out.println(backupFailedData);
706        TableBackupClient.cleanupAndRestoreBackupSystem(conn, backupInfo, conf);
707        // If backup session is updated to FAILED state - means we
708        // processed recovery already.
709        sysTable.updateBackupInfo(backupInfo);
710        sysTable.finishBackupExclusiveOperation();
711        System.out.println("REPAIR status: finished repair failed session:\n " + backupInfo);
712      }
713    }
714
715    private void repairFailedBackupDeletionIfAny(Connection conn, BackupSystemTable sysTable)
716      throws IOException {
717      String[] backupIds = sysTable.getListOfBackupIdsFromDeleteOperation();
718      if (backupIds == null || backupIds.length == 0) {
719        System.out.println("No failed backup DELETE operation found");
720        // Delete backup table snapshot if exists
721        BackupSystemTable.deleteSnapshot(conn);
722        return;
723      }
724      System.out.println("Found failed DELETE operation for: " + StringUtils.join(backupIds));
725      System.out.println("Running DELETE again ...");
726      // Restore table from snapshot
727      BackupSystemTable.restoreFromSnapshot(conn);
728      // Finish previous failed session
729      sysTable.finishBackupExclusiveOperation();
730      try (BackupAdmin admin = new BackupAdminImpl(conn)) {
731        admin.deleteBackups(backupIds);
732      }
733      System.out.println("DELETE operation finished OK: " + StringUtils.join(backupIds));
734    }
735
736    public static void repairFailedBackupMergeIfAny(Connection conn, BackupSystemTable sysTable)
737      throws IOException {
738
739      String[] backupIds = sysTable.getListOfBackupIdsFromMergeOperation();
740      if (backupIds == null || backupIds.length == 0) {
741        System.out.println("No failed backup MERGE operation found");
742        // Delete backup table snapshot if exists
743        BackupSystemTable.deleteSnapshot(conn);
744        return;
745      }
746      System.out.println("Found failed MERGE operation for: " + StringUtils.join(backupIds));
747      // Check if backup .tmp exists
748      BackupInfo bInfo = sysTable.readBackupInfo(backupIds[0]);
749      String backupRoot = bInfo.getBackupRootDir();
750      FileSystem fs = FileSystem.get(new Path(backupRoot).toUri(), new Configuration());
751      String backupId = BackupUtils.findMostRecentBackupId(backupIds);
752      Path tmpPath = HBackupFileSystem.getBackupTmpDirPathForBackupId(backupRoot, backupId);
753      if (fs.exists(tmpPath)) {
754        // Move data back
755        Path destPath = HBackupFileSystem.getBackupPath(backupRoot, backupId);
756        if (!fs.delete(destPath, true)) {
757          System.out.println("Failed to delete " + destPath);
758        }
759        boolean res = fs.rename(tmpPath, destPath);
760        if (!res) {
761          throw new IOException(
762            "MERGE repair: failed  to rename from " + tmpPath + " to " + destPath);
763        }
764        System.out
765          .println("MERGE repair: renamed from " + tmpPath + " to " + destPath + " res=" + res);
766      } else {
767        checkRemoveBackupImages(fs, backupRoot, backupIds);
768      }
769      // Restore table from snapshot
770      BackupSystemTable.restoreFromSnapshot(conn);
771      // Unlock backup system
772      sysTable.finishBackupExclusiveOperation();
773      // Finish previous failed session
774      sysTable.finishMergeOperation();
775
776      System.out.println("MERGE repair operation finished OK: " + StringUtils.join(backupIds));
777    }
778
779    private static void checkRemoveBackupImages(FileSystem fs, String backupRoot,
780      String[] backupIds) throws IOException {
781      String mergedBackupId = BackupUtils.findMostRecentBackupId(backupIds);
782      for (String backupId : backupIds) {
783        if (backupId.equals(mergedBackupId)) {
784          continue;
785        }
786        Path path = HBackupFileSystem.getBackupPath(backupRoot, backupId);
787        if (fs.exists(path)) {
788          if (!fs.delete(path, true)) {
789            System.out.println("MERGE repair removing: " + path + " - FAILED");
790          } else {
791            System.out.println("MERGE repair removing: " + path + " - OK");
792          }
793        }
794      }
795    }
796
797    @Override
798    protected void printUsage() {
799      System.out.println(REPAIR_CMD_USAGE);
800    }
801  }
802
803  public static class MergeCommand extends Command {
804    MergeCommand(Configuration conf, CommandLine cmdline) {
805      super(conf);
806      this.cmdline = cmdline;
807    }
808
809    @Override
810    protected boolean requiresNoActiveSession() {
811      return true;
812    }
813
814    @Override
815    protected boolean requiresConsistentState() {
816      return true;
817    }
818
819    @Override
820    public void execute() throws IOException {
821      super.execute();
822
823      String[] args = cmdline == null ? null : cmdline.getArgs();
824      if (args == null || (args.length != 2)) {
825        System.err
826          .println("ERROR: wrong number of arguments: " + (args == null ? null : args.length));
827        printUsage();
828        throw new IOException(INCORRECT_USAGE);
829      }
830
831      String[] backupIds = args[1].split(",");
832      if (backupIds.length < 2) {
833        String msg = "ERROR: can not merge a single backup image. "
834          + "Number of images must be greater than 1.";
835        System.err.println(msg);
836        throw new IOException(msg);
837
838      }
839      Configuration conf = getConf() != null ? getConf() : HBaseConfiguration.create();
840      try (final Connection conn = ConnectionFactory.createConnection(conf);
841        final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
842        admin.mergeBackups(backupIds);
843      }
844    }
845
846    @Override
847    protected void printUsage() {
848      System.out.println(MERGE_CMD_USAGE);
849    }
850  }
851
852  public static class HistoryCommand extends Command {
853    private final static int DEFAULT_HISTORY_LENGTH = 10;
854
855    HistoryCommand(Configuration conf, CommandLine cmdline) {
856      super(conf);
857      this.cmdline = cmdline;
858    }
859
860    @Override
861    public void execute() throws IOException {
862      int n = parseHistoryLength();
863      final TableName tableName = getTableName();
864      final String setName = getTableSetName();
865      BackupInfo.Filter tableNameFilter = info -> {
866        if (tableName == null) {
867          return true;
868        }
869
870        List<TableName> names = info.getTableNames();
871        return names.contains(tableName);
872      };
873      BackupInfo.Filter tableSetFilter = info -> {
874        if (setName == null) {
875          return true;
876        }
877
878        String backupId = info.getBackupId();
879        return backupId.startsWith(setName);
880      };
881      Path backupRootPath = getBackupRootPath();
882      List<BackupInfo> history;
883      if (backupRootPath == null) {
884        // Load from backup system table
885        super.execute();
886        try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
887          history = sysTable.getBackupHistory(tableNameFilter, tableSetFilter);
888          history = history.subList(0, Math.min(n, history.size()));
889        }
890      } else {
891        // load from backup FS
892        history =
893          BackupUtils.getHistory(getConf(), n, backupRootPath, tableNameFilter, tableSetFilter);
894      }
895      for (BackupInfo info : history) {
896        System.out.println(info.getShortDescription());
897      }
898    }
899
900    private Path getBackupRootPath() throws IOException {
901      String value = null;
902      try {
903        value = cmdline.getOptionValue(OPTION_PATH);
904
905        if (value == null) {
906          return null;
907        }
908
909        return new Path(value);
910      } catch (IllegalArgumentException e) {
911        System.out.println("ERROR: Illegal argument for backup root path: " + value);
912        printUsage();
913        throw new IOException(INCORRECT_USAGE);
914      }
915    }
916
917    private TableName getTableName() throws IOException {
918      String value = cmdline.getOptionValue(OPTION_TABLE);
919
920      if (value == null) {
921        return null;
922      }
923
924      try {
925        return TableName.valueOf(value);
926      } catch (IllegalArgumentException e) {
927        System.out.println("Illegal argument for table name: " + value);
928        printUsage();
929        throw new IOException(INCORRECT_USAGE);
930      }
931    }
932
933    private String getTableSetName() {
934      return cmdline.getOptionValue(OPTION_SET);
935    }
936
937    private int parseHistoryLength() throws IOException {
938      String value = cmdline.getOptionValue(OPTION_RECORD_NUMBER);
939      try {
940        if (value == null) {
941          return DEFAULT_HISTORY_LENGTH;
942        }
943
944        return Integer.parseInt(value);
945      } catch (NumberFormatException e) {
946        System.out.println("Illegal argument for history length: " + value);
947        printUsage();
948        throw new IOException(INCORRECT_USAGE);
949      }
950    }
951
952    @Override
953    protected void printUsage() {
954      System.out.println(HISTORY_CMD_USAGE);
955      Options options = new Options();
956      options.addOption(OPTION_RECORD_NUMBER, true, OPTION_RECORD_NUMBER_DESC);
957      options.addOption(OPTION_PATH, true, OPTION_PATH_DESC);
958      options.addOption(OPTION_TABLE, true, OPTION_TABLE_DESC);
959      options.addOption(OPTION_SET, true, OPTION_SET_DESC);
960
961      HelpFormatter helpFormatter = new HelpFormatter();
962      helpFormatter.setLeftPadding(2);
963      helpFormatter.setDescPadding(8);
964      helpFormatter.setWidth(100);
965      helpFormatter.setSyntaxPrefix("Options:");
966      helpFormatter.printHelp(" ", null, options, USAGE_FOOTER);
967    }
968  }
969
970  public static class BackupSetCommand extends Command {
971    private final static String SET_ADD_CMD = "add";
972    private final static String SET_REMOVE_CMD = "remove";
973    private final static String SET_DELETE_CMD = "delete";
974    private final static String SET_DESCRIBE_CMD = "describe";
975    private final static String SET_LIST_CMD = "list";
976
977    BackupSetCommand(Configuration conf, CommandLine cmdline) {
978      super(conf);
979      this.cmdline = cmdline;
980    }
981
982    @Override
983    public void execute() throws IOException {
984      // Command-line must have at least one element
985      if (cmdline == null || cmdline.getArgs() == null || cmdline.getArgs().length < 2) {
986        printUsage();
987        throw new IOException(INCORRECT_USAGE);
988      }
989
990      String[] args = cmdline.getArgs();
991      String cmdStr = args[1];
992      BackupCommand cmd = getCommand(cmdStr);
993
994      switch (cmd) {
995        case SET_ADD:
996          processSetAdd(args);
997          break;
998        case SET_REMOVE:
999          processSetRemove(args);
1000          break;
1001        case SET_DELETE:
1002          processSetDelete(args);
1003          break;
1004        case SET_DESCRIBE:
1005          processSetDescribe(args);
1006          break;
1007        case SET_LIST:
1008          processSetList();
1009          break;
1010        default:
1011          break;
1012      }
1013    }
1014
1015    private void processSetList() throws IOException {
1016      super.execute();
1017
1018      // List all backup set names
1019      // does not expect any args
1020      try (BackupAdminImpl admin = new BackupAdminImpl(conn)) {
1021        List<BackupSet> list = admin.listBackupSets();
1022        for (BackupSet bs : list) {
1023          System.out.println(bs);
1024        }
1025      }
1026    }
1027
1028    private void processSetDescribe(String[] args) throws IOException {
1029      if (args == null || args.length != 3) {
1030        printUsage();
1031        throw new IOException(INCORRECT_USAGE);
1032      }
1033      super.execute();
1034
1035      String setName = args[2];
1036      try (final BackupSystemTable sysTable = new BackupSystemTable(conn)) {
1037        List<TableName> tables = sysTable.describeBackupSet(setName);
1038        BackupSet set = tables == null ? null : new BackupSet(setName, tables);
1039        if (set == null) {
1040          System.out.println("Set '" + setName + "' does not exist.");
1041        } else {
1042          System.out.println(set);
1043        }
1044      }
1045    }
1046
1047    private void processSetDelete(String[] args) throws IOException {
1048      if (args == null || args.length != 3) {
1049        printUsage();
1050        throw new IOException(INCORRECT_USAGE);
1051      }
1052      super.execute();
1053
1054      String setName = args[2];
1055      try (final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
1056        boolean result = admin.deleteBackupSet(setName);
1057        if (result) {
1058          System.out.println("Delete set " + setName + " OK.");
1059        } else {
1060          System.out.println("Set " + setName + " does not exist");
1061        }
1062      }
1063    }
1064
1065    private void processSetRemove(String[] args) throws IOException {
1066      if (args == null || args.length != 4) {
1067        printUsage();
1068        throw new IOException(INCORRECT_USAGE);
1069      }
1070      super.execute();
1071
1072      String setName = args[2];
1073      String[] tables = args[3].split(",");
1074      TableName[] tableNames = toTableNames(tables);
1075      try (final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
1076        admin.removeFromBackupSet(setName, tableNames);
1077      }
1078    }
1079
1080    private TableName[] toTableNames(String[] tables) {
1081      TableName[] arr = new TableName[tables.length];
1082      for (int i = 0; i < tables.length; i++) {
1083        arr[i] = TableName.valueOf(tables[i]);
1084      }
1085      return arr;
1086    }
1087
1088    private void processSetAdd(String[] args) throws IOException {
1089      if (args == null || args.length != 4) {
1090        printUsage();
1091        throw new IOException(INCORRECT_USAGE);
1092      }
1093      super.execute();
1094      String setName = args[2];
1095      TableName[] tableNames =
1096        Splitter.on(',').splitToStream(args[3]).map(TableName::valueOf).toArray(TableName[]::new);
1097      try (final BackupAdminImpl admin = new BackupAdminImpl(conn)) {
1098        admin.addToBackupSet(setName, tableNames);
1099      }
1100    }
1101
1102    private BackupCommand getCommand(String cmdStr) throws IOException {
1103      switch (cmdStr) {
1104        case SET_ADD_CMD:
1105          return BackupCommand.SET_ADD;
1106        case SET_REMOVE_CMD:
1107          return BackupCommand.SET_REMOVE;
1108        case SET_DELETE_CMD:
1109          return BackupCommand.SET_DELETE;
1110        case SET_DESCRIBE_CMD:
1111          return BackupCommand.SET_DESCRIBE;
1112        case SET_LIST_CMD:
1113          return BackupCommand.SET_LIST;
1114        default:
1115          System.out.println("ERROR: Unknown command for 'set' :" + cmdStr);
1116          printUsage();
1117          throw new IOException(INCORRECT_USAGE);
1118      }
1119    }
1120
1121    @Override
1122    protected void printUsage() {
1123      System.out.println(SET_CMD_USAGE);
1124    }
1125  }
1126}