Skip to content
Permalink
Browse files
feat: add support for backups (#35)
* feat: implement backup support

* Apply suggestions from code review

Co-Authored-By: skuruppu <skuruppu@google.com>

* refactor restore to use source

Co-authored-by: larkee <larkee@users.noreply.github.com>
Co-authored-by: skuruppu <skuruppu@google.com>
  • Loading branch information
3 people committed Mar 13, 2020
1 parent a48c4b1 commit 39288e784826c5accca71096be11f99ad7f930f4
Showing with 1,725 additions and 1 deletion.
  1. +275 −0 google/cloud/spanner_v1/backup.py
  2. +116 −0 google/cloud/spanner_v1/database.py
  3. +167 −0 google/cloud/spanner_v1/instance.py
  4. +590 −0 tests/unit/test_backup.py
  5. +250 −1 tests/unit/test_database.py
  6. +327 −0 tests/unit/test_instance.py
@@ -0,0 +1,275 @@
# Copyright 2020 Google LLC All rights reserved.
#
# 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
#
# http://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.

"""User friendly container for Cloud Spanner Backup."""

import re

from google.cloud._helpers import _datetime_to_pb_timestamp, _pb_timestamp_to_datetime
from google.cloud.exceptions import NotFound

from google.cloud.spanner_admin_database_v1.gapic import enums
from google.cloud.spanner_v1._helpers import _metadata_with_prefix

_BACKUP_NAME_RE = re.compile(
r"^projects/(?P<project>[^/]+)/"
r"instances/(?P<instance_id>[a-z][-a-z0-9]*)/"
r"backups/(?P<backup_id>[a-z][a-z0-9_\-]*[a-z0-9])$"
)


class Backup(object):
"""Representation of a Cloud Spanner Backup.
We can use a :class`Backup` to:
* :meth:`create` the backup
* :meth:`update` the backup
* :meth:`delete` the backup
:type backup_id: str
:param backup_id: The ID of the backup.
:type instance: :class:`~google.cloud.spanner_v1.instance.Instance`
:param instance: The instance that owns the backup.
:type database: str
:param database: (Optional) The URI of the database that the backup is
for. Required if the create method needs to be called.
:type expire_time: :class:`datetime.datetime`
:param expire_time: (Optional) The expire time that will be used to
create the backup. Required if the create method
needs to be called.
"""

def __init__(self, backup_id, instance, database="", expire_time=None):
self.backup_id = backup_id
self._instance = instance
self._database = database
self._expire_time = expire_time
self._create_time = None
self._size_bytes = None
self._state = None
self._referencing_databases = None

@property
def name(self):
"""Backup name used in requests.
The backup name is of the form
``"projects/../instances/../backups/{backup_id}"``
:rtype: str
:returns: The backup name.
"""
return self._instance.name + "/backups/" + self.backup_id

@property
def database(self):
"""Database name used in requests.
The database name is of the form
``"projects/../instances/../backups/{backup_id}"``
:rtype: str
:returns: The database name.
"""
return self._database

@property
def expire_time(self):
"""Expire time used in creation requests.
:rtype: :class:`datetime.datetime`
:returns: a datetime object representing the expire time of
this backup
"""
return self._expire_time

@property
def create_time(self):
"""Create time of this backup.
:rtype: :class:`datetime.datetime`
:returns: a datetime object representing the create time of
this backup
"""
return self._create_time

@property
def size_bytes(self):
"""Size of this backup in bytes.
:rtype: int
:returns: the number size of this backup measured in bytes
"""
return self._size_bytes

@property
def state(self):
"""State of this backup.
:rtype: :class:`~google.cloud.spanner_admin_database_v1.gapic.enums.Backup.State`
:returns: an enum describing the state of the backup
"""
return self._state

@property
def referencing_databases(self):
"""List of databases referencing this backup.
:rtype: list of strings
:returns: a list of database path strings which specify the databases still
referencing this backup
"""
return self._referencing_databases

@classmethod
def from_pb(cls, backup_pb, instance):
"""Create an instance of this class from a protobuf message.
:type backup_pb: :class:`~google.spanner.admin.database.v1.Backup`
:param backup_pb: A backup protobuf object.
:type instance: :class:`~google.cloud.spanner_v1.instance.Instance`
:param instance: The instance that owns the backup.
:rtype: :class:`Backup`
:returns: The backup parsed from the protobuf response.
:raises ValueError:
if the backup name does not match the expected format or if
the parsed project ID does not match the project ID on the
instance's client, or if the parsed instance ID does not match
the instance's ID.
"""
match = _BACKUP_NAME_RE.match(backup_pb.name)
if match is None:
raise ValueError(
"Backup protobuf name was not in the expected format.", backup_pb.name
)
if match.group("project") != instance._client.project:
raise ValueError(
"Project ID on backup does not match the project ID"
"on the instance's client"
)
instance_id = match.group("instance_id")
if instance_id != instance.instance_id:
raise ValueError(
"Instance ID on database does not match the instance ID"
"on the instance"
)
backup_id = match.group("backup_id")
return cls(backup_id, instance)

def create(self):
"""Create this backup within its instance.
:rtype: :class:`~google.api_core.operation.Operation`
:returns: a future used to poll the status of the create request
:raises Conflict: if the backup already exists
:raises NotFound: if the instance owning the backup does not exist
:raises BadRequest: if the database or expire_time values are invalid
or expire_time is not set
"""
if not self._expire_time:
raise ValueError("expire_time not set")
if not self._database:
raise ValueError("database not set")
api = self._instance._client.database_admin_api
metadata = _metadata_with_prefix(self.name)
backup = {
"database": self._database,
"expire_time": _datetime_to_pb_timestamp(self.expire_time),
}

future = api.create_backup(
self._instance.name, self.backup_id, backup, metadata=metadata
)
return future

def exists(self):
"""Test whether this backup exists.
:rtype: bool
:returns: True if the backup exists, else False.
"""
api = self._instance._client.database_admin_api
metadata = _metadata_with_prefix(self.name)

try:
api.get_backup(self.name, metadata=metadata)
except NotFound:
return False
return True

def reload(self):
"""Reload this backup.
Refresh the stored backup properties.
:raises NotFound: if the backup does not exist
"""
api = self._instance._client.database_admin_api
metadata = _metadata_with_prefix(self.name)
pb = api.get_backup(self.name, metadata=metadata)
self._database = pb.database
self._expire_time = _pb_timestamp_to_datetime(pb.expire_time)
self._create_time = _pb_timestamp_to_datetime(pb.create_time)
self._size_bytes = pb.size_bytes
self._state = enums.Backup.State(pb.state)
self._referencing_databases = pb.referencing_databases

def update_expire_time(self, new_expire_time):
"""Update the expire time of this backup.
:type new_expire_time: :class:`datetime.datetime`
:param new_expire_time: the new expire time timestamp
"""
api = self._instance._client.database_admin_api
metadata = _metadata_with_prefix(self.name)
backup_update = {
"name": self.name,
"expire_time": _datetime_to_pb_timestamp(new_expire_time),
}
update_mask = {"paths": ["expire_time"]}
api.update_backup(backup_update, update_mask, metadata=metadata)
self._expire_time = new_expire_time

def is_ready(self):
"""Test whether this backup is ready for use.
:rtype: bool
:returns: True if the backup state is READY, else False.
"""
return self.state == enums.Backup.State.READY

def delete(self):
"""Delete this backup."""
api = self._instance._client.database_admin_api
metadata = _metadata_with_prefix(self.name)
api.delete_backup(self.name, metadata=metadata)


class BackupInfo(object):
def __init__(self, backup, create_time, source_database):
self.backup = backup
self.create_time = _pb_timestamp_to_datetime(create_time)
self.source_database = source_database

@classmethod
def from_pb(cls, pb):
return cls(pb.backup, pb.create_time, pb.source_database)

0 comments on commit 39288e7

Please sign in to comment.