tools/gce-change-disktype/main.py (230 lines of code) (raw):

#!/usr/bin/env python3 # Copyright 2019 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 # # 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 logging import re import sys import time import googleapiclient import googleapiclient.discovery DISK_REGEXP = r'^https:\/\/www\.googleapis\.com\/compute\/v1\/projects\/(.*?)\/zones\/(.*?)\/disks\/(.*?)$' def main(): parser = argparse.ArgumentParser( description= '''Update disks attached to a GCE instance to customer supplied disk-type ''' ) parser.add_argument('--project', required=True, dest='project', action='store', type=str, help='Project containing the GCE instance.') parser.add_argument('--zone', required=True, dest='zone', action='store', type=str, help='Zone containing the GCE instance.') parser.add_argument('--instance', required=True, dest='instance', action='store', type=str, help='Instance name.') parser.add_argument('--disktype', required=True, dest='disktype', action='store', type=str, help='New disk that will replace the old one') parser.add_argument( '--destructive', dest='destructive', action='store_const', const=True, default=False, help= 'Upon completion, delete source disks and snapshots created during migration process.' ) args = parser.parse_args() create_newdisk_process(args.project, args.zone, args.instance, args.disktype, args.destructive) def create_newdisk_process(project, zone, instance, disktype, destructive): start = time.time() region = zone.rpartition("-")[0] compute = googleapiclient.discovery.build('compute', 'v1') stop_instance(compute, project, zone, instance) disks = get_instance_disks(compute, project, zone, instance) disktype = 'https://www.googleapis.com/compute/v1/projects/{0}/zones/{1}/diskTypes/'.format( project, zone) + disktype for source_disk in disks: disk_url = source_disk['source'] boot = source_disk['boot'] auto_delete = source_disk['autoDelete'] deviceName = source_disk['deviceName'][0:46] existing_disk_name = re.search(DISK_REGEXP, disk_url).group(3) snapshot_name = '{}-update-disk-{}'.format( existing_disk_name[0:39], int(datetime.datetime.now().timestamp())) new_disk_name = '{}-disk-{}'.format( existing_disk_name[0:46], int(datetime.datetime.now().timestamp())) create_snapshot(compute, project, zone, existing_disk_name, snapshot_name) create_disk(compute, project, region, zone, snapshot_name, new_disk_name, disktype) detach_disk(compute, project, zone, instance, deviceName) attach_disk(compute, project, zone, instance, new_disk_name, boot, auto_delete, deviceName) if destructive: delete_disk(compute, project, zone, existing_disk_name) delete_snapshot(compute, project, snapshot_name) start_instance(compute, project, zone, instance) end = time.time() logging.info('Migration took %s seconds.', end - start) def get_disk_type(compute, project, zone, disk_name): logging.debug('Getting project=%s, zone=%s, disk_name=%s metadata', project, zone, disk_name) result = compute.disks().get(project=project, zone=zone, disk=disk_name).execute() logging.debug( 'Getting project=%s, zone=%s, disk_name=%s metadata complete.', project, zone, disk_name) return result['type'] def get_instance_disks(compute, project, zone, instance): logging.debug('Getting project=%s, zone=%s, instance=%s disks', project, zone, instance) result = compute.instances().get(project=project, zone=zone, instance=instance).execute() logging.debug('Getting project=%s, zone=%s, instance=%s disks complete.', project, zone, instance) return result['disks'] def create_snapshot(compute, project, zone, disk, snapshot_name): body = { 'name': snapshot_name, } logging.debug('Creating snapshot of disk project=%s, zone=%s, disk=%s', project, zone, disk) operation = compute.disks().createSnapshot(project=project, zone=zone, disk=disk, body=body).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug('Snapshotting of disk project=%s, zone=%s, disk=%s complete.', project, zone, disk) return result def delete_snapshot(compute, project, snapshot_name): logging.debug('Deleting snapshot project=%s, snapshot_name=%s', project, snapshot_name) operation = compute.snapshots().delete(project=project, snapshot=snapshot_name).execute() result = wait_for_global_operation(compute, project, operation) logging.debug('Deleting snapshot project=%s, snapshot_name=%s complete.', project, snapshot_name) return result def attach_disk(compute, project, zone, instance, disk, boot, auto_delete, deviceName): """ Attaches disk to instance. Requries iam.serviceAccountUser """ disk_url = 'projects/{0}/zones/{1}/disks/{2}'.format(project, zone, disk) body = { 'autoDelete': auto_delete, 'boot': boot, 'deviceName': deviceName, 'source': disk_url, } logging.debug('Attaching disk project=%s, zone=%s, instance=%s, disk=%s', project, zone, instance, disk_url) operation = compute.instances().attachDisk(project=project, zone=zone, instance=instance, body=body).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug( 'Attaching disk project=%s, zone=%s, instance=%s, disk=%s complete.', project, zone, instance, disk_url) return result def detach_disk(compute, project, zone, instance, disk): logging.debug('Detaching disk project=%s, zone=%s, instance=%s, disk=%s', project, zone, instance, disk) operation = compute.instances().detachDisk(project=project, zone=zone, instance=instance, deviceName=disk).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug( 'Detaching disk project=%s, zone=%s, instance=%s, disk=%s complete.', project, zone, instance, disk) return result def delete_disk(compute, project, zone, disk): logging.debug('Deleting disk project=%s, zone=%s, disk=%s', project, zone, disk) operation = compute.disks().delete(project=project, zone=zone, disk=disk).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug('Deleting disk project=%s, zone=%s, disk=%s complete.', project, zone, disk) return result def create_disk(compute, project, region, zone, snapshot_name, disk_name, disk_type): """Creates a new user supplied disk-type from snapshot""" source_snapshot = 'projects/{0}/global/snapshots/{1}'.format( project, snapshot_name) body = { 'name': disk_name, 'sourceSnapshot': source_snapshot, 'type': disk_type } logging.debug( 'Creating new disk project=%s, zone=%s, name=%s source_snapshot=%s, kmsKeyName=%s', project, zone, disk_name, source_snapshot) operation = compute.disks().insert(project=project, zone=zone, body=body).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug( 'Creating new disk project=%s, zone=%s, name=%s source_snapshot=%s, kmsKeyName=%s complete.', project, zone, disk_name, source_snapshot) return result def start_instance(compute, project, zone, instance): logging.debug('Starting project=%s, zone=%s, instance=%s', project, zone, instance) operation = compute.instances().start(project=project, zone=zone, instance=instance).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug('Starting project=%s, zone=%s, instance=%s complete.', project, zone, instance) return result def stop_instance(compute, project, zone, instance): logging.debug('Stopping project=%s, zone=%s, instance=%s', project, zone, instance) operation = compute.instances().stop(project=project, zone=zone, instance=instance).execute() result = wait_for_zonal_operation(compute, project, zone, operation) logging.debug('Stopping project=%s, zone=%s, instance=%s complete.', project, zone, instance) return result def wait_for_global_operation(compute, project, operation): """Helper for waiting for global operation to complete.""" operation = operation['name'] def build(): return compute.globalOperations().get(project=project, operation=operation) return _wait_for_operation(operation, build) def wait_for_zonal_operation(compute, project, zone, operation): """Helper for waiting for zonal operation to complete.""" operation = operation['name'] def build(): return compute.zoneOperations().get(project=project, zone=zone, operation=operation) return _wait_for_operation(operation, build) def _wait_for_operation(operation, build_request): """Helper for waiting for operation to complete.""" logging.debug('Waiting for %s', operation) while True: sys.stdout.flush() result = build_request().execute() if result['status'] == 'DONE': logging.debug('done!') if 'error' in result: logging.error('finished with an error') logging.error('Error %s', result['error']) raise Exception(result['error']) return result time.sleep(5) if __name__ == '__main__': main()