tools/gce-google-keys-to-cmek/main.py (251 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='''Convert disks attached to a GCE instance from Google-managed encryption keys to customer-managed encryption keys or from a customer-managed encryption key to an other.''' ) 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( '--key-ring', required=True, dest='key_ring', action='store', type=str, help='Name of the key ring containing the key to encrypt the disks. Must be in the same region as the instance or global.' ) parser.add_argument( '--key-name', required=True, dest='key_name', action='store', type=str, help='Name of the key to encrypt the disks. Must be in the same region as the instance or global.' ) parser.add_argument( '--key-version', required=True, dest='key_version', action='store', type=int, help='Version of the key to encrypt the disks.') parser.add_argument( '--key-global', dest='key_global', action='store_true', default=False, help='Use Cloud KMS global keys.') 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() migrate_instance_to_cmek(args.project, args.zone, args.instance, args.key_ring, args.key_name, args.key_version, args.key_global, args.destructive) def migrate_instance_to_cmek(project, zone, instance, key_ring, key_name, key_version, key_global, destructive): start = time.time() region = zone.rpartition("-")[0] key_region = "global" if key_global else region key_name = 'projects/{0}/locations/{1}/keyRings/{2}/cryptoKeys/{3}/cryptoKeyVersions/{4}'.format( project, key_region, key_ring, key_name, key_version) compute = googleapiclient.discovery.build('compute', 'v1') stop_instance(compute, project, zone, instance) disks = get_instance_disks(compute, project, zone, instance) 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) if 'diskEncryptionKey' in source_disk: if source_disk['diskEncryptionKey']['kmsKeyName'] == key_name: logging.info('Skipping %s, already encrypyed with %s', existing_disk_name, source_disk['diskEncryptionKey']) continue snapshot_name = '{}-update-cmek-{}'.format(existing_disk_name[0:39],int(datetime.datetime.now().timestamp())) new_disk_name = '{}-cmek-{}'.format(existing_disk_name[0:46],int(datetime.datetime.now().timestamp())) disk_type = get_disk_type(compute, project, zone, existing_disk_name) create_snapshot(compute, project, zone, existing_disk_name, snapshot_name) create_disk(compute, project, region, zone, snapshot_name, new_disk_name, disk_type, key_name) 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, key_name): """Creates a new CMEK encrypted persistent disk from a snapshot""" source_snapshot = 'projects/{0}/global/snapshots/{1}'.format( project, snapshot_name) body = { 'name': disk_name, 'sourceSnapshot': source_snapshot, 'type': disk_type, 'diskEncryptionKey': { 'kmsKeyName': key_name, }, } logging.debug( 'Creating new disk project=%s, zone=%s, name=%s source_snapshot=%s, kmsKeyName=%s', project, zone, disk_name, source_snapshot, key_name) 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, key_name) 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): 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): 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()