File 0002-gcp-vpc-move-vip-Add-support-for-multiple-alias-IP-r.patch of Package resource-agents.25668

From 19e001a6d670e02682e7c15a1a58824065c062e3 Mon Sep 17 00:00:00 2001
From: Reid wahl <nrwahl@protonmail.com>
Date: Tue, 30 Jun 2020 01:42:17 -0700
Subject: [PATCH 2/2] [gcp-vpc-move-vip] Add support for multiple alias IP
 ranges on one node

If a cluster contains two gcp-vpc-move-vip resources, only one can run
on a particular node at a given time. If a second gcp-vpc-move-vip
resource starts up on a node where one is already running, the existing
alias IP range is removed before the new one is added.

This places unnecessary limits on functionality. Per the GCP
documentation: "A VM instance virtual interface can have up to 10 alias
IP ranges assigned to it."

Configuring alias IP ranges
(https://cloud.google.com/vpc/docs/configure-alias-ip-ranges)

The existing behavior prevents one node from being able to effectively
host two floating IP addresses simultaneously (unless they are in a
contiguous range and can be managed as a single unit, which is
uncommon).

This patch modifies the RA so that it updates the existing list of
alias IP ranges attached to a VM, instead of re-initializing the list
to hold only `OCF_RESKEY_alias_ip`. With these improvements, multiple
gcp-vpc-move-vip resources can run on a single node, thus supporting as
many simultaneous alias assignments as GCP allows.
---
 heartbeat/gcp-vpc-move-vip.in | 156 ++++++++++++++++++++++++----------
 1 file changed, 110 insertions(+), 46 deletions(-)

diff --git a/heartbeat/gcp-vpc-move-vip.in b/heartbeat/gcp-vpc-move-vip.in
index dfa1ac91..85d59f6b 100755
--- a/heartbeat/gcp-vpc-move-vip.in
+++ b/heartbeat/gcp-vpc-move-vip.in
@@ -43,6 +43,10 @@ else:
   import urllib2 as urlrequest
 
 
+# Constants for alias add/remove modes
+ADD = 0
+REMOVE = 1
+
 CONN = None
 THIS_VM = None
 ALIAS = None
@@ -135,17 +139,21 @@ def wait_for_operation(project, zone, operation):
     time.sleep(1)
 
 
-def set_alias(project, zone, instance, alias, alias_range_name=None):
-  fingerprint = get_network_ifaces(project, zone, instance)[0]['fingerprint']
+def set_aliases(project, zone, instance, aliases, fingerprint):
+  """Sets the alias IP ranges for an instance.
+
+  Args:
+    project: string, the project in which the instance resides.
+    zone: string, the zone in which the instance resides.
+    instance: string, the name of the instance.
+    aliases: list, the list of dictionaries containing alias IP ranges
+      to be added to or removed from the instance.
+    fingerprint: string, the fingerprint of the network interface.
+  """
   body = {
-      'aliasIpRanges': [],
-      'fingerprint': fingerprint
+    'aliasIpRanges': aliases,
+    'fingerprint': fingerprint
   }
-  if alias:
-    obj = {'ipCidrRange': alias}
-    if alias_range_name:
-      obj['subnetworkRangeName'] = alias_range_name
-    body['aliasIpRanges'].append(obj)
 
   request = CONN.instances().updateNetworkInterface(
       instance=instance, networkInterface='nic0', project=project, zone=zone,
@@ -154,21 +162,75 @@ def set_alias(project, zone, instance, alias, alias_range_name=None):
   wait_for_operation(project, zone, operation)
 
 
-def get_alias(project, zone, instance):
-  iface = get_network_ifaces(project, zone, instance)
+def add_rm_alias(mode, project, zone, instance, alias, alias_range_name=None):
+  """Adds or removes an alias IP range for a GCE instance.
+
+  Args:
+    mode: int, a constant (ADD (0) or REMOVE (1)) indicating the
+      operation type.
+    project: string, the project in which the instance resides.
+    zone: string, the zone in which the instance resides.
+    instance: string, the name of the instance.
+    alias: string, the alias IP range to be added to or removed from
+      the instance.
+    alias_range_name: string, the subnet name for the alias IP range.
+
+  Returns:
+    True if the existing list of alias IP ranges was modified, or False
+    otherwise.
+  """
+  ifaces = get_network_ifaces(project, zone, instance)
+  fingerprint = ifaces[0]['fingerprint']
+
+  try:
+    old_aliases = ifaces[0]['aliasIpRanges']
+  except KeyError:
+    old_aliases = []
+
+  new_aliases = [a for a in old_aliases if a['ipCidrRange'] != alias]
+
+  if alias:
+    if mode == ADD:
+      obj = {'ipCidrRange': alias}
+      if alias_range_name:
+        obj['subnetworkRangeName'] = alias_range_name
+      new_aliases.append(obj)
+    elif mode == REMOVE:
+      pass    # already removed during new_aliases build
+    else:
+      raise ValueError('Invalid value for mode: {}'.format(mode))
+
+  if (sorted(new_aliases) != sorted(old_aliases)):
+    set_aliases(project, zone, instance, new_aliases, fingerprint)
+    return True
+  else:
+    return False
+
+
+def add_alias(project, zone, instance, alias, alias_range_name=None):
+  return add_rm_alias(ADD, project, zone, instance, alias, alias_range_name)
+
+
+def remove_alias(project, zone, instance, alias):
+  return add_rm_alias(REMOVE, project, zone, instance, alias)
+
+
+def get_aliases(project, zone, instance):
+  ifaces = get_network_ifaces(project, zone, instance)
   try:
-    return iface[0]['aliasIpRanges'][0]['ipCidrRange']
+    aliases = ifaces[0]['aliasIpRanges']
+    return [a['ipCidrRange'] for a in aliases]
   except KeyError:
-    return ''
+    return []
 
 
-def get_localhost_alias():
+def get_localhost_aliases():
   net_iface = get_metadata('instance/network-interfaces', {'recursive': True})
   net_iface = json.loads(net_iface)
   try:
-    return net_iface[0]['ipAliases'][0]
+    return net_iface[0]['ipAliases']
   except (KeyError, IndexError):
-    return ''
+    return []
 
 
 def get_zone(project, instance):
@@ -202,21 +264,17 @@ def get_instances_list(project, exclude):
 
 
 def gcp_alias_start(alias):
-  my_alias = get_localhost_alias()
+  my_aliases = get_localhost_aliases()
   my_zone = get_metadata('instance/zone').split('/')[-1]
   project = get_metadata('project/project-id')
 
-  # If I already have the IP, exit. If it has an alias IP that isn't the
-  # VIP, then remove it
-  if my_alias == alias:
+  if alias in my_aliases:
+    # TODO: Do we need to check alias_range_name?
     logger.info(
         '%s already has %s attached. No action required' % (THIS_VM, alias))
     sys.exit(OCF_SUCCESS)
-  elif my_alias:
-    logger.info('Removing %s from %s' % (my_alias, THIS_VM))
-    set_alias(project, my_zone, THIS_VM, '')
 
-  # Loops through all hosts & remove the alias IP from the host that has it
+  # If the alias is currently attached to another host, detach it.
   hostlist = os.environ.get('OCF_RESKEY_hostlist', '')
   if hostlist:
     hostlist = hostlist.replace(THIS_VM, '').split()
@@ -224,46 +282,52 @@ def gcp_alias_start(alias):
     hostlist = get_instances_list(project, THIS_VM)
   for host in hostlist:
     host_zone = get_zone(project, host)
-    host_alias = get_alias(project, host_zone, host)
-    if alias == host_alias:
+    host_aliases = get_aliases(project, host_zone, host)
+    if alias in host_aliases:
       logger.info(
-          '%s is attached to %s - Removing all alias IP addresses from %s' %
-          (alias, host, host))
-      set_alias(project, host_zone, host, '')
+          '%s is attached to %s - Removing %s from %s' %
+          (alias, host, alias, host))
+      remove_alias(project, host_zone, host, alias)
       break
 
-  # add alias IP to localhost
-  set_alias(
+  # Add alias IP range to localhost
+  add_alias(
       project, my_zone, THIS_VM, alias,
       os.environ.get('OCF_RESKEY_alias_range_name'))
 
-  # Check the IP has been added
-  my_alias = get_localhost_alias()
-  if alias == my_alias:
+  # Verify that the IP range has been added
+  my_aliases = get_localhost_aliases()
+  if alias in my_aliases:
     logger.info('Finished adding %s to %s' % (alias, THIS_VM))
-  elif my_alias:
-    logger.error(
-        'Failed to add alias IP range. %s has alias IP ranges attached but'
-        + ' they don\'t include %s' % (THIS_VM, alias))
-    sys.exit(OCF_ERR_GENERIC)
   else:
-    logger.error('Failed to add IP range %s to %s' % (alias, THIS_VM))
+    if my_aliases:
+      logger.error(
+          'Failed to add alias IP range %s. %s has alias IP ranges attached but'
+          + ' they don\'t include %s' % (alias, THIS_VM, alias))
+    else:
+      logger.error(
+          'Failed to add IP range %s. %s has no alias IP ranges attached'
+           % (alias, THIS_VM))
     sys.exit(OCF_ERR_GENERIC)
 
 
 def gcp_alias_stop(alias):
-  my_alias = get_localhost_alias()
+  my_aliases = get_localhost_aliases()
   my_zone = get_metadata('instance/zone').split('/')[-1]
   project = get_metadata('project/project-id')
 
-  if my_alias == alias:
-    logger.info('Removing %s from %s' % (my_alias, THIS_VM))
-    set_alias(project, my_zone, THIS_VM, '')
+  if alias in my_aliases:
+    logger.info('Removing %s from %s' % (alias, THIS_VM))
+    remove_alias(project, my_zone, THIS_VM, alias)
+  else:
+    logger.info(
+        '%s is not attached to %s. No action required'
+        % (alias, THIS_VM))
 
 
 def gcp_alias_status(alias):
-  my_alias = get_localhost_alias()
-  if alias == my_alias:
+  my_aliases = get_localhost_aliases()
+  if alias in my_aliases:
     logger.info('%s has the correct IP range attached' % THIS_VM)
   else:
     sys.exit(OCF_NOT_RUNNING)
-- 
2.26.2

openSUSE Build Service is sponsored by