Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F1701861
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
53 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Oct 29, 7:52 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
939034
Default Alt Text
(53 KB)
Attached To
Mode
rR Resource Agents
Attached
Detach File
Event Timeline
Log In to Comment