Page MenuHomeClusterLabs Projects

No OneTemporary

diff --git a/heartbeat/powervs-subnet.in b/heartbeat/powervs-subnet.in
index 087623060..83a468e02 100755
--- a/heartbeat/powervs-subnet.in
+++ b/heartbeat/powervs-subnet.in
@@ -1,1106 +1,1109 @@
#!@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 math
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", "%s/lib/heartbeat" % os.environ.get("OCF_ROOT")
)
sys.path.append(OCF_FUNCTIONS_DIR)
try:
import ocf
except ImportError:
sys.stderr.write("ImportError: ocf module import failed.")
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 = ocf.get_parameter("route_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("_nmcli_os_cmd: args: {}".format(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: {command}, subcommand: {subcommand}, name: {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: {command}, key: {match_key}, value: {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: {name}")
nm_object = obj_attrs
break
return nm_object
@classmethod
def cleanup(cls):
"""Clean up 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: {mac}, timeout: {timeout} s")
mac_address = mac.upper()
retries = math.ceil((timeout * 0.95) / 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: {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_IAM_PRIVATE = "https://private.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 = (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'
# backward compatibility: In the past, the key name was 'apikey'
api_key = keys.get("Apikey", "")
if not api_key:
api_key = keys.get("apikey", "")
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
+ url = self._URL_IAM
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: OK server_name: {server_name}, status: {status}, health: {health}"
)
else:
if not (self._ocf_action == "monitor"):
raise PowerCloudAPIError(
f"_instance_check_status: FAIL server_name: {server_name}, status: {status}, health: {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 _subnet_port_get_all(self):
"""Obtain information about the ports for a specific subnet."""
resource = f"/networks/{self.network_id}/ports"
response = self._rest_api_call("GET", resource)
return response["ports"]
def _subnet_port_delete(self, port_id):
"""Delete an orphaned port for a particular subnet."""
resource = f"/networks/{self.network_id}/ports/{port_id}"
_ = self._rest_api_call("DELETE", resource)
def _subnet_port_get_reserved(self):
"""Check if a port is already reserved on the subnet for the IP address."""
for port in self._subnet_port_get_all():
if self.ip == port["ipAddress"]:
return port["portID"]
return None
def _validate_and_set_options(self):
"""Validate the options of the resource agent and derive class variables from the options."""
self._ocf_action = os.environ.get("__OCF_ACTION")
if self._ocf_action is None and len(sys.argv) == 2:
self._ocf_action = sys.argv[1]
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: {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: {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: Option vsi_host_map: {vsi_host_map} 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: {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: option jumbo: {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 option proxy: {proxy}.",
ocf.OCF_ERR_CONFIGURED,
)
self._proxy = {"https": f"{proxy}"}
else:
raise PowerCloudAPIError(
f"_validate_and_set_options: the option proxy: {proxy} 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: option api_type: {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._URL_IAM = (
+ self._URL_IAM_PRIVATE if api_type == "private" else self._URL_IAM_GLOBAL
+ )
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: ip: {self.ip}, cidr: {self.cidr}, name: {self.subnet_name}"
)
if self.network_id:
ocf.logger.debug(
f"subnet_add: subnet cidr: {self.cidr} already exists with network id: {self.network_id}"
)
else:
ocf.logger.debug(
f"subnet_add: create subnet name: {self.subnet_name} with cidr: {self.cidr} and jumbo: {self.jumbo}"
)
self._subnet_create()
if self._instance_subnet_is_attached():
ocf.logger.debug(
f"subnet_add: subnet id {self.network_id} is already attached to instance id {self.instance_id}"
)
else:
ocf.logger.debug(
f"subnet_add: attach subnet id: {self.network_id} to instance id: {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: cidr: {self.cidr}, network id: {self.network_id}, instance id: {self.instance_id}"
)
if self.network_id:
ocf.logger.debug(
f"subnet_remove: subnet id: {self.network_id} with cidr: {self.cidr} exists"
)
if self._instance_subnet_is_attached():
ocf.logger.debug(
f"subnet_remove: subnet id: {self.network_id} is attached to instance id {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: {conn_name} with mac address {mac}"
)
nmcli.connection.down(conn_name)
nmcli.connection.delete(conn_name)
ocf.logger.debug(
f"subnet_remove: detach network id: {self.network_id} from instance id: {self.instance_id}"
)
self._instance_subnet_detach()
port_id = self._subnet_port_get_reserved()
if port_id:
ocf.logger.debug(
f"subnet_remove: delete port port_id: {port_id} for subnet network id: {self.network_id}"
)
self._subnet_port_delete(port_id)
ocf.logger.debug(f"subnet_remove: delete network id: {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: options: {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: cidr: {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: {nic}, ip: {ip_address}, mac: {mac}, gateway: {gateway}, jumbo: {ws.jumbo}, table {nmcli.ROUTING_TABLE}"
)
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 subnet: {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: options: {res_options}")
ws = PowerCloudAPI(**res_options)
ws.subnet_remove()
if monitor_action(**res_options) != ocf.OCF_NOT_RUNNING:
raise PowerCloudAPIError(f"stop_action: stop subnet {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: options: {res_options}, is_probe: {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: {gateway} and ip: {ip} successful"
)
return ocf.OCF_SUCCESS
else:
raise PowerCloudAPIError(
f"monitor_action: ping to ip: {ip} failed", ocf.OCF_ERR_GENERIC
)
if not is_probe:
ocf.logger.error(f"monitor_action: ping to gateway: {gateway} failed")
ws = PowerCloudAPI(**res_options)
ocf.logger.debug(f"monitor_action: instance id: {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: unknown problem with subnet id: {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 instantiation 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:
+ 2. A two-node cluster that is distributed across two different Power Virtual Server workspaces in two data centers in a region.
+
+ 3. 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,
- version=1.03,
+ version=1.04,
)
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_parameter(
"route_table",
shortdesc="route table ID",
longdesc="ID of the route table for the interface. Default is 500.",
content_type="string",
required=False,
default="500",
)
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
Wed, Jun 25, 4:13 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1952076
Default Alt Text
(39 KB)

Event Timeline