Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/doc/man/Makefile.am b/doc/man/Makefile.am
index d14b6c53c..100425514 100644
--- a/doc/man/Makefile.am
+++ b/doc/man/Makefile.am
@@ -1,264 +1,265 @@
#
# doc: Linux-HA resource agents
#
# Copyright (C) 2009 Florian Haas
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
MAINTAINERCLEANFILES = Makefile.in
EXTRA_DIST = $(doc_DATA) $(REFENTRY_STYLESHEET) \
mkappendix.sh ralist.sh
CLEANFILES = $(man_MANS) $(xmlfiles) metadata-*.xml
STYLESHEET_PREFIX ?= http://docbook.sourceforge.net/release/xsl/current
MANPAGES_STYLESHEET ?= $(STYLESHEET_PREFIX)/manpages/docbook.xsl
HTML_STYLESHEET ?= $(STYLESHEET_PREFIX)/xhtml/docbook.xsl
FO_STYLESHEET ?= $(STYLESHEET_PREFIX)/fo/docbook.xsl
REFENTRY_STYLESHEET ?= ra2refentry.xsl
XSLTPROC_OPTIONS ?= --xinclude
XSLTPROC_MANPAGES_OPTIONS ?= $(XSLTPROC_OPTIONS)
XSLTPROC_HTML_OPTIONS ?= $(XSLTPROC_OPTIONS)
XSLTPROC_FO_OPTIONS ?= $(XSLTPROC_OPTIONS)
radir = $(abs_top_builddir)/heartbeat
# required for out-of-tree build
symlinkstargets = \
ocf-distro ocf.py ocf-rarun ocf-returncodes \
findif.sh apache-conf.sh http-mon.sh mysql-common.sh \
nfsserver-redhat.sh openstack-common.sh ora-common.sh
preptree:
for i in $(symlinkstargets); do \
if [ ! -f $(radir)/$$i ]; then \
rm -rf $(radir)/$$i; \
ln -sf $(abs_top_srcdir)/heartbeat/$$i $(radir)/$$i; \
fi; \
done
$(radir)/%: $(abs_top_srcdir)/heartbeat/%
if [ ! -f $@ ]; then \
ln -sf $< $@; \
fi
# OCF_ROOT=. is necessary due to a sanity check in ocf-shellfuncs
# (which tests whether $OCF_ROOT points to a directory
metadata-%.xml: $(radir)/% preptree
OCF_ROOT=. OCF_FUNCTIONS_DIR=$(radir) $< meta-data > $@
metadata-IPv6addr.xml: $(radir)/IPv6addr
OCF_ROOT=. OCF_FUNCTIONS_DIR=$(radir) $< meta-data > $@
clean-local:
find $(radir) -type l -exec rm -rf {} \;
# Please note: we can't name the man pages
# ocf:heartbeat:<name>. Believe me, I've tried. It looks like it
# works, but then it doesn't. While make can deal correctly with
# colons in target names (when properly escaped), it royally messes up
# when it is deals with _dependencies_ that contain colons. See Bug
# 12126 on savannah.gnu.org. But, maybe it gets fixed soon, it was
# first reported in 1995 and added to Savannah in in 2005...
if BUILD_DOC
man_MANS = ocf_heartbeat_AoEtarget.7 \
ocf_heartbeat_AudibleAlarm.7 \
ocf_heartbeat_ClusterMon.7 \
ocf_heartbeat_CTDB.7 \
ocf_heartbeat_Delay.7 \
ocf_heartbeat_Dummy.7 \
ocf_heartbeat_EvmsSCC.7 \
ocf_heartbeat_Evmsd.7 \
ocf_heartbeat_Filesystem.7 \
ocf_heartbeat_ICP.7 \
ocf_heartbeat_IPaddr.7 \
ocf_heartbeat_IPaddr2.7 \
ocf_heartbeat_IPsrcaddr.7 \
ocf_heartbeat_LVM.7 \
ocf_heartbeat_LVM-activate.7 \
ocf_heartbeat_LinuxSCSI.7 \
ocf_heartbeat_MailTo.7 \
ocf_heartbeat_ManageRAID.7 \
ocf_heartbeat_ManageVE.7 \
ocf_heartbeat_NodeUtilization.7 \
ocf_heartbeat_Pure-FTPd.7 \
ocf_heartbeat_Raid1.7 \
ocf_heartbeat_Route.7 \
ocf_heartbeat_SAPDatabase.7 \
ocf_heartbeat_SAPInstance.7 \
ocf_heartbeat_SendArp.7 \
ocf_heartbeat_ServeRAID.7 \
ocf_heartbeat_SphinxSearchDaemon.7 \
ocf_heartbeat_Squid.7 \
ocf_heartbeat_Stateful.7 \
ocf_heartbeat_SysInfo.7 \
ocf_heartbeat_VIPArip.7 \
ocf_heartbeat_VirtualDomain.7 \
ocf_heartbeat_WAS.7 \
ocf_heartbeat_WAS6.7 \
ocf_heartbeat_WinPopup.7 \
ocf_heartbeat_Xen.7 \
ocf_heartbeat_Xinetd.7 \
ocf_heartbeat_ZFS.7 \
ocf_heartbeat_aliyun-vpc-move-ip.7 \
ocf_heartbeat_anything.7 \
ocf_heartbeat_apache.7 \
ocf_heartbeat_asterisk.7 \
ocf_heartbeat_aws-vpc-move-ip.7 \
ocf_heartbeat_aws-vpc-route53.7 \
ocf_heartbeat_awseip.7 \
ocf_heartbeat_awsvip.7 \
ocf_heartbeat_azure-lb.7 \
ocf_heartbeat_clvm.7 \
ocf_heartbeat_conntrackd.7 \
ocf_heartbeat_corosync-qnetd.7 \
ocf_heartbeat_crypt.7 \
ocf_heartbeat_db2.7 \
ocf_heartbeat_dhcpd.7 \
ocf_heartbeat_docker.7 \
ocf_heartbeat_docker-compose.7 \
ocf_heartbeat_dovecot.7 \
ocf_heartbeat_dnsupdate.7 \
ocf_heartbeat_dummypy.7 \
ocf_heartbeat_eDir88.7 \
ocf_heartbeat_ethmonitor.7 \
ocf_heartbeat_exportfs.7 \
ocf_heartbeat_fio.7 \
ocf_heartbeat_galera.7 \
ocf_heartbeat_garbd.7 \
ocf_heartbeat_gcp-ilb.7 \
ocf_heartbeat_gcp-vpc-move-ip.7 \
ocf_heartbeat_iSCSILogicalUnit.7 \
ocf_heartbeat_iSCSITarget.7 \
ocf_heartbeat_iface-bridge.7 \
ocf_heartbeat_iface-macvlan.7 \
ocf_heartbeat_iface-vlan.7 \
ocf_heartbeat_ipsec.7 \
ocf_heartbeat_ids.7 \
ocf_heartbeat_iscsi.7 \
ocf_heartbeat_jboss.7 \
ocf_heartbeat_jira.7 \
ocf_heartbeat_kamailio.7 \
ocf_heartbeat_lvmlockd.7 \
ocf_heartbeat_lxc.7 \
ocf_heartbeat_lxd-info.7 \
ocf_heartbeat_machine-info.7 \
ocf_heartbeat_mariadb.7 \
ocf_heartbeat_mdraid.7 \
ocf_heartbeat_minio.7 \
ocf_heartbeat_mpathpersist.7 \
ocf_heartbeat_mysql.7 \
ocf_heartbeat_mysql-proxy.7 \
ocf_heartbeat_nagios.7 \
ocf_heartbeat_named.7 \
ocf_heartbeat_nfsnotify.7 \
ocf_heartbeat_nfsserver.7 \
ocf_heartbeat_nginx.7 \
ocf_heartbeat_nvmet-subsystem.7 \
ocf_heartbeat_nvmet-namespace.7 \
ocf_heartbeat_nvmet-port.7 \
ocf_heartbeat_openstack-info.7 \
ocf_heartbeat_ocivip.7 \
ocf_heartbeat_openstack-cinder-volume.7 \
ocf_heartbeat_openstack-floating-ip.7 \
ocf_heartbeat_openstack-virtual-ip.7 \
ocf_heartbeat_oraasm.7 \
ocf_heartbeat_oracle.7 \
ocf_heartbeat_oralsnr.7 \
ocf_heartbeat_osceip.7 \
ocf_heartbeat_ovsmonitor.7 \
ocf_heartbeat_pgagent.7 \
ocf_heartbeat_pgsql.7 \
ocf_heartbeat_pingd.7 \
ocf_heartbeat_podman.7 \
ocf_heartbeat_portblock.7 \
ocf_heartbeat_postfix.7 \
ocf_heartbeat_pound.7 \
+ ocf_heartbeat_powervs-subnet.7 \
ocf_heartbeat_proftpd.7 \
ocf_heartbeat_rabbitmq-cluster.7 \
ocf_heartbeat_rabbitmq-server-ha.7 \
ocf_heartbeat_redis.7 \
ocf_heartbeat_rkt.7 \
ocf_heartbeat_rsyncd.7 \
ocf_heartbeat_rsyslog.7 \
ocf_heartbeat_scsi2reservation.7 \
ocf_heartbeat_sfex.7 \
ocf_heartbeat_slapd.7 \
ocf_heartbeat_smb-share.7 \
ocf_heartbeat_sybaseASE.7 \
ocf_heartbeat_sg_persist.7 \
ocf_heartbeat_storage-mon.7 \
ocf_heartbeat_symlink.7 \
ocf_heartbeat_syslog-ng.7 \
ocf_heartbeat_tomcat.7 \
ocf_heartbeat_varnish.7 \
ocf_heartbeat_vdo-vol.7 \
ocf_heartbeat_vmware.7 \
ocf_heartbeat_vsftpd.7 \
ocf_heartbeat_zabbixserver.7
if USE_IPV6ADDR_AGENT
man_MANS += ocf_heartbeat_IPv6addr.7
endif
if BUILD_AZURE_EVENTS
man_MANS += ocf_heartbeat_azure-events.7
endif
if BUILD_AZURE_EVENTS_AZ
man_MANS += ocf_heartbeat_azure-events-az.7
endif
if BUILD_GCP_PD_MOVE
man_MANS += ocf_heartbeat_gcp-pd-move.7
endif
if BUILD_GCP_VPC_MOVE_ROUTE
man_MANS += ocf_heartbeat_gcp-vpc-move-route.7
endif
if BUILD_GCP_VPC_MOVE_VIP
man_MANS += ocf_heartbeat_gcp-vpc-move-vip.7
endif
xmlfiles = $(man_MANS:.7=.xml)
%.1 %.5 %.7 %.8: %.xml
$(XSLTPROC) \
$(XSLTPROC_MANPAGES_OPTIONS) \
$(MANPAGES_STYLESHEET) $<
ocf_heartbeat_%.xml: metadata-%.xml $(srcdir)/$(REFENTRY_STYLESHEET)
$(XSLTPROC) --novalid \
--stringparam package $(PACKAGE_NAME) \
--stringparam version $(VERSION) \
--output $@ \
$(srcdir)/$(REFENTRY_STYLESHEET) $<
ocf_resource_agents.xml: $(xmlfiles) mkappendix.sh
./mkappendix.sh $(xmlfiles) > $@
%.html: %.xml
$(XSLTPROC) \
$(XSLTPROC_HTML_OPTIONS) \
--output $@ \
$(HTML_STYLESHEET) $<
xml: ocf_resource_agents.xml
endif
diff --git a/heartbeat/Makefile.am b/heartbeat/Makefile.am
index f7e1703af..7ca490690 100644
--- a/heartbeat/Makefile.am
+++ b/heartbeat/Makefile.am
@@ -1,250 +1,251 @@
# Makefile.am for OCF RAs
#
# Author: Sun Jing Dong
# Copyright (C) 2004 IBM
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
MAINTAINERCLEANFILES = Makefile.in
EXTRA_DIST = $(ocf_SCRIPTS) $(ocfcommon_DATA) \
$(common_DATA) $(hb_DATA) $(dtd_DATA) \
README README.galera
AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_srcdir)/linux-ha
halibdir = $(libexecdir)/heartbeat
ocfdir = $(OCF_RA_DIR_PREFIX)/heartbeat
dtddir = $(datadir)/$(PACKAGE_NAME)
dtd_DATA = ra-api-1.dtd metadata.rng
ocf_PROGRAMS =
if USE_IPV6ADDR_AGENT
ocf_PROGRAMS += IPv6addr
endif
halib_PROGRAMS =
if IPV6ADDR_COMPATIBLE
halib_PROGRAMS += send_ua
endif
IPv6addr_SOURCES = IPv6addr.c IPv6addr_utils.c
IPv6addr_LDADD = -lplumb $(LIBNETLIBS)
send_ua_SOURCES = send_ua.c IPv6addr_utils.c
send_ua_LDADD = $(LIBNETLIBS)
ocf_SCRIPTS = AoEtarget \
AudibleAlarm \
ClusterMon \
CTDB \
Delay \
Dummy \
EvmsSCC \
Evmsd \
Filesystem \
ICP \
IPaddr \
IPaddr2 \
IPsrcaddr \
LVM \
LinuxSCSI \
lvmlockd \
LVM-activate \
MailTo \
ManageRAID \
ManageVE \
NodeUtilization \
Pure-FTPd \
Raid1 \
Route \
SAPDatabase \
SAPInstance \
SendArp \
ServeRAID \
SphinxSearchDaemon \
Squid \
Stateful \
SysInfo \
VIPArip \
VirtualDomain \
WAS \
WAS6 \
WinPopup \
Xen \
Xinetd \
ZFS \
aliyun-vpc-move-ip \
anything \
apache \
asterisk \
aws-vpc-move-ip \
aws-vpc-route53 \
awseip \
awsvip \
azure-lb \
clvm \
conntrackd \
corosync-qnetd \
crypt \
db2 \
dhcpd \
dnsupdate \
dummypy \
docker \
docker-compose \
dovecot \
eDir88 \
ethmonitor \
exportfs \
fio \
galera \
garbd \
gcp-ilb \
gcp-vpc-move-ip \
iSCSILogicalUnit \
iSCSITarget \
ids \
iface-bridge \
iface-macvlan \
iface-vlan \
ipsec \
iscsi \
jboss \
jira \
kamailio \
lxc \
lxd-info \
machine-info \
mariadb \
mdraid \
minio \
mysql \
mysql-proxy \
nagios \
named \
nfsnotify \
nfsserver \
nginx \
nvmet-subsystem \
nvmet-namespace \
nvmet-port \
ocivip \
openstack-cinder-volume \
openstack-floating-ip \
openstack-info \
openstack-virtual-ip \
oraasm \
oracle \
oralsnr \
osceip \
ovsmonitor \
pgagent \
pgsql \
pingd \
podman \
portblock \
postfix \
pound \
+ powervs-subnet \
proftpd \
rabbitmq-cluster \
rabbitmq-server-ha \
redis \
rkt \
rsyncd \
rsyslog \
scsi2reservation \
sfex \
sg_persist \
mpathpersist \
slapd \
smb-share \
storage-mon \
sybaseASE \
symlink \
syslog-ng \
tomcat \
varnish \
vdo-vol \
vmware \
vsftpd \
zabbixserver
if BUILD_AZURE_EVENTS
ocf_SCRIPTS += azure-events
endif
if BUILD_AZURE_EVENTS_AZ
ocf_SCRIPTS += azure-events-az
endif
if BUILD_GCP_PD_MOVE
ocf_SCRIPTS += gcp-pd-move
endif
if BUILD_GCP_VPC_MOVE_ROUTE
ocf_SCRIPTS += gcp-vpc-move-route
endif
if BUILD_GCP_VPC_MOVE_VIP
ocf_SCRIPTS += gcp-vpc-move-vip
endif
ocfcommondir = $(OCF_LIB_DIR_PREFIX)/heartbeat
ocfcommon_DATA = ocf-shellfuncs \
ocf-binaries \
ocf-directories \
ocf-returncodes \
ocf-rarun \
ocf-distro \
apache-conf.sh \
http-mon.sh \
sapdb-nosha.sh \
sapdb.sh \
lvm-clvm.sh \
lvm-plain.sh \
lvm-tag.sh \
openstack-common.sh \
ora-common.sh \
mysql-common.sh \
nfsserver-redhat.sh \
findif.sh \
ocf.py
# Legacy locations
hbdir = $(sysconfdir)/ha.d
hb_DATA = shellfuncs
check: $(ocf_SCRIPTS:=.check)
%.check: %
OCF_ROOT=$(abs_srcdir) OCF_FUNCTIONS_DIR=$(abs_srcdir) ./$< meta-data | xmllint --path $(abs_srcdir) --noout --relaxng $(abs_srcdir)/metadata.rng -
do_spellcheck = printf '[%s]\n' "$(agent)"; \
OCF_ROOT=$(abs_srcdir) OCF_FUNCTIONS_DIR=$(abs_srcdir) \
./$(agent) meta-data 2>/dev/null \
| xsltproc $(top_srcdir)/make/extract_text.xsl - \
| aspell pipe list -d en_US --ignore-case \
--home-dir=$(top_srcdir)/make -p spellcheck-ignore \
| sed -n 's|^&\([^:]*\):.*|\1|p';
spellcheck:
@$(foreach agent,$(ocf_SCRIPTS), $(do_spellcheck))
clean-local:
rm -rf __pycache__ *.pyc
diff --git a/heartbeat/powervs-subnet.in b/heartbeat/powervs-subnet.in
new file mode 100755
index 000000000..35a2cca60
--- /dev/null
+++ b/heartbeat/powervs-subnet.in
@@ -0,0 +1,1053 @@
+#!@PYTHON@ -tt
+# ------------------------------------------------------------------------
+# Description: Resource Agent to move a Power Virtual Server subnet
+# and its IP address from one virtual server instance
+# to another.
+#
+# Authors: Edmund Haefele
+# Walter Orb
+#
+# Copyright (c) 2024 International Business Machines, 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.
+# ------------------------------------------------------------------------
+
+import ipaddress
+import json
+import os
+import re
+import socket
+import subprocess
+import sys
+import textwrap
+import time
+
+import requests
+import requests.adapters
+import urllib3.util
+
+OCF_FUNCTIONS_DIR = os.environ.get(
+ "OCF_FUNCTIONS_DIR", f"{os.environ.get('OCF_ROOT')}/lib/heartbeat"
+)
+sys.path.append(OCF_FUNCTIONS_DIR)
+
+try:
+ import ocf
+except ImportError:
+ print("ImportError: ocf module import failed.", file=sys.stderr)
+ sys.exit(5)
+
+
+class PowerCloudAPIError(Exception):
+ def __init__(self, message, exit_code):
+ ocf.ocf_exit_reason(message)
+ sys.exit(exit_code)
+
+
+class nmcli:
+ """A wrapper class to run nmcli system commands."""
+
+ NMCLI_SYSTEM_CMD = ["nmcli", "-t"]
+ CONN_PREFIX = "VIP_"
+ DEV_PREFIX = "env"
+ ROUTING_PRIO = 50
+ ROUTING_TABLE = 500
+ _WAIT_FOR_NIC_SLEEP = 3
+
+ def __init__(self):
+ """Class implements only classmethods or staticmethods, instantiation is not used."""
+ pass
+
+ @classmethod
+ def _nmcli_os_cmd(cls, nmcli_args):
+ """run os nmcli command with the specified arguments.
+
+ Returns the output as a dictionary.
+ """
+
+ ocf.logger.debug(f"_nmcli_os_cmd: args: {nmcli_args=}")
+ output = None
+ try:
+ result = subprocess.run(
+ cls.NMCLI_SYSTEM_CMD + nmcli_args,
+ capture_output=True,
+ text=True,
+ check=True,
+ env={"LANG": "C"},
+ )
+ if len(nmcli_args) == 1 or nmcli_args[0] == "-g" or nmcli_args[1] == "show":
+ # return output as dict
+ output = dict(
+ item.split(":", 1)
+ for item in result.stdout.rstrip().splitlines()
+ if ":" in item
+ )
+ except subprocess.CalledProcessError as e:
+ raise PowerCloudAPIError(
+ f"_nmcli_os_cmd: error executing nmcli: {e.stderr}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+ return output
+
+ @classmethod
+ def _nmcli_cmd(cls, command, subcommand=None, name=None, **kwargs):
+ """Prepare arguments to call nmcli command."""
+
+ ocf.logger.debug(f"_nmcli_cmd: args: {command=}, {subcommand=}, {name=}")
+ if command in ["connection", "device"]:
+ nmcli_args = [command]
+ else:
+ raise PowerCloudAPIError(
+ f"_nmcli_cmd: nmcli {command} not implemented",
+ ocf.OCF_ERR_GENERIC,
+ )
+ if name:
+ if subcommand in ("show", "delete", "down", "up"):
+ nmcli_args += [subcommand, name]
+ elif subcommand == "add":
+ nmcli_args += [subcommand, "type", "ethernet", "con-name", name]
+ else:
+ raise PowerCloudAPIError(
+ f"_nmcli_cmd: nmcli {command} {subcommand} not implemented",
+ ocf.OCF_ERR_GENERIC,
+ )
+ elif subcommand in ("add", "delete", "down", "up"):
+ raise PowerCloudAPIError(
+ f"_nmcli_cmd: name argument required for nmcli {command} {subcommand}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+ options = kwargs.get("options", {})
+ for k, v in options.items():
+ nmcli_args += [k, v]
+
+ return cls._nmcli_os_cmd(nmcli_args)
+
+ @classmethod
+ def _nmcli_find(cls, command, match_key, match_value):
+ """Find the network object whose attribute with the specified key matches the specified value."""
+
+ ocf.logger.debug(f"_nmcli_find: args: {command=}, {match_key=}, {match_value=}")
+
+ nm_object = None
+ for name in cls._nmcli_cmd(command=command, subcommand="show"):
+ if not re.search(f"({cls.CONN_PREFIX})?{cls.DEV_PREFIX}", name):
+ # check only connections or devices with device prefix in name
+ continue
+ obj_attrs = cls._nmcli_cmd(command=command, subcommand="show", name=name)
+ if re.search(match_value, obj_attrs.get(match_key, "")):
+ ocf.logger.debug(f"_nmcli_find: found match: {name=}")
+ nm_object = obj_attrs
+ break
+
+ return nm_object
+
+ @classmethod
+ def cleanup(cls):
+ """Cleanup orphaned Network Manager connections."""
+
+ connections = cls._nmcli_os_cmd(["-g", "UUID,NAME,ACTIVE", "connection"])
+ for uuid in connections:
+ name, active = connections[uuid].split(":")
+ if active == "no" and name.startswith(f"{cls.CONN_PREFIX}{cls.DEV_PREFIX}"):
+ ocf.logger.debug(f"nmcli.cleanup: delete orphaned connection {name}")
+ nmcli.connection.delete(uuid)
+
+ @classmethod
+ def wait_for_nic(cls, mac, timeout=720):
+ """Wait for a NIC with a given MAC address to become available."""
+
+ ocf.logger.debug(f"wait_for_nic: args: {mac=}, {timeout=}")
+
+ mac_address = mac.upper()
+ retries = (timeout // cls._WAIT_FOR_NIC_SLEEP) - 1
+
+ for attempt in range(1, retries + 1):
+ try:
+ ocf.logger.debug(
+ f"wait_for_nic: waiting for nic with mac address {mac_address} ..."
+ )
+ nm_object = cls._nmcli_find("device", "GENERAL.HWADDR", mac_address)
+ if nm_object:
+ break
+ finally:
+ time.sleep(cls._WAIT_FOR_NIC_SLEEP)
+ else: # no break
+ raise PowerCloudAPIError(
+ f"wait_for_nic: timeout while waiting for nic with MAC address {mac_address}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+ nic = nm_object.get("GENERAL.DEVICE")
+ wait_time = (attempt - 1) * cls._WAIT_FOR_NIC_SLEEP
+
+ ocf.logger.info(
+ f"wait_for_nic: found network device {nic} with MAC address {mac_address} after waiting {wait_time} seconds"
+ )
+
+ return nic
+
+ @classmethod
+ def find_gateway(cls, ip):
+ """Find the gateway address for a given IP."""
+
+ ocf.logger.debug(f"find_gateway: args: {ip=}")
+
+ gateway = None
+ ip_address = ip.split("/")[0]
+ dev = cls._nmcli_find("device", "IP4.ADDRESS[1]", ip_address)
+ if dev:
+ # Sample IP4.ROUTE[2]: dst = 0.0.0.0/0, nh = 10.10.10.101, mt = 102, table=200
+ # extract next hop (nh) value
+ ip4_route2 = dict(
+ item.split("=")
+ for item in dev["IP4.ROUTE[2]"].replace(" ", "").split(",")
+ )
+ gateway = ip4_route2.get("nh", None)
+
+ return gateway
+
+ class connection:
+ """Provides methods to run nmcli connection commands."""
+
+ @staticmethod
+ def show(name=None, **kwargs):
+ return nmcli._nmcli_cmd("connection", "show", name, **kwargs)
+
+ @staticmethod
+ def add(name, **kwargs):
+ return nmcli._nmcli_cmd("connection", "add", name, **kwargs)
+
+ @staticmethod
+ def delete(name, **kwargs):
+ return nmcli._nmcli_cmd("connection", "delete", name, **kwargs)
+
+ @staticmethod
+ def down(name, **kwargs):
+ return nmcli._nmcli_cmd("connection", "down", name, **kwargs)
+
+ @staticmethod
+ def up(name, **kwargs):
+ return nmcli._nmcli_cmd("connection", "up", name, **kwargs)
+
+ @staticmethod
+ def find(match_key, match_value):
+ return nmcli._nmcli_find("connection", match_key, match_value)
+
+ class device:
+ """Provides methods to run nmcli device commands."""
+
+ @staticmethod
+ def show(name=None, **kwargs):
+ return nmcli._nmcli_cmd("device", "show", name, **kwargs)
+
+ @staticmethod
+ def find(match_key, match_value):
+ return nmcli._nmcli_find("device", match_key, match_value)
+
+
+class PowerCloudAPI:
+ """Provides methods to manage Power Virtual Server resources through its REST API."""
+
+ _URL_IAM_GLOBAL = "https://iam.cloud.ibm.com/identity/token"
+ _URL_API_PUBLIC = "https://{}.power-iaas.cloud.ibm.com"
+ _URL_API_PRIVATE = "https://private.{}.power-iaas.cloud.ibm.com"
+ _URL_API_BASE = "/pcloud/v1/cloud-instances/{}"
+
+ _HTTP_MAX_RETRIES = 10
+ _HTTP_BACKOFF_FACTOR = 0.4
+ _HTTP_STATUS_FORCE_RETRIES = (403, 500, 502, 503, 504)
+ _HTTP_RETRY_ALLOWED_METHODS = frozenset({"GET", "POST", "DELETE"})
+
+ _START_TIME = time.time()
+ _RESOURCE_ACTION_TIMEOUT = int(
+ int(os.environ.get("OCF_RESKEY_CRM_meta_timeout", 7200000)) / 1000
+ )
+
+ def __init__(
+ self,
+ ip="",
+ cidr="",
+ subnet_name="",
+ api_key="",
+ api_type="",
+ region="",
+ crn_host_map="",
+ vsi_host_map="",
+ proxy="",
+ jumbo="",
+ use_remote_workspace=False,
+ ):
+ """Initialize class variables, including the API token, Cloud Resource Name (CRN), IBM Power Cloud API endpoint URL, and HTTP header."""
+
+ self._res_options = locals()
+
+ self._validate_and_set_options()
+ self._set_api_key()
+ self._set_token()
+ self._set_header()
+
+ self._instance_check_status()
+ self.network_id = self._subnet_search_by_cidr()
+
+ def _rest_create_session(self):
+ """Create a request session with a retry strategy."""
+
+ # Define the retry strategy
+ retry_strategy = urllib3.util.Retry(
+ total=self._HTTP_MAX_RETRIES, # Maximum number of retries
+ status_forcelist=self._HTTP_STATUS_FORCE_RETRIES, # HTTP status codes to retry on
+ allowed_methods=self._HTTP_RETRY_ALLOWED_METHODS, # Allowed methods for retry operation
+ backoff_factor=self._HTTP_BACKOFF_FACTOR, # Sleep for {backoff factor} * (2 ** ({number of previous retries}))
+ )
+
+ # Create an HTTP adapter with the retry strategy and mount it to session
+ adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
+
+ # Create a new session object
+ session = requests.Session()
+ session.mount("https://", adapter)
+
+ self._session = session
+
+ return session
+
+ def _rest_api_call(self, method, resource, **kwargs):
+ """Perform a REST call to the specified URL."""
+
+ url = self._url + self._base + resource
+ method = method.upper()
+ ocf.logger.debug(f"_rest_api_call: {method} {resource}")
+
+ session = self._session or self._rest_create_session()
+
+ r = session.request(
+ method, url, headers=self._header, proxies=self._proxy, **kwargs
+ )
+ if not r.ok:
+ raise PowerCloudAPIError(
+ f"_rest_api_call: {method} call {resource} to {url} failed with reason: {r.reason}, status code: {r.status_code}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+ return r.json()
+
+ def _set_api_key(self):
+ """Store an API key in a class variable.
+
+ api_key is a string. If the first character of the string is @,
+ the rest of the string is assumed to be the name of a file containing the API key.
+ """
+
+ api_key = self._res_options["api_key"]
+ if api_key[0] == "@":
+ api_key_file = api_key[1:]
+ try:
+ with open(api_key_file, "r") as f:
+ # read the API key from a file
+ try:
+ keys = json.loads(f.read())
+ # data seems to be in json format
+ # return the value of the item with the key 'apikey'
+ api_key = keys.get("apikey", keys)
+ except ValueError:
+ # data is text, return as is
+ api_key = f.read().strip()
+ except FileNotFoundError:
+ raise PowerCloudAPIError(
+ f"_set_api_key: API key file '{api_key_file}' not found",
+ ocf.OCF_ERR_ARGS,
+ )
+
+ self._api_key = api_key
+
+ def _set_token(self):
+ """Use the stored API key to obtain an IBM Cloud IAM access token."""
+
+ url = self._URL_IAM_GLOBAL
+
+ headers = {
+ "content-type": "application/x-www-form-urlencoded",
+ "accept": "application/json",
+ }
+ data = {
+ "grant_type": "urn:ibm:params:oauth:grant-type:apikey",
+ "apikey": f"{self._api_key}",
+ }
+ token_response = requests.post(
+ url, headers=headers, data=data, proxies=self._proxy
+ )
+ if token_response.status_code != 200:
+ raise PowerCloudAPIError(
+ f"_set_token: failed to obtain token from IBM Cloud IAM: {token_response.status_code}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+ self._token = json.loads(token_response.text)["access_token"]
+
+ def _set_header(self):
+ """Set the Cloud Resource Name (CRN), IBM Power Cloud API endpoint URL, and HTTP header."""
+
+ self._header = {
+ "Authorization": f"Bearer {self._token}",
+ "CRN": f"{self._crn}",
+ "Content-Type": "application/json",
+ }
+
+ def _instance_check_status(self):
+ """Check if instance exists in workspace and log the current status."""
+
+ resource = f"/pvm-instances/{self.instance_id}"
+ instance = self._rest_api_call("GET", resource)
+
+ server_name = instance["serverName"]
+ status = instance["status"]
+ health = instance["health"]["status"]
+
+ if status == "SHUTOFF" or (status == "ACTIVE" and health == "OK"):
+ ocf.logger.debug(
+ f"_instance_check_status: {server_name=}, {status=}, {health=}"
+ )
+ else:
+ raise PowerCloudAPIError(
+ f"_instance_check_status: invalid status: {server_name=} {status=}, {health=}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+ def _instance_subnet_is_attached(self):
+ """Check if a virtual server instance is connected to a specific subnet."""
+
+ for net in self._instance_subnet_list():
+ if self.network_id == net["networkID"]:
+ return True
+ return False
+
+ def _instance_subnet_get(self):
+ """Obtain information about a particular subnet connected to a virtual server instance."""
+
+ resource = f"/pvm-instances/{self.instance_id}/networks/{self.network_id}"
+ response = self._rest_api_call("GET", resource)
+ return response["networks"][0]
+
+ def _instance_subnet_list(self):
+ """List all subnets connected to a virtual server instance."""
+
+ resource = f"/pvm-instances/{self.instance_id}/networks"
+ response = self._rest_api_call("GET", resource)
+ return response["networks"]
+
+ def _instance_subnet_attach(self):
+ """Attach a subnet to a virtual server instance."""
+
+ data = (
+ f'{{"networkID":"{self.network_id}","ipAddress":"{self.ip}"}}'
+ if self.ip
+ else f'{{"networkID":"{self.network_id}"}}'
+ )
+
+ resource = f"/pvm-instances/{self.instance_id}/networks/"
+ _ = self._rest_api_call("POST", resource, data=data)
+
+ def _instance_subnet_detach(self):
+ """Detach a subnet from a virtual server instance."""
+
+ resource = f"/pvm-instances/{self.instance_id}/networks/{self.network_id}"
+ _ = self._rest_api_call("DELETE", resource)
+
+ def _subnet_create(self):
+ """Create a subnet in the workspace."""
+
+ data = (
+ f'{{"type":"vlan","cidr":"{self.cidr}","mtu":9000,"name":"{self.subnet_name}"}}'
+ if self.jumbo
+ else f'{{"type":"vlan","cidr":"{self.cidr}","name":"{self.subnet_name}"}}'
+ )
+ resource = "/networks"
+ response = self._rest_api_call("POST", resource, data=data)
+ self.network_id = response["networkID"]
+
+ def _subnet_delete(self):
+ """Delete a subnet in the workspace."""
+
+ resource = f"/networks/{self.network_id}"
+ _ = self._rest_api_call("DELETE", resource)
+
+ def _subnet_get(self, network_id):
+ """Get information about a specific subnet in the workspace."""
+
+ resource = f"/networks/{network_id}"
+ response = self._rest_api_call("GET", resource)
+ return response
+
+ def _subnet_list(self):
+ """List all subnets in the workspace."""
+
+ resource = "/networks/"
+ response = self._rest_api_call("GET", resource)
+ return response
+
+ def _subnet_search_by_cidr(self):
+ """Find the subnet for a given CIDR."""
+
+ for network in self._subnet_list()["networks"]:
+ network_id = network["networkID"]
+ if self.cidr == self._subnet_get(network_id)["cidr"]:
+ return network_id
+
+ return None
+
+ def _validate_and_set_options(self):
+ """Validate the options of the resource agent and derive class variables from the options."""
+
+ ip = self._res_options["ip"]
+ try:
+ validated_ip = ipaddress.ip_address(ip)
+ except ValueError:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {ip} is not a valid IP address.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+ self.ip = ip
+
+ cidr = self._res_options["cidr"]
+ try:
+ validated_cidr = ipaddress.ip_network(cidr)
+ except ValueError:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {cidr} is not a valid CIDR notation.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+ self.cidr = cidr
+
+ if validated_ip not in validated_cidr:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {ip} is not in {cidr} range.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+
+ subnet_name = self._res_options["subnet_name"]
+ self.subnet_name = subnet_name if subnet_name else self.cidr
+
+ crn_host_map = self._res_options["crn_host_map"]
+ try:
+ self._crn_host_map = dict(
+ item.split(":", 1) for item in crn_host_map.split(";")
+ )
+ except ValueError:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {crn_host_map=} has an invalid format.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+
+ self._hostname = os.uname().nodename
+ if self._res_options["use_remote_workspace"]:
+ self._nodename = [k for k in self._crn_host_map if k != self._hostname][0]
+ else:
+ self._nodename = self._hostname
+
+ if self._nodename not in self._crn_host_map:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {self._nodename} not found in {crn_host_map=}.",
+ ocf.OCF_ERR_ARGS,
+ )
+ self._crn = self._crn_host_map[self._nodename]
+
+ try:
+ self._cloud_instance_id = self._crn.split(":")[7]
+ except IndexError:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {self._crn} is not a valid CRN.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+
+ vsi_host_map = self._res_options["vsi_host_map"]
+ try:
+ self._vsi_host_map = dict(
+ item.split(":") for item in vsi_host_map.split(";")
+ )
+ except ValueError:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: the {vsi_host_map=} option has an invalid format.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+
+ if self._nodename not in self._vsi_host_map:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {self._nodename} not found in {vsi_host_map=}.",
+ ocf.OCF_ERR_ARGS,
+ )
+ self.instance_id = self._vsi_host_map[self._nodename]
+
+ jumbo = self._res_options["jumbo"].lower()
+ if ocf.is_true(jumbo):
+ self.jumbo = True
+ else:
+ if jumbo not in ("no", "false", "0", 0, "nein", "off", False):
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {jumbo=} does not match True or False.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+ self.jumbo = False
+
+ # Check connect to proxy server
+ self._proxy = ""
+ proxy = self._res_options["proxy"]
+
+ if proxy:
+ # extract ip address and port
+ match = re.search(r"^https?://([^:]+):(\d+)$", proxy)
+ if match:
+ proxy_ip, proxy_port = match.group(1), match.group(2)
+
+ try:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.settimeout(30)
+ s.connect((proxy_ip, int(proxy_port)))
+ except socket.error:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: cannot connect to port {proxy_port} at {proxy_ip}, check {proxy=} option.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+ self._proxy = {"https": f"{proxy}"}
+ else:
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: the {proxy=} option has an invalid format.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+
+ api_type = self._res_options["api_type"]
+ if api_type not in ("public", "private"):
+ raise PowerCloudAPIError(
+ f"_validate_and_set_options: {api_type=} does not match public or private.",
+ ocf.OCF_ERR_CONFIGURED,
+ )
+ # Set API endpoint url
+ url_api_fmt = (
+ self._URL_API_PRIVATE if api_type == "private" else self._URL_API_PUBLIC
+ )
+ self._url = url_api_fmt.format(self._res_options["region"])
+ self._base = self._URL_API_BASE.format(self._cloud_instance_id)
+ self._session = None
+
+ def subnet_add(self):
+ """Create and attach subnet in local workspace"""
+
+ ocf.logger.debug(
+ f"subnet_add: options: {self.ip=}, {self.cidr=}, {self.subnet_name=}"
+ )
+
+ if self.network_id:
+ ocf.logger.debug(
+ f"subnet_add: subnet {self.cidr=} already exists with {self.network_id=}"
+ )
+ else:
+ ocf.logger.debug(
+ f"subnet_add: create subnet {self.subnet_name=} with {self.cidr=} and {self.jumbo=}"
+ )
+ self._subnet_create()
+
+ if self._instance_subnet_is_attached():
+ ocf.logger.debug(
+ f"subnet_add: subnet {self.network_id=} is already attached to instance {self.instance_id=}"
+ )
+ else:
+ ocf.logger.debug(
+ f"subnet_add: attach subnet {self.network_id=} to instance {self.instance_id=} (IP address {self.ip=})"
+ )
+ self._instance_subnet_attach()
+
+ subnet = self._subnet_get(self.network_id)
+ gateway = subnet["gateway"]
+ port = self._instance_subnet_get()
+ mac = port["macAddress"]
+ ip_address = port["ipAddress"]
+ self.jumbo = subnet.get("mtu", "") == 9000
+
+ timeout = self._RESOURCE_ACTION_TIMEOUT - int(time.time() - self._START_TIME)
+ nic = nmcli.wait_for_nic(mac, timeout)
+
+ return nic, ip_address, mac, gateway
+
+ def subnet_remove(self):
+ """Detach and delete subnet in local or remote workspace"""
+
+ ocf.logger.debug(
+ f"subnet_remove: options: {self.cidr=}, {self.network_id=}, {self.instance_id}"
+ )
+
+ if self.network_id:
+ ocf.logger.debug(
+ f"subnet_remove: subnet {self.network_id=} with {self.cidr=} exists"
+ )
+ if self._instance_subnet_is_attached():
+ ocf.logger.debug(
+ f"subnet_remove: subnet {self.network_id=} is attached to instance {self.instance_id=}"
+ )
+ port = self._instance_subnet_get()
+ mac = port["macAddress"]
+ dev = nmcli.device.find("GENERAL.HWADDR", mac.upper())
+
+ if dev:
+ nm_object = nmcli.connection.find(
+ "GENERAL.IP-IFACE", dev["GENERAL.DEVICE"]
+ )
+ if nm_object:
+ conn_name = nm_object["connection.id"]
+ ocf.logger.debug(
+ f"stop_action: unconfigure network connection {conn_name=} with mac address {mac=}"
+ )
+ nmcli.connection.down(conn_name)
+ nmcli.connection.delete(conn_name)
+ ocf.logger.debug(
+ f"subnet_remove: detach network {self.network_id=} from instance {self.instance_id=}"
+ )
+ self._instance_subnet_detach()
+ ocf.logger.debug(f"subnet_remove: delete network {self.network_id=}")
+ self._subnet_delete()
+
+
+def os_ping(ip):
+ """Ping an IP address."""
+
+ command = ["ping", "-c", "1", ip]
+ response = subprocess.call(command)
+ return response == 0
+
+
+def start_action(
+ ip="",
+ cidr="",
+ subnet_name="",
+ api_key="",
+ api_type="",
+ region="",
+ crn_host_map="",
+ vsi_host_map="",
+ proxy="",
+ jumbo="",
+):
+ """start_action: assign the service ip.
+
+ Create a subnet in the workspace, connect it to the virtual server instance, and configure the NIC.
+ """
+
+ res_options = locals()
+
+ ocf.logger.info(f"start_action: {res_options=}")
+
+ # Detach and remove subnet in remote workspace
+ remote_ws = PowerCloudAPI(**res_options, use_remote_workspace=True)
+ ocf.logger.debug(
+ f"start_action: remove subnet from remote workspace: {remote_ws.cidr=}"
+ )
+ remote_ws.subnet_remove()
+
+ # Delete orphaned Network Manager connections
+ nmcli.cleanup()
+
+ # Create and attach subnet in local workspace
+ ws = PowerCloudAPI(**res_options)
+
+ nic, ip_address, mac, gateway = ws.subnet_add()
+
+ ocf.logger.debug(
+ f"start_action: add nmcli connection: {nic=}, {ip_address=}, {mac=}, {gateway=}, {ws.jumbo=}"
+ )
+
+ conn_name = f"{nmcli.CONN_PREFIX}{nic}"
+ conn_options = {
+ "ifname": nic,
+ "autoconnect": "no",
+ "ipv4.method": "manual",
+ "ipv4.addresses": ip_address,
+ "ipv4.routes": f"0.0.0.0/0 {gateway} table={nmcli.ROUTING_TABLE}",
+ "ipv4.routing-rules": f"priority {nmcli.ROUTING_PRIO} from {ws.cidr} table {nmcli.ROUTING_TABLE}",
+ }
+ if ws.jumbo:
+ conn_options.update({"802-3-ethernet.mtu": "9000", "ethtool.feature-tso": "on"})
+
+ nmcli.connection.add(conn_name, options=conn_options)
+ nmcli.connection.up(conn_name)
+
+ if monitor_action(**res_options) != ocf.OCF_SUCCESS:
+ raise PowerCloudAPIError(f"start_action: start {ws.subnet_name=} failed")
+
+ ocf.logger.info(
+ f"start_action: finished, added connection {conn_name} for subnet {ws.subnet_name}"
+ )
+
+ return ocf.OCF_SUCCESS
+
+
+def stop_action(
+ ip="",
+ cidr="",
+ subnet_name="",
+ api_key="",
+ api_type="",
+ region="",
+ crn_host_map="",
+ vsi_host_map="",
+ proxy="",
+ jumbo="",
+):
+ """stop_action: unassign the service ip.
+
+ Delete NIC, detach subnet from virtual server instance, and delete subnet.
+ """
+
+ res_options = locals()
+
+ ocf.logger.info(f"stop_action: {res_options=}")
+
+ ws = PowerCloudAPI(**res_options)
+
+ ws.subnet_remove()
+
+ if monitor_action(**res_options) != ocf.OCF_NOT_RUNNING:
+ raise PowerCloudAPIError(f"stop_action: stop {ws.subnet_name=} failed")
+
+ ocf.logger.info(
+ f"stop_action: finished, deleted connection for subnet {ws.subnet_name}"
+ )
+
+ return ocf.OCF_SUCCESS
+
+
+def monitor_action(
+ ip="",
+ cidr="",
+ subnet_name="",
+ api_key="",
+ api_type="",
+ region="",
+ crn_host_map="",
+ vsi_host_map="",
+ proxy="",
+ jumbo="",
+):
+ """monitor_action: check if service ip and gateway are responding."""
+
+ res_options = locals()
+ is_probe = ocf.is_probe()
+
+ ocf.logger.debug(f"monitor_action: {res_options=}, {is_probe=}")
+
+ gateway = nmcli.find_gateway(ip)
+ if gateway and os_ping(gateway):
+ if os_ping(ip):
+ ocf.logger.debug(
+ f"monitor_action: ping to {gateway=} and service {ip=} successful"
+ )
+ return ocf.OCF_SUCCESS
+ else:
+ raise PowerCloudAPIError(
+ f"monitor_action: ping to service {ip=} failed", ocf.OCF_ERR_GENERIC
+ )
+
+ if not is_probe:
+ ocf.logger.error(f"monitor_action: ping to {gateway=} failed")
+
+ ws = PowerCloudAPI(**res_options)
+
+ ocf.logger.debug(f"monitor_action: {ws.instance_id=}")
+
+ if not ws.network_id or is_probe:
+ return ocf.OCF_NOT_RUNNING
+
+ # monitor should never reach this code, exit with raise
+ raise PowerCloudAPIError(
+ f"monitor_action: unkown problem with subnet {ws.network_id=}",
+ ocf.OCF_ERR_GENERIC,
+ )
+
+
+def validate_all_action(
+ ip="",
+ cidr="",
+ subnet_name="",
+ api_key="",
+ api_type="",
+ region="",
+ crn_host_map="",
+ vsi_host_map="",
+ proxy="",
+ jumbo="",
+):
+ """validate_all_action: Validate the resource agent parameters."""
+
+ res_options = locals()
+
+ # The class instantation validates the resource agent options and that the instance exists
+ try:
+ # Check instance in local workspace
+ _ = PowerCloudAPI(**res_options, use_remote_workspace=False)
+ except Exception:
+ ocf.logger.error(
+ "validate_all_action: failed to instantiate class in local workspace."
+ )
+ raise
+
+ try:
+ # Check instance in remote workspace
+ _ = PowerCloudAPI(**res_options, use_remote_workspace=True)
+ except Exception:
+ ocf.logger.error(
+ "validate_all_action: failed to instantiate class in remote workspace."
+ )
+ raise
+
+ return ocf.OCF_SUCCESS
+
+
+def main():
+ """Instantiate the resource agent."""
+
+ agent_description = textwrap.dedent("""\
+ Resource Agent to move a Power Virtual Server subnet and its IP address
+ from one virtual server instance to another.
+ The prerequisites for the use of this resource agent are as follows:
+
+ 1. Red Hat Enterprise Linux 9.2 or higher:
+ Install with @server group to ensure that NetworkManager settings are correct.
+ Verify that the NetworkManager-config-server package is installed.
+
+ 2. IBM Cloud API Key:
+ Create a service API key that is privileged for both Power Virtual Server
+ workspaces. Save the service API key in a file and copy the file to both
+ cluster nodes. Use same filename and directory location on both cluster nodes.
+ Reference the path to the key file in the resource definition.
+
+ 3. The hostname of the virtual server instances must be same as the name
+ of the virtual server instances in the Power Virtual Server workspaces.
+
+ For comprehensive documentation on implementing high availability for
+ SAP applications on IBM Power Virtual Server, visit https://cloud.ibm.com/docs/sap?topic=sap-ha-overview.
+ """)
+
+ agent = ocf.Agent(
+ "powervs-subnet",
+ shortdesc="Manages moving a Power Virtual Server subnet",
+ longdesc=agent_description,
+ )
+
+ agent.add_parameter(
+ "ip",
+ shortdesc="IP address",
+ longdesc=(
+ "IP address within the subnet. The IP address moves together with the subnet."
+ ),
+ content_type="string",
+ required=True,
+ )
+
+ agent.add_parameter(
+ "cidr",
+ shortdesc="CIDR",
+ longdesc="Classless Inter-Domain Routing (CIDR) of the subnet.",
+ content_type="string",
+ required=True,
+ )
+
+ agent.add_parameter(
+ "subnet_name",
+ shortdesc="Name of the subnet",
+ longdesc="Name of the subnet. If not specified, CIDR is used as name.",
+ content_type="string",
+ required=False,
+ )
+
+ agent.add_parameter(
+ "api_type",
+ shortdesc="API type",
+ longdesc="Connect to Power Virtual Server regional endpoints over a public or private network (public|private).",
+ content_type="string",
+ required=True,
+ default="private",
+ )
+
+ agent.add_parameter(
+ "region",
+ shortdesc="Power Virtual Server region",
+ longdesc=(
+ "Region that represents the geographic area where the instance is located. "
+ "The region is used to identify the Cloud API endpoint."
+ ),
+ content_type="string",
+ required=True,
+ )
+
+ agent.add_parameter(
+ "api_key",
+ shortdesc="API Key or @API_KEY_FILE_PATH",
+ longdesc=(
+ "API Key or @API_KEY_FILE_PATH for IBM Cloud access. "
+ "The API key content or the path of an API key file that is indicated by the @ symbol."
+ ),
+ content_type="string",
+ required=True,
+ )
+
+ agent.add_parameter(
+ "crn_host_map",
+ shortdesc="Mapping of hostnames to IBM Cloud CRN",
+ longdesc=(
+ "Map the hostname of the Power Virtual Server instance to the CRN of the Power Virtual Server workspaces hosting the instance. "
+ "Separate hostname and CRN with a colon ':', separate different hostname and CRN pairs with a semicolon ';'. "
+ "Example: hostname01:CRN-of-Instance01;hostname02:CRN-of-Instance02"
+ ),
+ content_type="string",
+ required=True,
+ )
+
+ agent.add_parameter(
+ "vsi_host_map",
+ shortdesc="Mapping of hostnames to PowerVS instance ids",
+ longdesc=(
+ "Map the hostname of the Power Virtual Server instance to its instance id. "
+ "Separate hostname and instance id with a colon ':', separate different hostname and instance id pairs with a semicolon ';'. "
+ "Example: hostname01:instance-id-01;hostname02:instance-id-02"
+ ),
+ content_type="string",
+ required=True,
+ )
+
+ agent.add_parameter(
+ "proxy",
+ shortdesc="Proxy",
+ longdesc="Proxy server to access IBM Cloud API endpoints.",
+ content_type="string",
+ required=False,
+ )
+
+ agent.add_parameter(
+ "jumbo",
+ shortdesc="Use Jumbo frames",
+ longdesc="Create a Power Virtual Server subnet with an MTU size of 9000 (true|false).",
+ content_type="string",
+ required=False,
+ default="false",
+ )
+
+ agent.add_action("start", timeout=900, handler=start_action)
+ agent.add_action("stop", timeout=450, handler=stop_action)
+ agent.add_action(
+ "monitor", depth=0, timeout=60, interval=60, handler=monitor_action
+ )
+ agent.add_action("validate-all", timeout=300, handler=validate_all_action)
+ agent.run()
+
+
+if __name__ == "__main__":
+ main()

File Metadata

Mime Type
text/x-diff
Expires
Tue, Oct 29, 7:52 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
939034
Default Alt Text
(53 KB)

Event Timeline