Blame SOURCES/bz1846732-gcp-vpc-move-vip-support-multiple-alias-ips.patch

e72037
--- a/heartbeat/gcp-vpc-move-vip.in	2020-08-03 15:48:28.497845842 +0200
e72037
+++ b/heartbeat/gcp-vpc-move-vip.in	2020-08-03 15:49:24.129888908 +0200
e72037
@@ -22,7 +22,8 @@
e72037
 import sys
e72037
 import time
e72037
 
e72037
-OCF_FUNCTIONS_DIR="%s/lib/heartbeat" % os.environ.get("OCF_ROOT")
e72037
+OCF_FUNCTIONS_DIR = os.environ.get("OCF_FUNCTIONS_DIR", "%s/lib/heartbeat"
e72037
+                                   % os.environ.get("OCF_ROOT"))
e72037
 sys.path.append(OCF_FUNCTIONS_DIR)
e72037
 
e72037
 from ocf import *
e72037
@@ -43,6 +44,10 @@
e72037
   import urllib2 as urlrequest
e72037
 
e72037
 
e72037
+# Constants for alias add/remove modes
e72037
+ADD = 0
e72037
+REMOVE = 1
e72037
+
e72037
 CONN = None
e72037
 THIS_VM = None
e72037
 ALIAS = None
e72037
@@ -53,27 +58,27 @@
e72037
 
e72037
 <resource-agent name="gcp-vpc-move-vip">
e72037
   <version>1.0</version>
e72037
-  <longdesc lang="en">Floating IP Address on Google Cloud Platform - Using Alias IP address functionality to attach a secondary IP address to a running instance</longdesc>
e72037
-  <shortdesc lang="en">Floating IP Address on Google Cloud Platform</shortdesc>
e72037
+  <longdesc lang="en">Floating IP Address or Range on Google Cloud Platform - Using Alias IP address functionality to attach a secondary IP range to a running instance</longdesc>
e72037
+  <shortdesc lang="en">Floating IP Address or Range on Google Cloud Platform</shortdesc>
e72037
   <parameters>
e72037
     <parameter name="alias_ip" unique="1" required="1">
e72037
-      <longdesc lang="en">IP Address to be added including CIDR. E.g 192.168.0.1/32</longdesc>
e72037
-      <shortdesc lang="en">IP Address to be added including CIDR. E.g 192.168.0.1/32</shortdesc>
e72037
+      <longdesc lang="en">IP range to be added including CIDR netmask (e.g., 192.168.0.1/32)</longdesc>
e72037
+      <shortdesc lang="en">IP range to be added including CIDR netmask (e.g., 192.168.0.1/32)</shortdesc>
e72037
       <content type="string" default="" />
e72037
     </parameter>
e72037
-    <parameter name="alias_range_name" unique="1" required="0">
e72037
+    <parameter name="alias_range_name" unique="0" required="0">
e72037
       <longdesc lang="en">Subnet name for the Alias IP</longdesc>
e72037
       <shortdesc lang="en">Subnet name for the Alias IP</shortdesc>
e72037
       <content type="string" default="" />
e72037
     </parameter>
e72037
-    <parameter name="hostlist" unique="1" required="0">
e72037
-      <longdesc lang="en">List of hosts in the cluster</longdesc>
e72037
+    <parameter name="hostlist" unique="0" required="0">
e72037
+      <longdesc lang="en">List of hosts in the cluster, separated by spaces</longdesc>
e72037
       <shortdesc lang="en">Host list</shortdesc>
e72037
       <content type="string" default="" />
e72037
     </parameter>
e72037
     <parameter name="stackdriver_logging" unique="0" required="0">
e72037
-      <longdesc lang="en">If enabled (set to true), IP failover logs will be posted to stackdriver logging. Using stackdriver logging requires additional libraries (google-cloud-logging).</longdesc>
e72037
-      <shortdesc lang="en">Stackdriver-logging support. Requires additional libraries (google-cloud-logging).</shortdesc>
e72037
+      <longdesc lang="en">If enabled (set to true), IP failover logs will be posted to stackdriver logging</longdesc>
e72037
+      <shortdesc lang="en">Stackdriver-logging support</shortdesc>
e72037
       <content type="boolean" default="" />
e72037
     </parameter>
e72037
   </parameters>
e72037
@@ -107,7 +112,8 @@
e72037
   url = '%s?%s' % (metadata_url, params)
e72037
   request = urlrequest.Request(url, headers=METADATA_HEADERS)
e72037
   request_opener = urlrequest.build_opener(urlrequest.ProxyHandler({}))
e72037
-  return request_opener.open(request, timeout=timeout * 1.1).read()
e72037
+  return request_opener.open(
e72037
+      request, timeout=timeout * 1.1).read().decode("utf-8")
e72037
 
e72037
 
e72037
 def get_instance(project, zone, instance):
e72037
@@ -134,17 +140,21 @@
e72037
     time.sleep(1)
e72037
 
e72037
 
e72037
-def set_alias(project, zone, instance, alias, alias_range_name=None):
e72037
-  fingerprint = get_network_ifaces(project, zone, instance)[0]['fingerprint']
e72037
+def set_aliases(project, zone, instance, aliases, fingerprint):
e72037
+  """Sets the alias IP ranges for an instance.
e72037
+
e72037
+  Args:
e72037
+    project: string, the project in which the instance resides.
e72037
+    zone: string, the zone in which the instance resides.
e72037
+    instance: string, the name of the instance.
e72037
+    aliases: list, the list of dictionaries containing alias IP ranges
e72037
+      to be added to or removed from the instance.
e72037
+    fingerprint: string, the fingerprint of the network interface.
e72037
+  """
e72037
   body = {
e72037
-      'aliasIpRanges': [],
e72037
-      'fingerprint': fingerprint
e72037
+    'aliasIpRanges': aliases,
e72037
+    'fingerprint': fingerprint
e72037
   }
e72037
-  if alias:
e72037
-    obj = {'ipCidrRange': alias}
e72037
-    if alias_range_name:
e72037
-      obj['subnetworkRangeName'] = alias_range_name
e72037
-    body['aliasIpRanges'].append(obj)
e72037
 
e72037
   request = CONN.instances().updateNetworkInterface(
e72037
       instance=instance, networkInterface='nic0', project=project, zone=zone,
e72037
@@ -153,21 +163,75 @@
e72037
   wait_for_operation(project, zone, operation)
e72037
 
e72037
 
e72037
-def get_alias(project, zone, instance):
e72037
-  iface = get_network_ifaces(project, zone, instance)
e72037
+def add_rm_alias(mode, project, zone, instance, alias, alias_range_name=None):
e72037
+  """Adds or removes an alias IP range for a GCE instance.
e72037
+
e72037
+  Args:
e72037
+    mode: int, a constant (ADD (0) or REMOVE (1)) indicating the
e72037
+      operation type.
e72037
+    project: string, the project in which the instance resides.
e72037
+    zone: string, the zone in which the instance resides.
e72037
+    instance: string, the name of the instance.
e72037
+    alias: string, the alias IP range to be added to or removed from
e72037
+      the instance.
e72037
+    alias_range_name: string, the subnet name for the alias IP range.
e72037
+
e72037
+  Returns:
e72037
+    True if the existing list of alias IP ranges was modified, or False
e72037
+    otherwise.
e72037
+  """
e72037
+  ifaces = get_network_ifaces(project, zone, instance)
e72037
+  fingerprint = ifaces[0]['fingerprint']
e72037
+
e72037
+  try:
e72037
+    old_aliases = ifaces[0]['aliasIpRanges']
e72037
+  except KeyError:
e72037
+    old_aliases = []
e72037
+
e72037
+  new_aliases = [a for a in old_aliases if a['ipCidrRange'] != alias]
e72037
+
e72037
+  if alias:
e72037
+    if mode == ADD:
e72037
+      obj = {'ipCidrRange': alias}
e72037
+      if alias_range_name:
e72037
+        obj['subnetworkRangeName'] = alias_range_name
e72037
+      new_aliases.append(obj)
e72037
+    elif mode == REMOVE:
e72037
+      pass    # already removed during new_aliases build
e72037
+    else:
e72037
+      raise ValueError('Invalid value for mode: {}'.format(mode))
e72037
+
e72037
+  if (sorted(new_aliases) != sorted(old_aliases)):
e72037
+    set_aliases(project, zone, instance, new_aliases, fingerprint)
e72037
+    return True
e72037
+  else:
e72037
+    return False
e72037
+
e72037
+
e72037
+def add_alias(project, zone, instance, alias, alias_range_name=None):
e72037
+  return add_rm_alias(ADD, project, zone, instance, alias, alias_range_name)
e72037
+
e72037
+
e72037
+def remove_alias(project, zone, instance, alias):
e72037
+  return add_rm_alias(REMOVE, project, zone, instance, alias)
e72037
+
e72037
+
e72037
+def get_aliases(project, zone, instance):
e72037
+  ifaces = get_network_ifaces(project, zone, instance)
e72037
   try:
e72037
-    return iface[0]['aliasIpRanges'][0]['ipCidrRange']
e72037
+    aliases = ifaces[0]['aliasIpRanges']
e72037
+    return [a['ipCidrRange'] for a in aliases]
e72037
   except KeyError:
e72037
-    return ''
e72037
+    return []
e72037
 
e72037
 
e72037
-def get_localhost_alias():
e72037
+def get_localhost_aliases():
e72037
   net_iface = get_metadata('instance/network-interfaces', {'recursive': True})
e72037
-  net_iface = json.loads(net_iface.decode('utf-8'))
e72037
+  net_iface = json.loads(net_iface)
e72037
   try:
e72037
-    return net_iface[0]['ipAliases'][0]
e72037
+    return net_iface[0]['ipAliases']
e72037
   except (KeyError, IndexError):
e72037
-    return ''
e72037
+    return []
e72037
 
e72037
 
e72037
 def get_zone(project, instance):
e72037
@@ -201,21 +265,17 @@
e72037
 
e72037
 
e72037
 def gcp_alias_start(alias):
e72037
-  my_alias = get_localhost_alias()
e72037
+  my_aliases = get_localhost_aliases()
e72037
   my_zone = get_metadata('instance/zone').split('/')[-1]
e72037
   project = get_metadata('project/project-id')
e72037
 
e72037
-  # If I already have the IP, exit. If it has an alias IP that isn't the VIP,
e72037
-  # then remove it
e72037
-  if my_alias == alias:
e72037
+  if alias in my_aliases:
e72037
+    # TODO: Do we need to check alias_range_name?
e72037
     logger.info(
e72037
         '%s already has %s attached. No action required' % (THIS_VM, alias))
e72037
     sys.exit(OCF_SUCCESS)
e72037
-  elif my_alias:
e72037
-    logger.info('Removing %s from %s' % (my_alias, THIS_VM))
e72037
-    set_alias(project, my_zone, THIS_VM, '')
e72037
 
e72037
-  # Loops through all hosts & remove the alias IP from the host that has it
e72037
+  # If the alias is currently attached to another host, detach it.
e72037
   hostlist = os.environ.get('OCF_RESKEY_hostlist', '')
e72037
   if hostlist:
e72037
     hostlist = hostlist.replace(THIS_VM, '').split()
e72037
@@ -223,47 +283,53 @@
e72037
     hostlist = get_instances_list(project, THIS_VM)
e72037
   for host in hostlist:
e72037
     host_zone = get_zone(project, host)
e72037
-    host_alias = get_alias(project, host_zone, host)
e72037
-    if alias == host_alias:
e72037
+    host_aliases = get_aliases(project, host_zone, host)
e72037
+    if alias in host_aliases:
e72037
       logger.info(
e72037
-          '%s is attached to %s - Removing all alias IP addresses from %s' %
e72037
-          (alias, host, host))
e72037
-      set_alias(project, host_zone, host, '')
e72037
+          '%s is attached to %s - Removing %s from %s' %
e72037
+          (alias, host, alias, host))
e72037
+      remove_alias(project, host_zone, host, alias)
e72037
       break
e72037
 
e72037
-  # add alias IP to localhost
e72037
-  set_alias(
e72037
+  # Add alias IP range to localhost
e72037
+  add_alias(
e72037
       project, my_zone, THIS_VM, alias,
e72037
       os.environ.get('OCF_RESKEY_alias_range_name'))
e72037
 
e72037
-  # Check the IP has been added
e72037
-  my_alias = get_localhost_alias()
e72037
-  if alias == my_alias:
e72037
+  # Verify that the IP range has been added
e72037
+  my_aliases = get_localhost_aliases()
e72037
+  if alias in my_aliases:
e72037
     logger.info('Finished adding %s to %s' % (alias, THIS_VM))
e72037
-  elif my_alias:
e72037
-    logger.error(
e72037
-        'Failed to add IP. %s has an IP attached but it isn\'t %s' %
e72037
-        (THIS_VM, alias))
e72037
-    sys.exit(OCF_ERR_GENERIC)
e72037
   else:
e72037
-    logger.error('Failed to add IP address %s to %s' % (alias, THIS_VM))
e72037
+    if my_aliases:
e72037
+      logger.error(
e72037
+          'Failed to add alias IP range %s. %s has alias IP ranges attached but'
e72037
+          + ' they don\'t include %s' % (alias, THIS_VM, alias))
e72037
+    else:
e72037
+      logger.error(
e72037
+          'Failed to add IP range %s. %s has no alias IP ranges attached'
e72037
+           % (alias, THIS_VM))
e72037
     sys.exit(OCF_ERR_GENERIC)
e72037
 
e72037
 
e72037
 def gcp_alias_stop(alias):
e72037
-  my_alias = get_localhost_alias()
e72037
+  my_aliases = get_localhost_aliases()
e72037
   my_zone = get_metadata('instance/zone').split('/')[-1]
e72037
   project = get_metadata('project/project-id')
e72037
 
e72037
-  if my_alias == alias:
e72037
-    logger.info('Removing %s from %s' % (my_alias, THIS_VM))
e72037
-    set_alias(project, my_zone, THIS_VM, '')
e72037
+  if alias in my_aliases:
e72037
+    logger.info('Removing %s from %s' % (alias, THIS_VM))
e72037
+    remove_alias(project, my_zone, THIS_VM, alias)
e72037
+  else:
e72037
+    logger.info(
e72037
+        '%s is not attached to %s. No action required'
e72037
+        % (alias, THIS_VM))
e72037
 
e72037
 
e72037
 def gcp_alias_status(alias):
e72037
-  my_alias = get_localhost_alias()
e72037
-  if alias == my_alias:
e72037
-    logger.info('%s has the correct IP address attached' % THIS_VM)
e72037
+  my_aliases = get_localhost_aliases()
e72037
+  if alias in my_aliases:
e72037
+    logger.info('%s has the correct IP range attached' % THIS_VM)
e72037
   else:
e72037
     sys.exit(OCF_NOT_RUNNING)
e72037
 
e72037
@@ -275,7 +341,8 @@
e72037
 
e72037
   # Populate global vars
e72037
   try:
e72037
-    CONN = googleapiclient.discovery.build('compute', 'v1')
e72037
+    CONN = googleapiclient.discovery.build('compute', 'v1',
e72037
+                                           cache_discovery=False)
e72037
   except Exception as e:
e72037
     logger.error('Couldn\'t connect with google api: ' + str(e))
e72037
     sys.exit(OCF_ERR_CONFIGURED)
e72037
@@ -283,7 +350,8 @@
e72037
   try:
e72037
     THIS_VM = get_metadata('instance/name')
e72037
   except Exception as e:
e72037
-    logger.error('Couldn\'t get instance name, is this running inside GCE?: ' + str(e))
e72037
+    logger.error('Couldn\'t get instance name, is this running inside GCE?: '
e72037
+                 + str(e))
e72037
     sys.exit(OCF_ERR_CONFIGURED)
e72037
 
e72037
   ALIAS = os.environ.get('OCF_RESKEY_alias_ip')
e72037
@@ -309,7 +377,8 @@
e72037
         formatter = logging.Formatter('gcp:alias "%(message)s"')
e72037
         handler.setFormatter(formatter)
e72037
         log.addHandler(handler)
e72037
-        logger = logging.LoggerAdapter(log, {'OCF_RESOURCE_INSTANCE': OCF_RESOURCE_INSTANCE})
e72037
+        logger = logging.LoggerAdapter(log, {'OCF_RESOURCE_INSTANCE':
e72037
+                                             OCF_RESOURCE_INSTANCE})
e72037
       except ImportError:
e72037
         logger.error('Couldn\'t import google.cloud.logging, '
e72037
             'disabling Stackdriver-logging support')