#!/usr/bin/python3 -tt
# ------------------------------------------------------------------------
# Description: Resource agent for moving an overlay IP address between
#              virtual server instances in different PowerVS workspaces.
#
# Authors:      Edmund Haefele
#               Walter Orb
#
# Copyright (c) 2025 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 fcntl
import ipaddress
import json
import os
import socket
import subprocess
import sys
import textwrap
import time
from pathlib import Path
from urllib.parse import urlparse

try:
    sys.path.insert(0, '/usr/lib/fence-agents/support/ibm')
    import requests
    import requests.adapters
    import urllib3.util
except ImportError:
    pass

# Constants
OCF_FUNCTIONS_DIR = os.environ.get(
    "OCF_FUNCTIONS_DIR", "%s/lib/heartbeat" % os.environ.get("OCF_ROOT")
)
RESOURCE_OPTIONS = (
    "ip",
    "api_key",
    "api_type",
    "region",
    "route_host_map",
    "use_token_cache",
    "monitor_api",
    "device",
    "iflabel",
    "proxy",
)
IP_CMD = "/usr/sbin/ip"
IFLABEL_MAX_LEN = 15  # Maximum character limit for interface labels
REQUESTS_TIMEOUT = 5  # Timeout for requests calls
HTTP_MAX_RETRIES = 4  # Maximum number of retries for HTTP requests
HTTP_BACKOFF_FACTOR = 0.3  # Sleep (factor * (2^number of previous retries)) secs
HTTP_STATUS_FORCE_RETRIES = (500, 502, 503, 504)  # HTTP status codes to retry on
HTTP_RETRY_ALLOWED_METHODS = frozenset({"GET", "POST", "PUT", "DELETE"})
CIDR_NETMASK = "32"

sys.path.append(OCF_FUNCTIONS_DIR)
try:
    import ocf
except ImportError:
    sys.stderr.write("ImportError: ocf module import failed.")
    sys.exit(5)


class OCFExitError(Exception):
    """Exception class for OCF (Open Cluster Framework) exit errors."""

    def __init__(self, message, exit_code):
        ocf.ocf_exit_reason(message)
        sys.exit(exit_code)


class CmdError(OCFExitError):
    """Exception class for errors when running system commands."""

    def __init__(self, message, exit_code):
        super().__init__(f"[CmdError] {message}", exit_code)


def os_cmd(cmd_args, is_json=False, timeout=10):
    """Run a system command and optionally parse JSON output."""
    ocf.logger.debug(f"[os_cmd]: args: {cmd_args}")
    try:
        result = subprocess.run(
            cmd_args,
            capture_output=True,
            text=True,
            check=True,
            timeout=timeout,
            env={"LANG": "C"},
        )
        if is_json:
            try:
                return json.loads(result.stdout)
            except json.JSONDecodeError as e:
                raise CmdError(f"os_cmd: JSON parsing failed: {e}", ocf.OCF_ERR_GENERIC)

        return result.returncode

    except subprocess.CalledProcessError as e:
        raise CmdError(
            f"os_cmd: command failed: {e.stderr}",
            ocf.OCF_ERR_GENERIC,
        )
    except subprocess.TimeoutExpired:
        raise CmdError("os_cmd: command timed out", ocf.OCF_ERR_GENERIC)


def ip_cmd(*args, is_json=False):
    """Generic wrapper for the ip command."""
    return os_cmd([IP_CMD] + list(args), is_json=is_json)


def ip_address_show():
    """Show IP addresses in JSON format."""
    return ip_cmd("-json", "address", "show", is_json=True)


def ip_address_add(cidr, device, label=None):
    """Add an IP address to a device."""
    cmd = ["address", "add", cidr, "dev", device]
    if label:
        cmd += ["label", label]
    return ip_cmd(*cmd)


def ip_address_delete(cidr, device):
    """Delete an IP address from a device."""
    return ip_cmd("address", "delete", cidr, "dev", device)


def ip_find_device(ip):
    """Find the device associated with a given IP address."""
    for iface in ip_address_show():
        addresses = [a["local"] for a in iface["addr_info"]]
        if ip in addresses and "UP" in iface["flags"]:
            return iface["ifname"]

    return None


def ip_check_device(device):
    """Verify that a device with the specified interface name (device) exists."""
    for iface in ip_address_show():
        if iface["ifname"] == device and "UP" in iface["flags"]:
            return True

    return False


def ip_alias_add(ip, device, label=None):
    """Add an IP alias to the given device."""
    ip_cidr = f"{ip}/{CIDR_NETMASK}"
    ocf.logger.debug(
        f"[ip_alias_add]: adding IP alias '{ip_cidr}' with label '{label}' to interface '{device}'"
    )
    _ = ip_address_add(ip_cidr, device, label)


def ip_alias_remove(ip):
    """Find the device with the given IP alias and remove the alias."""
    device = ip_find_device(ip)
    if device:
        ip_cidr = f"{ip}/{CIDR_NETMASK}"
        ocf.logger.debug(
            f"[ip_alias_remove]: removing IP alias '{ip_cidr}' from interface '{device}'"
        )
        _ = ip_address_delete(ip_cidr, device)


def create_session_with_retries():
    """Create a request session with a retry strategy."""
    retry_strategy = urllib3.util.Retry(
        total=HTTP_MAX_RETRIES,
        status_forcelist=HTTP_STATUS_FORCE_RETRIES,
        allowed_methods=HTTP_RETRY_ALLOWED_METHODS,
        backoff_factor=HTTP_BACKOFF_FACTOR,
        raise_on_status=False,
    )
    adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
    session = requests.Session()
    session.mount("https://", adapter)
    return session


class PowerCloudTokenManagerError(OCFExitError):
    """Exception class for errors in the PowerCloudTokenManager."""

    def __init__(self, message, exit_code):
        super().__init__(f"[PowerCloudTokenManagerError] {message}", exit_code)


class PowerCloudTokenManager:
    """Request and cache IBM Cloud tokens."""

    _DEFAULT_RESOURCE_INSTANCE = "powervs-move-ip"
    _TOKEN_REFRESH_BUFFER = 900  # 15 minutes

    def __init__(
        self,
        api_type="",
        api_key="",
        proxy="",
        use_cache=False,
    ):
        self._auth_url = (
            "https://private.iam.cloud.ibm.com/identity/token"
            if api_type == "private"
            else "https://iam.cloud.ibm.com/identity/token"
        )
        self._api_key = self._load_api_key(api_key)
        self._proxy = proxy
        self._session = create_session_with_retries()
        self._cache_file = None

        if use_cache:
            resource_instance = os.environ.get(
                "OCF_RESOURCE_INSTANCE", self._DEFAULT_RESOURCE_INSTANCE
            )
            self._cache_file = Path(
                f"/var/run/resource-agents/{resource_instance}-token.json"
            )
            self._cache_file.parent.mkdir(parents=True, exist_ok=True)
            if not self._cache_file.exists():
                self._cache_file.touch()
                os.chmod(self._cache_file, 0o600)

    def _load_api_key(self, api_key):
        """Load API key from string or file."""
        if not api_key:
            raise PowerCloudTokenManagerError(
                "_load_api_key: API key is missing",
                ocf.OCF_ERR_CONFIGURED,
            )

        # API key in string
        if not api_key.startswith("@"):
            return api_key

        # API key in file
        api_key_path = Path(api_key[1:])
        if not api_key_path.is_file():
            raise PowerCloudTokenManagerError(
                f"_load_api_key: API key file not found: '{api_key_path}'",
                ocf.OCF_ERR_ARGS,
            )

        try:
            content = api_key_path.read_text().strip()
            api_key_field = json.loads(content).get("apikey", "")
        except json.JSONDecodeError:
            # data is text, return as is
            api_key_field = content

        if not api_key_field:
            raise PowerCloudTokenManagerError(
                f"_load_api_key: invalid API key in file '{api_key_path}'",
                ocf.OCF_ERR_ARGS,
            )

        return api_key_field

    def _request_new_token(self):
        """Request a new access token."""
        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}",
        }

        current_time = time.time()
        try:
            response = self._session.post(
                self._auth_url,
                headers=headers,
                data=data,
                proxies=self._proxy,
                timeout=REQUESTS_TIMEOUT,
            )
            response.raise_for_status()
            token_data = response.json()
            return (
                token_data["access_token"],
                current_time + token_data["expires_in"],
                current_time,
            )
        except requests.RequestException as e:
            ocf.logger.warning(
                f"[PowerCloudTokenManager] _request_new_token: failed to request token: '{e}'"
            )
            return None

    def _read_cache(self):
        """Read token cache."""
        try:
            with self._cache_file.open("r") as f:
                fcntl.flock(f, fcntl.LOCK_EX)
                try:
                    return json.load(f)
                finally:
                    fcntl.flock(f, fcntl.LOCK_UN)
        except (json.JSONDecodeError, FileNotFoundError, PermissionError) as e:
            ocf.logger.warning(
                f"[PowerCloudTokenManager] _read_cache: failed to read token cache read due to missing file or malformed JSON: '{e}'"
            )
            return {}

    def _write_cache(self, token, expiration, refreshed_at):
        """Write token cache."""
        try:
            with self._cache_file.open("w") as f:
                fcntl.flock(f, fcntl.LOCK_EX)
                try:
                    json.dump(
                        {
                            "token": token,
                            "expiration": expiration,
                            "refreshed_at": refreshed_at,
                        },
                        f,
                    )
                finally:
                    fcntl.flock(f, fcntl.LOCK_UN)
        except Exception as e:
            raise PowerCloudTokenManagerError(
                f"_write_cache: failed to write token cache file: '{e}'",
                ocf.OCF_ERR_GENERIC,
            )

    def _is_token_expired(self, expiration):
        """Check if token is expired or near expiry."""
        return time.time() + self._TOKEN_REFRESH_BUFFER >= expiration

    def get_token(self):
        """Get a valid access token, using cache if enabled."""
        if not self._cache_file:
            result = self._request_new_token()
            if result:
                token, _, _ = result
                return token
            raise PowerCloudTokenManagerError(
                "get_token: token request failed and no cache available",
                ocf.OCF_ERR_GENERIC,
            )

        cache = self._read_cache()
        token = cache.get("token")
        expiration = cache.get("expiration", 0)

        if not token or self._is_token_expired(expiration):
            result = self._request_new_token()
            if result:
                token, expiration, refreshed_at = result
                refresh_time = time.ctime(refreshed_at)
                ocf.logger.debug(
                    f"[PowerCloudTokenManager] get_token: refreshed token at '{refresh_time}'"
                )
                self._write_cache(token, expiration, refreshed_at)
            else:
                ocf.logger.error(
                    "[PowerCloudTokenManager] get_token: failed to refresh token"
                )
                if token and time.time() < expiration:
                    ocf.logger.warning(
                        "[PowerCloudTokenManager] get_token: using cached token as fallback"
                    )
                else:
                    raise PowerCloudTokenManagerError(
                        "get_token: no valid token available",
                        ocf.OCF_ERR_GENERIC,
                    )

        return token


class PowerCloudAPIError(OCFExitError):
    """Exception class for errors in PowerCloudAPI."""

    def __init__(self, message, exit_code):
        super().__init__(f"[PowerCloudAPIError] {message}", exit_code)


class PowerCloudAPI:
    """Offers a convenient method for sending requests to the IBM Power Cloud API."""

    _ALLOWED_API_TYPES = {"public", "private"}

    def __init__(
        self,
        api_key="",
        api_type="",
        region="",
        crn="",
        proxy="",
        use_cache=False,
    ):
        """Initialize class variables, including the IBM Power Cloud API endpoint URL and HTTP header, and get an API token."""

        self._crn = crn
        self._proxy = self._get_proxy(proxy)
        self._api_url = self._get_api_url(region, api_type)
        token_manager = PowerCloudTokenManager(
            api_type=api_type, api_key=api_key, proxy=self._proxy, use_cache=use_cache
        )
        self._token = token_manager.get_token()
        self._header = self._get_header()
        self._session = create_session_with_retries()

    def _get_proxy(self, proxy):
        """Validate a proxy URL and test TCP connectivity. Returns a proxy dict if reachable."""
        if not proxy:
            return None

        parsed_url = urlparse(proxy)
        is_valid_url = (
            parsed_url.hostname
            and parsed_url.port
            and parsed_url.scheme in ("http", "https")
        )

        if not is_valid_url:
            raise PowerCloudAPIError(
                f"_get_proxy: invalid proxy URL '{proxy}'",
                ocf.OCF_ERR_CONFIGURED,
            )

        try:
            with socket.create_connection(
                (parsed_url.hostname, parsed_url.port), timeout=REQUESTS_TIMEOUT
            ):
                return {"https": proxy}
        except OSError as e:
            raise PowerCloudAPIError(
                f"_get_proxy: cannot connect to proxy '{proxy}': {e}",
                ocf.OCF_ERR_ARGS,
            )

    def _get_api_url(self, region, api_type):
        """Generate and return the API URL for a given region and API type."""
        if not region:
            raise PowerCloudAPIError(
                "_get_api_url: missing region parameter",
                ocf.OCF_ERR_CONFIGURED,
            )

        api_type = str(api_type).lower()
        if api_type not in self._ALLOWED_API_TYPES:
            raise PowerCloudAPIError(
                f"_get_api_url: invalid api_type: '{api_type}', must be one of {self._ALLOWED_API_TYPES} ",
                ocf.OCF_ERR_CONFIGURED,
            )
        if api_type == "public" and not self._proxy:
            raise PowerCloudAPIError(
                "_get_api_url: api_type 'public' requires a proxy",
                ocf.OCF_ERR_CONFIGURED,
            )

        subdomain = "private." if api_type == "private" else ""
        return f"https://{subdomain}{region}.power-iaas.cloud.ibm.com"

    def _get_header(self):
        """Construct request header."""
        return {
            "Authorization": f"Bearer {self._token}",
            "CRN": self._crn,
            "Content-Type": "application/json",
        }

    def send_api_request(self, method, resource, **kwargs):
        """Perform an HTTP API call to the specified resource using the given method"""
        url = f"{self._api_url}{resource}"
        method = method.upper()
        ocf.logger.debug(f"[PowerCloudAPI] send_api_request: '{method}' '{resource}'")

        try:
            response = self._session.request(
                method,
                url,
                headers=self._header,
                proxies=self._proxy,
                timeout=REQUESTS_TIMEOUT,
                **kwargs,
            )
            response.raise_for_status()
            return response.json()
        except requests.RequestException as e:
            raise PowerCloudAPIError(
                f"send_api_request: request error occured: '{method}' - '{url}' - '{e}'",
                ocf.OCF_ERR_GENERIC,
            )


class PowerCloudRouteError(OCFExitError):
    """Exception class for errors encountered while managing PowerVS network routes."""

    def __init__(self, message, exit_code):
        super().__init__(f"[PowerCloudRouteError] {message}", exit_code)


class PowerCloudRoute(PowerCloudAPI):
    """Provides methods for managing network routes in Power Virtual Server."""

    _CRN_PREFIX_INDEX = 0
    _CRN_TYPE_INDEX = 8
    _CRN_ROUTE_ID_INDEX = 9
    _CRN_EXPECTED_LENGTH = 10

    def __init__(
        self,
        ip="",
        api_key="",
        api_type="",
        region="",
        route_host_map="",
        device="",
        iflabel="",
        proxy="",
        monitor_api="",
        use_token_cache="",
        is_remote_route=False,
    ):
        """Initialize PowerCloudRoute instance."""
        self._is_remote_route = is_remote_route
        self.ip = self._get_ip_info(ip)
        self.crn, self.route_id = self._parse_route_map(route_host_map)
        use_cache = str(use_token_cache).lower() == "true"
        super().__init__(
            api_key=api_key,
            api_type=api_type,
            region=region,
            crn=self.crn,
            proxy=proxy,
            use_cache=use_cache,
        )
        self.route_info = self._get_route_info()
        self.route_name = self.route_info["name"]
        self.device = self._get_device_name(device)
        self.iflabel = self._make_iflabel(iflabel)

    def _get_ip_info(self, ip):
        """Validate the given IP address and return its standard form."""
        try:
            return str(ipaddress.ip_address(ip))
        except ValueError:
            raise PowerCloudRouteError(
                f"_get_ip_info: invalid IP address '{ip}'",
                ocf.OCF_ERR_CONFIGURED,
            )

    def _parse_route_crn(self, route_crn):
        """Parses a PowerVS route CRN and extract its base CRN and route ID."""
        crn_parts = route_crn.split(":")

        if (
            len(crn_parts) != self._CRN_EXPECTED_LENGTH
            or crn_parts[self._CRN_PREFIX_INDEX] != "crn"
            or crn_parts[self._CRN_TYPE_INDEX] != "route"
        ):
            raise PowerCloudAPIError(
                f"_parse_route_crn: invalid CRN format for network-route: '{route_crn}'",
                ocf.OCF_ERR_CONFIGURED,
            )

        workspace_crn = ":".join(crn_parts[: self._CRN_TYPE_INDEX]) + "::"
        route_id = crn_parts[self._CRN_ROUTE_ID_INDEX]

        return workspace_crn, route_id

    def _parse_route_map(self, route_host_map):
        """Validate the route host map and extract the associated CRN and route ID."""
        try:
            route_map = dict(item.split(":", 1) for item in route_host_map.split(";"))
        except ValueError:
            raise PowerCloudRouteError(
                f"_parse_route_map: invalid route_host_map format: '{route_host_map}'",
                ocf.OCF_ERR_CONFIGURED,
            )

        hostname = os.uname().nodename
        # set nodename to local hostname or get hostname of remote host from route_map
        nodename = (
            hostname
            if not self._is_remote_route
            else next((host for host in route_map if host != hostname), None)
        )

        if not nodename or nodename not in route_map:
            raise PowerCloudRouteError(
                f"_parse_route_map: hostname '{nodename}' not found in route_host_map '{route_host_map}'",
                ocf.OCF_ERR_CONFIGURED,
            )

        return self._parse_route_crn(route_map[nodename])

    def _get_route_info(self):
        """Retrieve and validate attributes of a PowerVS network route."""
        resource = f"/v1/routes/{self.route_id}"
        route_info = self.send_api_request("GET", resource)

        zone = "remote" if self._is_remote_route else "local"
        ocf.logger.debug(
            f"[PowerCloudRoute] _get_route_info: {zone} route info: '{route_info}'"
        )

        if self.ip != route_info["destination"]:
            raise PowerCloudRouteError(
                f"_get_route_info: IP '{self.ip}' does not match the route destination address '{route_info['destination']}'",
                ocf.OCF_ERR_CONFIGURED,
            )

        if route_info["advertise"] != "enable":
            raise PowerCloudRouteError(
                f"_get_route_info: route '{route_info['name']}' advertise flag must be set to enable",
                ocf.OCF_ERR_CONFIGURED,
            )

        return route_info

    def _get_device_name(self, name):
        """Verify the existence of a network interface with the specified name."""
        if self._is_remote_route:
            return ""

        if name:
            if ip_check_device(name):
                return name
            raise PowerCloudRouteError(
                f"_get_device_name: network interface '{name}' does not exist or is down",
                ocf.OCF_ERR_CONFIGURED,
            )

        next_hop = self.route_info["nextHop"]
        interface_name = ip_find_device(next_hop)
        if interface_name:
            return interface_name

        raise PowerCloudRouteError(
            f"_get_device_name: network interface with next hop '{next_hop}' does not exist or is down",
            ocf.OCF_ERR_CONFIGURED,
        )

    def _make_iflabel(self, label=None):
        """Constructs an interface label in the format 'device:label' if both are provided."""
        if not label or self._is_remote_route:
            return None

        iflabel = f"{self.device}:{label}"

        if len(iflabel) > IFLABEL_MAX_LEN:
            raise PowerCloudRouteError(
                f"_make_iflabel: interface label '{iflabel}' exceeds limit of {IFLABEL_MAX_LEN} characters",
                ocf.OCF_ERR_CONFIGURED,
            )

        return iflabel

    def _set_route_enabled(self, enabled: bool):
        """Enable or disable the PowerVS network route."""
        resource = f"/v1/routes/{self.route_id}"
        data = json.dumps({"enabled": enabled})

        state = "enabled" if enabled else "disabled"
        response = self.send_api_request("PUT", resource, data=data)
        ocf.logger.debug(
            f"[PowerCloudRoute] _set_route_enabled: successfully {state} route '{self.route_name}', response: '{response}'"
        )

    def is_enabled(self):
        """Check whether the PowerVS network route is currently enabled."""
        return self.route_info["state"] == "deployed"

    def enable(self):
        """Enable the PowerVS network route."""
        if not self.is_enabled():
            self._set_route_enabled(True)

    def disable(self):
        """Disable the PowerVS network route."""
        if self.is_enabled():
            self._set_route_enabled(False)


def create_route_instance(options, is_remote_route=False, catch_exception=False):
    """Instantiate a PowerCloudRoute object and handle errors.

    Returns:
    - PowerCloudRoute: The initialized route object if successful.
    - None: If an error occurs and catch_exception is True.

    Raises:
    - PowerCloudRouteError: If instantiation fails and catch_exception is False.
    """
    # Filter only the valid resource agent options from options dictionary.
    resource_options = {k: options.get(k, "") for k in RESOURCE_OPTIONS}

    try:
        return PowerCloudRoute(**resource_options, is_remote_route=is_remote_route)
    except Exception as e:
        zone = "remote" if is_remote_route else "local"
        ocf.logger.error(
            f"[create_route_instance]: failed to instantiate {zone} route: '{e}'"
        )
        if catch_exception:
            return None
        raise


def start_action(
    ip="",
    api_key="",
    api_type="",
    region="",
    route_host_map="",
    use_token_cache="",
    monitor_api="",
    device="",
    iflabel="",
    proxy="",
):
    """Assign the service IP.

    This function performs the following actions:
    - Adds the specified IP address as an alias to the given network interface or the interface matching the route's next hop.
    - Disables the remote network route.
    - Enables the network route associated with the provided route host map.
    """
    resource_options = locals()

    ocf.logger.info("[start_action]: enabling overlay IP")
    ocf.logger.debug(f"[start_action]: options: '{resource_options}'")

    remote_route = create_route_instance(resource_options, is_remote_route=True)
    # Disable remote route
    ocf.logger.debug(
        f"[start_action]: disabling remote route '{remote_route.route_name}'"
    )
    remote_route.disable()

    local_route = create_route_instance(resource_options)

    # Add IP alias
    ip_alias_add(ip, local_route.device, local_route.iflabel)

    # Enable local route
    ocf.logger.debug(f"[start_action]: enabling local route '{local_route.route_name}'")
    local_route.enable()

    monitor_result = monitor_action(**resource_options)
    if monitor_result != ocf.OCF_SUCCESS:
        raise PowerCloudRouteError(
            f"start_action: failed to enable local route '{local_route.route_name}'",
            monitor_result,
        )

    ocf.logger.info(
        f"[start_action]: successfully added IP alias '{ip}' and enabled local route '{local_route.route_name}'"
    )
    return ocf.OCF_SUCCESS


def stop_action(
    ip="",
    api_key="",
    api_type="",
    region="",
    route_host_map="",
    use_token_cache="",
    monitor_api="",
    device="",
    iflabel="",
    proxy="",
):
    """Remove the service IP.

    This function performs the following actions:
    - Disables the network route associated with the provided route host map.
    - Removes the IP alias from the network interface.
    """

    resource_options = locals()

    ocf.logger.info("[stop_action]: disabling overlay IP")
    ocf.logger.debug(f"[stop_action]: options: '{resource_options}'")

    try:
        remote_route = create_route_instance(resource_options, is_remote_route=True)
        ocf.logger.debug(
            f"[stop_action]: disabling remote route '{remote_route.route_name}'"
        )
        remote_route.disable()

        local_route = create_route_instance(resource_options)
        ocf.logger.debug(
            f"[stop_action]: disabling local route '{local_route.route_name}'"
        )
        local_route.disable()
    finally:
        # Remove IP alias
        ip_alias_remove(ip)

    monitor_result = monitor_action(**resource_options)
    if monitor_result != ocf.OCF_NOT_RUNNING:
        raise PowerCloudRouteError(
            f"stop_action: failed to disable local route '{local_route.route_name}'",
            monitor_result,
        )

    ocf.logger.info(
        f"[stop_action]: successfully removed IP alias '{ip}' and disabled local route '{local_route.route_name}'"
    )
    return ocf.OCF_SUCCESS


def monitor_action(
    ip="",
    api_key="",
    api_type="",
    region="",
    route_host_map="",
    use_token_cache="",
    monitor_api="",
    device="",
    iflabel="",
    proxy="",
):
    """Monitor the service IP.

    Checks the status of the assigned service IP address.
    """
    resource_options = locals()
    is_probe = ocf.is_probe()
    use_extended_monitor = ocf.OCF_ACTION == "start" or (
        str(monitor_api).lower() == "true" and not is_probe
    )

    ocf.logger.debug(
        f"[monitor_action]: options: '{resource_options}', is_probe: '{is_probe}'"
    )

    interface_name = ip_find_device(ip)

    if not use_extended_monitor:
        if interface_name:
            ocf.logger.debug(f"[monitor_action]: IP alias '{ip}' is active'")
            return ocf.OCF_SUCCESS
        else:
            ocf.logger.debug(f"[monitor_action]: IP alias '{ip}' is not active")
            return ocf.OCF_NOT_RUNNING

    remote_route = create_route_instance(
        resource_options, is_remote_route=True, catch_exception=True
    )
    if remote_route is None:
        ocf.logger.error("[monitor_action]: failed to instantiate remote route")
        return ocf.OCF_ERR_GENERIC
    elif remote_route.is_enabled():
        ocf.logger.error(
            f"[monitor_action]: remote route '{remote_route.route_name}' is enabled"
        )
        return ocf.OCF_ERR_GENERIC

    local_route = create_route_instance(
        resource_options, is_remote_route=False, catch_exception=True
    )

    if local_route is None:
        ocf.logger.error("[monitor_action]: failed to instantiate local route")
        return ocf.OCF_ERR_GENERIC

    if interface_name:
        if local_route.is_enabled():
            ocf.logger.debug(
                f"[monitor_action]: IP alias '{ip}' is active, local route '{local_route.route_name}' is enabled"
            )
            return ocf.OCF_SUCCESS
        else:
            ocf.logger.error(
                f"[monitor_action]: local route '{local_route.route_name}' is not enabled"
            )
            return ocf.OCF_ERR_GENERIC
    else:
        if local_route.is_enabled():
            ocf.logger.error(
                f"[monitor_action]: local route '{local_route.route_name}' is enabled, but IP alias is not configured"
            )
            return ocf.OCF_ERR_GENERIC
        else:
            ocf.logger.debug(
                f"[monitor_action]: IP alias '{ip}' is not active and local route '{local_route.route_name}' is disabled"
            )
            return ocf.OCF_NOT_RUNNING


def validate_all_action(
    ip="",
    api_key="",
    api_type="",
    region="",
    route_host_map="",
    use_token_cache="",
    monitor_api="",
    device="",
    iflabel="",
    proxy="",
):
    """Validate resource agent parameters.

    Verifies the provided resource agent options by attempting to instantiate route objects for both local and remote routes.
    """
    resource_options = locals()

    ocf.logger.info("[validate_all_action]: validate local and remote routes")
    _ = create_route_instance(resource_options)
    _ = create_route_instance(resource_options, is_remote_route=True)

    return ocf.OCF_SUCCESS


def main():
    """Instantiate the resource agent."""
    agent_description = textwrap.dedent("""\
        Resource Agent to move an IP address from one Power Virtual Server instance to another.

        Prerequisites:
        1. Two-node cluster
           - Distributed across two PowerVS workspaces in separate data centers within the same region.

        2. IBM Cloud API Key:
           - Create a service API key with privileges for both workspaces.
           - Save the key in a file and copy it to both cluster nodes using the same path and filename.
           - Reference the key file path in the resource definition.

        For detailed guidance on high availability for SAP applications on PowerVS, visit:
        https://cloud.ibm.com/docs/sap?topic=sap-ha-overview.
    """)

    agent = ocf.Agent(
        "powervs-move-ip",
        shortdesc="Manages Power Virtual Server overlay IP routes.",
        longdesc=agent_description,
        version=1.01,
    )

    agent.add_parameter(
        "ip",
        shortdesc="IP address",
        longdesc=(
            "The virtual IP address is the destination address of a network route."
        ),
        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(
        "api_type",
        shortdesc="API type",
        longdesc="Connect to Power Virtual Server regional endpoints over a public or private network (public|private).",
        content_type="string",
        default="private",
        required=True,
    )
    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(
        "route_host_map",
        shortdesc="Mapping of hostnames to IBM Cloud route CRNs",
        longdesc=(
            "Map the hostname of the Power Virtual Server instance to the route CRN of the overlay IP route. "
            "Separate hostname and route CRN with a colon ':', separate different hostname and route CRN pairs with a semicolon ';'. "
            "Example: hostname1:route-crn-of-instance1;hostname2:route-crn-of-instance2"
        ),
        content_type="string",
        required=True,
    )
    agent.add_parameter(
        "use_token_cache",
        shortdesc="Enable API token cache",
        longdesc="Enable caching of the API access token in a local file to reduce authentication overhead. ",
        content_type="string",
        default="True",
        required=False,
    )
    agent.add_parameter(
        "monitor_api",
        shortdesc="Enhanced API Monitoring",
        longdesc="Enable enhanced monitoring by using Power Cloud API calls to verify route configuration correctness. ",
        content_type="string",
        default="False",
        required=False,
    )
    agent.add_parameter(
        "device",
        shortdesc="Network adapter for the overlay IP address",
        longdesc=(
            "Network adapter for the overlay IP address. "
            "The adapter must have the same name on all Power Virtual Server instances. "
            "If the `device` parameter is not specified, the IP alias is assigned to the interface whose configured IP address matches the route's next hop address. "
        ),
        content_type="string",
        default="",
        required=False,
    )
    agent.add_parameter(
        "iflabel",
        shortdesc="Network interface label",
        longdesc=(
            "A custom suffix for the IP address label. "
            "It is appended to the interface name in the format device:label. "
            "The full label must not exceed 15 characters. "
        ),
        content_type="string",
        required=False,
    )
    agent.add_parameter(
        "proxy",
        shortdesc="Proxy",
        longdesc=(
            "Proxy server used to access IBM Cloud API endpoints. "
            "The value must be a valid URL in the format 'http[s]://hostname:port'. "
        ),
        content_type="string",
        default="",
        required=False,
    )
    agent.add_action("start", timeout=60, handler=start_action)
    agent.add_action("stop", timeout=60, handler=stop_action)
    agent.add_action(
        "monitor", depth=0, timeout=60, interval=60, handler=monitor_action
    )
    agent.add_action("validate-all", timeout=60, handler=validate_all_action)
    agent.run()


if __name__ == "__main__":
    main()
