diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java index 6f3a1b1ca..0fffcc54f 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java @@ -19,21 +19,34 @@ import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.gax.longrunning.OperationFuture; import com.google.api.gax.rpc.ApiExceptions; import com.google.api.gax.rpc.NotFoundException; +import com.google.bigtable.admin.v2.DeleteBackupRequest; import com.google.bigtable.admin.v2.DeleteTableRequest; import com.google.bigtable.admin.v2.DropRowRangeRequest; +import com.google.bigtable.admin.v2.GetBackupRequest; import com.google.bigtable.admin.v2.GetTableRequest; +import com.google.bigtable.admin.v2.ListBackupsRequest; import com.google.bigtable.admin.v2.ListTablesRequest; +import com.google.bigtable.admin.v2.RestoreTableMetadata; import com.google.cloud.Policy; import com.google.cloud.Policy.DefaultMarshaller; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPage; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPagedResponse; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPage; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPagedResponse; import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.cloud.bigtable.admin.v2.models.Backup; +import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; import com.google.cloud.bigtable.admin.v2.models.GCRules; import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest; +import com.google.cloud.bigtable.admin.v2.models.OptimizeRestoredTableOperationToken; +import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest; +import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult; import com.google.cloud.bigtable.admin.v2.models.Table; +import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; @@ -47,6 +60,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutionException; import javax.annotation.Nonnull; /** @@ -775,6 +789,413 @@ public void awaitReplication(String tableId) { stub.awaitReplicationCallable().futureCall(tableName)); } + /** + * Creates a backup with the specified configuration. + * + *

Sample code + * + *

{@code
+   * CreateBackupRequest request =
+   *         CreateBackupRequest.of(clusterId, backupId)
+   *             .setSourceTableId(tableId)
+   *             .setExpireTime(expireTime);
+   * Backup response = client.createBackup(request);
+   * }
+ */ + public Backup createBackup(CreateBackupRequest request) { + return ApiExceptions.callAndTranslateApiException(createBackupAsync(request)); + } + + /** + * Creates a backup with the specified configuration asynchronously. + * + *

Sample code + * + *

{@code
+   * CreateBackupRequest request =
+   *         CreateBackupRequest.of(clusterId, backupId)
+   *             .setSourceTableId(tableId)
+   *             .setExpireTime(expireTime);
+   * ApiFuture future = client.createBackupAsync(request);
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Backup backup) {
+   *       System.out.println("Successfully create the backup " + backup.getId());
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture createBackupAsync(CreateBackupRequest request) { + return ApiFutures.transform( + stub.createBackupOperationCallable().futureCall(request.toProto(projectId, instanceId)), + new ApiFunction() { + @Override + public Backup apply(com.google.bigtable.admin.v2.Backup backupProto) { + return Backup.fromProto(backupProto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Gets a backup with the specified backup ID in the specified cluster. + * + *

Sample code + * + *

{@code
+   * Backup backup = client.getBackup(clusterId, backupId);
+   * }
+ */ + public Backup getBackup(String clusterId, String backupId) { + return ApiExceptions.callAndTranslateApiException(getBackupAsync(clusterId, backupId)); + } + + /** + * Gets a backup with the specified backup ID in the specified cluster asynchronously. + * + *

Sample code + * + *

{@code
+   * ApiFuture future = client.getBackupAsync(clusterId, backupId);
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Backup backup) {
+   *       System.out.println("Successfully get the backup " + backup.getId());
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture getBackupAsync(String clusterId, String backupId) { + GetBackupRequest request = + GetBackupRequest.newBuilder() + .setName(NameUtil.formatBackupName(projectId, instanceId, clusterId, backupId)) + .build(); + return ApiFutures.transform( + this.stub.getBackupCallable().futureCall(request), + new ApiFunction() { + @Override + public Backup apply(com.google.bigtable.admin.v2.Backup backup) { + return Backup.fromProto(backup); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Lists backups in the specified cluster. + * + *

Sample code + * + *

{@code
+   * List backups = client.listBackups(clusterId);
+   * }
+ */ + public List listBackups(String clusterId) { + return ApiExceptions.callAndTranslateApiException(listBackupsAsync(clusterId)); + } + + /** + * Lists backups in the specified cluster asynchronously. + * + *

Sample code: + * + *

{@code
+   * ApiFuture> listFuture = client.listBackupsAsync(clusterId);
+   *
+   * ApiFutures.addCallback(
+   *   listFuture,
+   *   new ApiFutureCallback>() {
+   *     public void onSuccess(List backupIds) {
+   *       System.out.println("Got list of backups:");
+   *       for (String backupId : backupIds) {
+   *         System.out.println(backupId);
+   *       }
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture> listBackupsAsync(String clusterId) { + ListBackupsRequest request = + ListBackupsRequest.newBuilder() + .setParent(NameUtil.formatClusterName(projectId, instanceId, clusterId)) + .build(); + + // TODO(igorbernstein2): try to upstream pagination spooling or figure out a way to expose the + // paginated responses while maintaining the wrapper facade. + + // Fetches the first page. + ApiFuture firstPageFuture = + ApiFutures.transform( + stub.listBackupsPagedCallable().futureCall(request), + new ApiFunction() { + @Override + public ListBackupsPage apply(ListBackupsPagedResponse response) { + return response.getPage(); + } + }, + MoreExecutors.directExecutor()); + + // Fetches the rest of the pages by chaining the futures. + ApiFuture> allProtos = + ApiFutures.transformAsync( + firstPageFuture, + new ApiAsyncFunction>() { + List responseAccumulator = Lists.newArrayList(); + + @Override + public ApiFuture> apply( + ListBackupsPage page) { + // Add all entries from the page + responseAccumulator.addAll(Lists.newArrayList(page.getValues())); + + // If this is the last page, just return the accumulated responses. + if (!page.hasNextPage()) { + return ApiFutures.immediateFuture(responseAccumulator); + } + + // Otherwise fetch the next page. + return ApiFutures.transformAsync( + page.getNextPageAsync(), this, MoreExecutors.directExecutor()); + } + }, + MoreExecutors.directExecutor()); + + // Wraps all of the accumulated protos. + return ApiFutures.transform( + allProtos, + new ApiFunction, List>() { + @Override + public List apply(List protos) { + List results = Lists.newArrayListWithCapacity(protos.size()); + for (com.google.bigtable.admin.v2.Backup proto : protos) { + results.add(NameUtil.extractBackupIdFromBackupName(proto.getName())); + } + return results; + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Deletes a backup with the specified backup ID in the specified cluster. + * + *

Sample code + * + *

{@code
+   * client.deleteBackup(clusterId, backupId);
+   * }
+ */ + public void deleteBackup(String clusterId, String backupId) { + ApiExceptions.callAndTranslateApiException(deleteBackupAsync(clusterId, backupId)); + } + + /** + * Deletes a backup with the specified backup ID in the specified cluster asynchronously. + * + *

Sample code + * + *

{@code
+   * ApiFuture future = client.deleteBackupAsync(clusterId, backupId);
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Void unused) {
+   *       System.out.println("Successfully delete the backup.");
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture deleteBackupAsync(String clusterId, String backupId) { + DeleteBackupRequest request = + DeleteBackupRequest.newBuilder() + .setName(NameUtil.formatBackupName(projectId, instanceId, clusterId, backupId)) + .build(); + + return transformToVoid(this.stub.deleteBackupCallable().futureCall(request)); + } + + /** + * Updates a backup with the specified configuration. + * + *

Sample code + * + *

{@code
+   * Backup backup = client.updateBackup(clusterId, backupId);
+   * }
+ */ + public Backup updateBackup(UpdateBackupRequest request) { + return ApiExceptions.callAndTranslateApiException(updateBackupAsync(request)); + } + + /** + * Updates a backup with the specified configuration asynchronously. + * + *

Sample code + * + *

{@code
+   * ApiFuture future = client.updateBackupAsync(clusterId, backupId);
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Backup backup) {
+   *       System.out.println("Successfully update the backup " + backup.getId());
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture updateBackupAsync(UpdateBackupRequest request) { + return ApiFutures.transform( + stub.updateBackupCallable().futureCall(request.toProto(projectId, instanceId)), + new ApiFunction() { + @Override + public Backup apply(com.google.bigtable.admin.v2.Backup proto) { + return Backup.fromProto(proto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Restores a backup to a new table with the specified configuration. + * + *

Sample code + * + *

{@code
+   * RestoredTableResult result =
+   *     client.restoreTable(RestoreTableRequest.of(clusterId, backupId).setTableId(tableId));
+   * }
+ */ + public RestoredTableResult restoreTable(RestoreTableRequest request) + throws ExecutionException, InterruptedException { + return ApiExceptions.callAndTranslateApiException(restoreTableAsync(request)); + } + + /** Restores a backup to a new table with the specified configuration asynchronously. + *

Sample code + * + *

{@code
+   * ApiFuture future = client.restoreTableAsync(
+   *     RestoreTableRequest.of(clusterId, backupId).setTableId(tableId));
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(RestoredTableResult result) {
+   *       System.out.println("Successfully restore the table.");
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * 
+ * */ + public ApiFuture restoreTableAsync(RestoreTableRequest request) { + final OperationFuture future = + this.stub + .restoreTableOperationCallable() + .futureCall(request.toProto(projectId, instanceId)); + return ApiFutures.transformAsync( + future, + new ApiAsyncFunction() { + @Override + public ApiFuture apply(com.google.bigtable.admin.v2.Table table) + throws Exception { + return ApiFutures.immediateFuture( + // When apply is called, the future should have been resolved and it's safe to + // pull the metadata. + new RestoredTableResult( + Table.fromProto(table), + future.getMetadata().get().getOptimizeTableOperationName())); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Awaits a restored table is fully optimized. + * + *

Sample code + * + *

{@code
+   * RestoredTableResult result =
+   *     client.restoreTable(RestoreTableRequest.of(clusterId, backupId).setTableId(tableId));
+   * client.awaitOptimizeRestoredTable(result.getOptimizeRestoredTableOperationToken());
+   * }
+ */ + public void awaitOptimizeRestoredTable(OptimizeRestoredTableOperationToken token) + throws ExecutionException, InterruptedException { + awaitOptimizeRestoredTableAsync(token).get(); + } + + /** Awaits a restored table is fully optimized asynchronously. + * + *

Sample code + * + *

{@code
+   * RestoredTableResult result =
+   *     client.restoreTable(RestoreTableRequest.of(clusterId, backupId).setTableId(tableId));
+   * ApiFuture future = client.awaitOptimizeRestoredTableAsync(
+   *     result.getOptimizeRestoredTableOperationToken());
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Void unused) {
+   *       System.out.println("The optimization of the restored table is done.");
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * */
+  public ApiFuture awaitOptimizeRestoredTableAsync(
+      OptimizeRestoredTableOperationToken token) {
+    return transformToVoid(
+        stub.awaitOptimizeRestoredTableCallable().resumeFutureCall(token.getOperationName()));
+  }
+
   /**
    * Returns a future that is resolved when replication has caught up to the point when this method
    * was called. This allows callers to make sure that their mutations have been replicated across
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java
index 54fc1f88f..8cccf3d57 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java
@@ -31,6 +31,8 @@ public class NameUtil {
       Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)");
   private static final Pattern LOCATION_PATTERN =
       Pattern.compile("projects/([^/]+)/locations/([^/]+)");
+  private static final Pattern BACKUP_PATTERN =
+      Pattern.compile("projects/([^/]+)/instances/([^/]+)/clusters/([^/]+)/backups/([^/]+)");
 
   public static String formatProjectName(String projectId) {
     return "projects/" + projectId;
@@ -48,6 +50,11 @@ public static String formatLocationName(String projectId, String zone) {
     return formatProjectName(projectId) + "/locations/" + zone;
   }
 
+  public static String formatBackupName(
+      String projectId, String instanceId, String clusterId, String backupId) {
+    return formatClusterName(projectId, instanceId, clusterId) + "/backups/" + backupId;
+  }
+
   public static String extractTableIdFromTableName(String fullTableName) {
     Matcher matcher = TABLE_PATTERN.matcher(fullTableName);
     if (!matcher.matches()) {
@@ -56,6 +63,14 @@ public static String extractTableIdFromTableName(String fullTableName) {
     return matcher.group(3);
   }
 
+  public static String extractBackupIdFromBackupName(String fullBackupName) {
+    Matcher matcher = BACKUP_PATTERN.matcher(fullBackupName);
+    if (!matcher.matches()) {
+      throw new IllegalArgumentException("Invalid backup name: " + fullBackupName);
+    }
+    return matcher.group(4);
+  }
+
   public static String extractZoneIdFromLocationName(String fullLocationName) {
     Matcher matcher = LOCATION_PATTERN.matcher(fullLocationName);
     if (!matcher.matches()) {
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java
new file mode 100644
index 000000000..54002da63
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/Backup.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.admin.v2.models;
+
+import com.google.api.core.InternalApi;
+import com.google.bigtable.admin.v2.BackupName;
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.protobuf.util.Timestamps;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.threeten.bp.Instant;
+
+/**
+ * A backup lets you save a copy of a table's schema and data and restore the backup to a new table
+ * at a later time.
+ */
+public class Backup {
+  public enum State {
+    /** Not specified. */
+    STATE_UNSPECIFIED(com.google.bigtable.admin.v2.Backup.State.STATE_UNSPECIFIED),
+
+    /**
+     * The pending backup is still being created. Operations on the backup may fail with
+     * `FAILED_PRECONDITION` in this state.
+     */
+    CREATING(com.google.bigtable.admin.v2.Backup.State.CREATING),
+    /** The backup is complete and ready for use. */
+    READY(com.google.bigtable.admin.v2.Backup.State.READY),
+
+    /** The state of the backup is not known by this client. Please upgrade your client. */
+    UNRECOGNIZED(com.google.bigtable.admin.v2.Backup.State.UNRECOGNIZED);
+
+    private final com.google.bigtable.admin.v2.Backup.State proto;
+
+    State(com.google.bigtable.admin.v2.Backup.State proto) {
+      this.proto = proto;
+    }
+
+    /**
+     * Wraps the protobuf. This method is considered an internal implementation detail and not meant
+     * to be used by applications.
+     */
+    @InternalApi
+    public static Backup.State fromProto(com.google.bigtable.admin.v2.Backup.State proto) {
+      for (Backup.State state : values()) {
+        if (state.proto.equals(proto)) {
+          return state;
+        }
+      }
+      return STATE_UNSPECIFIED;
+    }
+
+    /**
+     * Creates the request protobuf. This method is considered an internal implementation detail and
+     * not meant to be used by applications.
+     */
+    @InternalApi
+    public com.google.bigtable.admin.v2.Backup.State toProto() {
+      return proto;
+    }
+  }
+
+  @Nonnull private final com.google.bigtable.admin.v2.Backup proto;
+  @Nonnull private final String id;
+  @Nonnull private final String instanceId;
+
+  @InternalApi
+  public static Backup fromProto(@Nonnull com.google.bigtable.admin.v2.Backup proto) {
+    return new Backup(proto);
+  }
+
+  private Backup(@Nonnull com.google.bigtable.admin.v2.Backup proto) {
+    Preconditions.checkNotNull(proto);
+    Preconditions.checkArgument(!proto.getName().isEmpty(), "Name must be set");
+    Preconditions.checkArgument(!proto.getSourceTable().isEmpty(), "Source table must be set");
+
+    BackupName name = BackupName.parse(proto.getName());
+    this.id = name.getBackup();
+    this.instanceId = name.getInstance();
+    this.proto = proto;
+  }
+
+  /** Get the ID of this backup. */
+  public String getId() {
+    return id;
+  }
+
+  /** Get the source table ID from which the backup is created. */
+  public String getSourceTableId() {
+    return NameUtil.extractTableIdFromTableName(proto.getSourceTable());
+  }
+
+  /** Get the instance ID where this backup is located. */
+  public String getInstanceId() {
+    return instanceId;
+  }
+
+  /** Get the expire time of this backup. */
+  public Instant getExpireTime() {
+    return Instant.ofEpochMilli(Timestamps.toMillis(proto.getExpireTime()));
+  }
+
+  /** Get the start time when this backup is taken. */
+  public @Nullable Instant getStartTime() {
+    if (proto.hasStartTime()) {
+      return Instant.ofEpochMilli(Timestamps.toMillis(proto.getStartTime()));
+    }
+    return null;
+  }
+
+  /** Get the end time when the creation of this backup has completed. */
+  public @Nullable Instant getEndTime() {
+    if (proto.hasEndTime()) {
+      return Instant.ofEpochMilli(Timestamps.toMillis(proto.getEndTime()));
+    }
+    return null;
+  }
+
+  /** Get the size of this backup. */
+  public long getSizeBytes() {
+    return proto.getSizeBytes();
+  }
+
+  /** Get the state of this backup. */
+  public State getState() {
+    return State.fromProto(proto.getState());
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Backup backup = (Backup) o;
+    return Objects.equal(proto, backup.proto);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(proto);
+  }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateBackupRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateBackupRequest.java
new file mode 100644
index 000000000..1a27546c8
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateBackupRequest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.protobuf.util.Timestamps;
+import javax.annotation.Nonnull;
+import org.threeten.bp.Instant;
+
+/** Fluent wrapper for {@link com.google.bigtable.admin.v2.CreateBackupRequest} */
+public final class CreateBackupRequest {
+  private final com.google.bigtable.admin.v2.CreateBackupRequest.Builder requestBuilder =
+      com.google.bigtable.admin.v2.CreateBackupRequest.newBuilder();
+  private final String clusterId;
+  private String sourceTableId;
+
+  public static CreateBackupRequest of(String clusterId, String backupId) {
+    CreateBackupRequest request = new CreateBackupRequest(clusterId, backupId);
+    return request;
+  }
+
+  private CreateBackupRequest(String clusterId, String backupId) {
+    Preconditions.checkNotNull(clusterId);
+    Preconditions.checkNotNull(backupId);
+
+    requestBuilder.setBackupId(backupId);
+    this.clusterId = clusterId;
+    this.sourceTableId = null;
+  }
+
+  public CreateBackupRequest setSourceTableId(String sourceTableId) {
+    Preconditions.checkNotNull(sourceTableId);
+    this.sourceTableId = sourceTableId;
+    return this;
+  }
+
+  public CreateBackupRequest setExpireTime(Instant expireTime) {
+    Preconditions.checkNotNull(expireTime);
+    requestBuilder
+        .getBackupBuilder()
+        .setExpireTime(Timestamps.fromMillis(expireTime.toEpochMilli()));
+    return this;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    CreateBackupRequest that = (CreateBackupRequest) o;
+    return Objects.equal(requestBuilder.getBackupId(), that.requestBuilder.getBackupId())
+        && Objects.equal(clusterId, that.clusterId)
+        && Objects.equal(sourceTableId, that.sourceTableId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(requestBuilder.getBackupId(), clusterId, sourceTableId);
+  }
+
+  @InternalApi
+  public com.google.bigtable.admin.v2.CreateBackupRequest toProto(
+      @Nonnull String projectId, @Nonnull String instanceId) {
+    Preconditions.checkNotNull(projectId);
+    Preconditions.checkNotNull(instanceId);
+
+    requestBuilder
+        .getBackupBuilder()
+        .setSourceTable(NameUtil.formatTableName(projectId, instanceId, sourceTableId));
+    return requestBuilder
+        .setParent(NameUtil.formatClusterName(projectId, instanceId, clusterId))
+        .build();
+  }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/OptimizeRestoredTableOperationToken.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/OptimizeRestoredTableOperationToken.java
new file mode 100644
index 000000000..d38f82e4f
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/OptimizeRestoredTableOperationToken.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import com.google.api.core.InternalApi;
+import com.google.common.base.Preconditions;
+
+/**
+ * OptimizeRestoredTableOperationToken is a wrapper for the name of OptimizeRestoredTable operation.
+ */
+public class OptimizeRestoredTableOperationToken {
+  private final String operationName;
+
+  @InternalApi
+  public static OptimizeRestoredTableOperationToken of(String operationName) {
+    return new OptimizeRestoredTableOperationToken(operationName);
+  }
+
+  private OptimizeRestoredTableOperationToken(String operationName) {
+    Preconditions.checkNotNull(operationName);
+    Preconditions.checkState(!operationName.isEmpty());
+    this.operationName = operationName;
+  }
+
+  @InternalApi
+  public String getOperationName() {
+    return operationName;
+  }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/RestoreTableRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/RestoreTableRequest.java
new file mode 100644
index 000000000..fa47ba582
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/RestoreTableRequest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import javax.annotation.Nonnull;
+
+/** Fluent wrapper for {@link com.google.bigtable.admin.v2.RestoreTableRequest} */
+public final class RestoreTableRequest {
+  private final com.google.bigtable.admin.v2.RestoreTableRequest.Builder requestBuilder =
+      com.google.bigtable.admin.v2.RestoreTableRequest.newBuilder();
+  private final String backupId;
+  private final String clusterId;
+
+  public static RestoreTableRequest of(String clusterId, String backupId) {
+    RestoreTableRequest request = new RestoreTableRequest(clusterId, backupId);
+    return request;
+  }
+
+  private RestoreTableRequest(String clusterId, String backupId) {
+    Preconditions.checkNotNull(clusterId);
+    Preconditions.checkNotNull(backupId);
+    this.backupId = backupId;
+    this.clusterId = clusterId;
+  }
+
+  public RestoreTableRequest setTableId(String tableId) {
+    Preconditions.checkNotNull(tableId);
+    requestBuilder.setTableId(tableId);
+    return this;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    RestoreTableRequest that = (RestoreTableRequest) o;
+    return Objects.equal(requestBuilder.getTableId(), that.requestBuilder.getTableId())
+        && Objects.equal(clusterId, that.clusterId)
+        && Objects.equal(backupId, that.backupId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(requestBuilder.getTableId(), clusterId, backupId);
+  }
+
+  @InternalApi
+  public com.google.bigtable.admin.v2.RestoreTableRequest toProto(
+      @Nonnull String projectId, @Nonnull String instanceId) {
+    Preconditions.checkNotNull(projectId);
+    Preconditions.checkNotNull(instanceId);
+
+    return requestBuilder
+        .setParent(NameUtil.formatInstanceName(projectId, instanceId))
+        .setBackup(NameUtil.formatBackupName(projectId, instanceId, clusterId, backupId))
+        .build();
+  }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/RestoredTableResult.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/RestoredTableResult.java
new file mode 100644
index 000000000..e31d8c4b4
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/RestoredTableResult.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import com.google.api.core.InternalApi;
+import com.google.common.base.Strings;
+import javax.annotation.Nullable;
+
+/**
+ * A RestoredTableResult holds the restored table object and the {@link
+ * OptimizeRestoredTableOperationToken} object (if any).
+ */
+public class RestoredTableResult {
+
+  private final Table table;
+  private final OptimizeRestoredTableOperationToken optimizeRestoredTableOperationToken;
+
+  @InternalApi
+  public RestoredTableResult(
+      Table restoredTable, @Nullable String optimizeRestoredTableOperationName) {
+    this.table = restoredTable;
+    this.optimizeRestoredTableOperationToken =
+        Strings.isNullOrEmpty(optimizeRestoredTableOperationName)
+            ? null
+            : OptimizeRestoredTableOperationToken.of(optimizeRestoredTableOperationName);
+  }
+
+  public Table getTable() {
+    return table;
+  }
+
+  /**
+   * OptimizeRestoredTable operation may not be started when the table was restored from a backup
+   * stored in HDD clusters.
+   */
+  @Nullable
+  public OptimizeRestoredTableOperationToken getOptimizeRestoredTableOperationToken() {
+    return this.optimizeRestoredTableOperationToken;
+  }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateBackupRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateBackupRequest.java
new file mode 100644
index 000000000..9f8aa6a79
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateBackupRequest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import com.google.api.core.InternalApi;
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.util.Timestamps;
+import javax.annotation.Nonnull;
+import org.threeten.bp.Instant;
+
+/** Fluent wrapper for {@link com.google.bigtable.admin.v2.UpdateBackupRequest} */
+public final class UpdateBackupRequest {
+  private final com.google.bigtable.admin.v2.UpdateBackupRequest.Builder requestBuilder =
+      com.google.bigtable.admin.v2.UpdateBackupRequest.newBuilder();
+  private final String backupId;
+  private final String clusterId;
+
+  public static UpdateBackupRequest of(String clusterId, String backupId) {
+    UpdateBackupRequest request = new UpdateBackupRequest(clusterId, backupId);
+    return request;
+  }
+
+  private UpdateBackupRequest(String clusterId, String backupId) {
+    Preconditions.checkNotNull(clusterId);
+    Preconditions.checkNotNull(backupId);
+    this.backupId = backupId;
+    this.clusterId = clusterId;
+  }
+
+  public UpdateBackupRequest setExpireTime(Instant expireTime) {
+    Preconditions.checkNotNull(expireTime);
+    requestBuilder
+        .getBackupBuilder()
+        .setExpireTime(Timestamps.fromMillis(expireTime.toEpochMilli()));
+    requestBuilder.setUpdateMask(FieldMask.newBuilder().addPaths("expire_time"));
+    return this;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    UpdateBackupRequest that = (UpdateBackupRequest) o;
+    return Objects.equal(
+            requestBuilder.getBackupBuilder().getExpireTime(),
+            that.requestBuilder.getBackupBuilder().getExpireTime())
+        && Objects.equal(requestBuilder.getUpdateMask(), that.requestBuilder.getUpdateMask())
+        && Objects.equal(clusterId, that.clusterId)
+        && Objects.equal(backupId, that.backupId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(
+        requestBuilder.getBackupBuilder().getExpireTime(),
+        requestBuilder.getUpdateMask(),
+        backupId);
+  }
+
+  @InternalApi
+  public com.google.bigtable.admin.v2.UpdateBackupRequest toProto(
+      @Nonnull String projectId, @Nonnull String instanceId) {
+    Preconditions.checkNotNull(projectId);
+    Preconditions.checkNotNull(instanceId);
+
+    requestBuilder
+        .getBackupBuilder()
+        .setName(NameUtil.formatBackupName(projectId, instanceId, clusterId, backupId));
+    return requestBuilder.build();
+  }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/EnhancedBigtableTableAdminStub.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/EnhancedBigtableTableAdminStub.java
index bf1913a36..0a6e8efec 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/EnhancedBigtableTableAdminStub.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/EnhancedBigtableTableAdminStub.java
@@ -15,12 +15,29 @@
  */
 package com.google.cloud.bigtable.admin.v2.stub;
 
+import com.google.api.core.ApiFunction;
 import com.google.api.core.InternalApi;
+import com.google.api.gax.grpc.GrpcCallSettings;
+import com.google.api.gax.grpc.GrpcCallableFactory;
+import com.google.api.gax.grpc.ProtoOperationTransformers.MetadataTransformer;
+import com.google.api.gax.grpc.ProtoOperationTransformers.ResponseTransformer;
+import com.google.api.gax.longrunning.OperationSnapshot;
+import com.google.api.gax.longrunning.OperationTimedPollAlgorithm;
 import com.google.api.gax.retrying.RetrySettings;
 import com.google.api.gax.rpc.ClientContext;
+import com.google.api.gax.rpc.OperationCallSettings;
+import com.google.api.gax.rpc.OperationCallable;
+import com.google.api.gax.rpc.UnaryCallSettings;
 import com.google.api.gax.rpc.UnaryCallable;
+import com.google.bigtable.admin.v2.OptimizeRestoredTableMetadata;
 import com.google.bigtable.admin.v2.TableName;
+import com.google.longrunning.Operation;
+import com.google.protobuf.Empty;
+import io.grpc.MethodDescriptor;
+import io.grpc.MethodDescriptor.Marshaller;
+import io.grpc.MethodDescriptor.MethodType;
 import java.io.IOException;
+import java.io.InputStream;
 import org.threeten.bp.Duration;
 
 /**
@@ -36,6 +53,8 @@ public class EnhancedBigtableTableAdminStub extends GrpcBigtableTableAdminStub {
   private final ClientContext clientContext;
 
   private final AwaitReplicationCallable awaitReplicationCallable;
+  private final OperationCallable
+      optimizeRestoredTableOperationBaseCallable;
 
   public static EnhancedBigtableTableAdminStub createEnhanced(
       BigtableTableAdminStubSettings settings) throws IOException {
@@ -49,6 +68,8 @@ private EnhancedBigtableTableAdminStub(
     this.settings = settings;
     this.clientContext = clientContext;
     this.awaitReplicationCallable = createAwaitReplicationCallable();
+    this.optimizeRestoredTableOperationBaseCallable =
+        createOptimizeRestoredTableOperationBaseCallable();
   }
 
   private AwaitReplicationCallable createAwaitReplicationCallable() {
@@ -78,7 +99,103 @@ private AwaitReplicationCallable createAwaitReplicationCallable() {
         pollingSettings);
   }
 
+  // Plug into gax operation infrastructure
+  // gax assumes that all operations are started immediately and doesn't provide support for child
+  // operations
+  // this method wraps a fake method "OptimizeTable" in an OperationCallable. This should not be
+  // exposed to
+  // end users, but will be used for its resumeOperation functionality via a wrapper
+  private OperationCallable
+      createOptimizeRestoredTableOperationBaseCallable() {
+    // Fake initial callable settings. Since this is child operation, it doesn't have an initial
+    // callable but gax doesn't have support for child operation, so this creates a fake method
+    // descriptor that will never be used.
+    GrpcCallSettings unusedInitialCallSettings =
+        GrpcCallSettings.create(
+            MethodDescriptor.newBuilder()
+                .setType(MethodType.UNARY)
+                .setFullMethodName(
+                    "google.bigtable.admin.v2.BigtableTableAdmin/OptimizeRestoredTable")
+                .setRequestMarshaller(
+                    new Marshaller() {
+                      @Override
+                      public InputStream stream(Void value) {
+                        throw new UnsupportedOperationException("not used");
+                      }
+
+                      @Override
+                      public Void parse(InputStream stream) {
+                        throw new UnsupportedOperationException("not used");
+                      }
+                    })
+                .setResponseMarshaller(
+                    new Marshaller() {
+                      @Override
+                      public InputStream stream(Operation value) {
+                        throw new UnsupportedOperationException("not used");
+                      }
+
+                      @Override
+                      public Operation parse(InputStream stream) {
+                        throw new UnsupportedOperationException("not used");
+                      }
+                    })
+                .build());
+
+    // helpers to extract the underlying protos from the operation
+    final MetadataTransformer protoMetadataTransformer =
+        MetadataTransformer.create(OptimizeRestoredTableMetadata.class);
+
+    final ResponseTransformer protoResponseTransformer =
+        ResponseTransformer.create(com.google.protobuf.Empty.class);
+
+    // TODO(igorbernstein2): expose polling settings
+    OperationCallSettings operationCallSettings =
+        OperationCallSettings.newBuilder()
+            // Since this is used for a child operation, the initial call settings will not be used
+            .setInitialCallSettings(
+                UnaryCallSettings.newUnaryCallSettingsBuilder()
+                    .setSimpleTimeoutNoRetries(Duration.ZERO)
+                    .build())
+            // Configure the extractors to wrap the protos
+            .setMetadataTransformer(
+                new ApiFunction() {
+                  @Override
+                  public OptimizeRestoredTableMetadata apply(OperationSnapshot input) {
+                    return protoMetadataTransformer.apply(input);
+                  }
+                })
+            .setResponseTransformer(
+                new ApiFunction() {
+                  @Override
+                  public Empty apply(OperationSnapshot input) {
+                    return protoResponseTransformer.apply(input);
+                  }
+                })
+            .setPollingAlgorithm(
+                OperationTimedPollAlgorithm.create(
+                    RetrySettings.newBuilder()
+                        .setInitialRetryDelay(Duration.ofMillis(500L))
+                        .setRetryDelayMultiplier(1.5)
+                        .setMaxRetryDelay(Duration.ofMillis(5000L))
+                        .setInitialRpcTimeout(Duration.ZERO) // ignored
+                        .setRpcTimeoutMultiplier(1.0) // ignored
+                        .setMaxRpcTimeout(Duration.ZERO) // ignored
+                        .setTotalTimeout(Duration.ofMillis(600000L))
+                        .build()))
+            .build();
+
+    // Create the callable
+    return GrpcCallableFactory.createOperationCallable(
+        unusedInitialCallSettings, operationCallSettings, clientContext, getOperationsStub());
+  }
+
   public UnaryCallable awaitReplicationCallable() {
     return awaitReplicationCallable;
   }
+
+  public OperationCallable
+      awaitOptimizeRestoredTableCallable() {
+    return optimizeRestoredTableOperationBaseCallable;
+  }
 }
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java
index 358d2990c..2cda42b3e 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTest.java
@@ -20,29 +20,55 @@
 import com.google.api.core.ApiFuture;
 import com.google.api.core.ApiFutures;
 import com.google.api.gax.grpc.GrpcStatusCode;
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.api.gax.longrunning.OperationFutures;
+import com.google.api.gax.longrunning.OperationSnapshot;
 import com.google.api.gax.rpc.NotFoundException;
+import com.google.api.gax.rpc.OperationCallable;
 import com.google.api.gax.rpc.UnaryCallable;
+import com.google.api.gax.rpc.testing.FakeOperationSnapshot;
+import com.google.bigtable.admin.v2.Backup.State;
+import com.google.bigtable.admin.v2.BackupInfo;
 import com.google.bigtable.admin.v2.ColumnFamily;
+import com.google.bigtable.admin.v2.CreateBackupMetadata;
+import com.google.bigtable.admin.v2.DeleteBackupRequest;
 import com.google.bigtable.admin.v2.DeleteTableRequest;
 import com.google.bigtable.admin.v2.DropRowRangeRequest;
 import com.google.bigtable.admin.v2.GcRule;
+import com.google.bigtable.admin.v2.GetBackupRequest;
 import com.google.bigtable.admin.v2.GetTableRequest;
+import com.google.bigtable.admin.v2.ListBackupsRequest;
 import com.google.bigtable.admin.v2.ListTablesRequest;
 import com.google.bigtable.admin.v2.ModifyColumnFamiliesRequest.Modification;
+import com.google.bigtable.admin.v2.RestoreSourceType;
+import com.google.bigtable.admin.v2.RestoreTableMetadata;
 import com.google.bigtable.admin.v2.Table.View;
 import com.google.bigtable.admin.v2.TableName;
+import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPage;
+import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPagedResponse;
 import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPage;
 import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPagedResponse;
 import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.cloud.bigtable.admin.v2.models.Backup;
+import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest;
 import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest;
 import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest;
+import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest;
+import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult;
 import com.google.cloud.bigtable.admin.v2.models.Table;
+import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest;
 import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub;
 import com.google.common.collect.Lists;
+import com.google.longrunning.Operation;
+import com.google.protobuf.Any;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Empty;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Timestamps;
 import io.grpc.Status;
+import io.grpc.Status.Code;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.junit.Before;
 import org.junit.Test;
@@ -53,6 +79,7 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.runners.MockitoJUnitRunner;
 import org.mockito.stubbing.Answer;
+import org.threeten.bp.Instant;
 
 @RunWith(MockitoJUnitRunner.class)
 public class BigtableTableAdminClientTest {
@@ -60,6 +87,8 @@ public class BigtableTableAdminClientTest {
   private static final String PROJECT_ID = "my-project";
   private static final String INSTANCE_ID = "my-instance";
   private static final String TABLE_ID = "my-table";
+  private static final String CLUSTER_ID = "my-cluster";
+  private static final String BACKUP_ID = "my-backup";
 
   private static final String PROJECT_NAME = NameUtil.formatProjectName(PROJECT_ID);
   private static final String INSTANCE_NAME = NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID);
@@ -89,6 +118,40 @@ public class BigtableTableAdminClientTest {
   @Mock private UnaryCallable mockDropRowRangeCallable;
   @Mock private UnaryCallable mockAwaitReplicationCallable;
 
+  @Mock
+  private UnaryCallable
+      mockCreateBackupCallable;
+
+  @Mock
+  private OperationCallable<
+          com.google.bigtable.admin.v2.CreateBackupRequest,
+          com.google.bigtable.admin.v2.Backup,
+          CreateBackupMetadata>
+      mockCreateBackupOperationCallable;
+
+  @Mock
+  private UnaryCallable
+      mockGetBackupCallable;
+
+  @Mock
+  private UnaryCallable<
+          com.google.bigtable.admin.v2.UpdateBackupRequest, com.google.bigtable.admin.v2.Backup>
+      mockUpdateBackupCallable;
+
+  @Mock private UnaryCallable mockListBackupCallable;
+  @Mock private UnaryCallable mockDeleteBackupCallable;
+
+  @Mock
+  private UnaryCallable
+      mockRestoreTableCallable;
+
+  @Mock
+  private OperationCallable<
+          com.google.bigtable.admin.v2.RestoreTableRequest,
+          com.google.bigtable.admin.v2.Table,
+          RestoreTableMetadata>
+      mockRestoreTableOperationCallable;
+
   @Before
   public void setUp() {
     adminClient = BigtableTableAdminClient.create(PROJECT_ID, INSTANCE_ID, mockStub);
@@ -100,6 +163,16 @@ public void setUp() {
     Mockito.when(mockStub.listTablesPagedCallable()).thenReturn(mockListTableCallable);
     Mockito.when(mockStub.dropRowRangeCallable()).thenReturn(mockDropRowRangeCallable);
     Mockito.when(mockStub.awaitReplicationCallable()).thenReturn(mockAwaitReplicationCallable);
+    Mockito.when(mockStub.createBackupOperationCallable())
+        .thenReturn(mockCreateBackupOperationCallable);
+    Mockito.when(mockStub.createBackupCallable()).thenReturn(mockCreateBackupCallable);
+    Mockito.when(mockStub.getBackupCallable()).thenReturn(mockGetBackupCallable);
+    Mockito.when(mockStub.listBackupsPagedCallable()).thenReturn(mockListBackupCallable);
+    Mockito.when(mockStub.updateBackupCallable()).thenReturn(mockUpdateBackupCallable);
+    Mockito.when(mockStub.deleteBackupCallable()).thenReturn(mockDeleteBackupCallable);
+    Mockito.when(mockStub.restoreTableCallable()).thenReturn(mockRestoreTableCallable);
+    Mockito.when(mockStub.restoreTableOperationCallable())
+        .thenReturn(mockRestoreTableOperationCallable);
   }
 
   @Test
@@ -336,4 +409,253 @@ public void testExistsFalse() {
     // Verify
     assertThat(found).isFalse();
   }
+
+  @Test
+  public void testCreateBackup() {
+    // Setup
+    String backupName = NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID);
+    Timestamp startTime = Timestamp.newBuilder().setSeconds(123).build();
+    Timestamp endTime = Timestamp.newBuilder().setSeconds(456).build();
+    Timestamp expireTime = Timestamp.newBuilder().setSeconds(789).build();
+    long sizeBytes = 123456789;
+    CreateBackupRequest req =
+        CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setSourceTableId(TABLE_ID);
+    mockOperationResult(
+        mockCreateBackupOperationCallable,
+        req.toProto(PROJECT_ID, INSTANCE_ID),
+        com.google.bigtable.admin.v2.Backup.newBuilder()
+            .setName(backupName)
+            .setSourceTable(TABLE_NAME)
+            .setStartTime(startTime)
+            .setEndTime(endTime)
+            .setExpireTime(expireTime)
+            .setSizeBytes(sizeBytes)
+            .build(),
+        CreateBackupMetadata.newBuilder()
+            .setName(backupName)
+            .setStartTime(startTime)
+            .setEndTime(endTime)
+            .setSourceTable(TABLE_NAME)
+            .build());
+    // Execute
+    Backup actualResult = adminClient.createBackup(req);
+
+    // Verify
+    assertThat(actualResult.getId()).isEqualTo(BACKUP_ID);
+    assertThat(actualResult.getSourceTableId()).isEqualTo(TABLE_ID);
+    assertThat(actualResult.getStartTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(startTime)));
+    assertThat(actualResult.getEndTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(endTime)));
+    assertThat(actualResult.getExpireTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(expireTime)));
+    assertThat(actualResult.getSizeBytes()).isEqualTo(sizeBytes);
+  }
+
+  @Test
+  public void testGetBackup() {
+    // Setup
+    Timestamp expireTime = Timestamp.newBuilder().setSeconds(123456789).build();
+    Timestamp startTime = Timestamp.newBuilder().setSeconds(1234).build();
+    Timestamp endTime = Timestamp.newBuilder().setSeconds(5678).build();
+    com.google.bigtable.admin.v2.Backup.State state = State.CREATING;
+    long sizeBytes = 12345L;
+    GetBackupRequest testRequest =
+        GetBackupRequest.newBuilder()
+            .setName(NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID))
+            .build();
+    Mockito.when(mockGetBackupCallable.futureCall(testRequest))
+        .thenReturn(
+            ApiFutures.immediateFuture(
+                com.google.bigtable.admin.v2.Backup.newBuilder()
+                    .setName(
+                        NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID))
+                    .setSourceTable(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
+                    .setExpireTime(expireTime)
+                    .setStartTime(startTime)
+                    .setEndTime(endTime)
+                    .setSizeBytes(sizeBytes)
+                    .setState(state)
+                    .build()));
+
+    // Execute
+    Backup actualResult = adminClient.getBackup(CLUSTER_ID, BACKUP_ID);
+
+    // Verify
+    assertThat(actualResult.getId()).isEqualTo(BACKUP_ID);
+    assertThat(actualResult.getSourceTableId()).isEqualTo(TABLE_ID);
+    assertThat(actualResult.getExpireTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(expireTime)));
+    assertThat(actualResult.getStartTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(startTime)));
+    assertThat(actualResult.getEndTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(endTime)));
+    assertThat(actualResult.getSizeBytes()).isEqualTo(sizeBytes);
+    assertThat(actualResult.getState()).isEqualTo(Backup.State.fromProto(state));
+  }
+
+  @Test
+  public void testUpdateBackup() {
+    // Setup
+    Timestamp expireTime = Timestamp.newBuilder().setSeconds(123456789).build();
+    long sizeBytes = 12345L;
+    UpdateBackupRequest req = UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID);
+    Mockito.when(mockUpdateBackupCallable.futureCall(req.toProto(PROJECT_ID, INSTANCE_ID)))
+        .thenReturn(
+            ApiFutures.immediateFuture(
+                com.google.bigtable.admin.v2.Backup.newBuilder()
+                    .setName(
+                        NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID))
+                    .setSourceTable(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
+                    .setExpireTime(expireTime)
+                    .setSizeBytes(sizeBytes)
+                    .build()));
+
+    // Execute
+    Backup actualResult = adminClient.updateBackup(req);
+
+    // Verify
+    assertThat(actualResult.getId()).isEqualTo(BACKUP_ID);
+    assertThat(actualResult.getSourceTableId()).isEqualTo(TABLE_ID);
+    assertThat(actualResult.getExpireTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(expireTime)));
+    assertThat(actualResult.getSizeBytes()).isEqualTo(sizeBytes);
+  }
+
+  @Test
+  public void testRestoreTable() throws ExecutionException, InterruptedException {
+    // Setup
+    Timestamp startTime = Timestamp.newBuilder().setSeconds(1234).build();
+    Timestamp endTime = Timestamp.newBuilder().setSeconds(5678).build();
+    String operationName = "my-operation";
+    RestoreTableRequest req = RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId(TABLE_ID);
+    Mockito.when(mockRestoreTableCallable.futureCall(req.toProto(PROJECT_ID, INSTANCE_ID)))
+        .thenReturn(
+            ApiFutures.immediateFuture(
+                Operation.newBuilder()
+                    .setMetadata(
+                        Any.pack(
+                            RestoreTableMetadata.newBuilder()
+                                .setName(
+                                    NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
+                                .setOptimizeTableOperationName(operationName)
+                                .setSourceType(RestoreSourceType.BACKUP)
+                                .setBackupInfo(
+                                    BackupInfo.newBuilder()
+                                        .setBackup(BACKUP_ID)
+                                        .setSourceTable(
+                                            NameUtil.formatTableName(
+                                                PROJECT_ID, INSTANCE_ID, TABLE_ID))
+                                        .setStartTime(startTime)
+                                        .setEndTime(endTime)
+                                        .build())
+                                .build()))
+                    .build()));
+    mockOperationResult(
+        mockRestoreTableOperationCallable,
+        req.toProto(PROJECT_ID, INSTANCE_ID),
+        com.google.bigtable.admin.v2.Table.newBuilder().setName(TABLE_NAME).build(),
+        RestoreTableMetadata.newBuilder()
+            .setName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
+            .setOptimizeTableOperationName(operationName)
+            .setSourceType(RestoreSourceType.BACKUP)
+            .setBackupInfo(
+                BackupInfo.newBuilder()
+                    .setBackup(BACKUP_ID)
+                    .setSourceTable(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
+                    .setStartTime(startTime)
+                    .setEndTime(endTime)
+                    .build())
+            .build());
+    // Execute
+    RestoredTableResult actualResult = adminClient.restoreTable(req);
+
+    // Verify
+    assertThat(actualResult.getTable().getId()).isEqualTo(TABLE_ID);
+  }
+
+  @Test
+  public void testDeleteBackup() {
+    // Setup
+    DeleteBackupRequest testRequest =
+        DeleteBackupRequest.newBuilder()
+            .setName(NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID))
+            .build();
+    Mockito.when(mockDeleteBackupCallable.futureCall(testRequest))
+        .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
+
+    // Execute
+    adminClient.deleteBackup(CLUSTER_ID, BACKUP_ID);
+
+    // Verify
+    Mockito.verify(mockDeleteBackupCallable, Mockito.times(1)).futureCall(testRequest);
+  }
+
+  @Test
+  public void testListBackups() {
+    // Setup
+    com.google.bigtable.admin.v2.ListBackupsRequest testRequest =
+        com.google.bigtable.admin.v2.ListBackupsRequest.newBuilder()
+            .setParent(NameUtil.formatClusterName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID))
+            .build();
+
+    // 3 Backups spread across 2 pages
+    List expectedProtos = Lists.newArrayList();
+    for (int i = 0; i < 3; i++) {
+      expectedProtos.add(
+          com.google.bigtable.admin.v2.Backup.newBuilder()
+              .setName(
+                  NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID + i))
+              .build());
+    }
+
+    // 2 on the first page
+    ListBackupsPage page0 = Mockito.mock(ListBackupsPage.class);
+    Mockito.when(page0.getValues()).thenReturn(expectedProtos.subList(0, 2));
+    Mockito.when(page0.getNextPageToken()).thenReturn("next-page");
+    Mockito.when(page0.hasNextPage()).thenReturn(true);
+
+    // 1 on the last page
+    ListBackupsPage page1 = Mockito.mock(ListBackupsPage.class);
+    Mockito.when(page1.getValues()).thenReturn(expectedProtos.subList(2, 3));
+
+    // Link page0 to page1
+    Mockito.when(page0.getNextPageAsync()).thenReturn(ApiFutures.immediateFuture(page1));
+
+    // Link page to the response
+    ListBackupsPagedResponse response0 = Mockito.mock(ListBackupsPagedResponse.class);
+    Mockito.when(response0.getPage()).thenReturn(page0);
+
+    Mockito.when(mockListBackupCallable.futureCall(testRequest))
+        .thenReturn(ApiFutures.immediateFuture(response0));
+
+    // Execute
+    List actualResults = adminClient.listBackups(CLUSTER_ID);
+
+    // Verify
+    List expectedResults = Lists.newArrayList();
+    for (com.google.bigtable.admin.v2.Backup expectedProto : expectedProtos) {
+      expectedResults.add(NameUtil.extractBackupIdFromBackupName(expectedProto.getName()));
+    }
+
+    assertThat(actualResults).containsExactlyElementsIn(expectedResults);
+  }
+
+  private  void mockOperationResult(
+      OperationCallable callable,
+      ReqT request,
+      RespT response,
+      MetaT metadata) {
+    OperationSnapshot operationSnapshot =
+        FakeOperationSnapshot.newBuilder()
+            .setDone(true)
+            .setErrorCode(GrpcStatusCode.of(Code.OK))
+            .setName("fake-name")
+            .setResponse(response)
+            .setMetadata(metadata)
+            .build();
+    OperationFuture operationFuture =
+        OperationFutures.immediateOperationFuture(operationSnapshot);
+    Mockito.when(callable.futureCall(request)).thenReturn(operationFuture);
+  }
 }
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java
new file mode 100644
index 000000000..a452a2bc5
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NameUtilTest {
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void extractBackupIdFromBackupNameTest() {
+    String testBackupName =
+        "projects/my-project/instances/my-instance/clusters/my-cluster/backups/my-backup";
+    assertThat(NameUtil.extractBackupIdFromBackupName(testBackupName)).isEqualTo("my-backup");
+
+    exception.expect(IllegalArgumentException.class);
+    NameUtil.extractBackupIdFromBackupName("bad-format");
+  }
+
+  @Test
+  public void formatBackupNameTest() {
+    String testBackupName =
+        "projects/my-project/instances/my-instance/clusters/my-cluster/backups/my-backup";
+
+    assertThat(NameUtil.formatBackupName("my-project", "my-instance", "my-cluster", "my-backup"))
+        .isEqualTo(testBackupName);
+  }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableBackupIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableBackupIT.java
new file mode 100644
index 000000000..1e1bc8464
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableBackupIT.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.it;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static io.grpc.Status.Code.NOT_FOUND;
+import static org.junit.Assert.fail;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.api.gax.rpc.ApiException;
+import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient;
+import com.google.cloud.bigtable.admin.v2.BigtableTableAdminSettings;
+import com.google.cloud.bigtable.admin.v2.models.Backup;
+import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest;
+import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest;
+import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest;
+import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult;
+import com.google.cloud.bigtable.admin.v2.models.Table;
+import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest;
+import com.google.cloud.bigtable.data.v2.BigtableDataClient;
+import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.models.RowMutation;
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Lists;
+import com.google.protobuf.Timestamp;
+import io.grpc.StatusRuntimeException;
+import java.io.IOException;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+import org.junit.*;
+import org.threeten.bp.Duration;
+import org.threeten.bp.Instant;
+
+public class BigtableBackupIT {
+  private static final Logger LOGGER = Logger.getLogger(BigtableBackupIT.class.getName());
+
+  private static final String PROJECT_PROPERTY_NAME = "bigtable.project";
+  private static final String INSTANCE_PROPERTY_NAME = "bigtable.instance";
+  private static final String CLUSTER_PROPERTY_NAME = "bigtable.cluster";
+  private static final String ADMIN_ENDPOINT_PROPERTY_NAME = "bigtable.adminendpoint";
+  private static final String DATA_ENDPOINT_PROPERTY_NAME = "bigtable.dataendpoint";
+  private static final String TABLE_SIZE_PROPERTY_NAME = "bigtable.tablesizekb";
+  private static final int[] BACKOFF_DURATION = {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024};
+
+  private static final String TEST_TABLE_SUFFIX = "test-table-for-backup-it";
+  private static final String TEST_BACKUP_SUFFIX = "test-backup-for-backup-it";
+
+  private static final int DAYS_IN_SECONDS = 24 * 60 * 60;
+
+  private static BigtableTableAdminClient tableAdmin;
+  private static BigtableDataClient dataClient;
+
+  private static String targetProject;
+  private static String targetInstance;
+  private static String targetCluster;
+  private static Table testTable;
+  private static String prefix;
+
+  @BeforeClass
+  public static void createClient()
+      throws IOException, InterruptedException, ExecutionException, TimeoutException {
+    List missingProperties = Lists.newArrayList();
+
+    targetProject = System.getProperty(PROJECT_PROPERTY_NAME);
+    if (targetProject == null) {
+      missingProperties.add(PROJECT_PROPERTY_NAME);
+    }
+
+    targetInstance = System.getProperty(INSTANCE_PROPERTY_NAME);
+    if (targetInstance == null) {
+      missingProperties.add(INSTANCE_PROPERTY_NAME);
+    }
+
+    targetCluster = System.getProperty(CLUSTER_PROPERTY_NAME);
+    if (targetCluster == null) {
+      missingProperties.add(CLUSTER_PROPERTY_NAME);
+    }
+
+    String adminApiEndpoint = System.getProperty(ADMIN_ENDPOINT_PROPERTY_NAME);
+    if (adminApiEndpoint == null) {
+      adminApiEndpoint = "bigtableadmin.googleapis.com:443";
+    }
+
+    int tableSize = MoreObjects.firstNonNull(Integer.getInteger(TABLE_SIZE_PROPERTY_NAME), 1);
+    if (!missingProperties.isEmpty()) {
+      LOGGER.warning("Missing properties: " + Joiner.on(",").join(missingProperties));
+      return;
+    }
+
+    // Setup a prefix to avoid collisions between concurrent test runs
+    prefix = String.format("020%d", System.currentTimeMillis());
+
+    BigtableTableAdminSettings.Builder settings =
+        BigtableTableAdminSettings.newBuilder()
+            .setInstanceId(targetInstance)
+            .setProjectId(targetProject);
+    settings.stubSettings().setEndpoint(adminApiEndpoint);
+    tableAdmin = BigtableTableAdminClient.create(settings.build());
+
+    testTable =
+        tableAdmin.createTable(
+            CreateTableRequest.of(generateId(TEST_TABLE_SUFFIX)).addFamily("cf1"));
+
+    // Populate test data.
+    if (tableSize > 0) {
+      String dataApiEndpoint = System.getProperty(DATA_ENDPOINT_PROPERTY_NAME);
+      if (dataApiEndpoint == null) {
+        dataApiEndpoint = "bigtable.googleapis.com:443";
+      }
+      BigtableDataSettings.Builder dataSettings =
+          BigtableDataSettings.newBuilder()
+              .setInstanceId(targetInstance)
+              .setProjectId(targetProject);
+      dataSettings.stubSettings().setEndpoint(dataApiEndpoint);
+      dataClient = BigtableDataClient.create(dataSettings.build());
+      byte[] rowBytes = new byte[1024];
+      Random random = new Random();
+      random.nextBytes(rowBytes);
+
+      List> futures = Lists.newArrayList();
+      for (int i = 0; i < tableSize; i++) {
+        ApiFuture future =
+            dataClient.mutateRowAsync(
+                RowMutation.create(testTable.getId(), "test-row-" + i)
+                    .setCell("cf1", "", rowBytes.toString()));
+        futures.add(future);
+      }
+      ApiFutures.allAsList(futures).get(3, TimeUnit.MINUTES);
+    }
+
+    // Cleanup old backups and tables, under normal circumstances this will do nothing
+    String stalePrefix =
+        String.format("020%d", System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2));
+    for (String backupId : tableAdmin.listBackups(targetCluster)) {
+      if (backupId.endsWith(TEST_BACKUP_SUFFIX) && stalePrefix.compareTo(backupId) > 0) {
+        LOGGER.info("Deleting stale backup: " + backupId);
+        tableAdmin.deleteBackup(targetCluster, backupId);
+      }
+    }
+    for (String tableId : tableAdmin.listTables()) {
+      if (tableId.endsWith("TEST_TABLE_SUFFIX") && stalePrefix.compareTo(tableId) > 0) {
+        LOGGER.info("Deleting stale backup: " + tableId);
+        tableAdmin.deleteTable(tableId);
+      }
+    }
+  }
+
+  @AfterClass
+  public static void closeClient() {
+    if (testTable != null) {
+      try {
+        tableAdmin.deleteTable(testTable.getId());
+      } catch (Exception e) {
+        // Ignore.
+      }
+    }
+
+    if (tableAdmin != null) {
+      tableAdmin.close();
+    }
+
+    if (dataClient != null) {
+      dataClient.close();
+    }
+  }
+
+  @Before
+  public void setup() {
+    if (tableAdmin == null) {
+      throw new AssumptionViolatedException(
+          "Required properties are not set, skipping integration tests.");
+    }
+  }
+
+  @Test
+  public void createAndGetBackupTest() throws InterruptedException {
+    Instant expireTime = Instant.now().plus(Duration.ofDays(15));
+    String backupId = generateId(TEST_BACKUP_SUFFIX);
+    CreateBackupRequest request =
+        CreateBackupRequest.of(targetCluster, backupId)
+            .setSourceTableId(testTable.getId())
+            .setExpireTime(expireTime);
+    try {
+      Backup response = tableAdmin.createBackup(request);
+      assertWithMessage("Got wrong backup Id in CreateBackup")
+          .that(response.getId())
+          .isEqualTo(backupId);
+      assertWithMessage("Got wrong source table name in CreateBackup")
+          .that(response.getSourceTableId())
+          .isEqualTo(testTable.getId());
+      assertWithMessage("Got wrong expire time in CreateBackup")
+          .that(response.getExpireTime())
+          .isEqualTo(expireTime);
+
+      Backup result = tableAdmin.getBackup(targetCluster, backupId);
+      assertWithMessage("Got wrong backup Id in GetBackup API")
+          .that(result.getId())
+          .isEqualTo(backupId);
+      assertWithMessage("Got wrong source table name in GetBackup API")
+          .that(result.getSourceTableId())
+          .isEqualTo(testTable.getId());
+      assertWithMessage("Got wrong expire time in GetBackup API")
+          .that(result.getExpireTime())
+          .isEqualTo(expireTime);
+      assertWithMessage("Got empty start time in GetBackup API")
+          .that(result.getStartTime())
+          .isNotEqualTo(Timestamp.getDefaultInstance());
+      assertWithMessage("Got wrong size bytes in GetBackup API")
+          .that(result.getSizeBytes())
+          .isEqualTo(0L);
+      assertWithMessage("Got wrong state in GetBackup API")
+          .that(result.getState())
+          .isAnyOf(Backup.State.CREATING, Backup.State.READY);
+
+    } finally {
+      tableAdmin.deleteBackup(targetCluster, backupId);
+    }
+  }
+
+  @Test
+  public void listBackupTest() throws InterruptedException {
+    String backupId1 = generateId("list-1-" + TEST_BACKUP_SUFFIX);
+    String backupId2 = generateId("list-2-" + TEST_BACKUP_SUFFIX);
+
+    try {
+      createBackupAndWait(backupId1);
+      createBackupAndWait(backupId2);
+
+      List response = tableAdmin.listBackups(targetCluster);
+      // Concurrent tests running may cause flakiness. Use containsAtLeast instead of
+      // containsExactly.
+      assertWithMessage("Incorrect backup name")
+          .that(response)
+          .containsAtLeast(backupId1, backupId2);
+    } finally {
+      tableAdmin.deleteBackup(targetCluster, backupId1);
+      tableAdmin.deleteBackup(targetCluster, backupId2);
+    }
+  }
+
+  @Test
+  public void updateBackupTest() throws InterruptedException {
+    String backupId = generateId("update-" + TEST_BACKUP_SUFFIX);
+    createBackupAndWait(backupId);
+
+    Instant expireTime = Instant.now().plus(Duration.ofDays(20));
+    UpdateBackupRequest req =
+        UpdateBackupRequest.of(targetCluster, backupId).setExpireTime(expireTime);
+    try {
+      Backup backup = tableAdmin.updateBackup(req);
+      assertWithMessage("Incorrect expire time").that(backup.getExpireTime()).isEqualTo(expireTime);
+    } finally {
+      tableAdmin.deleteBackup(targetCluster, backupId);
+    }
+  }
+
+  @Test
+  public void deleteBackupTest() throws InterruptedException {
+    String backupId = generateId("delete-" + TEST_BACKUP_SUFFIX);
+
+    createBackupAndWait(backupId);
+    tableAdmin.deleteBackup(targetCluster, backupId);
+
+    try {
+      for (int i = 0; i < BACKOFF_DURATION.length; i++) {
+        tableAdmin.getBackup(targetCluster, backupId);
+
+        LOGGER.info("Wait for " + BACKOFF_DURATION[i] + " seconds for deleting backup " + backupId);
+        Thread.sleep(BACKOFF_DURATION[i] * 1000);
+      }
+      fail("backup was not deleted.");
+    } catch (ApiException ex) {
+      assertWithMessage("Incorrect exception type")
+          .that(ex.getCause())
+          .isInstanceOf(StatusRuntimeException.class);
+      assertWithMessage("Incorrect error message")
+          .that(((StatusRuntimeException) ex.getCause()).getStatus().getCode())
+          .isEqualTo(NOT_FOUND);
+    }
+  }
+
+  @Test
+  public void restoreTableTest() throws InterruptedException, ExecutionException {
+    String backupId = generateId("restore-" + TEST_BACKUP_SUFFIX);
+    String tableId = generateId("restored-table");
+    createBackupAndWait(backupId);
+
+    // Wait 2 minutes so that the RestoreTable API will trigger an optimize restored
+    // table operation.
+    Thread.sleep(120 * 1000);
+
+    try {
+      RestoreTableRequest req = RestoreTableRequest.of(targetCluster, backupId).setTableId(tableId);
+      RestoredTableResult result = tableAdmin.restoreTable(req);
+      assertWithMessage("Incorrect restored table id")
+          .that(result.getTable().getId())
+          .isEqualTo(tableId);
+
+      // The assertion might be missing if the test is running against a HDD cluster or an
+      // optimization is not necessary.
+      assertWithMessage("Empty OptimizeRestoredTable token")
+          .that(result.getOptimizeRestoredTableOperationToken())
+          .isNotNull();
+      tableAdmin.awaitOptimizeRestoredTable(result.getOptimizeRestoredTableOperationToken());
+      tableAdmin.getTable(tableId);
+    } finally {
+      tableAdmin.deleteBackup(targetCluster, backupId);
+      tableAdmin.deleteTable(tableId);
+    }
+  }
+
+  private CreateBackupRequest createBackupRequest(String backupName) {
+    return CreateBackupRequest.of(targetCluster, backupName)
+        .setSourceTableId(testTable.getId())
+        .setExpireTime(Instant.now().plus(Duration.ofDays(15)));
+  }
+
+  private static String generateId(String name) {
+    return prefix + "-" + name;
+  }
+
+  private void createBackupAndWait(String backupId) throws InterruptedException {
+    tableAdmin.createBackup(createBackupRequest(backupId));
+    for (int i = 0; i < BACKOFF_DURATION.length; i++) {
+      try {
+        Backup backup = tableAdmin.getBackup(targetCluster, backupId);
+        if (backup.getState() == Backup.State.READY) {
+          return;
+        }
+      } catch (ApiException ex) {
+        LOGGER.info("Wait for " + BACKOFF_DURATION[i] + " seconds for creating backup " + backupId);
+      }
+
+      Thread.sleep(BACKOFF_DURATION[i] * 1000);
+    }
+
+    fail("Creating Backup Timeout");
+  }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java
new file mode 100644
index 000000000..be32058e2
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/BackupTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Timestamps;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Instant;
+
+@RunWith(JUnit4.class)
+public class BackupTest {
+  @Test
+  public void testBackupStateEnumUpToDate() {
+    List validProtoValues =
+        Lists.newArrayList(com.google.bigtable.admin.v2.Backup.State.values());
+
+    List validModelValues = Lists.newArrayList(Backup.State.values());
+
+    List actualModelValues = Lists.newArrayList();
+
+    for (com.google.bigtable.admin.v2.Backup.State protoValue : validProtoValues) {
+      Backup.State modelValue = Backup.State.fromProto(protoValue);
+      actualModelValues.add(modelValue);
+    }
+
+    assertThat(actualModelValues).containsExactlyElementsIn(validModelValues);
+  }
+
+  @Test
+  public void testFromProto() {
+    Timestamp expireTime = Timestamp.newBuilder().setSeconds(1234).build();
+    Timestamp startTime = Timestamp.newBuilder().setSeconds(1234).build();
+    Timestamp endTime = Timestamp.newBuilder().setSeconds(1234).build();
+    com.google.bigtable.admin.v2.Backup proto =
+        com.google.bigtable.admin.v2.Backup.newBuilder()
+            .setName("projects/my-project/instances/instance1/clusters/cluster1/backups/backup1")
+            .setSourceTable("projects/my-project/instances/instance1/tables/table1")
+            .setExpireTime(expireTime)
+            .setStartTime(startTime)
+            .setEndTime(endTime)
+            .setSizeBytes(123456)
+            .setState(com.google.bigtable.admin.v2.Backup.State.READY)
+            .build();
+
+    Backup result = Backup.fromProto(proto);
+
+    assertThat(result.getId()).isEqualTo("backup1");
+    assertThat(result.getSourceTableId()).isEqualTo("table1");
+    assertThat(result.getExpireTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(expireTime)));
+    assertThat(result.getStartTime())
+        .isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(startTime)));
+    assertThat(result.getEndTime()).isEqualTo(Instant.ofEpochMilli(Timestamps.toMillis(endTime)));
+    assertThat(result.getSizeBytes()).isEqualTo(123456);
+    assertThat(result.getState()).isEqualTo(Backup.State.READY);
+  }
+
+  @Test
+  public void testRequiresName() {
+    com.google.bigtable.admin.v2.Backup proto =
+        com.google.bigtable.admin.v2.Backup.newBuilder()
+            .setSourceTable("projects/my-project/instances/instance1/tables/table1")
+            .setExpireTime(Timestamp.newBuilder().setSeconds(1234).build())
+            .setStartTime(Timestamp.newBuilder().setSeconds(123).build())
+            .setEndTime(Timestamp.newBuilder().setSeconds(456).build())
+            .setSizeBytes(123456)
+            .setState(com.google.bigtable.admin.v2.Backup.State.READY)
+            .build();
+
+    Exception actualException = null;
+
+    try {
+      Backup.fromProto(proto);
+    } catch (Exception e) {
+      actualException = e;
+    }
+
+    assertThat(actualException).isInstanceOf(IllegalArgumentException.class);
+  }
+
+  @Test
+  public void testRequiresSourceTable() {
+    com.google.bigtable.admin.v2.Backup proto =
+        com.google.bigtable.admin.v2.Backup.newBuilder()
+            .setName("projects/my-project/instances/instance1/clusters/cluster1/backups/backup1")
+            .setExpireTime(Timestamp.newBuilder().setSeconds(1234).build())
+            .setStartTime(Timestamp.newBuilder().setSeconds(123).build())
+            .setEndTime(Timestamp.newBuilder().setSeconds(456).build())
+            .setSizeBytes(123456)
+            .setState(com.google.bigtable.admin.v2.Backup.State.READY)
+            .build();
+
+    Exception actualException = null;
+
+    try {
+      Backup.fromProto(proto);
+    } catch (Exception e) {
+      actualException = e;
+    }
+
+    assertThat(actualException).isInstanceOf(IllegalArgumentException.class);
+  }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateBackupRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateBackupRequestTest.java
new file mode 100644
index 000000000..f4a1e12f6
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateBackupRequestTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.bigtable.admin.v2.Backup;
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.protobuf.util.Timestamps;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+import org.threeten.bp.Instant;
+
+@RunWith(JUnit4.class)
+public class CreateBackupRequestTest {
+
+  private static final String TABLE_ID = "my-table";
+  private static final String BACKUP_ID = "my-backup";
+  private static final String PROJECT_ID = "my-project";
+  private static final String INSTANCE_ID = "my-instance";
+  private static final String CLUSTER_ID = "my-cluster";
+  private static final Instant EXPIRE_TIME = Instant.now().plus(Duration.ofDays(15));
+
+  @Test
+  public void testToProto() {
+    CreateBackupRequest request =
+        CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+            .setSourceTableId(TABLE_ID)
+            .setExpireTime(EXPIRE_TIME);
+
+    com.google.bigtable.admin.v2.CreateBackupRequest requestProto =
+        com.google.bigtable.admin.v2.CreateBackupRequest.newBuilder()
+            .setBackupId(BACKUP_ID)
+            .setBackup(
+                Backup.newBuilder()
+                    .setSourceTable(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID))
+                    .setExpireTime(Timestamps.fromMillis(EXPIRE_TIME.toEpochMilli()))
+                    .build())
+            .setParent(NameUtil.formatClusterName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID))
+            .build();
+    assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto);
+  }
+
+  @Test
+  public void testEquality() {
+    CreateBackupRequest request =
+        CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+            .setSourceTableId(TABLE_ID)
+            .setExpireTime(EXPIRE_TIME);
+
+    assertThat(request)
+        .isEqualTo(
+            CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+                .setSourceTableId(TABLE_ID)
+                .setExpireTime(EXPIRE_TIME));
+
+    assertThat(request)
+        .isNotEqualTo(
+            CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+                .setSourceTableId("another-table")
+                .setExpireTime(EXPIRE_TIME));
+  }
+
+  @Test
+  public void testHashCode() {
+    CreateBackupRequest request =
+        CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+            .setSourceTableId(TABLE_ID)
+            .setExpireTime(EXPIRE_TIME);
+
+    assertThat(request.hashCode())
+        .isEqualTo(
+            CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+                .setSourceTableId(TABLE_ID)
+                .setExpireTime(EXPIRE_TIME)
+                .hashCode());
+
+    assertThat(request.hashCode())
+        .isNotEqualTo(
+            CreateBackupRequest.of(CLUSTER_ID, BACKUP_ID)
+                .setSourceTableId("another-table")
+                .setExpireTime(EXPIRE_TIME)
+                .hashCode());
+  }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/RestoreTableRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/RestoreTableRequestTest.java
new file mode 100644
index 000000000..3ed165042
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/RestoreTableRequestTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RestoreTableRequestTest {
+
+  private static final String TABLE_ID = "my-table";
+  private static final String BACKUP_ID = "my-backup";
+  private static final String PROJECT_ID = "my-project";
+  private static final String INSTANCE_ID = "my-instance";
+  private static final String CLUSTER_ID = "my-cluster";
+
+  @Test
+  public void testToProto() {
+    RestoreTableRequest request =
+        RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId(TABLE_ID);
+
+    com.google.bigtable.admin.v2.RestoreTableRequest requestProto =
+        com.google.bigtable.admin.v2.RestoreTableRequest.newBuilder()
+            .setParent(NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID))
+            .setBackup(NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID))
+            .setTableId(TABLE_ID)
+            .build();
+    assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto);
+  }
+
+  @Test
+  public void testEquality() {
+    RestoreTableRequest request =
+        RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId(TABLE_ID);
+
+    assertThat(request)
+        .isEqualTo(RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId(TABLE_ID));
+    assertThat(request)
+        .isNotEqualTo(RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId("another-table"));
+  }
+
+  @Test
+  public void testHashCode() {
+    RestoreTableRequest request =
+        RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId(TABLE_ID);
+    assertThat(request.hashCode())
+        .isEqualTo(RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId(TABLE_ID).hashCode());
+    assertThat(request.hashCode())
+        .isNotEqualTo(
+            RestoreTableRequest.of(CLUSTER_ID, BACKUP_ID).setTableId("another-table").hashCode());
+  }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateBackupRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateBackupRequestTest.java
new file mode 100644
index 000000000..c8d34833f
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateBackupRequestTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.admin.v2.models;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.bigtable.admin.v2.Backup;
+import com.google.cloud.bigtable.admin.v2.internal.NameUtil;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.util.Timestamps;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+import org.threeten.bp.Instant;
+
+@RunWith(JUnit4.class)
+public class UpdateBackupRequestTest {
+
+  private static final String TABLE_ID = "my-table";
+  private static final String BACKUP_ID = "my-backup";
+  private static final String PROJECT_ID = "my-project";
+  private static final String INSTANCE_ID = "my-instance";
+  private static final String CLUSTER_ID = "my-cluster";
+  private static final Instant EXPIRE_TIME = Instant.now().plus(Duration.ofDays(15));
+  private static final Instant EXPIRE_TIME_2 = Instant.now().plus(Duration.ofDays(20));
+
+  @Test
+  public void testToProto() {
+    UpdateBackupRequest request =
+        UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME);
+
+    com.google.bigtable.admin.v2.UpdateBackupRequest requestProto =
+        com.google.bigtable.admin.v2.UpdateBackupRequest.newBuilder()
+            .setBackup(
+                Backup.newBuilder()
+                    .setName(
+                        NameUtil.formatBackupName(PROJECT_ID, INSTANCE_ID, CLUSTER_ID, BACKUP_ID))
+                    .setExpireTime(Timestamps.fromMillis(EXPIRE_TIME.toEpochMilli()))
+                    .build())
+            .setUpdateMask(FieldMask.newBuilder().addPaths("expire_time").build())
+            .build();
+    assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto);
+  }
+
+  @Test
+  public void testEquality() {
+    UpdateBackupRequest request =
+        UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME);
+    assertThat(request)
+        .isEqualTo(UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME));
+    assertThat(request)
+        .isNotEqualTo(UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME_2));
+  }
+
+  @Test
+  public void testHashCode() {
+    UpdateBackupRequest request =
+        UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME);
+    assertThat(request.hashCode())
+        .isEqualTo(
+            UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME).hashCode());
+    assertThat(request.hashCode())
+        .isNotEqualTo(
+            UpdateBackupRequest.of(CLUSTER_ID, BACKUP_ID).setExpireTime(EXPIRE_TIME_2).hashCode());
+  }
+}