Skip to content

Commit 88cb861

Browse files
larkeeskuruppu
andauthored
spanner: Add Cloud Spanner Backup samples (#3101)
* add backup samples * update required spanner version * fix lint errors * run backup samples tests against a new instance * fix lint * wait for instance creation to complete * Apply suggestions from code review Co-Authored-By: skuruppu <skuruppu@google.com> * add list_backups test * fix lint * add missing newline character in assert * update samples to be consistent with other languages * lint fix * add pagination sample * reorder tests Co-authored-by: larkee <larkee@users.noreply.github.com> Co-authored-by: skuruppu <skuruppu@google.com>
1 parent 38ddad9 commit 88cb861

File tree

2 files changed

+397
-0
lines changed

2 files changed

+397
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Copyright 2020 Google Inc. 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+
"""This application demonstrates how to create and restore from backups
16+
using Cloud Spanner.
17+
18+
For more information, see the README.rst under /spanner.
19+
"""
20+
21+
import argparse
22+
from datetime import (
23+
datetime,
24+
timedelta
25+
)
26+
import time
27+
28+
from google.cloud import spanner
29+
30+
31+
# [START spanner_create_backup]
32+
def create_backup(instance_id, database_id, backup_id):
33+
"""Creates a backup for a database."""
34+
spanner_client = spanner.Client()
35+
instance = spanner_client.instance(instance_id)
36+
database = instance.database(database_id)
37+
38+
# Create a backup
39+
expire_time = datetime.utcnow() + timedelta(days=14)
40+
backup = instance.backup(
41+
backup_id, database=database, expire_time=expire_time)
42+
operation = backup.create()
43+
44+
# Wait for backup operation to complete.
45+
operation.result()
46+
47+
# Verify that the backup is ready.
48+
backup.reload()
49+
assert backup.is_ready() is True
50+
51+
# Get the name, create time and backup size.
52+
backup.reload()
53+
print("Backup {} of size {} bytes was created at {}".format(
54+
backup.name, backup.size_bytes, backup.create_time))
55+
# [END spanner_create_backup]
56+
57+
58+
# [START spanner_restore_database]
59+
def restore_database(instance_id, new_database_id, backup_id):
60+
"""Restores a database from a backup."""
61+
spanner_client = spanner.Client()
62+
instance = spanner_client.instance(instance_id)
63+
# Create a backup on database_id.
64+
65+
# Start restoring backup to a new database.
66+
backup = instance.backup(backup_id)
67+
new_database = instance.database(new_database_id)
68+
operation = new_database.restore(backup)
69+
70+
# Wait for restore operation to complete.
71+
operation.result()
72+
73+
# Newly created database has restore information.
74+
new_database.reload()
75+
restore_info = new_database.restore_info
76+
print("Database {} restored to {} from backup {}.".format(
77+
restore_info.backup_info.source_database,
78+
new_database_id,
79+
restore_info.backup_info.backup))
80+
# [END spanner_restore_database]
81+
82+
83+
# [START spanner_cancel_backup]
84+
def cancel_backup(instance_id, database_id, backup_id):
85+
spanner_client = spanner.Client()
86+
instance = spanner_client.instance(instance_id)
87+
database = instance.database(database_id)
88+
89+
expire_time = datetime.utcnow() + timedelta(days=30)
90+
91+
# Create a backup.
92+
backup = instance.backup(
93+
backup_id, database=database, expire_time=expire_time)
94+
operation = backup.create()
95+
96+
# Cancel backup creation.
97+
operation.cancel()
98+
99+
# Cancel operations are best effort so either it will complete or
100+
# be cancelled.
101+
while not operation.done():
102+
time.sleep(300) # 5 mins
103+
104+
# Deal with resource if the operation succeeded.
105+
if backup.exists():
106+
print("Backup was created before the cancel completed.")
107+
backup.delete()
108+
print("Backup deleted.")
109+
else:
110+
print("Backup creation was successfully cancelled.")
111+
# [END spanner_cancel_backup]
112+
113+
114+
# [START spanner_list_backup_operations]
115+
def list_backup_operations(instance_id, database_id):
116+
spanner_client = spanner.Client()
117+
instance = spanner_client.instance(instance_id)
118+
119+
# List the CreateBackup operations.
120+
filter_ = (
121+
"(metadata.database:{}) AND "
122+
"(metadata.@type:type.googleapis.com/"
123+
"google.spanner.admin.database.v1.CreateBackupMetadata)"
124+
).format(database_id)
125+
operations = instance.list_backup_operations(filter_=filter_)
126+
for op in operations:
127+
metadata = op.metadata
128+
print("Backup {} on database {}: {}% complete.".format(
129+
metadata.name, metadata.database,
130+
metadata.progress.progress_percent))
131+
# [END spanner_list_backup_operations]
132+
133+
134+
# [START spanner_list_database_operations]
135+
def list_database_operations(instance_id):
136+
spanner_client = spanner.Client()
137+
instance = spanner_client.instance(instance_id)
138+
139+
# List the progress of restore.
140+
filter_ = (
141+
"(metadata.@type:type.googleapis.com/"
142+
"google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata)"
143+
)
144+
operations = instance.list_database_operations(filter_=filter_)
145+
for op in operations:
146+
print("Database {} restored from backup is {}% optimized.".format(
147+
op.metadata.name, op.metadata.progress.progress_percent))
148+
# [END spanner_list_database_operations]
149+
150+
151+
# [START spanner_list_backups]
152+
def list_backups(instance_id, database_id, backup_id):
153+
spanner_client = spanner.Client()
154+
instance = spanner_client.instance(instance_id)
155+
156+
# List all backups.
157+
print("All backups:")
158+
for backup in instance.list_backups():
159+
print(backup.name)
160+
161+
# List all backups that contain a name.
162+
print("All backups with backup name containing \"{}\":".format(backup_id))
163+
for backup in instance.list_backups(filter_="name:{}".format(backup_id)):
164+
print(backup.name)
165+
166+
# List all backups for a database that contains a name.
167+
print("All backups with database name containing \"{}\":".format(database_id))
168+
for backup in instance.list_backups(filter_="database:{}".format(database_id)):
169+
print(backup.name)
170+
171+
# List all backups that expire before a timestamp.
172+
expire_time = datetime.utcnow().replace(microsecond=0) + timedelta(days=30)
173+
print("All backups with expire_time before \"{}-{}-{}T{}:{}:{}Z\":".format(
174+
*expire_time.timetuple()))
175+
for backup in instance.list_backups(
176+
filter_="expire_time < \"{}-{}-{}T{}:{}:{}Z\"".format(
177+
*expire_time.timetuple())):
178+
print(backup.name)
179+
180+
# List all backups with a size greater than some bytes.
181+
print("All backups with backup size more than 100 bytes:")
182+
for backup in instance.list_backups(filter_="size_bytes > 100"):
183+
print(backup.name)
184+
185+
# List backups that were created after a timestamp that are also ready.
186+
create_time = datetime.utcnow().replace(microsecond=0) - timedelta(days=1)
187+
print("All backups created after \"{}-{}-{}T{}:{}:{}Z\" and are READY:".format(
188+
*create_time.timetuple()))
189+
for backup in instance.list_backups(
190+
filter_="create_time >= \"{}-{}-{}T{}:{}:{}Z\" AND state:READY".format(
191+
*create_time.timetuple())):
192+
print(backup.name)
193+
194+
print("All backups with pagination")
195+
for page in instance.list_backups(page_size=2).pages:
196+
for backup in page:
197+
print(backup.name)
198+
# [END spanner_list_backups]
199+
200+
201+
# [START spanner_delete_backup]
202+
def delete_backup(instance_id, backup_id):
203+
spanner_client = spanner.Client()
204+
instance = spanner_client.instance(instance_id)
205+
backup = instance.backup(backup_id)
206+
backup.reload()
207+
208+
# Wait for databases that reference this backup to finish optimizing.
209+
while backup.referencing_databases:
210+
time.sleep(30)
211+
backup.reload()
212+
213+
# Delete the backup.
214+
backup.delete()
215+
216+
# Verify that the backup is deleted.
217+
assert backup.exists() is False
218+
print("Backup {} has been deleted.".format(backup.name))
219+
# [END spanner_delete_backup]
220+
221+
222+
# [START spanner_update_backup]
223+
def update_backup(instance_id, backup_id):
224+
spanner_client = spanner.Client()
225+
instance = spanner_client.instance(instance_id)
226+
backup = instance.backup(backup_id)
227+
backup.reload()
228+
229+
# Expire time must be within 366 days of the create time of the backup.
230+
old_expire_time = backup.expire_time
231+
new_expire_time = old_expire_time + timedelta(days=30)
232+
backup.update_expire_time(new_expire_time)
233+
print("Backup {} expire time was updated from {} to {}.".format(
234+
backup.name, old_expire_time, new_expire_time))
235+
# [END spanner_update_backup]
236+
237+
238+
if __name__ == '__main__': # noqa: C901
239+
parser = argparse.ArgumentParser(
240+
description=__doc__,
241+
formatter_class=argparse.RawDescriptionHelpFormatter)
242+
parser.add_argument(
243+
'instance_id', help='Your Cloud Spanner instance ID.')
244+
parser.add_argument(
245+
'--database-id', help='Your Cloud Spanner database ID.',
246+
default='example_db')
247+
parser.add_argument(
248+
'--backup-id', help='Your Cloud Spanner backup ID.',
249+
default='example_backup')
250+
251+
subparsers = parser.add_subparsers(dest='command')
252+
subparsers.add_parser('create_backup', help=create_backup.__doc__)
253+
subparsers.add_parser('cancel_backup', help=cancel_backup.__doc__)
254+
subparsers.add_parser('update_backup', help=update_backup.__doc__)
255+
subparsers.add_parser('restore_database', help=restore_database.__doc__)
256+
subparsers.add_parser('list_backups', help=list_backups.__doc__)
257+
subparsers.add_parser('list_backup_operations', help=list_backup_operations.__doc__)
258+
subparsers.add_parser('list_database_operations',
259+
help=list_database_operations.__doc__)
260+
subparsers.add_parser('delete_backup', help=delete_backup.__doc__)
261+
262+
args = parser.parse_args()
263+
264+
if args.command == 'create_backup':
265+
create_backup(args.instance_id, args.database_id, args.backup_id)
266+
elif args.command == 'cancel_backup':
267+
cancel_backup(args.instance_id, args.database_id, args.backup_id)
268+
elif args.command == 'update_backup':
269+
update_backup(args.instance_id, args.backup_id)
270+
elif args.command == 'restore_database':
271+
restore_database(args.instance_id, args.database_id, args.backup_id)
272+
elif args.command == 'list_backups':
273+
list_backups(args.instance_id, args.database_id, args.backup_id)
274+
elif args.command == 'list_backup_operations':
275+
list_backup_operations(args.instance_id, args.database_id)
276+
elif args.command == 'list_database_operations':
277+
list_database_operations(args.instance_id)
278+
elif args.command == 'delete_backup':
279+
delete_backup(args.instance_id, args.backup_id)
280+
else:
281+
print("Command {} did not match expected commands.".format(args.command))
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Copyright 2020 Google Inc. 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+
from google.cloud import spanner
16+
import pytest
17+
import random
18+
import string
19+
20+
import backup_sample
21+
22+
23+
def unique_instance_id():
24+
""" Creates a unique id for the database. """
25+
return 'test-instance-{}'.format(''.join(random.choice(
26+
string.ascii_lowercase + string.digits) for _ in range(5)))
27+
28+
29+
def unique_database_id():
30+
""" Creates a unique id for the database. """
31+
return 'test-db-{}'.format(''.join(random.choice(
32+
string.ascii_lowercase + string.digits) for _ in range(5)))
33+
34+
35+
def unique_backup_id():
36+
""" Creates a unique id for the backup. """
37+
return 'test-backup-{}'.format(''.join(random.choice(
38+
string.ascii_lowercase + string.digits) for _ in range(5)))
39+
40+
41+
INSTANCE_ID = unique_instance_id()
42+
DATABASE_ID = unique_database_id()
43+
RESTORE_DB_ID = unique_database_id()
44+
BACKUP_ID = unique_backup_id()
45+
46+
47+
@pytest.fixture(scope='module')
48+
def spanner_instance():
49+
spanner_client = spanner.Client()
50+
instance_config = '{}/instanceConfigs/{}'.format(
51+
spanner_client.project_name, 'regional-us-central1')
52+
instance = spanner_client.instance(INSTANCE_ID, instance_config)
53+
op = instance.create()
54+
op.result(30) # block until completion
55+
yield instance
56+
instance.delete()
57+
58+
59+
@pytest.fixture(scope='module')
60+
def database(spanner_instance):
61+
""" Creates a temporary database that is removed after testing. """
62+
db = spanner_instance.database(DATABASE_ID)
63+
db.create()
64+
yield db
65+
db.drop()
66+
67+
68+
def test_create_backup(capsys, database):
69+
backup_sample.create_backup(INSTANCE_ID, DATABASE_ID, BACKUP_ID)
70+
out, _ = capsys.readouterr()
71+
assert BACKUP_ID in out
72+
73+
74+
def test_restore_database(capsys):
75+
backup_sample.restore_database(INSTANCE_ID, RESTORE_DB_ID, BACKUP_ID)
76+
out, _ = capsys.readouterr()
77+
assert (DATABASE_ID + " restored to ") in out
78+
assert (RESTORE_DB_ID + " from backup ") in out
79+
assert BACKUP_ID in out
80+
81+
82+
def test_list_backup_operations(capsys, spanner_instance):
83+
backup_sample.list_backup_operations(INSTANCE_ID, DATABASE_ID)
84+
out, _ = capsys.readouterr()
85+
assert BACKUP_ID in out
86+
assert DATABASE_ID in out
87+
88+
89+
def test_list_backups(capsys, spanner_instance):
90+
backup_sample.list_backups(INSTANCE_ID, DATABASE_ID, BACKUP_ID)
91+
out, _ = capsys.readouterr()
92+
id_count = out.count(BACKUP_ID)
93+
assert id_count == 7
94+
95+
96+
def test_update_backup(capsys):
97+
backup_sample.update_backup(INSTANCE_ID, BACKUP_ID)
98+
out, _ = capsys.readouterr()
99+
assert BACKUP_ID in out
100+
101+
102+
def test_delete_backup(capsys, spanner_instance):
103+
backup_sample.delete_backup(INSTANCE_ID, BACKUP_ID)
104+
out, _ = capsys.readouterr()
105+
assert BACKUP_ID in out
106+
107+
108+
def test_cancel_backup(capsys):
109+
backup_sample.cancel_backup(INSTANCE_ID, DATABASE_ID, BACKUP_ID)
110+
out, _ = capsys.readouterr()
111+
cancel_success = "Backup creation was successfully cancelled." in out
112+
cancel_failure = (
113+
("Backup was created before the cancel completed." in out) and
114+
("Backup deleted." in out)
115+
)
116+
assert cancel_success or cancel_failure

0 commit comments

Comments
 (0)