Skip to content

Commit 39288e7

Browse files
larkeeskuruppu
andauthored
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>
1 parent a48c4b1 commit 39288e7

File tree

6 files changed

+1725
-1
lines changed

6 files changed

+1725
-1
lines changed

google/cloud/spanner_v1/backup.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# Copyright 2020 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""User friendly container for Cloud Spanner Backup."""
16+
17+
import re
18+
19+
from google.cloud._helpers import _datetime_to_pb_timestamp, _pb_timestamp_to_datetime
20+
from google.cloud.exceptions import NotFound
21+
22+
from google.cloud.spanner_admin_database_v1.gapic import enums
23+
from google.cloud.spanner_v1._helpers import _metadata_with_prefix
24+
25+
_BACKUP_NAME_RE = re.compile(
26+
r"^projects/(?P<project>[^/]+)/"
27+
r"instances/(?P<instance_id>[a-z][-a-z0-9]*)/"
28+
r"backups/(?P<backup_id>[a-z][a-z0-9_\-]*[a-z0-9])$"
29+
)
30+
31+
32+
class Backup(object):
33+
"""Representation of a Cloud Spanner Backup.
34+
35+
We can use a :class`Backup` to:
36+
37+
* :meth:`create` the backup
38+
* :meth:`update` the backup
39+
* :meth:`delete` the backup
40+
41+
:type backup_id: str
42+
:param backup_id: The ID of the backup.
43+
44+
:type instance: :class:`~google.cloud.spanner_v1.instance.Instance`
45+
:param instance: The instance that owns the backup.
46+
47+
:type database: str
48+
:param database: (Optional) The URI of the database that the backup is
49+
for. Required if the create method needs to be called.
50+
51+
:type expire_time: :class:`datetime.datetime`
52+
:param expire_time: (Optional) The expire time that will be used to
53+
create the backup. Required if the create method
54+
needs to be called.
55+
"""
56+
57+
def __init__(self, backup_id, instance, database="", expire_time=None):
58+
self.backup_id = backup_id
59+
self._instance = instance
60+
self._database = database
61+
self._expire_time = expire_time
62+
self._create_time = None
63+
self._size_bytes = None
64+
self._state = None
65+
self._referencing_databases = None
66+
67+
@property
68+
def name(self):
69+
"""Backup name used in requests.
70+
71+
The backup name is of the form
72+
73+
``"projects/../instances/../backups/{backup_id}"``
74+
75+
:rtype: str
76+
:returns: The backup name.
77+
"""
78+
return self._instance.name + "/backups/" + self.backup_id
79+
80+
@property
81+
def database(self):
82+
"""Database name used in requests.
83+
84+
The database name is of the form
85+
86+
``"projects/../instances/../backups/{backup_id}"``
87+
88+
:rtype: str
89+
:returns: The database name.
90+
"""
91+
return self._database
92+
93+
@property
94+
def expire_time(self):
95+
"""Expire time used in creation requests.
96+
97+
:rtype: :class:`datetime.datetime`
98+
:returns: a datetime object representing the expire time of
99+
this backup
100+
"""
101+
return self._expire_time
102+
103+
@property
104+
def create_time(self):
105+
"""Create time of this backup.
106+
107+
:rtype: :class:`datetime.datetime`
108+
:returns: a datetime object representing the create time of
109+
this backup
110+
"""
111+
return self._create_time
112+
113+
@property
114+
def size_bytes(self):
115+
"""Size of this backup in bytes.
116+
117+
:rtype: int
118+
:returns: the number size of this backup measured in bytes
119+
"""
120+
return self._size_bytes
121+
122+
@property
123+
def state(self):
124+
"""State of this backup.
125+
126+
:rtype: :class:`~google.cloud.spanner_admin_database_v1.gapic.enums.Backup.State`
127+
:returns: an enum describing the state of the backup
128+
"""
129+
return self._state
130+
131+
@property
132+
def referencing_databases(self):
133+
"""List of databases referencing this backup.
134+
135+
:rtype: list of strings
136+
:returns: a list of database path strings which specify the databases still
137+
referencing this backup
138+
"""
139+
return self._referencing_databases
140+
141+
@classmethod
142+
def from_pb(cls, backup_pb, instance):
143+
"""Create an instance of this class from a protobuf message.
144+
145+
:type backup_pb: :class:`~google.spanner.admin.database.v1.Backup`
146+
:param backup_pb: A backup protobuf object.
147+
148+
:type instance: :class:`~google.cloud.spanner_v1.instance.Instance`
149+
:param instance: The instance that owns the backup.
150+
151+
:rtype: :class:`Backup`
152+
:returns: The backup parsed from the protobuf response.
153+
:raises ValueError:
154+
if the backup name does not match the expected format or if
155+
the parsed project ID does not match the project ID on the
156+
instance's client, or if the parsed instance ID does not match
157+
the instance's ID.
158+
"""
159+
match = _BACKUP_NAME_RE.match(backup_pb.name)
160+
if match is None:
161+
raise ValueError(
162+
"Backup protobuf name was not in the expected format.", backup_pb.name
163+
)
164+
if match.group("project") != instance._client.project:
165+
raise ValueError(
166+
"Project ID on backup does not match the project ID"
167+
"on the instance's client"
168+
)
169+
instance_id = match.group("instance_id")
170+
if instance_id != instance.instance_id:
171+
raise ValueError(
172+
"Instance ID on database does not match the instance ID"
173+
"on the instance"
174+
)
175+
backup_id = match.group("backup_id")
176+
return cls(backup_id, instance)
177+
178+
def create(self):
179+
"""Create this backup within its instance.
180+
181+
:rtype: :class:`~google.api_core.operation.Operation`
182+
:returns: a future used to poll the status of the create request
183+
:raises Conflict: if the backup already exists
184+
:raises NotFound: if the instance owning the backup does not exist
185+
:raises BadRequest: if the database or expire_time values are invalid
186+
or expire_time is not set
187+
"""
188+
if not self._expire_time:
189+
raise ValueError("expire_time not set")
190+
if not self._database:
191+
raise ValueError("database not set")
192+
api = self._instance._client.database_admin_api
193+
metadata = _metadata_with_prefix(self.name)
194+
backup = {
195+
"database": self._database,
196+
"expire_time": _datetime_to_pb_timestamp(self.expire_time),
197+
}
198+
199+
future = api.create_backup(
200+
self._instance.name, self.backup_id, backup, metadata=metadata
201+
)
202+
return future
203+
204+
def exists(self):
205+
"""Test whether this backup exists.
206+
207+
:rtype: bool
208+
:returns: True if the backup exists, else False.
209+
"""
210+
api = self._instance._client.database_admin_api
211+
metadata = _metadata_with_prefix(self.name)
212+
213+
try:
214+
api.get_backup(self.name, metadata=metadata)
215+
except NotFound:
216+
return False
217+
return True
218+
219+
def reload(self):
220+
"""Reload this backup.
221+
222+
Refresh the stored backup properties.
223+
224+
:raises NotFound: if the backup does not exist
225+
"""
226+
api = self._instance._client.database_admin_api
227+
metadata = _metadata_with_prefix(self.name)
228+
pb = api.get_backup(self.name, metadata=metadata)
229+
self._database = pb.database
230+
self._expire_time = _pb_timestamp_to_datetime(pb.expire_time)
231+
self._create_time = _pb_timestamp_to_datetime(pb.create_time)
232+
self._size_bytes = pb.size_bytes
233+
self._state = enums.Backup.State(pb.state)
234+
self._referencing_databases = pb.referencing_databases
235+
236+
def update_expire_time(self, new_expire_time):
237+
"""Update the expire time of this backup.
238+
239+
:type new_expire_time: :class:`datetime.datetime`
240+
:param new_expire_time: the new expire time timestamp
241+
"""
242+
api = self._instance._client.database_admin_api
243+
metadata = _metadata_with_prefix(self.name)
244+
backup_update = {
245+
"name": self.name,
246+
"expire_time": _datetime_to_pb_timestamp(new_expire_time),
247+
}
248+
update_mask = {"paths": ["expire_time"]}
249+
api.update_backup(backup_update, update_mask, metadata=metadata)
250+
self._expire_time = new_expire_time
251+
252+
def is_ready(self):
253+
"""Test whether this backup is ready for use.
254+
255+
:rtype: bool
256+
:returns: True if the backup state is READY, else False.
257+
"""
258+
return self.state == enums.Backup.State.READY
259+
260+
def delete(self):
261+
"""Delete this backup."""
262+
api = self._instance._client.database_admin_api
263+
metadata = _metadata_with_prefix(self.name)
264+
api.delete_backup(self.name, metadata=metadata)
265+
266+
267+
class BackupInfo(object):
268+
def __init__(self, backup, create_time, source_database):
269+
self.backup = backup
270+
self.create_time = _pb_timestamp_to_datetime(create_time)
271+
self.source_database = source_database
272+
273+
@classmethod
274+
def from_pb(cls, pb):
275+
return cls(pb.backup, pb.create_time, pb.source_database)

0 commit comments

Comments
 (0)