[prev in list] [next in list] [prev in thread] [next in thread] 

List:       linux-ha-dev
Subject:    [Linux-ha-dev] [PATCH] gcp-alias.py: manage ip alias
From:       Helen Koike <helen.koike () collabora ! com>
Date:       2018-06-26 11:13:29
Message-ID: 20180626111329.513-1-helen.koike () collabora ! com
[Download RAW message or body]

Add a resource agent to manage ip alias in the cluster.

start:
	Check if any machine in hostlist has the alias_ip assigned and
	disassociate it.
	Assign alias_ip to the current machine.

stop:
	Disassociate the alias_ip from the current machine.

status/monitor:
	Check if alias_ip is assigned with the current machine.

---

This is a port to the following bash script to python:
https://storage.googleapis.com/sapdeploy/pacemaker-gcp/alias

The problem with the bash script is the use of gcloud whose command line
API is not stable.

ocf-tester.in results:

	> sudo ./tools/ocf-tester.in -o alias_ip='10.128.1.0/32' -n gcp-alias \
heartbeat/gcp-alias.py  Beginning tests for heartbeat/gcp-alias.py...
	./tools/ocf-tester.in: line 226: cd: @datadir@/resource-agents: No such file or \
directory  close failed in file object destructor:
	sys.excepthook is missing
	lost sys.stderr
	* rc=1: Your agent produces meta-data which does not conform to ra-api-1.dtd
	Tests failed: /home/koike/resource-agents/heartbeat/gcp-alias.py failed 1 tests

The only test failig is the meta-data, but all the agents that I tried
also fails on this. If this is a concern, could you please point me out
to a test which succeeds so I can check what I am doing differently?
The object destructor error is because the test pipe the output to a
command that doesn't read stdin, it can be reproduced by:
	python -c "print 'foo'" | ls

This commit can also be viewed at:
	https://github.com/collabora-gce/resource-agents/tree/alias

Thanks
---
 doc/man/Makefile.am   |   1 +
 heartbeat/Makefile.am |   1 +
 heartbeat/gcp-alias   | 310 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 312 insertions(+)
 create mode 100755 heartbeat/gcp-alias

diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am
index 4f766a30..9119aab4 100644
--- a/doc/man/Makefile.am
+++ b/doc/man/Makefile.am
@@ -113,6 +113,7 @@ man_MANS	       = ocf_heartbeat_AoEtarget.7 \
                           ocf_heartbeat_fio.7 \
                           ocf_heartbeat_galera.7 \
                           ocf_heartbeat_garbd.7 \
+                          ocf_heartbeat_gcp-alias.7 \
                           ocf_heartbeat_gcp-vpc-move-ip.7 \
                           ocf_heartbeat_iSCSILogicalUnit.7 \
                           ocf_heartbeat_iSCSITarget.7 \
diff --git a/heartbeat/Makefile.am b/heartbeat/Makefile.am
index 7c3a2e94..942e945a 100644
--- a/heartbeat/Makefile.am
+++ b/heartbeat/Makefile.am
@@ -110,6 +110,7 @@ ocf_SCRIPTS	     =  AoEtarget		\
 			fio			\
 			galera			\
 			garbd			\
+			gcp-alias		\
 			gcp-vpc-move-ip		\
 			iSCSILogicalUnit	\
 			iSCSITarget		\
diff --git a/heartbeat/gcp-alias b/heartbeat/gcp-alias
new file mode 100755
index 00000000..de8a798f
--- /dev/null
+++ b/heartbeat/gcp-alias
@@ -0,0 +1,310 @@
+#!/usr/bin/env python
+# ---------------------------------------------------------------------
+# Copyright 2016 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 - Floating IP Address (Alias)
+# ---------------------------------------------------------------------
+
+import json
+import os
+import platform
+import sys
+import time
+
+import googleapiclient.discovery
+
+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
+LOGGER = None
+HOSTNAME = None
+OCF_SUCCESS = 0
+OCF_ERR_GENERIC = 1
+OCF_ERR_CONFIGURED = 6
+OCF_NOT_RUNNING = 7
+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:alias">
+  <version>1.0</version>
+  <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> +  <shortdesc lang="en">Floating IP Address on Google Cloud \
Platform</shortdesc> +  <parameters>
+    <parameter name="hostlist" unique="1" required="1">
+      <longdesc lang="en">List of hosts in the cluster</longdesc>
+      <shortdesc lang="en">Host list</shortdesc>
+      <content type="string" default="" />
+    </parameter>
+    <parameter name="logging" unique="0" required="0">
+      <longdesc lang="en">If enabled (set to true), IP failover logs will be posted \
to stackdriver logging</longdesc> +      <shortdesc lang="en">Stackdriver-logging \
support</shortdesc> +      <content type="boolean" default="" />
+    </parameter>
+    <parameter name="alias_ip" unique="1" required="1">
+      <longdesc lang="en">IP Address to be added including CIDR. E.g \
192.168.0.1/32</longdesc> +      <shortdesc lang="en">IP Address to be added \
including CIDR. E.g 192.168.0.1/32</shortdesc> +      <content type="string" \
default="" /> +    </parameter>
+    <parameter name="alias_range_name" unique="1" required="0">
+      <longdesc lang="en">Subnet name for the Alias IP2</longdesc>
+      <shortdesc lang="en">Subnet name for the Alias IP</shortdesc>
+      <content type="string" default="" />
+    </parameter>
+  </parameters>
+  <actions>
+    <action name="start" timeout="300" />
+    <action name="stop" timeout="15" />
+    <action name="monitor" timeout="15" interval="60" depth="0" />
+    <action name="meta-data" timeout="15" />
+  </actions>
+</resource-agent>'''
+
+
+def log(severity, msg):
+  print('gcp:alias - %s - %s' % (severity, msg))
+  if LOGGER:
+    msg = '%s %s "%s"' % (
+        HOSTNAME, os.environ.get('OCF_RESOURCE_INSTANCE'), msg)
+    LOGGER.log_text(msg, severity=severity)
+
+
+def log_info(msg):
+  log('INFO', msg)
+
+
+def log_error(msg):
+  log('ERROR', msg)
+
+
+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 get_instance(project, zone, instance):
+  request = CONN.instances().get(
+      project=project, zone=zone, instance=instance)
+  return request.execute()
+
+
+def get_network_ifaces(project, zone, instance):
+  return get_instance(project, zone, instance)['networkInterfaces']
+
+
+def wait_for_operation(project, zone, 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 set_alias(project, zone, instance, alias, alias_range_name=None):
+  fingerprint = get_network_ifaces(project, zone, instance)[0]['fingerprint']
+  body = {
+      'aliasIpRanges': [],
+      '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,
+      body=body)
+  operation = request.execute()
+  wait_for_operation(project, zone, operation)
+
+
+def get_alias(project, zone, instance):
+  iface = get_network_ifaces(project, zone, instance)
+  try:
+    return iface[0]['aliasIpRanges'][0]['ipCidrRange']
+  except KeyError:
+    return ''
+
+
+def get_localhost_alias():
+  net_iface = get_metadata('instance/network-interfaces', {'recursive': True})
+  net_iface = json.loads(net_iface.decode('utf-8'))
+  try:
+    return net_iface[0]['ipAliases'][0]
+  except (KeyError, IndexError):
+    return ''
+
+
+def get_zone(project, instance):
+  request = CONN.zones().list(project=project)
+  while request is not None:
+    response = request.execute()
+    for zone in response['items']:
+      try:
+        get_instance(project, zone['name'], instance)
+      except googleapiclient.errors.HttpError:
+        pass
+      else:
+        return zone['name']
+    request = CONN.zones().list_next(
+        previous_request=request, previous_response=response)
+
+
+def gcp_alias_start(alias):
+  if not alias:
+    sys.exit(OCF_ERR_CONFIGURED)
+  my_alias = get_localhost_alias()
+  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:
+    log_info(
+        '%s already has %s attached. No action required' % (HOSTNAME, alias))
+    sys.exit(OCF_SUCCESS)
+  elif my_alias:
+    log_info('Removing %s from %s' % (my_alias, HOSTNAME))
+    set_alias(project, my_zone, HOSTNAME, '')
+
+  # Loops through all hosts & remove the alias IP from the host that has it
+  hostlist = os.environ.get('OCF_RESKEY_hostlist', '')
+  hostlist.replace(HOSTNAME, '')
+  for host in hostlist.split():
+    host_zone = get_zone(project, host)
+    host_alias = get_alias(project, host_zone, host)
+    if alias == host_alias:
+      log_info(
+          '%s is attached to %s - Removing all alias IP addresses from %s' %
+          (alias, host, host))
+      set_alias(project, host_zone, host, '')
+      break
+
+  # add alias IP to localhost
+  set_alias(
+      project, my_zone, HOSTNAME, alias,
+      os.environ.get('OCF_RESKEY_alias_range_name'))
+
+  # Check the IP has been added
+  my_alias = get_localhost_alias()
+  if alias == my_alias:
+    log_info('Finished adding %s to %s' % (alias, HOSTNAME))
+  elif my_alias:
+    log_error(
+        'Failed to add IP. %s has an IP attached but it isn\'t %s' %
+        (HOSTNAME, alias))
+    sys.exit(OCF_ERR_GENERIC)
+  else:
+    log_error('Failed to add IP address %s to %s' % (alias, HOSTNAME))
+    sys.exit(OCF_ERR_GENERIC)
+
+
+def gcp_alias_stop(alias):
+  if not alias:
+    sys.exit(OCF_ERR_CONFIGURED)
+  my_alias = get_localhost_alias()
+  my_zone = get_metadata('instance/zone').split('/')[-1]
+  project = get_metadata('project/project-id')
+
+  if my_alias == alias:
+    log_info('Removing %s from %s' % (my_alias, HOSTNAME))
+    set_alias(project, my_zone, HOSTNAME, '')
+
+
+def gcp_alias_status(alias):
+  if not alias:
+    sys.exit(OCF_ERR_CONFIGURED)
+  my_alias = get_localhost_alias()
+  if alias == my_alias:
+    log_info('%s has the correct IP address attached' % HOSTNAME)
+  else:
+    sys.exit(OCF_NOT_RUNNING)
+
+
+def configure():
+  global CONN
+  global LOGGER
+  global HOSTNAME
+
+  # Populate global vars
+  CONN = googleapiclient.discovery.build('compute', 'v1')
+  HOSTNAME = platform.node()
+
+  # Prepare logging
+  logging_env = os.environ.get('OCF_RESKEY_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
+        client = google.cloud.logging.Client()
+        LOGGER = client.logger(HOSTNAME)
+      except ImportError:
+        log_error('Couldn\'t import google.cloud.logging, '
+            'disabling Stackdriver-logging support')
+
+
+def main():
+  configure()
+
+  alias = os.environ.get('OCF_RESKEY_alias_ip')
+  if 'start' in sys.argv[1]:
+    gcp_alias_start(alias)
+  elif 'stop' in sys.argv[1]:
+    gcp_alias_stop(alias)
+  elif 'status' in sys.argv[1] or 'monitor' in sys.argv[1]:
+    gcp_alias_status(alias)
+  elif 'meta-data' in sys.argv[1]:
+    print(METADATA)
+  else:
+    log_error('gcp:alias - no such function %s' % str(sys.argv[1]))
+
+
+if __name__ == "__main__":
+  main()
-- 
2.17.0

_______________________________________________________
Linux-HA-Dev: Linux-HA-Dev@lists.linux-ha.org
http://lists.linux-ha.org/mailman/listinfo/linux-ha-dev
Home Page: http://linux-ha.org/


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic