From dedf420b8aa7e7e64fa56eeda2d7aeb5b2a5fcd9 Mon Sep 17 00:00:00 2001 From: Gustavo Serra Scalet Date: Mon, 17 Sep 2018 12:29:51 -0300 Subject: [PATCH] Add gcp-pd-move python script --- configure.ac | 1 + doc/man/Makefile.am | 1 + heartbeat/Makefile.am | 1 + heartbeat/gcp-pd-move.in | 370 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+) create mode 100755 heartbeat/gcp-pd-move.in diff --git a/configure.ac b/configure.ac index 10f5314da..b7ffb99f3 100644 --- a/configure.ac +++ b/configure.ac @@ -958,6 +958,7 @@ AC_CONFIG_FILES([heartbeat/conntrackd], [chmod +x heartbeat/conntrackd]) AC_CONFIG_FILES([heartbeat/dnsupdate], [chmod +x heartbeat/dnsupdate]) AC_CONFIG_FILES([heartbeat/eDir88], [chmod +x heartbeat/eDir88]) AC_CONFIG_FILES([heartbeat/fio], [chmod +x heartbeat/fio]) +AC_CONFIG_FILES([heartbeat/gcp-pd-move], [chmod +x heartbeat/gcp-pd-move]) AC_CONFIG_FILES([heartbeat/gcp-vpc-move-ip], [chmod +x heartbeat/gcp-vpc-move-ip]) AC_CONFIG_FILES([heartbeat/gcp-vpc-move-vip], [chmod +x heartbeat/gcp-vpc-move-vip]) AC_CONFIG_FILES([heartbeat/gcp-vpc-move-route], [chmod +x heartbeat/gcp-vpc-move-route]) diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am index 0bef88740..0235c9af6 100644 --- a/doc/man/Makefile.am +++ b/doc/man/Makefile.am @@ -115,6 +115,7 @@ man_MANS = ocf_heartbeat_AoEtarget.7 \ ocf_heartbeat_fio.7 \ ocf_heartbeat_galera.7 \ ocf_heartbeat_garbd.7 \ + ocf_heartbeat_gcp-pd-move.7 \ ocf_heartbeat_gcp-vpc-move-ip.7 \ ocf_heartbeat_gcp-vpc-move-vip.7 \ ocf_heartbeat_gcp-vpc-move-route.7 \ diff --git a/heartbeat/Makefile.am b/heartbeat/Makefile.am index 993bff042..843186c98 100644 --- a/heartbeat/Makefile.am +++ b/heartbeat/Makefile.am @@ -111,6 +111,7 @@ ocf_SCRIPTS = AoEtarget \ fio \ galera \ garbd \ + gcp-pd-move \ gcp-vpc-move-ip \ gcp-vpc-move-vip \ gcp-vpc-move-route \ diff --git a/heartbeat/gcp-pd-move.in b/heartbeat/gcp-pd-move.in new file mode 100755 index 000000000..f9f6c3163 --- /dev/null +++ b/heartbeat/gcp-pd-move.in @@ -0,0 +1,370 @@ +#!@PYTHON@ -tt +# - *- coding: utf- 8 - *- +# +# --------------------------------------------------------------------- +# Copyright 2018 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. +# --------------------------------------------------------------------- +# Description: Google Cloud Platform - Disk attach +# --------------------------------------------------------------------- + +import json +import logging +import os +import re +import sys +import time + +OCF_FUNCTIONS_DIR = "%s/lib/heartbeat" % os.environ.get("OCF_ROOT") +sys.path.append(OCF_FUNCTIONS_DIR) + +import ocf + +try: + import googleapiclient.discovery +except ImportError: + pass + +if sys.version_info >= (3, 0): + # Python 3 imports. + import urllib.parse as urlparse + import urllib.request as urlrequest +else: + # Python 2 imports. + import urllib as urlparse + import urllib2 as urlrequest + + +CONN = None +PROJECT = None +ZONE = None +REGION = None +LIST_DISK_ATTACHED_INSTANCES = None +INSTANCE_NAME = None + +PARAMETERS = { + 'disk_name': None, + 'disk_scope': None, + 'disk_csek_file': None, + 'mode': None, + 'device_name': None, +} + +MANDATORY_PARAMETERS = ['disk_name', 'disk_scope'] + +METADATA_SERVER = 'http://metadata.google.internal/computeMetadata/v1/' +METADATA_HEADERS = {'Metadata-Flavor': 'Google'} +METADATA = ''' + + +1.0 + +Resource Agent that can attach or detach a regional/zonal disk on current GCP +instance. +Requirements : +- Disk has to be properly created as regional/zonal in order to be used +correctly. + +Attach/Detach a persistent disk on current GCP instance + + +The name of the GCP disk. +Disk name + + + +Disk scope +Network name + + + +Path to a Customer-Supplied Encryption Key (CSEK) key file +Customer-Supplied Encryption Key file + + + +Attachment mode (rw, ro) +Attachment mode + + + +An optional name that indicates the disk name the guest operating system will see. +Optional device name + + + + + + + + + +''' + + +def get_metadata(metadata_key, params=None, timeout=None): + """Performs a GET request with the metadata headers. + + Args: + metadata_key: string, the metadata to perform a GET request on. + params: dictionary, the query parameters in the GET request. + timeout: int, timeout in seconds for metadata requests. + + Returns: + HTTP response from the GET request. + + Raises: + urlerror.HTTPError: raises when the GET request fails. + """ + timeout = timeout or 60 + metadata_url = os.path.join(METADATA_SERVER, metadata_key) + params = urlparse.urlencode(params or {}) + url = '%s?%s' % (metadata_url, params) + request = urlrequest.Request(url, headers=METADATA_HEADERS) + request_opener = urlrequest.build_opener(urlrequest.ProxyHandler({})) + return request_opener.open(request, timeout=timeout * 1.1).read() + + +def populate_vars(): + global CONN + global INSTANCE_NAME + global PROJECT + global ZONE + global REGION + global LIST_DISK_ATTACHED_INSTANCES + + global PARAMETERS + + # Populate global vars + try: + CONN = googleapiclient.discovery.build('compute', 'v1') + except Exception as e: + logger.error('Couldn\'t connect with google api: ' + str(e)) + sys.exit(ocf.OCF_ERR_CONFIGURED) + + for param in PARAMETERS: + value = os.environ.get('OCF_RESKEY_%s' % param, None) + if not value and param in MANDATORY_PARAMETERS: + logger.error('Missing %s mandatory parameter' % param) + sys.exit(ocf.OCF_ERR_CONFIGURED) + PARAMETERS[param] = value + + try: + INSTANCE_NAME = get_metadata('instance/name') + except Exception as e: + logger.error( + 'Couldn\'t get instance name, is this running inside GCE?: ' + str(e)) + sys.exit(ocf.OCF_ERR_CONFIGURED) + + PROJECT = get_metadata('project/project-id') + ZONE = get_metadata('instance/zone').split('/')[-1] + REGION = ZONE[:-2] + LIST_DISK_ATTACHED_INSTANCES = get_disk_attached_instances( + PARAMETERS['disk_name']) + + +def configure_logs(): + # Prepare logging + global logger + logging.getLogger('googleapiclient').setLevel(logging.WARN) + logging_env = os.environ.get('OCF_RESKEY_stackdriver_logging') + if logging_env: + logging_env = logging_env.lower() + if any(x in logging_env for x in ['yes', 'true', 'enabled']): + try: + import google.cloud.logging.handlers + client = google.cloud.logging.Client() + handler = google.cloud.logging.handlers.CloudLoggingHandler( + client, name=INSTANCE_NAME) + handler.setLevel(logging.INFO) + formatter = logging.Formatter('gcp:alias "%(message)s"') + handler.setFormatter(formatter) + ocf.log.addHandler(handler) + logger = logging.LoggerAdapter( + ocf.log, {'OCF_RESOURCE_INSTANCE': ocf.OCF_RESOURCE_INSTANCE}) + except ImportError: + logger.error('Couldn\'t import google.cloud.logging, ' + 'disabling Stackdriver-logging support') + + +def wait_for_operation(operation): + while True: + result = CONN.zoneOperations().get( + project=PROJECT, + zone=ZONE, + operation=operation['name']).execute() + + if result['status'] == 'DONE': + if 'error' in result: + raise Exception(result['error']) + return + time.sleep(1) + + +def get_disk_attached_instances(disk): + def get_users_list(): + fl = 'name="%s"' % disk + request = CONN.disks().aggregatedList(project=PROJECT, filter=fl) + while request is not None: + response = request.execute() + locations = response.get('items', {}) + for location in locations.values(): + for d in location.get('disks', []): + if d['name'] == disk: + return d.get('users', []) + request = CONN.instances().aggregatedList_next( + previous_request=request, previous_response=response) + raise Exception("Unable to find disk %s" % disk) + + def get_only_instance_name(user): + return re.sub('.*/instances/', '', user) + + return map(get_only_instance_name, get_users_list()) + + +def is_disk_attached(instance): + return instance in LIST_DISK_ATTACHED_INSTANCES + + +def detach_disk(instance, disk_name): + # Python API misses disk-scope argument. + + # Detaching a disk is only possible by using deviceName, which is retrieved + # as a disk parameter when listing the instance information + request = CONN.instances().get( + project=PROJECT, zone=ZONE, instance=instance) + response = request.execute() + + device_name = None + for disk in response['disks']: + if disk_name in disk['source']: + device_name = disk['deviceName'] + break + + if not device_name: + logger.error("Didn't find %(d)s deviceName attached to %(i)s" % { + 'd': disk_name, + 'i': instance, + }) + return + + request = CONN.instances().detachDisk( + project=PROJECT, zone=ZONE, instance=instance, deviceName=device_name) + wait_for_operation(request.execute()) + + +def attach_disk(instance, disk_name): + location = 'zones/%s' % ZONE + if PARAMETERS['disk_scope'] == 'regional': + location = 'regions/%s' % REGION + prefix = 'https://www.googleapis.com/compute/v1' + body = { + 'source': '%(prefix)s/projects/%(project)s/%(location)s/disks/%(disk)s' % { + 'prefix': prefix, + 'project': PROJECT, + 'location': location, + 'disk': disk_name, + }, + } + + # Customer-Supplied Encryption Key (CSEK) + if PARAMETERS['disk_csek_file']: + with open(PARAMETERS['disk_csek_file']) as csek_file: + body['diskEncryptionKey'] = { + 'rawKey': csek_file.read(), + } + + if PARAMETERS['device_name']: + body['deviceName'] = PARAMETERS['device_name'] + + if PARAMETERS['mode']: + body['mode'] = PARAMETERS['mode'] + + force_attach = None + if PARAMETERS['disk_scope'] == 'regional': + # Python API misses disk-scope argument. + force_attach = True + else: + # If this disk is attached to some instance, detach it first. + for other_instance in LIST_DISK_ATTACHED_INSTANCES: + logger.info("Detaching disk %(disk_name)s from other instance %(i)s" % { + 'disk_name': PARAMETERS['disk_name'], + 'i': other_instance, + }) + detach_disk(other_instance, PARAMETERS['disk_name']) + + request = CONN.instances().attachDisk( + project=PROJECT, zone=ZONE, instance=instance, body=body, + forceAttach=force_attach) + wait_for_operation(request.execute()) + + +def fetch_data(): + configure_logs() + populate_vars() + + +def gcp_pd_move_start(): + fetch_data() + if not is_disk_attached(INSTANCE_NAME): + logger.info("Attaching disk %(disk_name)s to %(instance)s" % { + 'disk_name': PARAMETERS['disk_name'], + 'instance': INSTANCE_NAME, + }) + attach_disk(INSTANCE_NAME, PARAMETERS['disk_name']) + + +def gcp_pd_move_stop(): + fetch_data() + if is_disk_attached(INSTANCE_NAME): + logger.info("Detaching disk %(disk_name)s to %(instance)s" % { + 'disk_name': PARAMETERS['disk_name'], + 'instance': INSTANCE_NAME, + }) + detach_disk(INSTANCE_NAME, PARAMETERS['disk_name']) + + +def gcp_pd_move_status(): + fetch_data() + if is_disk_attached(INSTANCE_NAME): + logger.info("Disk %(disk_name)s is correctly attached to %(instance)s" % { + 'disk_name': PARAMETERS['disk_name'], + 'instance': INSTANCE_NAME, + }) + else: + sys.exit(ocf.OCF_NOT_RUNNING) + + +def main(): + if len(sys.argv) < 2: + logger.error('Missing argument') + return + + command = sys.argv[1] + if 'meta-data' in command: + print(METADATA) + return + + if command in 'start': + gcp_pd_move_start() + elif command in 'stop': + gcp_pd_move_stop() + elif command in ('monitor', 'status'): + gcp_pd_move_status() + else: + configure_logs() + logger.error('no such function %s' % str(command)) + + +if __name__ == "__main__": + main()