426 lines
14 KiB
Diff
426 lines
14 KiB
Diff
|
From dedf420b8aa7e7e64fa56eeda2d7aeb5b2a5fcd9 Mon Sep 17 00:00:00 2001
|
||
|
From: Gustavo Serra Scalet <gustavo.scalet@collabora.com>
|
||
|
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 = '''<?xml version="1.0"?>
|
||
|
+<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
|
||
|
+<resource-agent name="gcp-pd-move">
|
||
|
+<version>1.0</version>
|
||
|
+<longdesc lang="en">
|
||
|
+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.
|
||
|
+</longdesc>
|
||
|
+<shortdesc lang="en">Attach/Detach a persistent disk on current GCP instance</shortdesc>
|
||
|
+<parameters>
|
||
|
+<parameter name="disk_name" unique="1" required="1">
|
||
|
+<longdesc lang="en">The name of the GCP disk.</longdesc>
|
||
|
+<shortdesc lang="en">Disk name</shortdesc>
|
||
|
+<content type="string" default="" />
|
||
|
+</parameter>
|
||
|
+<parameter name="disk_scope" unique="1" required="1">
|
||
|
+<longdesc lang="en">Disk scope </longdesc>
|
||
|
+<shortdesc lang="en">Network name</shortdesc>
|
||
|
+<content type="string" default="regional" />
|
||
|
+</parameter>
|
||
|
+<parameter name="disk_csek_file" unique="1" required="0">
|
||
|
+<longdesc lang="en">Path to a Customer-Supplied Encryption Key (CSEK) key file</longdesc>
|
||
|
+<shortdesc lang="en">Customer-Supplied Encryption Key file</shortdesc>
|
||
|
+<content type="string" default="" />
|
||
|
+</parameter>
|
||
|
+<parameter name="mode" unique="1" required="0">
|
||
|
+<longdesc lang="en">Attachment mode (rw, ro)</longdesc>
|
||
|
+<shortdesc lang="en">Attachment mode</shortdesc>
|
||
|
+<content type="string" default="rw" />
|
||
|
+</parameter>
|
||
|
+<parameter name="device_name" unique="0" required="0">
|
||
|
+<longdesc lang="en">An optional name that indicates the disk name the guest operating system will see.</longdesc>
|
||
|
+<shortdesc lang="en">Optional device name</shortdesc>
|
||
|
+<content type="boolean" default="" />
|
||
|
+</parameter>
|
||
|
+</parameters>
|
||
|
+<actions>
|
||
|
+<action name="start" timeout="300s" />
|
||
|
+<action name="stop" timeout="15s" />
|
||
|
+<action name="monitor" timeout="15s" interval="10s" depth="0" />
|
||
|
+<action name="meta-data" timeout="5s" />
|
||
|
+</actions>
|
||
|
+</resource-agent>'''
|
||
|
+
|
||
|
+
|
||
|
+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()
|