Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added script that manages a set of rolling backups. #381

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions compute/backup/manage-backups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env python

# Copyright (C) 2016 Google Inc.
#
# 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.

"""Example of using the Compute Engine API to manage a set of rolling
disk snapshots.

./manage-backups.py --disk <disk1>,<zone1> \
--disk <disk2>,<zone2> \
--project <project>

"""

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every file needs this license header:

# Copyright (C) 2016 Google Inc.
#
# 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.

import argparse
import datetime
import json
import re
import sys

from googleapiclient import discovery
import iso8601
from oauth2client.client import GoogleCredentials
import pytz


DISK_ZONE_MAP = {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two newlines after this, please.


def list_snapshots(compute, project, filter=None, pageToken=None):
return compute.snapshots().list(project=project, pageToken=pageToken, filter=filter).execute()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linebreak after list(


def should_snapshot(items, minimum_delta):
"""Given a list of snapshot items, return True if a snapshot should be
taken, False if not.
"""
sorted_items = items[:]
sorted_items.sort(key=lambda x: x['creationTimestamp'])
sorted_items.reverse()

now = datetime.datetime.now(pytz.utc)
created = iso8601.parse_date(sorted_items[0]['creationTimestamp'])

if now > created + minimum_delta:
return True

return False


def deletable_items(items):
"""Given a list of snapshot items, return the snapshots than can be
deleted.
"""
_items = items[:]
_items.sort(key=lambda x: x['creationTimestamp'])
_items.reverse()

result = []
now = datetime.datetime.now(pytz.utc)
one_week = datetime.timedelta(days=7)
three_months = datetime.timedelta(weeks=13)
one_year = datetime.timedelta(weeks=52)
minimum_number = 1

# Strategy: look for a reason not to delete. If none found,
# add to list.

# Global reasons

if len(items) < minimum_number:
print("Fewer than {0} snapshots, not deleting any".format(minimum_number))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use single-quotes for strings throughout.

return result

# Item-specific reasons

for item in _items[1:]: #always skip newest snapshot

item_timestamp = iso8601.parse_date(item['creationTimestamp'])

if now - item_timestamp < one_week:
print("Snapshot '{0}' too new, not deleting.".format(item['name']))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't support py2.6, so you can use {} instead of {0}.

continue

if item_timestamp.weekday() == 1 and now - item_timestamp < three_months:
print("Snapshot '{0}' is weekly timestamp and too new, not deleting.".format(item['name']))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep lines less than 79 characters throughout

continue

if item_timestamp.day == 1 and now - item_timestamp < one_year:
print("Snapshot '{0}' is monthly timestamp and too new, not deleting.".format(item['name']))
continue

print("Adding snapshot '{0}' to the delete list".format(item['name']))
result.append(item)

return result

def create_snapshot(compute, project, disk, dry_run):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This newline is unnecessary.

now = datetime.datetime.now(pytz.utc)
name = "{0}-{1}" % (disk, now.strftime('%Y-%m-%d'))
zone = zone_from_disk(disk)
print("Creating snapshot '{0}' in zone '{1}'".format(disk, zone))
if not dry_run:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline above control statements.

result = compute.disks().createSnapshot(project=project, disk=disk, body={"name":name}, zone=zone).execute()

def delete_snapshots(compute, project, snapshots, dry_run):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two newlines between all functions throughout.

for snapshot in snapshots:
print("Deleting snapshot '{0}'".format(snapshot['name']))
if not dry_run:
result = compute.snapshots().delete(project=project, snapshot=snapshot['name']).execute()

def zone_from_disk(disk):
return DISK_ZONE_MAP[disk]

def update_snapshots(compute, project, disk, dry_run):

filter = "name eq {0}-[0-9]{{4}}-[0-9]{{2}}-[0-9]{{2}}".format(disk)
result = list_snapshots(compute, project, filter=filter)

if not result.has_key('items'):
print("Disk '{0}' has no snapshots. Possibly it's new or you have a typo.".format(disk))
snapshot_p = True
items_to_delete = []
else:
snapshot_p = should_snapshot(result['items'], datetime.timedelta(days=1))
items_to_delete = deletable_items(result['items'])

if snapshot_p:
create_snapshot(compute, project, disk, dry_run)

if len(items_to_delete):
delete_snapshots(compute, project, items_to_delete, dry_run)

def main(args):

disks = []
for diskzone in args.disk:
disk, zone = diskzone.split(',')
DISK_ZONE_MAP[disk] = zone
disks.append(disk)

credentials = GoogleCredentials.get_application_default()
compute = discovery.build('compute', 'v1', credentials=credentials)
project = args.project

for disk in disks:
update_snapshots(compute, project, disk, args.dry_run)

if __name__ == "__main__":

parser = argparse.ArgumentParser(
description=__doc__)
parser.add_argument('--dry-run', help="Show what actions would be run, but don't actually run them.",
action="store_true")
parser.add_argument('--project', help="GCE project.", required=True)
parser.add_argument('--disk', help="Disk and zone, comma-separated.",
action="append", required=True)

args = parser.parse_args()

for diskzone in args.disk:
try:
diskzone.index(',')
except ValueError:
print("Disk '{0}' has no comma. Should be <disk,zone>.".format(disk))
sys.exit(1)

main(args)
4 changes: 4 additions & 0 deletions compute/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
google-api-python-client==1.5.1
iso8601==0.1.11
rfc3339==5
pytz==2014.7