Blame SOURCES/bz1633251-gcp-pd-move-1.patch

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