Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package fence-agents for openSUSE:Factory 
checked in at 2026-07-28 18:21:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/fence-agents (Old)
 and      /work/SRC/openSUSE:Factory/.fence-agents.new.2004 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "fence-agents"

Tue Jul 28 18:21:23 2026 rev:94 rq:1368206 version:4.17.0+git.1784121740.9871084

Changes:
--------
--- /work/SRC/openSUSE:Factory/fence-agents/fence-agents.changes        
2026-06-22 17:43:25.927184699 +0200
+++ /work/SRC/openSUSE:Factory/.fence-agents.new.2004/fence-agents.changes      
2026-07-28 18:22:36.240369794 +0200
@@ -1,0 +2,8 @@
+Tue Jul 21 13:35:32 UTC 2026 - [email protected]
+
+- Update to version 4.17.0+git.1784121740.9871084:
+  * fence_vmware_rest: add token-based authentication support
+  * fence_aws: add tag/eni/ebs identity-method instance resolution
+  * fence_kubevirt: report Succeeded and Failed as OFF
+
+-------------------------------------------------------------------

Old:
----
  fence-agents-4.17.0+git.1781703439.0804740.tar.xz

New:
----
  fence-agents-4.17.0+git.1784121740.9871084.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ fence-agents.spec ++++++
--- /var/tmp/diff_new_pack.WA9VGn/_old  2026-07-28 18:22:38.220439104 +0200
+++ /var/tmp/diff_new_pack.WA9VGn/_new  2026-07-28 18:22:38.224439244 +0200
@@ -19,7 +19,7 @@
 %define agent_list aliyun alom apc apc_snmp aws azure_arm bladecenter brocade 
cisco_mds cisco_ucs drac5 dummy eaton_snmp eaton_ssh emerson eps evacuate gce 
hds_cb hpblade ibmblade ibmz ibm_powervs ibm_vpc ifmib ilo ilo_moonshot ilo_mp 
ilo_ssh intelmodular ipdu ipmilan ironic kdump lpar mpath netio nutanix_ahv 
powerman pve raritan rcd_serial redfish rsa rsb sanbox2 sbd scsi vbox virsh 
vmware vmware_rest wti zvm
 Name:           fence-agents
 Summary:        Set of unified programs capable of host isolation ("fencing")
-Version:        4.17.0+git.1781703439.0804740
+Version:        4.17.0+git.1784121740.9871084
 Release:        0
 License:        GPL-2.0-or-later AND LGPL-2.0-or-later
 Group:          Productivity/Clustering/HA

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.WA9VGn/_old  2026-07-28 18:22:38.332443024 +0200
+++ /var/tmp/diff_new_pack.WA9VGn/_new  2026-07-28 18:22:38.336443165 +0200
@@ -3,6 +3,6 @@
             <param 
name="url">git://github.com/ClusterLabs/fence-agents.git</param>
           <param 
name="changesrevision">8d746be92f191aa289f13a3703031c122a5e6cf3</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/ClusterLabs/fence-agents</param>
-              <param 
name="changesrevision">080474034850df23538f97e74e8114283670cb72</param></service></servicedata>
+              <param 
name="changesrevision">9871084e6652815b0e07ac38baa13614f8f7eb37</param></service></servicedata>
 (No newline at EOF)
 

++++++ fence-agents-4.17.0+git.1781703439.0804740.tar.xz -> 
fence-agents-4.17.0+git.1784121740.9871084.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fence-agents-4.17.0+git.1781703439.0804740/agents/aws/fence_aws.py 
new/fence-agents-4.17.0+git.1784121740.9871084/agents/aws/fence_aws.py
--- old/fence-agents-4.17.0+git.1781703439.0804740/agents/aws/fence_aws.py      
2026-06-17 15:37:19.000000000 +0200
+++ new/fence-agents-4.17.0+git.1784121740.9871084/agents/aws/fence_aws.py      
2026-07-15 15:22:20.000000000 +0200
@@ -3,6 +3,7 @@
 import sys, re
 import logging
 import atexit
+import time
 sys.path.append("@FENCEAGENTSLIBDIR@")
 from fencing import *
 from fencing import fail, fail_usage, run_delay, EC_STATUS, SyslogLibHandler
@@ -26,47 +27,403 @@
                "running": "on",
                "stopped": "off",
                "pending": "unknown",
-               "stopping": "unknown",
-               "shutting-down": "unknown",
-               "terminated": "unknown"
+               "stopping": "off",
+               "shutting-down": "off",
+               "terminated": "off"
 }
 
-def get_instance_id(options):
+# IMDSv2 endpoints. Timeout is (connect, read) in seconds; the fence path must
+# never block on an unreachable metadata service.
+IMDS_TOKEN_URL = "http://169.254.169.254/latest/api/token";
+IMDS_META_URL = "http://169.254.169.254/latest/meta-data";
+IMDS_TIMEOUT = (2, 5)
+
+def _imds_fetch(path, options):
+       """Fetch a single IMDSv2 metadata path. Returns the value as str, or 
None on any failure."""
        try:
-               token = requests.put('http://169.254.169.254/latest/api/token', 
headers={"X-aws-ec2-metadata-token-ttl-seconds" : 
"21600"}).content.decode("UTF-8")
-               r = 
requests.get('http://169.254.169.254/latest/meta-data/instance-id', 
headers={"X-aws-ec2-metadata-token" : token}).content.decode("UTF-8")
-               return r
+               token = requests.put(
+                       IMDS_TOKEN_URL,
+                       headers={"X-aws-ec2-metadata-token-ttl-seconds": 
"21600"},
+                       timeout=IMDS_TIMEOUT).content.decode("UTF-8")
+               return requests.get(
+                       "{}/{}".format(IMDS_META_URL, path),
+                       headers={"X-aws-ec2-metadata-token": token},
+                       timeout=IMDS_TIMEOUT).content.decode("UTF-8")
        except HTTPError as http_err:
-               logger.error('HTTP error occurred while trying to access EC2 
metadata server: %s', http_err)
+               logger.error("HTTP error accessing EC2 metadata (%s): %s", 
path, http_err)
        except Exception as err:
                if "--skip-race-check" not in options:
-                       logger.error('A fatal error occurred while trying to 
access EC2 metadata server: %s', err)
+                       logger.error("Error accessing EC2 metadata (%s): %s", 
path, err)
                else:
-                       logger.debug('A fatal error occurred while trying to 
access EC2 metadata server: %s', err)
+                       logger.debug("Error accessing EC2 metadata (%s): %s", 
path, err)
+       return None
+
+def get_instance_id(options):
+       return _imds_fetch("instance-id", options)
+
+
+def get_instance_by_tag(conn, tag_name, tag_value, options, max_retries=3, 
retry_delay=2):
+       """
+       Look up EC2 instance ID by tag name and value.
+       Returns instance ID if found, None otherwise.
+
+       Includes retry logic for AWS API eventual consistency.
+
+       For blue/green deployments with multiple running instances:
+       1. Get current instance's build_number tag (from cached values)
+       2. Filter for non-terminated instances with matching tag
+       3. Prefer instance with SAME build_number as current instance
+       4. If no build_number match and multiple instances, REFUSE to guess
+       """
+       last_error = None
+
+       for attempt in range(1, max_retries + 1):
+               try:
+                       region = options.get("--region")
+                       logger.debug("Looking up instance by tag %s=%s in 
region %s (attempt %d/%d)",
+                               tag_name, tag_value, region, attempt, 
max_retries)
+
+                       my_build_number = options.get("my_build_number")
+
+                       filters = [
+                               {"Name": "tag:{}".format(tag_name), "Values": 
[tag_value]},
+                               {"Name": "instance-state-name", "Values": 
["pending", "running", "stopping", "stopped"]}
+                       ]
+                       instances = list(conn.instances.filter(Filters=filters))
+
+                       if not instances:
+                               if attempt < max_retries:
+                                       logger.debug("No instance found with 
tag %s=%s in fenceable states (attempt %d/%d), retrying in %ds",
+                                               tag_name, tag_value, attempt, 
max_retries, retry_delay)
+                                       time.sleep(retry_delay)
+                                       continue
+                               logger.warning("No instance found with tag 
%s=%s in fenceable states after %d attempts",
+                                       tag_name, tag_value, max_retries)
+                               return None
+
+                       if len(instances) > 1:
+                               logger.warning("Multiple running instances 
found with tag %s=%s:", tag_name, tag_value)
+
+                               instances_with_build = []
+                               for inst in instances:
+                                       build_num = None
+                                       if inst.tags:
+                                               for tag in inst.tags:
+                                                       if tag['Key'] == 
'build_number':
+                                                               build_num = 
tag['Value']
+                                                               break
+                                       instances_with_build.append((inst, 
build_num))
+                                       logger.warning("  - %s 
(build_number=%s, launched=%s)",
+                                               inst.id, build_num if build_num 
else "N/A", inst.launch_time)
+
+                               selected_inst = None
+                               if my_build_number:
+                                       for inst, build_num in 
instances_with_build:
+                                               if build_num == my_build_number:
+                                                       selected_inst = inst
+                                                       
logger.warning("Selecting instance with matching build_number=%s: %s",
+                                                               
my_build_number, inst.id)
+                                                       break
+
+                               if not selected_inst:
+                                       logger.error("Multiple instances match 
tag %s=%s but none match build_number=%s. "
+                                               "Refusing to guess. Manual 
intervention required.",
+                                               tag_name, tag_value, 
my_build_number)
+                                       return None
+
+                               instance_id = selected_inst.id
+                       else:
+                               instance_id = instances[0].id
+                               logger.debug("Single instance found: %s", 
instance_id)
+
+                       logger.debug("Selected instance %s with tag %s=%s", 
instance_id, tag_name, tag_value)
+                       return instance_id
+
+               except (ClientError, EndpointConnectionError, ConnectionError) 
as e:
+                       last_error = e
+                       if attempt < max_retries:
+                               logger.warning("AWS API error during tag lookup 
(attempt %d/%d): %s. Retrying in %ds",
+                                       attempt, max_retries, e, retry_delay)
+                               time.sleep(retry_delay)
+                               continue
+                       logger.error("Failed to lookup instance by tag after %d 
attempts: %s", max_retries, e)
+                       return None
+
+       logger.error("Failed to lookup instance by tag after %d attempts: %s", 
max_retries, last_error)
        return None
 
 
+def get_instance_by_eni(conn, eni_id, max_retries=3, retry_delay=2):
+       """
+       Resolve ENI ID to attached instance ID.
+       Returns (instance_id, None) on success or (None, error_msg) on failure.
+
+       When the ENI exists but is not attached, the target instance is gone
+       (terminated or being replaced). The caller decides the semantics:
+       - get_power_status treats "not attached" as OFF (safe)
+       - set_power_status fails safe when the target cannot be resolved
+       """
+       last_error = None
+
+       for attempt in range(1, max_retries + 1):
+               try:
+                       client = conn.meta.client
+                       response = 
client.describe_network_interfaces(NetworkInterfaceIds=[eni_id])
+                       enis = response.get('NetworkInterfaces', [])
+
+                       if not enis:
+                               return (None, "ENI {} not found".format(eni_id))
+
+                       eni = enis[0]
+                       eni_status = eni.get('Status', 'unknown')
+                       attachment = eni.get('Attachment')
+
+                       if not attachment:
+                               return (None, "ENI {} exists but not attached 
(status: {})".format(eni_id, eni_status))
+
+                       attach_status = attachment.get('Status', 'unknown')
+                       if attach_status not in ('attached', 'attaching'):
+                               return (None, "ENI {} attachment in 
transitional state: {}".format(eni_id, attach_status))
+
+                       instance_id = attachment.get('InstanceId')
+                       if not instance_id:
+                               return (None, "ENI {} attached but no 
InstanceId in response".format(eni_id))
+
+                       logger.debug("ENI %s is attached to instance %s", 
eni_id, instance_id)
+                       return (instance_id, None)
+
+               except ClientError as e:
+                       error_code = e.response.get('Error', {}).get('Code', '')
+                       if error_code == 'InvalidNetworkInterfaceID.NotFound':
+                               return (None, "ENI {} does not 
exist".format(eni_id))
+                       last_error = e
+                       if attempt < max_retries:
+                               logger.warning("AWS API error during ENI lookup 
(attempt %d/%d): %s. Retrying in %ds",
+                                       attempt, max_retries, e, retry_delay)
+                               time.sleep(retry_delay)
+                               continue
+                       return (None, "AWS API error after {} attempts: 
{}".format(max_retries, e))
+               except (EndpointConnectionError, ConnectionError) as e:
+                       last_error = e
+                       if attempt < max_retries:
+                               logger.warning("Connection error during ENI 
lookup (attempt %d/%d): %s. Retrying in %ds",
+                                       attempt, max_retries, e, retry_delay)
+                               time.sleep(retry_delay)
+                               continue
+                       return (None, "Connection error after {} attempts: 
{}".format(max_retries, e))
+
+       return (None, "ENI lookup failed after {} attempts: 
{}".format(max_retries, last_error))
+
+
+def get_instance_by_volume(conn, volume_id, max_retries=3, retry_delay=2):
+       """
+       Resolve EBS volume ID to attached instance ID.
+       Returns (instance_id, None) on success or (None, error_msg) on failure.
+
+       When the volume exists but is not attached, the target instance is gone
+       (terminated or being replaced). Same caller semantics as ENI resolution.
+       """
+       last_error = None
+
+       for attempt in range(1, max_retries + 1):
+               try:
+                       client = conn.meta.client
+                       response = 
client.describe_volumes(VolumeIds=[volume_id])
+                       volumes = response.get('Volumes', [])
+
+                       if not volumes:
+                               return (None, "Volume {} not 
found".format(volume_id))
+
+                       volume = volumes[0]
+                       vol_state = volume.get('State', 'unknown')
+                       attachments = volume.get('Attachments', [])
+
+                       if not attachments:
+                               return (None, "Volume {} exists but not 
attached (state: {})".format(volume_id, vol_state))
+
+                       if len(attachments) > 1:
+                               return (None, "Volume {} has {} attachments 
(multi-attach). Cannot determine target.".format(
+                                       volume_id, len(attachments)))
+
+                       attach = attachments[0]
+                       attach_status = attach.get('State', 'unknown')
+                       if attach_status not in ('attached', 'attaching'):
+                               return (None, "Volume {} attachment in 
transitional state: {}".format(volume_id, attach_status))
+
+                       instance_id = attach.get('InstanceId')
+                       if not instance_id:
+                               return (None, "Volume {} attached but no 
InstanceId in response".format(volume_id))
+
+                       logger.debug("Volume %s is attached to instance %s", 
volume_id, instance_id)
+                       return (instance_id, None)
+
+               except ClientError as e:
+                       error_code = e.response.get('Error', {}).get('Code', '')
+                       if error_code == 'InvalidVolume.NotFound':
+                               return (None, "Volume {} does not 
exist".format(volume_id))
+                       last_error = e
+                       if attempt < max_retries:
+                               logger.warning("AWS API error during EBS lookup 
(attempt %d/%d): %s. Retrying in %ds",
+                                       attempt, max_retries, e, retry_delay)
+                               time.sleep(retry_delay)
+                               continue
+                       return (None, "AWS API error after {} attempts: 
{}".format(max_retries, e))
+               except (EndpointConnectionError, ConnectionError) as e:
+                       last_error = e
+                       if attempt < max_retries:
+                               logger.warning("Connection error during EBS 
lookup (attempt %d/%d): %s. Retrying in %ds",
+                                       attempt, max_retries, e, retry_delay)
+                               time.sleep(retry_delay)
+                               continue
+                       return (None, "Connection error after {} attempts: 
{}".format(max_retries, e))
+
+       return (None, "EBS lookup failed after {} attempts: 
{}".format(max_retries, last_error))
+
+
+def resolve_plug_to_instance_id(conn, options):
+       """
+       Resolve the --plug parameter to an instance ID.
+
+       Dispatches by --identity-method:
+         instance-id (default): --plug is the instance ID directly
+         tag:                   --plug is a tag value, resolved via 
DescribeInstances
+         eni:                   --plug is an ENI ID, resolved via 
DescribeNetworkInterfaces
+         ebs:                   --plug is a volume ID, resolved via 
DescribeVolumes
+
+       Uses a per-invocation cache to minimise AWS control plane calls.
+       The identity -> instance ID mapping is resolved ONCE and reused for all
+       subsequent calls within the same fence operation (status checks,
+       power actions, polling).
+
+       The cache is safe because:
+       - The instance ID cannot change while the instance is being fenced
+       - The agent process is short-lived (one fence operation per invocation)
+       - If the cached ID becomes invalid (instance terminated between calls),
+         the StopInstances call fails gracefully with 
InvalidInstanceID.NotFound
+       """
+       plug_value = options.get("--plug")
+       identity_method = options.get("--identity-method", "instance-id")
+
+       if not plug_value:
+               logger.error("No --plug parameter provided")
+               return None
+
+       cache_key = "cached_instance_id"
+       cached = options.get(cache_key)
+       if cached:
+               logger.debug("Using cached instance ID %s for plug=%s", cached, 
plug_value)
+               return cached
+
+       instance_id = None
+
+       if identity_method == "eni":
+               logger.debug("ENI-based lookup: %s", plug_value)
+               instance_id, error = get_instance_by_eni(conn, plug_value)
+               if error:
+                       logger.error("ENI resolution failed: %s", error)
+                       return None
+
+       elif identity_method == "ebs":
+               logger.debug("EBS-based lookup: %s", plug_value)
+               instance_id, error = get_instance_by_volume(conn, plug_value)
+               if error:
+                       logger.error("EBS resolution failed: %s", error)
+                       return None
+
+       elif identity_method == "tag" or options.get("--tag"):
+               tag_name = options.get("--tag", "Name")
+               logger.debug("Tag-based lookup: %s=%s", tag_name, plug_value)
+               instance_id = get_instance_by_tag(conn, tag_name, plug_value, 
options)
+               if not instance_id:
+                       logger.error("Failed to find instance with tag %s=%s", 
tag_name, plug_value)
+
+       else:
+               logger.debug("Direct instance ID: %s", plug_value)
+               instance_id = plug_value
+
+       if instance_id:
+               options[cache_key] = instance_id
+               logger.debug("Resolved plug=%s to instance %s (method: %s)",
+                       plug_value, instance_id, identity_method)
+
+       return instance_id
+
+
+def check_tag_target_is_dead(conn, options):
+       """
+       When tag lookup returns no fenceable instances, determine whether the
+       target is genuinely dead (all terminated) or if the lookup failed for
+       other reasons (wrong tag, API error, etc.).
+
+       Returns True if the target is confirmed dead, False otherwise.
+       """
+       tag_name = options.get("--tag")
+       plug_value = options.get("--plug")
+
+       if not tag_name:
+               return False
+
+       try:
+               all_states = list(conn.instances.filter(Filters=[
+                       {"Name": "tag:{}".format(tag_name), "Values": 
[plug_value]}
+               ]))
+
+               if all_states and all(i.state["Name"] in ("terminated", 
"shutting-down") for i in all_states):
+                       logger.info("All instances with tag %s=%s are 
terminated/shutting-down. Target confirmed dead.",
+                               tag_name, plug_value)
+                       return True
+
+               if not all_states:
+                       logger.error("No instance has ever existed with tag 
%s=%s. This is a configuration error.",
+                               tag_name, plug_value)
+                       return False
+
+               live_states = [i.state["Name"] for i in all_states if 
i.state["Name"] not in ("terminated", "shutting-down")]
+               logger.error("Instances with tag %s=%s exist in unexpected 
states: %s. Cannot confirm target is dead.",
+                       tag_name, plug_value, live_states)
+               return False
+
+       except (ClientError, EndpointConnectionError, ConnectionError) as e:
+               logger.error("AWS API error during dead-target check: %s", e)
+               return False
+
+
 def get_nodes_list(conn, options):
        logger.debug("Starting monitor operation")
        result = {}
+       filter = []
        try:
+               tag_name = options.get("--tag")
+
                if "--filter" in options:
                        filter_key   = options["--filter"].split("=")[0].strip()
                        filter_value = options["--filter"].split("=")[1].strip()
                        filter = [{ "Name": filter_key, "Values": 
[filter_value] }]
                        logging.debug("Filter: {}".format(filter))
 
-               for instance in conn.instances.filter(Filters=filter if 
'filter' in vars() else []):
+               for instance in conn.instances.filter(Filters=filter):
                        instance_name = ""
                        for tag in instance.tags or []:
                                if tag.get("Key") == "Name":
                                        instance_name = tag["Value"]
+                                       break
+
+                       port_name = instance.id
+                       if tag_name and instance.tags:
+                               for tag in instance.tags:
+                                       if tag['Key'] == tag_name:
+                                               port_name = tag['Value']
+                                               logger.debug("Mapped instance 
%s to port name %s via tag %s",
+                                                       instance.id, port_name, 
tag_name)
+                                               break
+
                        try:
-                               result[instance.id] = (instance_name, 
status[instance.state["Name"]])
+                               result[port_name] = (instance_name, 
status[instance.state["Name"]])
                        except KeyError as e:
                                if options.get("--original-action") == 
"list-status":
-                                       logger.error("Unknown status \"{}\" 
returned for {} ({})".format(instance.state["Name"], instance.id, 
instance_name))
-                               result[instance.id] = (instance_name, "unknown")
+                                       logger.error("Unknown status \"{}\" 
returned for {} ({})".format(
+                                               instance.state["Name"], 
instance.id, instance_name))
+                               result[port_name] = (instance_name, "unknown")
        except ClientError:
                fail_usage("Failed: Incorrect Access Key or Secret Key.")
        except EndpointConnectionError:
@@ -78,12 +435,86 @@
        logger.debug("Monitor operation OK: %s",result)
        return result
 
+def check_eni_ebs_target_is_dead(conn, options):
+       """
+       For ENI/EBS modes, when resolve returns None, check whether
+       the resource exists but is simply not attached (target dead)
+       vs a real error (resource doesn't exist, API failure, etc.).
+
+       Returns True if the target is confirmed dead, False otherwise.
+       """
+       identity_method = options.get("--identity-method", "instance-id")
+       plug_value = options.get("--plug")
+
+       if identity_method == "eni":
+               try:
+                       client = conn.meta.client
+                       response = 
client.describe_network_interfaces(NetworkInterfaceIds=[plug_value])
+                       enis = response.get('NetworkInterfaces', [])
+                       if enis and not enis[0].get('Attachment'):
+                               logger.info("ENI %s exists but not attached. 
Target confirmed dead.", plug_value)
+                               return True
+               except ClientError as e:
+                       error_code = e.response.get('Error', {}).get('Code', '')
+                       if error_code == 'InvalidNetworkInterfaceID.NotFound':
+                               logger.error("ENI %s does not exist. 
Configuration error.", plug_value)
+                       else:
+                               logger.error("AWS API error during ENI 
dead-target check [%s]: %s", error_code, e)
+               return False
+
+       elif identity_method == "ebs":
+               try:
+                       client = conn.meta.client
+                       response = 
client.describe_volumes(VolumeIds=[plug_value])
+                       volumes = response.get('Volumes', [])
+                       if volumes and not volumes[0].get('Attachments'):
+                               logger.info("Volume %s exists but not attached. 
Target confirmed dead.", plug_value)
+                               return True
+               except ClientError as e:
+                       error_code = e.response.get('Error', {}).get('Code', '')
+                       if error_code == 'InvalidVolume.NotFound':
+                               logger.error("Volume %s does not exist. 
Configuration error.", plug_value)
+                       else:
+                               logger.error("AWS API error during EBS 
dead-target check [%s]: %s", error_code, e)
+               return False
+
+       return False
+
+
 def get_power_status(conn, options):
        logger.debug("Starting status operation")
        try:
-               instance = conn.instances.filter(Filters=[{"Name": 
"instance-id", "Values": [options["--plug"]]}])
-               state = list(instance)[0].state["Name"]
-               logger.debug("Status operation for EC2 instance %s returned 
state: %s",options["--plug"],state.upper())
+               instance_id = resolve_plug_to_instance_id(conn, options)
+               if not instance_id:
+                       # The fencing library learns target state only through 
this function
+                       # (fence_action pre-check and the post-off status 
poll). For tag/eni/ebs
+                       # identity a terminated instance no longer resolves, so 
the
+                       # confirmed-dead-vs-unknown decision must be made here: 
report OFF only
+                       # when the target is positively confirmed dead, 
otherwise fail.
+                       identity_method = options.get("--identity-method", 
"instance-id")
+
+                       if identity_method in ("eni", "ebs"):
+                               if check_eni_ebs_target_is_dead(conn, options):
+                                       logger.info("No fenceable instance for 
plug=%s — target confirmed dead (method: %s). Reporting OFF.",
+                                               options.get("--plug"), 
identity_method)
+                                       return "off"
+                       elif check_tag_target_is_dead(conn, options):
+                               logger.info("No fenceable instance for plug=%s 
— target confirmed dead. Reporting OFF.",
+                                       options.get("--plug"))
+                               return "off"
+
+                       logger.error("No instance resolved for plug=%s and 
target not confirmed dead. Reporting FAILED.",
+                               options.get("--plug"))
+                       fail(EC_STATUS)
+
+               instance = conn.instances.filter(Filters=[{"Name": 
"instance-id", "Values": [instance_id]}])
+               instance_list = list(instance)
+               if not instance_list:
+                       logger.debug("Instance %s not found (likely 
terminated). Reporting OFF.", instance_id)
+                       return "off"
+
+               state = instance_list[0].state["Name"]
+               logger.debug("Status operation for EC2 instance %s returned 
state: %s", instance_id, state.upper())
                try:
                        return status[state]
                except KeyError as e:
@@ -94,7 +525,8 @@
        except EndpointConnectionError:
                fail_usage("Failed: Incorrect Region.")
        except IndexError:
-               fail(EC_STATUS)
+               logger.debug("Instance not found (IndexError). Reporting OFF.")
+               return "off"
        except Exception as e:
                logger.error("Failed to get power status: %s", e)
                fail(EC_STATUS)
@@ -104,12 +536,12 @@
                instance = conn.instances.filter(Filters=[{"Name": 
"instance-id", "Values": [instance_id]}])
                state = list(instance)[0].state["Name"]
                if state == "running":
-                       logger.debug("Captured my (%s) state and it %s - 
returning OK - Proceeding with fencing",instance_id,state.upper())
+                       logger.debug("Captured my (%s) state and it %s - 
returning OK - Proceeding with fencing", instance_id, state.upper())
                        return "ok"
                else:
-                       logger.debug("Captured my (%s) state it is %s - 
returning Alert - Unable to fence other nodes",instance_id,state.upper())
+                       logger.debug("Captured my (%s) state it is %s - 
returning Alert - Unable to fence other nodes", instance_id, state.upper())
                        return "alert"
-       
+
        except ClientError:
                fail_usage("Failed: Incorrect Access Key or Secret Key.")
        except EndpointConnectionError:
@@ -118,9 +550,20 @@
                return "fail"
 
 def set_power_status(conn, options):
-       my_instance = get_instance_id(options)
+       my_instance = options.get("my_instance_id") or get_instance_id(options)
        try:
-               if options.get("--skip-os-shutdown", "false").lower() in ["1", 
"yes", "on", "true"]:
+               instance_id = resolve_plug_to_instance_id(conn, options)
+               if not instance_id:
+                       # get_power_status() runs before and after this call 
and owns the
+                       # confirmed-dead-vs-unknown decision (and the resolve 
result is cached
+                       # within the invocation). If we reach here we could not 
resolve a target,
+                       # so fail safe. Never report success here — that would 
be a false-positive
+                       # fence. The status poll and Pacemaker's retry 
re-confirm the outcome.
+                       logger.error("Could not resolve instance ID for 
plug=%s; failing safe.",
+                               options.get("--plug"))
+                       fail(EC_STATUS)
+
+               if options.get("--skip-os-shutdown", "true").lower() in ["1", 
"yes", "on", "true"]:
                        shutdown_option = {
                                "SkipOsShutdown": True,
                                "Force": True
@@ -132,19 +575,26 @@
                        }
                if (options["--action"]=="off"):
                        if "--skip-race-check" in options or 
get_self_power_status(conn,my_instance) == "ok":
-                               
conn.instances.filter(InstanceIds=[options["--plug"]]).stop(**shutdown_option)
-                               logger.debug("Called StopInstance API call for 
%s", options["--plug"])
+                               try:
+                                       
conn.instances.filter(InstanceIds=[instance_id]).stop(**shutdown_option)
+                                       logger.info("Called StopInstance API 
call for %s", instance_id)
+                               except ParamValidationError:
+                                       logger.warning("SkipOsShutdown not 
supported with the current boto3 version %s - falling back to graceful 
shutdown", boto3.__version__)
+                                       
conn.instances.filter(InstanceIds=[instance_id]).stop(Force=True)
+                               except ClientError as e:
+                                       error_code = e.response.get('Error', 
{}).get('Code', '')
+                                       if error_code in 
('InvalidInstanceID.NotFound', 'IncorrectInstanceState'):
+                                               logger.info("Instance %s cannot 
be stopped (error: %s). Assuming already OFF.", instance_id, error_code)
+                                       else:
+                                               raise
                        else:
-                               logger.debug("Skipping fencing as instance is 
not in running status")
+                               logger.warning("Skipping fencing as instance is 
not in running status")
                elif (options["--action"]=="on"):
-                       
conn.instances.filter(InstanceIds=[options["--plug"]]).start()
-       except ParamValidationError:
-               if (options["--action"] == "off"):
-                       logger.warning(f"SkipOsShutdown not supported with the 
current boto3 version {boto3.__version__} - falling back to graceful shutdown")
-                       
conn.instances.filter(InstanceIds=[options["--plug"]]).stop(Force=True)
+                       conn.instances.filter(InstanceIds=[instance_id]).start()
+                       logger.info("Called StartInstance API call for %s", 
instance_id)
        except Exception as e:
-               logger.debug("Failed to power %s %s: %s", \
-                               options["--action"], options["--plug"], e)
+               logger.error("Failed to power %s %s: %s", \
+                               options["--action"], instance_id, e)
                fail(EC_STATUS)
 
 def define_new_opts():
@@ -153,7 +603,7 @@
                "longopt" : "region",
                "help" : "-r, --region=[region]          Region, e.g. 
us-east-1",
                "shortdesc" : "Region.",
-               "required" : "0",
+               "required" : "1",
                "order" : 2
        }
        all_opt["access_key"] = {
@@ -206,12 +656,28 @@
                "default" : "true",
                "order" : 8
        }
+       all_opt["tag"] = {
+               "getopt" : ":",
+               "longopt" : "tag",
+               "help" : "--tag=[tag_name]               Tag name for instance 
lookup (e.g. 'Name'). When specified, --plug is treated as tag value instead of 
instance ID",
+               "shortdesc": "Tag name for instance identification",
+               "required": "0",
+               "order": 9
+       }
+       all_opt["identity_method"] = {
+               "getopt" : ":",
+               "longopt" : "identity-method",
+               "help" : "--identity-method=[method]     Identity resolution 
method: instance-id (default), tag, eni, ebs",
+               "shortdesc": "How to resolve --plug to an instance ID. 
'instance-id' treats plug as a direct instance ID, 'tag' uses EC2 tag lookup, 
'eni' resolves via ENI attachment, 'ebs' resolves via EBS volume attachment.",
+               "required": "0",
+               "default": "instance-id",
+               "order": 10
+       }
 
-# Main agent method
 def main():
        conn = None
 
-       device_opt = ["port", "no_password", "region", "access_key", 
"secret_key", "filter", "boto3_debug", "skip_race_check", "skip_os_shutdown"]
+       device_opt = ["port", "no_password", "region", "access_key", 
"secret_key", "filter", "boto3_debug", "skip_race_check", "skip_os_shutdown", 
"tag", "identity_method"]
 
        atexit.register(atexit_handler)
 
@@ -222,10 +688,25 @@
        options = check_input(device_opt, process_input(device_opt))
 
        docs = {}
-       docs["shortdesc"] = "Fence agent for AWS (Amazon Web Services)"
+       docs["shortdesc"] = "Fence agent for AWS (Amazon Web Services) with 
multiple identity resolution methods"
        docs["longdesc"] = "fence_aws is a Power Fencing agent for AWS (Amazon 
Web\
 Services). It uses the boto3 library to connect to AWS.\
 \n.P\n\
+It supports four identity resolution methods via --identity-method:\
+\n.P\n\
+instance-id (default): --plug is treated as a direct EC2 instance ID.\
+\n.P\n\
+tag: --plug is treated as a tag value. Requires --tag to specify the tag name.\
+For example: --identity-method=tag --tag=Name --plug=hostname\
+\n.P\n\
+eni: --plug is treated as an ENI ID. The agent resolves the ENI attachment to 
find\
+the instance. Ideal for architectures with persistent ENIs that survive 
instance replacement.\
+For example: --identity-method=eni --plug=eni-0a1b2c3d4e5f67890\
+\n.P\n\
+ebs: --plug is treated as an EBS volume ID. The agent resolves the volume 
attachment to\
+find the instance. Ideal for architectures with persistent EBS volumes.\
+For example: --identity-method=ebs --plug=vol-0a1b2c3d4e5f67890\
+\n.P\n\
 boto3 can be configured with AWS CLI or by creating ~/.aws/credentials.\n\
 For instructions see: 
https://boto3.readthedocs.io/en/latest/guide/quickstart.html#configuration";
        docs["vendorurl"] = "http://www.amazon.com";
@@ -242,7 +723,7 @@
                lhf = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s 
- %(message)s')
                lh.setFormatter(lhf)
                lh.setLevel(logging.DEBUG)
-       
+
        if options["--boto3_debug"].lower() not in ["1", "yes", "on", "true"]:
                boto3.set_stream_logger('boto3',logging.INFO)
                boto3.set_stream_logger('botocore',logging.CRITICAL)
@@ -266,12 +747,27 @@
                                      aws_access_key_id=access_key,
                                      aws_secret_access_key=secret_key)
        except Exception as e:
-               if not options.get("--action", "") in ["metadata", "manpage", 
"validate-all"]:
+               if options.get("--action", "") not in ["metadata", "manpage", 
"validate-all"]:
                        fail_usage("Failed: Unable to connect to AWS: " + 
str(e))
-               else:
-                       pass
 
-       # Operate the fencing device
+       # Cache own instance ID and build_number at startup
+       # These values never change during the instance's lifetime.
+       # Caching here eliminates IMDS calls from the fencing hot path.
+       options["my_instance_id"] = get_instance_id(options)
+       if options.get("my_instance_id"):
+               logger.debug("Cached own instance ID: %s", 
options["my_instance_id"])
+               try:
+                       my_inst = list(conn.instances.filter(
+                               Filters=[{"Name": "instance-id", "Values": 
[options["my_instance_id"]]}]))
+                       if my_inst and my_inst[0].tags:
+                               for tag in my_inst[0].tags:
+                                       if tag['Key'] == 'build_number':
+                                               options["my_build_number"] = 
tag['Value']
+                                               logger.debug("Cached own 
build_number: %s", options["my_build_number"])
+                                               break
+               except Exception as e:
+                       logger.debug("Could not cache own build_number: %s", e)
+
        result = fence_action(conn, options, set_power_status, 
get_power_status, get_nodes_list)
        sys.exit(result)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fence-agents-4.17.0+git.1781703439.0804740/agents/kubevirt/fence_kubevirt.py
 
new/fence-agents-4.17.0+git.1784121740.9871084/agents/kubevirt/fence_kubevirt.py
--- 
old/fence-agents-4.17.0+git.1781703439.0804740/agents/kubevirt/fence_kubevirt.py
    2026-06-17 15:37:19.000000000 +0200
+++ 
new/fence-agents-4.17.0+git.1784121740.9871084/agents/kubevirt/fence_kubevirt.py
    2026-07-15 15:22:20.000000000 +0200
@@ -65,8 +65,11 @@
         fail(EC_STATUS)
 
 def translate_status(instance_status):
+    logging.debug(f"translate_status(): {instance_status}")
     if instance_status == "Running":
         return "on"
+    elif instance_status in ["Succeeded", "Failed"]:
+        return "off"
     return "unknown"
 
 def set_power_status(conn, options):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fence-agents-4.17.0+git.1781703439.0804740/agents/vmware_rest/fence_vmware_rest.py
 
new/fence-agents-4.17.0+git.1784121740.9871084/agents/vmware_rest/fence_vmware_rest.py
--- 
old/fence-agents-4.17.0+git.1781703439.0804740/agents/vmware_rest/fence_vmware_rest.py
      2026-06-17 15:37:19.000000000 +0200
+++ 
new/fence-agents-4.17.0+git.1784121740.9871084/agents/vmware_rest/fence_vmware_rest.py
      2026-07-15 15:22:20.000000000 +0200
@@ -6,7 +6,7 @@
 import atexit
 sys.path.append("@FENCEAGENTSLIBDIR@")
 from fencing import *
-from fencing import fail, run_delay, EC_LOGIN_DENIED, EC_STATUS
+from fencing import fail, fail_usage, run_command, run_delay, EC_LOGIN_DENIED, 
EC_STATUS
 
 if sys.version_info[0] > 2: import urllib.parse as urllib
 else: import urllib
@@ -69,6 +69,8 @@
        return outlets
 
 def connect(opt):
+       if "--token" not in opt and ("--username" not in opt or "--password" 
not in opt):
+               fail_usage("Failed: You must provide either a token, a 
token-script, or a username/password.")
        conn = pycurl.Curl()
 
        ## setup correct URL
@@ -88,9 +90,6 @@
                "Accept: application/json",
        ])
 
-       conn.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
-       conn.setopt(pycurl.USERPWD, opt["--username"] + ":" + opt["--password"])
-
        conn.setopt(pycurl.TIMEOUT, int(opt["--shell-timeout"]))
 
        if "--ssl-secure" in opt:
@@ -100,16 +99,22 @@
                conn.setopt(pycurl.SSL_VERIFYPEER, 0)
                conn.setopt(pycurl.SSL_VERIFYHOST, 0)
 
-       try:
-               result = send_command(conn, "com/vmware/cis/session", "POST")
-       except Exception as e:
-               logging.debug("Failed: {}".format(e))
-               fail(EC_LOGIN_DENIED)
+       if "--token" not in opt:
+               conn.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
+               conn.setopt(pycurl.USERPWD, opt["--username"] + ":" + 
opt["--password"])
+
+               try:
+                       result = send_command(conn, "com/vmware/cis/session", 
"POST")
+               except Exception as e:
+                       logging.debug("Failed: {}".format(e))
+                       fail(EC_LOGIN_DENIED)
+
+               conn.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_NONE)
 
        # set session id for later requests
        conn.setopt(pycurl.HTTPHEADER, [
                "Accept: application/json",
-               "vmware-api-session-id: {}".format(result["value"]),
+               "vmware-api-session-id: {}".format(opt["--token"] if "--token" 
in opt else result["value"]),
        ])
 
        return conn
@@ -182,6 +187,20 @@
                "shortdesc" : "Filter to only return relevant VMs. It can be 
used to avoid "
                              "the agent failing when more than 1000 VMs should 
be returned.",
                "order" : 2}
+       all_opt["token"] = {
+               "getopt" : ":",
+               "longopt" : "token",
+               "help" : "--token=[token]                API Token",
+               "required" : "0",
+               "shortdesc" : "API Token",
+               "order" : 2}
+       all_opt["token_script"] = {
+               "getopt" : ":",
+               "longopt" : "token-script",
+               "help" : "--token-script=[script]        Script to retrieve a 
token",
+               "required" : "0",
+               "shortdesc" : "Script to retrieve a token",
+               "order" : 2}
 
 
 def main():
@@ -190,11 +209,15 @@
                "api_path",
                "login",
                "passwd",
+               "no_login",
+               "no_password",
                "ssl",
                "notls",
                "web",
                "port",
                "filter",
+               "token",
+               "token_script",
        ]
 
        atexit.register(atexit_handler)
@@ -204,6 +227,12 @@
        all_opt["power_wait"]["default"] = "1"
 
        options = check_input(device_opt, process_input(device_opt))
+       if "--token-script" in options:
+               try:
+                       options["--token"] = run_command(options, 
options["--token-script"])[1].strip()
+               except Exception as e:
+                       logging.error("Failed to execute token script: 
{}".format(e))
+                       sys.exit(EC_LOGIN_DENIED)
 
        docs = {}
        docs["shortdesc"] = "Fence agent for VMware REST API"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fence-agents-4.17.0+git.1781703439.0804740/tests/data/metadata/fence_aws.xml
 
new/fence-agents-4.17.0+git.1784121740.9871084/tests/data/metadata/fence_aws.xml
--- 
old/fence-agents-4.17.0+git.1781703439.0804740/tests/data/metadata/fence_aws.xml
    2026-06-17 15:37:19.000000000 +0200
+++ 
new/fence-agents-4.17.0+git.1784121740.9871084/tests/data/metadata/fence_aws.xml
    2026-07-15 15:22:20.000000000 +0200
@@ -1,7 +1,17 @@
 <?xml version="1.0" ?>
-<resource-agent name="fence_aws" shortdesc="Fence agent for AWS (Amazon Web 
Services)" >
+<resource-agent name="fence_aws" shortdesc="Fence agent for AWS (Amazon Web 
Services) with multiple identity resolution methods" >
 <longdesc>fence_aws is a Power Fencing agent for AWS (Amazon WebServices). It 
uses the boto3 library to connect to AWS.
 
+It supports four identity resolution methods via --identity-method:
+
+instance-id (default): --plug is treated as a direct EC2 instance ID.
+
+tag: --plug is treated as a tag value. Requires --tag to specify the tag 
name.For example: --identity-method=tag --tag=Name --plug=hostname
+
+eni: --plug is treated as an ENI ID. The agent resolves the ENI attachment to 
findthe instance. Ideal for architectures with persistent ENIs that survive 
instance replacement.For example: --identity-method=eni 
--plug=eni-0a1b2c3d4e5f67890
+
+ebs: --plug is treated as an EBS volume ID. The agent resolves the volume 
attachment tofind the instance. Ideal for architectures with persistent EBS 
volumes.For example: --identity-method=ebs --plug=vol-0a1b2c3d4e5f67890
+
 boto3 can be configured with AWS CLI or by creating ~/.aws/credentials.
 For instructions see: 
https://boto3.readthedocs.io/en/latest/guide/quickstart.html#configuration</longdesc>
 <vendor-url>http://www.amazon.com</vendor-url>
@@ -21,7 +31,7 @@
                <content type="string"  />
                <shortdesc lang="en">Physical plug number on device, UUID or 
identification of machine</shortdesc>
        </parameter>
-       <parameter name="region" unique="0" required="0">
+       <parameter name="region" unique="0" required="1">
                <getopt mixed="-r, --region=[region]" />
                <content type="string"  />
                <shortdesc lang="en">Region.</shortdesc>
@@ -56,6 +66,16 @@
                <content type="string" default="true"  />
                <shortdesc lang="en">Use SkipOsShutdown flag to stop the EC2 
instance</shortdesc>
        </parameter>
+       <parameter name="tag" unique="0" required="0">
+               <getopt mixed="--tag=[tag_name]" />
+               <content type="string"  />
+               <shortdesc lang="en">Tag name for instance 
identification</shortdesc>
+       </parameter>
+       <parameter name="identity_method" unique="0" required="0">
+               <getopt mixed="--identity-method=[method]" />
+               <content type="string" default="instance-id"  />
+               <shortdesc lang="en">How to resolve --plug to an instance ID. 
'instance-id' treats plug as a direct instance ID, 'tag' uses EC2 tag lookup, 
'eni' resolves via ENI attachment, 'ebs' resolves via EBS volume 
attachment.</shortdesc>
+       </parameter>
        <parameter name="quiet" unique="0" required="0">
                <getopt mixed="-q, --quiet" />
                <content type="boolean"  />
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fence-agents-4.17.0+git.1781703439.0804740/tests/data/metadata/fence_vmware_rest.xml
 
new/fence-agents-4.17.0+git.1784121740.9871084/tests/data/metadata/fence_vmware_rest.xml
--- 
old/fence-agents-4.17.0+git.1781703439.0804740/tests/data/metadata/fence_vmware_rest.xml
    2026-06-17 15:37:19.000000000 +0200
+++ 
new/fence-agents-4.17.0+git.1784121740.9871084/tests/data/metadata/fence_vmware_rest.xml
    2026-07-15 15:22:20.000000000 +0200
@@ -25,7 +25,7 @@
                <content type="integer" default="80"  />
                <shortdesc lang="en">TCP/UDP port to use for connection with 
device</shortdesc>
        </parameter>
-       <parameter name="login" unique="0" required="1" deprecated="1">
+       <parameter name="login" unique="0" required="0" deprecated="1">
                <getopt mixed="-l, --username=[name]" />
                <content type="string"  />
                <shortdesc lang="en">Login name</shortdesc>
@@ -80,7 +80,7 @@
                <content type="boolean"  />
                <shortdesc lang="en">Use SSL connection with verifying 
certificate</shortdesc>
        </parameter>
-       <parameter name="username" unique="0" required="1" obsoletes="login">
+       <parameter name="username" unique="0" required="0" obsoletes="login">
                <getopt mixed="-l, --username=[name]" />
                <content type="string"  />
                <shortdesc lang="en">Login name</shortdesc>
@@ -94,6 +94,16 @@
                <content type="string"  />
                <shortdesc lang="en">Filter to only return relevant VMs. It can 
be used to avoid the agent failing when more than 1000 VMs should be 
returned.</shortdesc>
        </parameter>
+       <parameter name="token" unique="0" required="0">
+               <getopt mixed="--token=[token]" />
+               <content type="string"  />
+               <shortdesc lang="en">API Token</shortdesc>
+       </parameter>
+       <parameter name="token_script" unique="0" required="0">
+               <getopt mixed="--token-script=[script]" />
+               <content type="string"  />
+               <shortdesc lang="en">Script to retrieve a token</shortdesc>
+       </parameter>
        <parameter name="quiet" unique="0" required="0">
                <getopt mixed="-q, --quiet" />
                <content type="boolean"  />

Reply via email to