Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F3154207
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
38 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/heartbeat/powervs-subnet.in b/heartbeat/powervs-subnet.in
index 83a468e02..84e86c0c4 100755
--- a/heartbeat/powervs-subnet.in
+++ b/heartbeat/powervs-subnet.in
@@ -1,1109 +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
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. 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.
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.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,
+ required=False,
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
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Feb 26, 11:06 AM (22 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1452577
Default Alt Text
(38 KB)
Attached To
Mode
rR Resource Agents
Attached
Detach File
Event Timeline
Log In to Comment