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