diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:50:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 06:50:17 +0000 |
commit | 86ed03f8adee56c050c73018537371c230a664a6 (patch) | |
tree | eae3d04cdf1c49848e5a671327ab38297f4acb0d /lib | |
parent | Initial commit. (diff) | |
download | fence-agents-upstream.tar.xz fence-agents-upstream.zip |
Adding upstream version 4.12.1.upstream/4.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | lib/Makefile.am | 34 | ||||
-rw-r--r-- | lib/XenAPI.py.py | 212 | ||||
-rw-r--r-- | lib/azure_fence.py.py | 393 | ||||
-rw-r--r-- | lib/check_used_options.py | 68 | ||||
-rw-r--r-- | lib/fence.rng.head | 7 | ||||
-rw-r--r-- | lib/fence.rng.tail | 13 | ||||
-rw-r--r-- | lib/fence2man.xsl | 78 | ||||
-rw-r--r-- | lib/fence2rng.xsl | 184 | ||||
-rw-r--r-- | lib/fence2wiki.xsl | 14 | ||||
-rw-r--r-- | lib/fencing.py.py | 1748 | ||||
-rw-r--r-- | lib/fencing_snmp.py.py | 128 | ||||
-rw-r--r-- | lib/metadata.rng | 80 | ||||
-rw-r--r-- | lib/tests/test_fencing.py | 123 |
13 files changed, 3082 insertions, 0 deletions
diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..0fe7096 --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,34 @@ +MAINTAINERCLEANFILES = Makefile.in + +TARGET = fencing.py fencing_snmp.py azure_fence.py + +if BUILD_XENAPILIB +TARGET += XenAPI.py +endif + +SRC = fencing.py.py fencing_snmp.py.py XenAPI.py.py azure_fence.py.py check_used_options.py + +XSL = fence2man.xsl fence2rng.xsl fence2wiki.xsl + +FASRNG = fence.rng.head fence.rng.tail metadata.rng + +EXTRA_DIST = $(SRC) $(XSL) $(FASRNG) + +fencelibdir = ${FENCEAGENTSLIBDIR} + +fencelib_DATA = $(TARGET) + +rngdir = ${CLUSTERDATA}/relaxng + +rng_DATA = $(XSL) $(FASRNG) + +azure_fence.py: fencing.py +fencing_snmp.py: fencing.py +check_used_options.py: fencing.py + +include $(top_srcdir)/make/fencebuild.mk + +xml-check: all +xml-upload: all + +clean-man: diff --git a/lib/XenAPI.py.py b/lib/XenAPI.py.py new file mode 100644 index 0000000..6fe11ef --- /dev/null +++ b/lib/XenAPI.py.py @@ -0,0 +1,212 @@ +#============================================================================ +# This library is free software; you can redistribute it and/or +# modify it under the terms of version 2.1 of the GNU Lesser General Public +# License as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +#============================================================================ +# Copyright (C) 2006 XenSource Inc. +#============================================================================ +# +# Parts of this file are based upon xmlrpclib.py, the XML-RPC client +# interface included in the Python distribution. +# +# Copyright (c) 1999-2002 by Secret Labs AB +# Copyright (c) 1999-2002 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + +import sys +import gettext +import socket +import logging + +if sys.version_info[0] > 2: + import xmlrpc.client as xmlrpclib + import http.client as httplib +else: + import xmlrpclib + import httplib + +translation = gettext.translation('xen-xm', fallback=True) + +class Failure(Exception): + def __init__(self, details): + try: + # If this failure is MESSAGE_PARAMETER_COUNT_MISMATCH, then we + # correct the return values here, to account for the fact that we + # transparently add the session handle as the first argument. + if details[0] == 'MESSAGE_PARAMETER_COUNT_MISMATCH': + details[2] = str(int(details[2]) - 1) + details[3] = str(int(details[3]) - 1) + + self.details = details + except Exception as exn: + self.details = ['INTERNAL_ERROR', 'Client-side: ' + str(exn)] + + def __str__(self): + try: + return translation.ugettext(self.details[0]) % self._details_map() + except TypeError as exn: + return "Message database broken: %s.\nXen-API failure: %s" % \ + (exn, str(self.details)) + except Exception as exn: + logging.error("%s\n", str(exn)) + return "Xen-API failure: %s" % str(self.details) + + def _details_map(self): + return dict([(str(i), self.details[i]) + for i in range(len(self.details))]) + + +_RECONNECT_AND_RETRY = (lambda _: ()) + +class UDSHTTPConnection(httplib.HTTPConnection): + """ Stupid hacked up HTTPConnection subclass to allow HTTP over Unix domain + sockets. """ + def connect(self): + path = self.host.replace("_", "/") + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(path) + +class UDSTransport(xmlrpclib.Transport): + def make_connection(self, host): + return httplib.HTTPConnection(host) + +class Session(xmlrpclib.ServerProxy): + """A server proxy and session manager for communicating with Xend using + the Xen-API. + + Example: + + session = Session('http://localhost:9363/') + session.login_with_password('me', 'mypassword') + session.xenapi.VM.start(vm_uuid) + session.xenapi.session.logout() + + For now, this class also supports the legacy XML-RPC API, using + session.xend.domain('Domain-0') and similar. This support will disappear + once there is a working Xen-API replacement for every call in the legacy + API. + """ + + def __init__(self, uri, transport=None, encoding=None, verbose=0, + allow_none=1): + xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, + verbose, allow_none) + self._session = None + self.last_login_method = None + self.last_login_params = None + + + def xenapi_request(self, methodname, params): + if methodname.startswith('login'): + self._login(methodname, params) + return None + else: + retry_count = 0 + while retry_count < 3: + full_params = (self._session,) + params + result = _parse_result(getattr(self, methodname)(*full_params)) + if result == _RECONNECT_AND_RETRY: + retry_count += 1 + if self.last_login_method: + self._login(self.last_login_method, + self.last_login_params) + else: + raise xmlrpclib.Fault(401, 'You must log in') + else: + return result + raise xmlrpclib.Fault( + 500, 'Tried 3 times to get a valid session, but failed') + + + def _login(self, method, params): + result = _parse_result(getattr(self, 'session.%s' % method)(*params)) + if result == _RECONNECT_AND_RETRY: + raise xmlrpclib.Fault( + 500, 'Received SESSION_INVALID when logging in') + self._session = result + self.last_login_method = method + self.last_login_params = params + + + def __getattr__(self, name): + if name == 'xenapi': + return _Dispatcher(self.xenapi_request, None) + elif name.startswith('login'): + return lambda *params: self._login(name, params) + else: + return xmlrpclib.ServerProxy.__getattr__(self, name) + +def xapi_local(): + return Session("http://_var_xapi_xapi/", transport=UDSTransport()) + +def _parse_result(result): + if type(result) != dict or 'Status' not in result: + raise xmlrpclib.Fault(500, 'Missing Status in response from server' + result) + if result['Status'] == 'Success': + if 'Value' in result: + return result['Value'] + else: + raise xmlrpclib.Fault(500, + 'Missing Value in response from server') + else: + if 'ErrorDescription' in result: + if result['ErrorDescription'][0] == 'SESSION_INVALID': + return _RECONNECT_AND_RETRY + else: + raise Failure(result['ErrorDescription']) + else: + raise xmlrpclib.Fault( + 500, 'Missing ErrorDescription in response from server') + + +# Based upon _Method from xmlrpclib. +class _Dispatcher: + def __init__(self, send, name): + self.__send = send + self.__name = name + + def __repr__(self): + if self.__name: + return '<XenAPI._Dispatcher for %s>' % self.__name + else: + return '<XenAPI._Dispatcher>' + + def __getattr__(self, name): + if self.__name is None: + return _Dispatcher(self.__send, name) + else: + return _Dispatcher(self.__send, "%s.%s" % (self.__name, name)) + + def __call__(self, *args): + return self.__send(self.__name, args) diff --git a/lib/azure_fence.py.py b/lib/azure_fence.py.py new file mode 100644 index 0000000..5ca71eb --- /dev/null +++ b/lib/azure_fence.py.py @@ -0,0 +1,393 @@ +import logging, re, time
+from fencing import fail_usage
+
+FENCE_SUBNET_NAME = "fence-subnet"
+FENCE_INBOUND_RULE_NAME = "FENCE_DENY_ALL_INBOUND"
+FENCE_INBOUND_RULE_DIRECTION = "Inbound"
+FENCE_OUTBOUND_RULE_NAME = "FENCE_DENY_ALL_OUTBOUND"
+FENCE_OUTBOUND_RULE_DIRECTION = "Outbound"
+FENCE_STATE_OFF = "off"
+FENCE_STATE_ON = "on"
+FENCE_TAG_SUBNET_ID = "FENCE_TAG_SUBNET_ID"
+FENCE_TAG_IP_TYPE = "FENCE_TAG_IP_TYPE"
+FENCE_TAG_IP = "FENCE_TAG_IP"
+IP_TYPE_DYNAMIC = "Dynamic"
+MAX_RETRY = 10
+RETRY_WAIT = 5
+
+class AzureSubResource:
+ Type = None
+ Name = None
+
+class AzureResource:
+ Id = None
+ SubscriptionId = None
+ ResourceGroupName = None
+ ResourceName = None
+ SubResources = []
+
+class AzureConfiguration:
+ RGName = None
+ VMName = None
+ SubscriptionId = None
+ Cloud = None
+ UseMSI = None
+ Tenantid = None
+ ApplicationId = None
+ ApplicationKey = None
+ Verbose = None
+
+def get_from_metadata(parameter):
+ import requests
+ try:
+ r = requests.get('http://169.254.169.254/metadata/instance?api-version=2017-08-01', headers = {"Metadata":"true"})
+ logging.debug("metadata: " + str(r.json()))
+ return str(r.json()["compute"][parameter])
+ except:
+ logging.warning("Not able to use metadata service. Am I running in Azure?")
+
+ return None
+
+def get_azure_resource(id):
+ match = re.match('(/subscriptions/([^/]*)/resourceGroups/([^/]*))(/providers/([^/]*/[^/]*)/([^/]*))?((/([^/]*)/([^/]*))*)', id)
+ if not match:
+ fail_usage("{get_azure_resource} cannot parse resource id %s" % id)
+
+ logging.debug("{get_azure_resource} found %s matches for %s" % (len(match.groups()), id))
+ iGroup = 0
+ while iGroup < len(match.groups()):
+ logging.debug("{get_azure_resource} group %s: %s" %(iGroup, match.group(iGroup)))
+ iGroup += 1
+
+ resource = AzureResource()
+ resource.Id = id
+ resource.SubscriptionId = match.group(2)
+ resource.SubResources = []
+
+ if len(match.groups()) > 3:
+ resource.ResourceGroupName = match.group(3)
+ logging.debug("{get_azure_resource} resource group %s" % resource.ResourceGroupName)
+
+ if len(match.groups()) > 6:
+ resource.ResourceName = match.group(6)
+ logging.debug("{get_azure_resource} resource name %s" % resource.ResourceName)
+
+ if len(match.groups()) > 7 and match.group(7):
+ splits = match.group(7).split("/")
+ logging.debug("{get_azure_resource} splitting subtypes '%s' (%s)" % (match.group(7), len(splits)))
+ i = 1 # the string starts with / so the first split is empty
+ while i < len(splits) - 1:
+ logging.debug("{get_azure_resource} creating subresource with type %s and name %s" % (splits[i], splits[i+1]))
+ subRes = AzureSubResource()
+ subRes.Type = splits[i]
+ subRes.Name = splits[i+1]
+ resource.SubResources.append(subRes)
+ i += 2
+
+ return resource
+
+def get_fence_subnet_for_config(ipConfig, network_client):
+ subnetResource = get_azure_resource(ipConfig.subnet.id)
+ logging.debug("{get_fence_subnet_for_config} testing virtual network %s in resource group %s for a fence subnet" %(subnetResource.ResourceName, subnetResource.ResourceGroupName))
+ vnet = network_client.virtual_networks.get(subnetResource.ResourceGroupName, subnetResource.ResourceName)
+ return get_subnet(vnet, FENCE_SUBNET_NAME)
+
+def get_subnet(vnet, subnetName):
+ for avSubnet in vnet.subnets:
+ logging.debug("{get_subnet} searching subnet %s testing subnet %s" % (subnetName, avSubnet.name))
+ if (avSubnet.name.lower() == subnetName.lower()):
+ logging.debug("{get_subnet} subnet found %s" % avSubnet)
+ return avSubnet
+
+def test_fence_subnet(fenceSubnet, nic, network_client):
+ logging.info("{test_fence_subnet}")
+ testOk = True
+ if not fenceSubnet:
+ testOk = False
+ logging.info("{test_fence_subnet} No fence subnet found for virtual network of network interface %s" % nic.id)
+ else:
+ if not fenceSubnet.network_security_group:
+ testOk = False
+ logging.info("{test_fence_subnet} Fence subnet %s has not network security group" % fenceSubnet.id)
+ else:
+ nsgResource = get_azure_resource(fenceSubnet.network_security_group.id)
+ logging.info("{test_fence_subnet} Getting network security group %s in resource group %s" % (nsgResource.ResourceName, nsgResource.ResourceGroupName))
+ nsg = network_client.network_security_groups.get(nsgResource.ResourceGroupName, nsgResource.ResourceName)
+ inboundRule = get_inbound_rule_for_nsg(nsg)
+ outboundRule = get_outbound_rule_for_nsg(nsg)
+ if not outboundRule:
+ testOk = False
+ logging.info("{test_fence_subnet} Network Securiy Group %s of fence subnet %s has no outbound security rule that blocks all traffic" % (nsgResource.ResourceName, fenceSubnet.id))
+ elif not inboundRule:
+ testOk = False
+ logging.info("{test_fence_subnet} Network Securiy Group %s of fence subnet %s has no inbound security rule that blocks all traffic" % (nsgResource.ResourceName, fenceSubnet.id))
+
+ return testOk
+
+def get_inbound_rule_for_nsg(nsg):
+ return get_rule_for_nsg(nsg, FENCE_INBOUND_RULE_NAME, FENCE_INBOUND_RULE_DIRECTION)
+
+def get_outbound_rule_for_nsg(nsg):
+ return get_rule_for_nsg(nsg, FENCE_OUTBOUND_RULE_NAME, FENCE_OUTBOUND_RULE_DIRECTION)
+
+def get_rule_for_nsg(nsg, ruleName, direction):
+ logging.info("{get_rule_for_nsg} Looking for security rule %s with direction %s" % (ruleName, direction))
+ if not nsg:
+ logging.info("{get_rule_for_nsg} Network security group not set")
+ return None
+
+ for rule in nsg.security_rules:
+ logging.info("{get_rule_for_nsg} Testing a %s securiy rule %s" % (rule.direction, rule.name))
+ if (rule.access == "Deny") and (rule.direction == direction) \
+ and (rule.source_port_range == "*") and (rule.destination_port_range == "*") \
+ and (rule.protocol == "*") and (rule.destination_address_prefix == "*") \
+ and (rule.source_address_prefix == "*") and (rule.provisioning_state == "Succeeded") \
+ and (rule.priority == 100) and (rule.name == ruleName):
+ logging.info("{get_rule_for_nsg} %s rule found" % direction)
+ return rule
+
+ return None
+
+def get_network_state(compute_client, network_client, rgName, vmName):
+ result = FENCE_STATE_ON
+
+ try:
+ vm = compute_client.virtual_machines.get(rgName, vmName, "instanceView")
+
+ allNICOK = True
+ for nicRef in vm.network_profile.network_interfaces:
+ nicresource = get_azure_resource(nicRef.id)
+ nic = network_client.network_interfaces.get(nicresource.ResourceGroupName, nicresource.ResourceName)
+ for ipConfig in nic.ip_configurations:
+ logging.info("{get_network_state} Testing ip configuration %s" % ipConfig.name)
+ fenceSubnet = get_fence_subnet_for_config(ipConfig, network_client)
+ testOk = test_fence_subnet(fenceSubnet, nic, network_client)
+ if not testOk:
+ allNICOK = False
+ elif fenceSubnet.id.lower() != ipConfig.subnet.id.lower():
+ logging.info("{get_network_state} IP configuration %s is not in fence subnet (ip subnet: %s, fence subnet: %s)" % (ipConfig.name, ipConfig.subnet.id.lower(), fenceSubnet.id.lower()))
+ allNICOK = False
+ if allNICOK:
+ logging.info("{get_network_state} All IP configurations of all network interfaces are in the fence subnet. Declaring VM as off")
+ result = FENCE_STATE_OFF
+ except Exception as e:
+ fail_usage("{get_network_state} Failed: %s" % e)
+
+ return result
+
+def set_network_state(compute_client, network_client, rgName, vmName, operation):
+ import msrestazure.azure_exceptions
+ logging.info("{set_network_state} Setting state %s for %s in resource group %s" % (operation, vmName, rgName))
+
+ vm = compute_client.virtual_machines.get(rgName, vmName, "instanceView")
+
+ operations = []
+ for nicRef in vm.network_profile.network_interfaces:
+ for attempt in range(0, MAX_RETRY):
+ try:
+ nicresource = get_azure_resource(nicRef.id)
+ nic = network_client.network_interfaces.get(nicresource.ResourceGroupName, nicresource.ResourceName)
+
+ if not nic.tags and operation == "block":
+ nic.tags = {}
+
+ logging.info("{set_network_state} Searching for tags required to unfence this virtual machine")
+ for ipConfig in nic.ip_configurations:
+ if operation == "block":
+ fenceSubnet = get_fence_subnet_for_config(ipConfig, network_client)
+ testOk = test_fence_subnet(fenceSubnet, nic, network_client)
+ if testOk:
+ logging.info("{set_network_state} Changing subnet of ip config of nic %s" % nic.id)
+ nic.tags[("%s_%s" % (FENCE_TAG_SUBNET_ID, ipConfig.name))] = ipConfig.subnet.id
+ nic.tags[("%s_%s" % (FENCE_TAG_IP_TYPE, ipConfig.name))] = ipConfig.private_ip_allocation_method
+ nic.tags[("%s_%s" % (FENCE_TAG_IP, ipConfig.name))] = ipConfig.private_ip_address
+ ipConfig.subnet = fenceSubnet
+ ipConfig.private_ip_allocation_method = IP_TYPE_DYNAMIC
+ else:
+ fail_usage("{set_network_state} Network interface id %s does not have a network security group." % nic.id)
+ elif operation == "unblock":
+ if not nic.tags:
+ fail_usage("{set_network_state} IP configuration %s is missing the required resource tags (empty)" % ipConfig.name)
+
+ subnetId = nic.tags.pop("%s_%s" % (FENCE_TAG_SUBNET_ID, ipConfig.name))
+ ipType = nic.tags.pop("%s_%s" % (FENCE_TAG_IP_TYPE, ipConfig.name))
+ ipAddress = nic.tags.pop("%s_%s" % (FENCE_TAG_IP, ipConfig.name))
+
+ if (subnetId and ipType and (ipAddress or (ipType.lower() == IP_TYPE_DYNAMIC.lower()))):
+ logging.info("{set_network_state} tags found (subnetId: %s, ipType: %s, ipAddress: %s)" % (subnetId, ipType, ipAddress))
+
+ subnetResource = get_azure_resource(subnetId)
+ vnet = network_client.virtual_networks.get(subnetResource.ResourceGroupName, subnetResource.ResourceName)
+ logging.info("{set_network_state} looking for subnet %s" % len(subnetResource.SubResources))
+ oldSubnet = get_subnet(vnet, subnetResource.SubResources[0].Name)
+ if not oldSubnet:
+ fail_usage("{set_network_state} subnet %s not found" % subnetId)
+
+ ipConfig.subnet = oldSubnet
+ ipConfig.private_ip_allocation_method = ipType
+ if ipAddress:
+ ipConfig.private_ip_address = ipAddress
+ else:
+ fail_usage("{set_network_state} IP configuration %s is missing the required resource tags(subnetId: %s, ipType: %s, ipAddress: %s)" % (ipConfig.name, subnetId, ipType, ipAddress))
+
+ logging.info("{set_network_state} updating nic %s" % (nic.id))
+ op = network_client.network_interfaces.create_or_update(nicresource.ResourceGroupName, nicresource.ResourceName, nic)
+ operations.append(op)
+ break
+ except msrestazure.azure_exceptions.CloudError as cex:
+ logging.error("{set_network_state} CloudError in attempt %s '%s'" % (attempt, cex))
+ if cex.error and cex.error.error and cex.error.error.lower() == "PrivateIPAddressIsBeingCleanedUp":
+ logging.error("{set_network_state} PrivateIPAddressIsBeingCleanedUp")
+ time.sleep(RETRY_WAIT)
+
+ except Exception as ex:
+ logging.error("{set_network_state} Exception of type %s: %s" % (type(ex).__name__, ex))
+ break
+
+def get_azure_config(options):
+ config = AzureConfiguration()
+
+ config.RGName = options.get("--resourceGroup")
+ config.VMName = options.get("--plug")
+ config.SubscriptionId = options.get("--subscriptionId")
+ config.Cloud = options.get("--cloud")
+ config.UseMSI = "--msi" in options
+ config.Tenantid = options.get("--tenantId")
+ config.ApplicationId = options.get("--username")
+ config.ApplicationKey = options.get("--password")
+ config.Verbose = options.get("--verbose")
+
+ if not config.RGName:
+ logging.info("resourceGroup not provided. Using metadata service")
+ config.RGName = get_from_metadata("resourceGroupName")
+
+ if not config.SubscriptionId:
+ logging.info("subscriptionId not provided. Using metadata service")
+ config.SubscriptionId = get_from_metadata("subscriptionId")
+
+ return config
+
+def get_azure_cloud_environment(config):
+ cloud_environment = None
+ if config.Cloud:
+ if (config.Cloud.lower() == "china"):
+ from msrestazure.azure_cloud import AZURE_CHINA_CLOUD
+ cloud_environment = AZURE_CHINA_CLOUD
+ elif (config.Cloud.lower() == "germany"):
+ from msrestazure.azure_cloud import AZURE_GERMAN_CLOUD
+ cloud_environment = AZURE_GERMAN_CLOUD
+ elif (config.Cloud.lower() == "usgov"):
+ from msrestazure.azure_cloud import AZURE_US_GOV_CLOUD
+ cloud_environment = AZURE_US_GOV_CLOUD
+
+ return cloud_environment
+
+def get_azure_credentials(config):
+ credentials = None
+ cloud_environment = get_azure_cloud_environment(config)
+ if config.UseMSI and cloud_environment:
+ try:
+ from azure.identity import ManagedIdentityCredential
+ credentials = ManagedIdentityCredential(cloud_environment=cloud_environment)
+ except ImportError:
+ from msrestazure.azure_active_directory import MSIAuthentication
+ credentials = MSIAuthentication(cloud_environment=cloud_environment)
+ elif config.UseMSI:
+ try:
+ from azure.identity import ManagedIdentityCredential
+ credentials = ManagedIdentityCredential()
+ except ImportError:
+ from msrestazure.azure_active_directory import MSIAuthentication
+ credentials = MSIAuthentication()
+ elif cloud_environment:
+ try:
+ # try to use new libraries ClientSecretCredential (azure.identity, based on azure.core)
+ from azure.identity import ClientSecretCredential
+ credentials = ClientSecretCredential(
+ client_id = config.ApplicationId,
+ client_secret = config.ApplicationKey,
+ tenant_id = config.Tenantid,
+ cloud_environment=cloud_environment
+ )
+ except ImportError:
+ # use old libraries ServicePrincipalCredentials (azure.common) if new one is not available
+ from azure.common.credentials import ServicePrincipalCredentials
+ credentials = ServicePrincipalCredentials(
+ client_id = config.ApplicationId,
+ secret = config.ApplicationKey,
+ tenant = config.Tenantid,
+ cloud_environment=cloud_environment
+ )
+ else:
+ try:
+ # try to use new libraries ClientSecretCredential (azure.identity, based on azure.core)
+ from azure.identity import ClientSecretCredential
+ credentials = ClientSecretCredential(
+ client_id = config.ApplicationId,
+ client_secret = config.ApplicationKey,
+ tenant_id = config.Tenantid
+ )
+ except ImportError:
+ # use old libraries ServicePrincipalCredentials (azure.common) if new one is not available
+ from azure.common.credentials import ServicePrincipalCredentials
+ credentials = ServicePrincipalCredentials(
+ client_id = config.ApplicationId,
+ secret = config.ApplicationKey,
+ tenant = config.Tenantid
+ )
+
+ return credentials
+
+def get_azure_compute_client(config):
+ from azure.mgmt.compute import ComputeManagementClient
+
+ cloud_environment = get_azure_cloud_environment(config)
+ credentials = get_azure_credentials(config)
+
+ if cloud_environment:
+ try:
+ compute_client = ComputeManagementClient(
+ credentials,
+ config.SubscriptionId,
+ base_url=cloud_environment.endpoints.resource_manager,
+ credential_scopes=[cloud_environment.endpoints.resource_manager + "/.default"]
+ )
+ except TypeError:
+ compute_client = ComputeManagementClient(
+ credentials,
+ config.SubscriptionId,
+ base_url=cloud_environment.endpoints.resource_manager
+ )
+ else:
+ compute_client = ComputeManagementClient(
+ credentials,
+ config.SubscriptionId
+ )
+ return compute_client
+
+def get_azure_network_client(config):
+ from azure.mgmt.network import NetworkManagementClient
+
+ cloud_environment = get_azure_cloud_environment(config)
+ credentials = get_azure_credentials(config)
+
+ if cloud_environment:
+ try:
+ network_client = NetworkManagementClient(
+ credentials,
+ config.SubscriptionId,
+ base_url=cloud_environment.endpoints.resource_manager,
+ credential_scopes=[cloud_environment.endpoints.resource_manager + "/.default"]
+ )
+ except TypeError:
+ network_client = NetworkManagementClient(
+ credentials,
+ config.SubscriptionId,
+ base_url=cloud_environment.endpoints.resource_manager
+ )
+ else:
+ network_client = NetworkManagementClient(
+ credentials,
+ config.SubscriptionId
+ )
+ return network_client
diff --git a/lib/check_used_options.py b/lib/check_used_options.py new file mode 100644 index 0000000..ad4a387 --- /dev/null +++ b/lib/check_used_options.py @@ -0,0 +1,68 @@ +## Check if fence agent uses only options["--??"] which are defined in fencing library or +## fence agent itself +## +## Usage: ./check_used_options.py fence-agent (e.g. lpar/fence_lpar.py) +## + +import sys, re +sys.path.append("@FENCEAGENTSLIBDIR@") +from fencing import all_opt + +def main(): + agent = sys.argv[1] + + available = {} + + ## all_opt from fencing library are imported + for k in list(all_opt.keys()): + if "longopt" in all_opt[k]: + available["--" + all_opt[k]["longopt"]] = True + + ## add UUID which is derived automatically from --plug if possible + available["--uuid"] = True + + ## all_opt defined in fence agent are found + agent_file = open(agent) + opt_re = re.compile(r"\s*all_opt\[\"([^\"]*)\"\] = {") + opt_longopt_re = re.compile(r"\"longopt\"\s*:\s*\"([^\"]*)\"") + + in_opt = False + for line in agent_file: + if opt_re.search(line) != None: + in_opt = True + if in_opt and opt_longopt_re.search(line) != None: + available["--" + opt_longopt_re.search(line).group(1)] = True + in_opt = False + + ## check if all options are defined + agent_file = open(agent) + option_use_re = re.compile(r"options\[\"(-[^\"]*)\"\]") + option_in_re = re.compile(r"\"(-[^\"]*)\" in options") + option_has_re = re.compile(r"options.has_key\(\"(-[^\"]*)\"\)") + + counter = 0 + without_errors = True + for line in agent_file: + counter += 1 + + for option in option_use_re.findall(line): + if option not in available: + print("ERROR on line %d in %s: option %s is not defined" % (counter, agent, option_use_re.search(line).group(1))) + without_errors = False + + for option in option_in_re.findall(line): + if option not in available: + print("ERROR on line %d in %s: option %s is not defined" % (counter, agent, option_has_re.search(line).group(1))) + without_errors = False + + for option in option_has_re.findall(line): + print("ERROR on line %d in %s: option %s: has_key() not supported in Python 3" % (counter, agent, option_has_re.search(line).group(1))) + without_errors = False + + if without_errors: + sys.exit(0) + else: + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/lib/fence.rng.head b/lib/fence.rng.head new file mode 100644 index 0000000..22f4452 --- /dev/null +++ b/lib/fence.rng.head @@ -0,0 +1,7 @@ +<!-- Autogenerated fence definitions --> + <define name="FENCEDEVICEOPTIONS"> + <optional> + <choice> + <!-- begin specific fence devices --> + + <!-- begin auto-generated device definitions --> diff --git a/lib/fence.rng.tail b/lib/fence.rng.tail new file mode 100644 index 0000000..2feab91 --- /dev/null +++ b/lib/fence.rng.tail @@ -0,0 +1,13 @@ + <!-- end auto-generated device definitions --> + + <group> + <optional> + <empty/> + </optional> + </group> + + <!-- end specific fence devices --> + </choice> + </optional> + </define> +<!-- end fence attribute group definitions --> diff --git a/lib/fence2man.xsl b/lib/fence2man.xsl new file mode 100644 index 0000000..3dc0b39 --- /dev/null +++ b/lib/fence2man.xsl @@ -0,0 +1,78 @@ +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:output method="text" indent="no"/> +<xsl:template match="parameter"> +<xsl:param name="show" /> +<xsl:if test="not(@deprecated)"> +.TP +<xsl:if test="$show = 'getopt'">.B <xsl:value-of select="getopt/@mixed" /></xsl:if> +<xsl:if test="$show = 'stdin'">.B <xsl:value-of select="@name"/></xsl:if> +. +<xsl:value-of select="normalize-space(shortdesc)"/> +<xsl:if test="count(content/option) > 1"> + <xsl:text> (</xsl:text> + <xsl:for-each select="content/option"> + <xsl:value-of select="@value" /> + <xsl:if test="position() < last()"> + <xsl:text>|</xsl:text> + </xsl:if> + </xsl:for-each> + <xsl:text>)</xsl:text> +</xsl:if> +<xsl:if test="not(content/@default)"><xsl:if test="@required = 1"> This parameter is always required.</xsl:if></xsl:if> +<xsl:if test="content/@default"> (Default Value: <xsl:value-of select="content/@default"/>)</xsl:if> +<xsl:if test="$show = 'stdin'"> +<xsl:if test="@obsoletes"> Obsoletes: <xsl:value-of select="@obsoletes" /></xsl:if> +</xsl:if> + +</xsl:if> +</xsl:template> + +<xsl:template match="action"> +.TP +\fB<xsl:value-of select="@name"/> \fP +<xsl:choose> +<xsl:when test="@name = 'on'">Power on machine.</xsl:when> +<xsl:when test="@name = 'off'">Power off machine.</xsl:when> +<xsl:when test="@name = 'enable'">Enable fabric access.</xsl:when> +<xsl:when test="@name = 'disable'">Disable fabric access.</xsl:when> +<xsl:when test="@name = 'reboot'">Reboot machine.</xsl:when> +<xsl:when test="@name = 'diag'">Pulse a diagnostic interrupt to the processor(s).</xsl:when> +<xsl:when test="@name = 'monitor'">Check the health of fence device</xsl:when> +<xsl:when test="@name = 'metadata'">Display the XML metadata describing this resource.</xsl:when> +<xsl:when test="@name = 'list'">List available plugs with aliases/virtual machines if there is support for more then one device. Returns N/A otherwise.</xsl:when> +<xsl:when test="@name = 'list-status'">List available plugs with aliases/virtual machines and their power state if it can be obtained without additional commands.</xsl:when> +<xsl:when test="@name = 'status'">This returns the status of the plug/virtual machine.</xsl:when> +<xsl:when test="@name = 'validate-all'">Validate if all required parameters are entered.</xsl:when> +<!-- Ehhh --> +<xsl:otherwise> The operational behavior of this is not known.</xsl:otherwise> +</xsl:choose> +</xsl:template> + +<xsl:template match="/resource-agent"> +.TH FENCE_AGENT 8 2009-10-20 "<xsl:value-of select="@name"/> (Fence Agent)" +.SH NAME +<xsl:value-of select="@name" /> - <xsl:value-of select="@shortdesc" /> +<xsl:for-each select="symlink"> +.P +<xsl:value-of select="@name" /> - <xsl:value-of select="@shortdesc" /> (symlink) +</xsl:for-each> +.SH DESCRIPTION +.P +<xsl:value-of select="longdesc"/> +.P +<xsl:value-of select="@name" /> accepts options on the command line as well +as from stdin. Fenced sends parameters through stdin when it execs the +agent. <xsl:value-of select="@name" /> can be run by itself with command +line options. This is useful for testing and for turning outlets on or off +from scripts. +<xsl:if test="vendor-url"> +Vendor URL: <xsl:value-of select="vendor-url" /> +</xsl:if> +.SH PARAMETERS +<xsl:apply-templates select="parameters"><xsl:with-param name="show">getopt</xsl:with-param></xsl:apply-templates> +.SH ACTIONS +<xsl:apply-templates select="actions"/> +.SH STDIN PARAMETERS +<xsl:apply-templates select="parameters"><xsl:with-param name="show">stdin</xsl:with-param></xsl:apply-templates> +</xsl:template> +</xsl:stylesheet> diff --git a/lib/fence2rng.xsl b/lib/fence2rng.xsl new file mode 100644 index 0000000..f6d465e --- /dev/null +++ b/lib/fence2rng.xsl @@ -0,0 +1,184 @@ +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:output method="text" indent="no"/> + +<xsl:param name="init-indent" select="' '"/> +<xsl:param name="indent" select="' '"/> + + +<!-- + helpers + --> + +<xsl:variable name="SP" select="' '"/> +<xsl:variable name="NL" select="'
'"/> +<xsl:variable name="Q" select="'"'"/> +<xsl:variable name="TS" select="'<'"/> +<xsl:variable name="TSc" select="'</'"/> +<xsl:variable name="TE" select="'>'"/> +<xsl:variable name="TEc" select="'/>'"/> + +<xsl:template name="comment"> + <xsl:param name="text" select="''"/> + <xsl:param name="indent" select="''"/> + <xsl:if test="$indent != 'none'"> + <xsl:value-of select="concat($init-indent, $indent)"/> + </xsl:if> + <xsl:value-of select="concat($TS, '!-- ', $text, ' --',$TE)"/> +</xsl:template> + +<xsl:template name="tag-start"> + <xsl:param name="name"/> + <xsl:param name="attrs" select="''"/> + <xsl:param name="indent" select="''"/> + <xsl:if test="$indent != 'none'"> + <xsl:value-of select="concat($init-indent, $indent)"/> + </xsl:if> + <xsl:value-of select="concat($TS, $name)"/> + <xsl:if test="$attrs != ''"> + <xsl:value-of select="concat($SP, $attrs)"/> + </xsl:if> + <xsl:value-of select="$TE"/> +</xsl:template> + +<xsl:template name="tag-end"> + <xsl:param name="name"/> + <xsl:param name="attrs" select="''"/> + <xsl:param name="indent" select="''"/> + <xsl:if test="$indent != 'none'"> + <xsl:value-of select="concat($init-indent, $indent)"/> + </xsl:if> + <xsl:value-of select="concat($TSc, $name)"/> + <xsl:if test="$attrs != ''"> + <xsl:value-of select="concat($SP, $attrs)"/> + </xsl:if> + <xsl:value-of select="$TE"/> +</xsl:template> + +<xsl:template name="tag-self"> + <xsl:param name="name"/> + <xsl:param name="attrs" select="''"/> + <xsl:param name="indent" select="''"/> + <xsl:if test="$indent != 'none'"> + <xsl:value-of select="concat($init-indent, $indent)"/> + </xsl:if> + <xsl:value-of select="concat($TS, $name)"/> + <xsl:if test="$attrs != ''"> + <xsl:value-of select="concat($SP, $attrs)"/> + </xsl:if> + <xsl:value-of select="$TEc"/> +</xsl:template> + + +<!-- + proceed + --> + +<xsl:template match="/resource-agent"> + <xsl:value-of select="$NL"/> + + <!-- (comment denoting the fence agent name) --> + <xsl:call-template name="comment"> + <xsl:with-param name="text" select="@name"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <!-- group rha:name=... rha:description=... (start) --> + <xsl:call-template name="tag-start"> + <xsl:with-param name="name" select="'group'"/> + <xsl:with-param name="attrs" select="concat( + 'rha:name=', $Q, @name, $Q, $SP, + 'rha:description=', $Q, @shortdesc, $Q)"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <!-- optional (start) --> + <xsl:call-template name="tag-start"> + <xsl:with-param name="name" select="'optional'"/> + <xsl:with-param name="indent" select="$indent"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <!-- attribute name="option" --> + <xsl:call-template name="tag-self"> + <xsl:with-param name="name" select="'attribute'"/> + <xsl:with-param name="attrs" select="concat( + 'name=', $Q, 'option', $Q)"/> + <xsl:with-param name="indent" select="concat($indent, $indent)"/> + </xsl:call-template> + <xsl:value-of select="$SP"/> + <!-- (comment mentioning that "option" is deprecated) --> + <xsl:call-template name="comment"> + <xsl:with-param name="text"> + <xsl:text>deprecated; for compatibility. use "action"</xsl:text> + </xsl:with-param> + <xsl:with-param name="indent" select="'none'"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <!-- optional (end) --> + <xsl:call-template name="tag-end"> + <xsl:with-param name="name" select="'optional'"/> + <xsl:with-param name="indent" select="$indent"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <xsl:for-each select="parameters/parameter"> + <xsl:variable name="escapeddesc"> + <xsl:call-template name="escape_quot"> + <xsl:with-param name="replace" select="shortdesc"/> + </xsl:call-template> + </xsl:variable> + + <!-- optional (start) --> + <xsl:call-template name="tag-start"> + <xsl:with-param name="name" select="'optional'"/> + <xsl:with-param name="indent" select="$indent"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <!-- attribute name=... rha:description=... --> + <xsl:call-template name="tag-self"> + <xsl:with-param name="name" select="'attribute'"/> + <xsl:with-param name="attrs" select="concat( + 'name=', $Q, @name, $Q, $SP, + 'rha:description=', $Q, normalize-space($escapeddesc), $Q, $SP)"/> + <xsl:with-param name="indent" select="concat($indent, $indent)"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <!-- optional (end) --> + <xsl:call-template name="tag-end"> + <xsl:with-param name="name" select="'optional'"/> + <xsl:with-param name="indent" select="$indent"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + </xsl:for-each> + + <!-- group rha:name=... rha:description=... (end) --> + <xsl:call-template name="tag-end"> + <xsl:with-param name="name" select="'group'"/> + </xsl:call-template> + <xsl:value-of select="$NL"/> + + <xsl:value-of select="$NL"/> +</xsl:template> + +<xsl:template name="escape_quot"> + <xsl:param name="replace"/> + <xsl:choose> + <xsl:when test="contains($replace,'"')"> + <xsl:value-of select="substring-before($replace,'"')"/> + <!-- escape quot--> + <xsl:text>&quot;</xsl:text> + <xsl:call-template name="escape_quot"> + <xsl:with-param name="replace" select="substring-after($replace,'"')"/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$replace"/> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + +</xsl:stylesheet> diff --git a/lib/fence2wiki.xsl b/lib/fence2wiki.xsl new file mode 100644 index 0000000..8112169 --- /dev/null +++ b/lib/fence2wiki.xsl @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet version='1.0' xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + +<xsl:template match="/resource-agent"> +[=#<xsl:value-of select="@name" />] +||='''<xsl:value-of select="@shortdesc" />''' =||='''<xsl:value-of select="@name" />''' =|| +|| '''Name Of The Argument For STDIN''' || '''Name Of The Argument For Command-Line''' || '''Default Value''' ||'''Description''' || +<xsl:apply-templates select="parameters/parameter" /> +</xsl:template> + +<xsl:template match="parameters/parameter">|| <xsl:value-of select="@name" /> || <xsl:value-of select="getopt/@mixed" /> || {{{<xsl:value-of select="content/@default" disable-output-escaping="yes"/>}}} || <xsl:value-of select="shortdesc" /> || +</xsl:template> + +</xsl:stylesheet>
\ No newline at end of file diff --git a/lib/fencing.py.py b/lib/fencing.py.py new file mode 100644 index 0000000..c5b5e94 --- /dev/null +++ b/lib/fencing.py.py @@ -0,0 +1,1748 @@ +#!@PYTHON@ -tt + +import sys, getopt, time, os, uuid, pycurl, stat +import pexpect, re, syslog +import logging +import subprocess +import threading +import shlex +import socket +import textwrap +import __main__ + +import itertools + +RELEASE_VERSION = "@RELEASE_VERSION@" + +__all__ = ['atexit_handler', 'check_input', 'process_input', 'all_opt', 'show_docs', + 'fence_login', 'fence_action', 'fence_logout'] + +EC_OK = 0 +EC_GENERIC_ERROR = 1 +EC_BAD_ARGS = 2 +EC_LOGIN_DENIED = 3 +EC_CONNECTION_LOST = 4 +EC_TIMED_OUT = 5 +EC_WAITING_ON = 6 +EC_WAITING_OFF = 7 +EC_STATUS = 8 +EC_STATUS_HMC = 9 +EC_PASSWORD_MISSING = 10 +EC_INVALID_PRIVILEGES = 11 +EC_FETCH_VM_UUID = 12 + +LOG_FORMAT = "%(asctime)-15s %(levelname)s: %(message)s" + +all_opt = { + "help" : { + "getopt" : "h", + "longopt" : "help", + "help" : "-h, --help Display this help and exit", + "required" : "0", + "shortdesc" : "Display help and exit", + "order" : 55}, + "version" : { + "getopt" : "V", + "longopt" : "version", + "help" : "-V, --version Display version information and exit", + "required" : "0", + "shortdesc" : "Display version information and exit", + "order" : 54}, + "verbose" : { + "getopt" : "v", + "longopt" : "verbose", + "help" : "-v, --verbose Verbose mode. " + "Multiple -v flags can be stacked on the command line " + "(e.g., -vvv) to increase verbosity.", + "required" : "0", + "order" : 51}, + "verbose_level" : { + "getopt" : ":", + "longopt" : "verbose-level", + "type" : "integer", + "help" : "--verbose-level " + "Level of debugging detail in output. Defaults to the " + "number of --verbose flags specified on the command " + "line, or to 1 if verbose=1 in a stonith device " + "configuration (i.e., on stdin).", + "required" : "0", + "order" : 52}, + "debug" : { + "getopt" : "D:", + "longopt" : "debug-file", + "help" : "-D, --debug-file=[debugfile] Debugging to output file", + "required" : "0", + "shortdesc" : "Write debug information to given file", + "order" : 53}, + "delay" : { + "getopt" : ":", + "longopt" : "delay", + "type" : "second", + "help" : "--delay=[seconds] Wait X seconds before fencing is started", + "required" : "0", + "default" : "0", + "order" : 200}, + "agent" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "web" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "force_on" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "action" : { + "getopt" : "o:", + "longopt" : "action", + "help" : "-o, --action=[action] Action: status, reboot (default), off or on", + "required" : "1", + "shortdesc" : "Fencing action", + "default" : "reboot", + "order" : 1}, + "fabric_fencing" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "ipaddr" : { + "getopt" : "a:", + "longopt" : "ip", + "help" : "-a, --ip=[ip] IP address or hostname of fencing device", + "required" : "1", + "order" : 1}, + "ipport" : { + "getopt" : "u:", + "longopt" : "ipport", + "type" : "integer", + "help" : "-u, --ipport=[port] TCP/UDP port to use for connection", + "required" : "0", + "shortdesc" : "TCP/UDP port to use for connection with device", + "order" : 1}, + "login" : { + "getopt" : "l:", + "longopt" : "username", + "help" : "-l, --username=[name] Login name", + "required" : "?", + "order" : 1}, + "no_login" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_password" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_port" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_status" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_on" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "no_off" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "telnet" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "diag" : { + "getopt" : "", + "help" : "", + "order" : 1}, + "passwd" : { + "getopt" : "p:", + "longopt" : "password", + "help" : "-p, --password=[password] Login password or passphrase", + "required" : "0", + "order" : 1}, + "passwd_script" : { + "getopt" : "S:", + "longopt" : "password-script", + "help" : "-S, --password-script=[script] Script to run to retrieve password", + "required" : "0", + "order" : 1}, + "identity_file" : { + "getopt" : "k:", + "longopt" : "identity-file", + "help" : "-k, --identity-file=[filename] Identity file (private key) for SSH", + "required" : "0", + "order" : 1}, + "cmd_prompt" : { + "getopt" : "c:", + "longopt" : "command-prompt", + "help" : "-c, --command-prompt=[prompt] Force Python regex for command prompt", + "required" : "0", + "order" : 1}, + "secure" : { + "getopt" : "x", + "longopt" : "ssh", + "help" : "-x, --ssh Use SSH connection", + "required" : "0", + "order" : 1}, + "ssh_options" : { + "getopt" : ":", + "longopt" : "ssh-options", + "help" : "--ssh-options=[options] SSH options to use", + "required" : "0", + "order" : 1}, + "ssl" : { + "getopt" : "z", + "longopt" : "ssl", + "help" : "-z, --ssl Use SSL connection with verifying certificate", + "required" : "0", + "order" : 1}, + "ssl_insecure" : { + "getopt" : "", + "longopt" : "ssl-insecure", + "help" : "--ssl-insecure Use SSL connection without verifying certificate", + "required" : "0", + "order" : 1}, + "ssl_secure" : { + "getopt" : "", + "longopt" : "ssl-secure", + "help" : "--ssl-secure Use SSL connection with verifying certificate", + "required" : "0", + "order" : 1}, + "notls" : { + "getopt" : "t", + "longopt" : "notls", + "help" : "-t, --notls " + "Disable TLS negotiation and force SSL3.0. " + "This should only be used for devices that do not support TLS1.0 and up.", + "required" : "0", + "order" : 1}, + "tls1.0" : { + "getopt" : "", + "longopt" : "tls1.0", + "help" : "--tls1.0 " + "Disable TLS negotiation and force TLS1.0. " + "This should only be used for devices that do not support TLS1.1 and up.", + "required" : "0", + "order" : 1}, + "port" : { + "getopt" : "n:", + "longopt" : "plug", + "help" : "-n, --plug=[id] " + "Physical plug number on device, UUID or identification of machine", + "required" : "1", + "order" : 1}, + "switch" : { + "getopt" : "s:", + "longopt" : "switch", + "help" : "-s, --switch=[id] Physical switch number on device", + "required" : "0", + "order" : 1}, + "exec" : { + "getopt" : "e:", + "longopt" : "exec", + "help" : "-e, --exec=[command] Command to execute", + "required" : "0", + "order" : 1}, + "vmware_type" : { + "getopt" : "d:", + "longopt" : "vmware_type", + "help" : "-d, --vmware_type=[type] Type of VMware to connect", + "required" : "0", + "order" : 1}, + "vmware_datacenter" : { + "getopt" : "s:", + "longopt" : "vmware-datacenter", + "help" : "-s, --vmware-datacenter=[dc] VMWare datacenter filter", + "required" : "0", + "order" : 2}, + "snmp_version" : { + "getopt" : "d:", + "longopt" : "snmp-version", + "help" : "-d, --snmp-version=[version] Specifies SNMP version to use (1|2c|3)", + "required" : "0", + "shortdesc" : "Specifies SNMP version to use", + "choices" : ["1", "2c", "3"], + "order" : 1}, + "community" : { + "getopt" : "c:", + "longopt" : "community", + "help" : "-c, --community=[community] Set the community string", + "required" : "0", + "order" : 1}, + "snmp_auth_prot" : { + "getopt" : "b:", + "longopt" : "snmp-auth-prot", + "help" : "-b, --snmp-auth-prot=[prot] Set authentication protocol (MD5|SHA)", + "required" : "0", + "shortdesc" : "Set authentication protocol", + "choices" : ["MD5", "SHA"], + "order" : 1}, + "snmp_sec_level" : { + "getopt" : "E:", + "longopt" : "snmp-sec-level", + "help" : "-E, --snmp-sec-level=[level] " + "Set security level (noAuthNoPriv|authNoPriv|authPriv)", + "required" : "0", + "shortdesc" : "Set security level", + "choices" : ["noAuthNoPriv", "authNoPriv", "authPriv"], + "order" : 1}, + "snmp_priv_prot" : { + "getopt" : "B:", + "longopt" : "snmp-priv-prot", + "help" : "-B, --snmp-priv-prot=[prot] Set privacy protocol (DES|AES)", + "required" : "0", + "shortdesc" : "Set privacy protocol", + "choices" : ["DES", "AES"], + "order" : 1}, + "snmp_priv_passwd" : { + "getopt" : "P:", + "longopt" : "snmp-priv-passwd", + "help" : "-P, --snmp-priv-passwd=[pass] Set privacy protocol password", + "required" : "0", + "order" : 1}, + "snmp_priv_passwd_script" : { + "getopt" : "R:", + "longopt" : "snmp-priv-passwd-script", + "help" : "-R, --snmp-priv-passwd-script Script to run to retrieve privacy password", + "required" : "0", + "order" : 1}, + "inet4_only" : { + "getopt" : "4", + "longopt" : "inet4-only", + "help" : "-4, --inet4-only Forces agent to use IPv4 addresses only", + "required" : "0", + "order" : 1}, + "inet6_only" : { + "getopt" : "6", + "longopt" : "inet6-only", + "help" : "-6, --inet6-only Forces agent to use IPv6 addresses only", + "required" : "0", + "order" : 1}, + "plug_separator" : { + "getopt" : ":", + "longopt" : "plug-separator", + "help" : "--plug-separator=[char] Separator for plug parameter when specifying more than 1 plug", + "default" : ",", + "required" : "0", + "order" : 100}, + "separator" : { + "getopt" : "C:", + "longopt" : "separator", + "help" : "-C, --separator=[char] Separator for CSV created by 'list' operation", + "default" : ",", + "required" : "0", + "order" : 100}, + "login_timeout" : { + "getopt" : ":", + "longopt" : "login-timeout", + "type" : "second", + "help" : "--login-timeout=[seconds] Wait X seconds for cmd prompt after login", + "default" : "5", + "required" : "0", + "order" : 200}, + "shell_timeout" : { + "getopt" : ":", + "longopt" : "shell-timeout", + "type" : "second", + "help" : "--shell-timeout=[seconds] Wait X seconds for cmd prompt after issuing command", + "default" : "3", + "required" : "0", + "order" : 200}, + "power_timeout" : { + "getopt" : ":", + "longopt" : "power-timeout", + "type" : "second", + "help" : "--power-timeout=[seconds] Test X seconds for status change after ON/OFF", + "default" : "20", + "required" : "0", + "order" : 200}, + "disable_timeout" : { + "getopt" : ":", + "longopt" : "disable-timeout", + "help" : "--disable-timeout=[true/false] Disable timeout (true/false) (default: true when run from Pacemaker 2.0+)", + "required" : "0", + "order" : 200}, + "power_wait" : { + "getopt" : ":", + "longopt" : "power-wait", + "type" : "second", + "help" : "--power-wait=[seconds] Wait X seconds after issuing ON/OFF", + "default" : "0", + "required" : "0", + "order" : 200}, + "stonith_status_sleep" : { + "getopt" : ":", + "longopt" : "stonith-status-sleep", + "type" : "second", + "help" : "--stonith-status-sleep=[seconds] Sleep X seconds between status calls during a STONITH action", + "default" : "1", + "required" : "0", + "order" : 200}, + "missing_as_off" : { + "getopt" : "", + "longopt" : "missing-as-off", + "help" : "--missing-as-off Missing port returns OFF instead of failure", + "required" : "0", + "order" : 200}, + "retry_on" : { + "getopt" : ":", + "longopt" : "retry-on", + "type" : "integer", + "help" : "--retry-on=[attempts] Count of attempts to retry power on", + "default" : "1", + "required" : "0", + "order" : 201}, + "session_url" : { + "getopt" : "s:", + "longopt" : "session-url", + "help" : "-s, --session-url URL to connect to XenServer on", + "required" : "1", + "order" : 1}, + "sudo" : { + "getopt" : "", + "longopt" : "use-sudo", + "help" : "--use-sudo Use sudo (without password) when calling 3rd party software", + "required" : "0", + "order" : 205}, + "method" : { + "getopt" : "m:", + "longopt" : "method", + "help" : "-m, --method=[method] Method to fence (onoff|cycle) (Default: onoff)", + "required" : "0", + "shortdesc" : "Method to fence", + "default" : "onoff", + "choices" : ["onoff", "cycle"], + "order" : 1}, + "telnet_path" : { + "getopt" : ":", + "longopt" : "telnet-path", + "help" : "--telnet-path=[path] Path to telnet binary", + "required" : "0", + "default" : "@TELNET_PATH@", + "order": 300}, + "ssh_path" : { + "getopt" : ":", + "longopt" : "ssh-path", + "help" : "--ssh-path=[path] Path to ssh binary", + "required" : "0", + "default" : "@SSH_PATH@", + "order": 300}, + "gnutlscli_path" : { + "getopt" : ":", + "longopt" : "gnutlscli-path", + "help" : "--gnutlscli-path=[path] Path to gnutls-cli binary", + "required" : "0", + "default" : "@GNUTLSCLI_PATH@", + "order": 300}, + "sudo_path" : { + "getopt" : ":", + "longopt" : "sudo-path", + "help" : "--sudo-path=[path] Path to sudo binary", + "required" : "0", + "default" : "@SUDO_PATH@", + "order": 300}, + "snmpwalk_path" : { + "getopt" : ":", + "longopt" : "snmpwalk-path", + "help" : "--snmpwalk-path=[path] Path to snmpwalk binary", + "required" : "0", + "default" : "@SNMPWALK_PATH@", + "order" : 300}, + "snmpset_path" : { + "getopt" : ":", + "longopt" : "snmpset-path", + "help" : "--snmpset-path=[path] Path to snmpset binary", + "required" : "0", + "default" : "@SNMPSET_PATH@", + "order" : 300}, + "snmpget_path" : { + "getopt" : ":", + "longopt" : "snmpget-path", + "help" : "--snmpget-path=[path] Path to snmpget binary", + "required" : "0", + "default" : "@SNMPGET_PATH@", + "order" : 300}, + "snmp": { + "getopt" : "", + "help" : "", + "order" : 1}, + "port_as_ip": { + "getopt" : "", + "longopt" : "port-as-ip", + "help" : "--port-as-ip Make \"port/plug\" to be an alias to IP address", + "required" : "0", + "order" : 200}, + "on_target": { + "getopt" : "", + "help" : "", + "order" : 1}, + "quiet": { + "getopt" : "q", + "longopt": "quiet", + "help" : "-q, --quiet Disable logging to stderr. Does not affect --verbose or --debug-file or logging to syslog.", + "required" : "0", + "order" : 50} +} + +# options which are added automatically if 'key' is encountered ("default" is always added) +DEPENDENCY_OPT = { + "default" : ["help", "debug", "verbose", "verbose_level", + "version", "action", "agent", "power_timeout", + "shell_timeout", "login_timeout", "disable_timeout", + "power_wait", "stonith_status_sleep", "retry_on", "delay", + "plug_separator", "quiet"], + "passwd" : ["passwd_script"], + "sudo" : ["sudo_path"], + "secure" : ["identity_file", "ssh_options", "ssh_path", "inet4_only", "inet6_only"], + "telnet" : ["telnet_path"], + "ipaddr" : ["ipport"], + "port" : ["separator"], + "ssl" : ["ssl_secure", "ssl_insecure", "gnutlscli_path"], + "snmp" : ["snmp_auth_prot", "snmp_sec_level", "snmp_priv_prot", \ + "snmp_priv_passwd", "snmp_priv_passwd_script", "community", \ + "snmpset_path", "snmpget_path", "snmpwalk_path"] + } + +class fspawn(pexpect.spawn): + def __init__(self, options, command, **kwargs): + if sys.version_info[0] > 2: + kwargs.setdefault('encoding', 'utf-8') + logging.info("Running command: %s", command) + pexpect.spawn.__init__(self, command, **kwargs) + self.opt = options + + def log_expect(self, pattern, timeout): + result = self.expect(pattern, timeout if timeout != 0 else None) + logging.debug("Received: %s", self.before + self.after) + return result + + def read_nonblocking(self, size, timeout): + return pexpect.spawn.read_nonblocking(self, size=100, timeout=timeout if timeout != 0 else None) + + def send(self, message): + logging.debug("Sent: %s", message) + return pexpect.spawn.send(self, message) + + # send EOL according to what was detected in login process (telnet) + def send_eol(self, message): + return self.send(message + self.opt["eol"]) + +def frun(command, timeout=30, withexitstatus=False, events=None, + extra_args=None, logfile=None, cwd=None, env=None, **kwargs): + if sys.version_info[0] > 2: + kwargs.setdefault('encoding', 'utf-8') + return pexpect.run(command, timeout=timeout if timeout != 0 else None, + withexitstatus=withexitstatus, events=events, + extra_args=extra_args, logfile=logfile, cwd=cwd, + env=env, **kwargs) + +def atexit_handler(): + try: + sys.stdout.close() + os.close(1) + except IOError: + logging.error("%s failed to close standard output\n", sys.argv[0]) + sys.exit(EC_GENERIC_ERROR) + +def _add_dependency_options(options): + ## Add also options which are available for every fence agent + added_opt = [] + for opt in options + ["default"]: + if opt in DEPENDENCY_OPT: + added_opt.extend([y for y in DEPENDENCY_OPT[opt] if options.count(y) == 0]) + + if not "port" in (options + added_opt) and \ + not "nodename" in (options + added_opt) and \ + "ipaddr" in (options + added_opt): + added_opt.append("port_as_ip") + all_opt["port"]["help"] = "-n, --plug=[ip] IP address or hostname of fencing device " \ + "(together with --port-as-ip)" + + return added_opt + +def fail_usage(message="", stop=True): + if len(message) > 0: + logging.error("%s\n", message) + if stop: + logging.error("Please use '-h' for usage\n") + sys.exit(EC_GENERIC_ERROR) + +def fail(error_code, stop=True): + message = { + EC_LOGIN_DENIED : "Unable to connect/login to fencing device", + EC_CONNECTION_LOST : "Connection lost", + EC_TIMED_OUT : "Connection timed out", + EC_WAITING_ON : "Failed: Timed out waiting to power ON", + EC_WAITING_OFF : "Failed: Timed out waiting to power OFF", + EC_STATUS : "Failed: Unable to obtain correct plug status or plug is not available", + EC_STATUS_HMC : "Failed: Either unable to obtain correct plug status, " + "partition is not available or incorrect HMC version used", + EC_PASSWORD_MISSING : "Failed: You have to set login password", + EC_INVALID_PRIVILEGES : "Failed: The user does not have the correct privileges to do the requested action.", + EC_FETCH_VM_UUID : "Failed: Can not find VM UUID by its VM name given in the <plug> parameter." + + }[error_code] + "\n" + logging.error("%s\n", message) + if stop: + sys.exit(EC_GENERIC_ERROR) + +def usage(avail_opt): + print("Usage:") + print("\t" + os.path.basename(sys.argv[0]) + " [options]") + print("Options:") + + sorted_list = [(key, all_opt[key]) for key in avail_opt] + sorted_list.sort(key=lambda x: x[1]["order"]) + + for key, value in sorted_list: + if len(value["help"]) != 0: + print(" " + _join_wrap([value["help"]], first_indent=3)) + +def metadata(options, avail_opt, docs): + # avail_opt has to be unique, if there are duplicities then they should be removed + sorted_list = [(key, all_opt[key]) for key in list(set(avail_opt)) if "longopt" in all_opt[key]] + # Find keys that are going to replace inconsistent names + mapping = dict([(opt["longopt"].replace("-", "_"), key) for (key, opt) in sorted_list if (key != opt["longopt"].replace("-", "_"))]) + new_options = [(key, all_opt[mapping[key]]) for key in mapping] + sorted_list.extend(new_options) + + sorted_list.sort(key=lambda x: (x[1]["order"], x[0])) + + if options["--action"] == "metadata": + docs["longdesc"] = re.sub("\\\\f[BPIR]|\.P|\.TP|\.br\n", "", docs["longdesc"]) + + print("<?xml version=\"1.0\" ?>") + print("<resource-agent name=\"" + os.path.basename(sys.argv[0]) + \ + "\" shortdesc=\"" + docs["shortdesc"] + "\" >") + for (symlink, desc) in docs.get("symlink", []): + print("<symlink name=\"" + symlink + "\" shortdesc=\"" + desc + "\"/>") + print("<longdesc>" + docs["longdesc"] + "</longdesc>") + print("<vendor-url>" + docs["vendorurl"] + "</vendor-url>") + print("<parameters>") + for (key, opt) in sorted_list: + info = "" + if key in all_opt: + if key != all_opt[key].get('longopt', key).replace("-", "_"): + info = "deprecated=\"1\"" + else: + info = "obsoletes=\"%s\"" % (mapping.get(key)) + + if "help" in opt and len(opt["help"]) > 0: + if info != "": + info = " " + info + print("\t<parameter name=\"" + key + "\" unique=\"0\" required=\"" + opt["required"] + "\"" + info + ">") + + default = "" + if "default" in opt: + default = "default=\"" + _encode_html_entities(str(opt["default"])) + "\" " + + mixed = opt["help"] + ## split it between option and help text + res = re.compile(r"^(.*?--\S+)\s+", re.IGNORECASE | re.S).search(mixed) + if None != res: + mixed = res.group(1) + mixed = _encode_html_entities(mixed) + + if not "shortdesc" in opt: + shortdesc = re.sub(".*\s\s+", "", opt["help"][31:]) + else: + shortdesc = opt["shortdesc"] + + print("\t\t<getopt mixed=\"" + mixed + "\" />") + if "choices" in opt: + print("\t\t<content type=\"select\" "+default+" >") + for choice in opt["choices"]: + print("\t\t\t<option value=\"%s\" />" % (choice)) + print("\t\t</content>") + elif opt["getopt"].count(":") > 0: + t = opt.get("type", "string") + print("\t\t<content type=\"%s\" " % (t) +default+" />") + else: + print("\t\t<content type=\"boolean\" "+default+" />") + print("\t\t<shortdesc lang=\"en\">" + shortdesc + "</shortdesc>") + print("\t</parameter>") + print("</parameters>") + print("<actions>") + + (available_actions, _) = _get_available_actions(avail_opt) + + if "on" in available_actions: + available_actions.remove("on") + on_target = ' on_target="1"' if avail_opt.count("on_target") else '' + print("\t<action name=\"on\"%s automatic=\"%d\"/>" % (on_target, avail_opt.count("fabric_fencing"))) + + for action in available_actions: + print("\t<action name=\"%s\" />" % (action)) + print("</actions>") + print("</resource-agent>") + +def process_input(avail_opt): + avail_opt.extend(_add_dependency_options(avail_opt)) + + # @todo: this should be put elsewhere? + os.putenv("LANG", "C") + os.putenv("LC_ALL", "C") + + if "port_as_ip" in avail_opt: + avail_opt.append("port") + + if len(sys.argv) > 1: + opt = _parse_input_cmdline(avail_opt) + else: + opt = _parse_input_stdin(avail_opt) + + if "--port-as-ip" in opt and "--plug" in opt: + opt["--ip"] = opt["--plug"] + + return opt + +## +## This function checks input and answers if we want to have same answers +## in each of the fencing agents. It looks for possible errors and run +## password script to set a correct password +###### +def check_input(device_opt, opt, other_conditions = False): + device_opt.extend(_add_dependency_options(device_opt)) + + options = dict(opt) + options["device_opt"] = device_opt + + _update_metadata(options) + options = _set_default_values(options) + options["--action"] = options["--action"].lower() + + ## In special cases (show help, metadata or version) we don't need to check anything + ##### + # OCF compatibility + if options["--action"] == "meta-data": + options["--action"] = "metadata" + + if options["--action"] in ["metadata", "manpage"] or any(k in options for k in ("--help", "--version")): + return options + + try: + options["--verbose-level"] = int(options["--verbose-level"]) + except ValueError: + options["--verbose-level"] = -1 + + if options["--verbose-level"] < 0: + logging.warning("Parse error: Option 'verbose_level' must " + "be an integer greater than or equal to 0. " + "Setting verbose_level to 0.") + options["--verbose-level"] = 0 + + if options["--verbose-level"] == 0 and "--verbose" in options: + logging.warning("Parse error: Ignoring option 'verbose' " + "because it conflicts with verbose_level=0") + del options["--verbose"] + + if options["--verbose-level"] > 0: + # Ensure verbose key exists + options["--verbose"] = 1 + + if "--verbose" in options: + logging.getLogger().setLevel(logging.DEBUG) + + formatter = logging.Formatter(LOG_FORMAT) + + ## add logging to syslog + logging.getLogger().addHandler(SyslogLibHandler()) + if "--quiet" not in options: + ## add logging to stderr + stderrHandler = logging.StreamHandler(sys.stderr) + stderrHandler.setFormatter(formatter) + logging.getLogger().addHandler(stderrHandler) + + (acceptable_actions, _) = _get_available_actions(device_opt) + + if 1 == device_opt.count("fabric_fencing"): + acceptable_actions.extend(["enable", "disable"]) + + if 0 == acceptable_actions.count(options["--action"]): + fail_usage("Failed: Unrecognised action '" + options["--action"] + "'") + + ## Compatibility layer + ##### + if options["--action"] == "enable": + options["--action"] = "on" + if options["--action"] == "disable": + options["--action"] = "off" + + + if options["--action"] == "validate-all" and not other_conditions: + if not _validate_input(options, False): + fail_usage("validate-all failed") + sys.exit(EC_OK) + else: + _validate_input(options, True) + + if "--debug-file" in options: + try: + debug_file = logging.FileHandler(options["--debug-file"]) + debug_file.setLevel(logging.DEBUG) + debug_file.setFormatter(formatter) + logging.getLogger().addHandler(debug_file) + except IOError: + logging.error("Unable to create file %s", options["--debug-file"]) + fail_usage("Failed: Unable to create file " + options["--debug-file"]) + + if "--snmp-priv-passwd-script" in options: + options["--snmp-priv-passwd"] = os.popen(options["--snmp-priv-passwd-script"]).read().rstrip() + + if "--password-script" in options: + options["--password"] = os.popen(options["--password-script"]).read().rstrip() + + if "--ssl-secure" in options or "--ssl-insecure" in options: + options["--ssl"] = "" + + if "--ssl" in options and "--ssl-insecure" not in options: + options["--ssl-secure"] = "" + + if os.environ.get("PCMK_service") == "pacemaker-fenced" and "--disable-timeout" not in options: + options["--disable-timeout"] = "1" + + if options.get("--disable-timeout", "").lower() in ["1", "yes", "on", "true"]: + options["--power-timeout"] = options["--shell-timeout"] = options["--login-timeout"] = 0 + + return options + +## Obtain a power status from possibly more than one plug +## "on" is returned if at least one plug is ON +###### +def get_multi_power_fn(connection, options, get_power_fn): + status = "off" + plugs = options["--plugs"] if "--plugs" in options else [""] + + for plug in plugs: + try: + options["--uuid"] = str(uuid.UUID(plug)) + except ValueError: + pass + except KeyError: + pass + + options["--plug"] = plug + plug_status = get_power_fn(connection, options) + if plug_status != "off": + status = plug_status + + return status + +def async_set_multi_power_fn(connection, options, set_power_fn, get_power_fn, retry_attempts): + plugs = options["--plugs"] if "--plugs" in options else [""] + + for _ in range(retry_attempts): + for plug in plugs: + try: + options["--uuid"] = str(uuid.UUID(plug)) + except ValueError: + pass + except KeyError: + pass + + options["--plug"] = plug + set_power_fn(connection, options) + time.sleep(int(options["--power-wait"])) + + for _ in itertools.count(1): + if get_multi_power_fn(connection, options, get_power_fn) != options["--action"]: + time.sleep(int(options["--stonith-status-sleep"])) + else: + return True + + if int(options["--power-timeout"]) > 0 and _ >= int(options["--power-timeout"]): + break + + return False + +def sync_set_multi_power_fn(connection, options, sync_set_power_fn, retry_attempts): + success = True + plugs = options["--plugs"] if "--plugs" in options else [""] + + for plug in plugs: + try: + options["--uuid"] = str(uuid.UUID(plug)) + except ValueError: + pass + except KeyError: + pass + + options["--plug"] = plug + for retry in range(retry_attempts): + if sync_set_power_fn(connection, options): + break + if retry == retry_attempts-1: + success = False + time.sleep(int(options["--power-wait"])) + + return success + + +def set_multi_power_fn(connection, options, set_power_fn, get_power_fn, sync_set_power_fn, retry_attempts=1): + + if set_power_fn != None: + if get_power_fn != None: + return async_set_multi_power_fn(connection, options, set_power_fn, get_power_fn, retry_attempts) + elif sync_set_power_fn != None: + return sync_set_multi_power_fn(connection, options, sync_set_power_fn, retry_attempts) + + return False + +def multi_reboot_cycle_fn(connection, options, reboot_cycle_fn, retry_attempts=1): + success = True + plugs = options["--plugs"] if "--plugs" in options else [""] + + for plug in plugs: + try: + options["--uuid"] = str(uuid.UUID(plug)) + except ValueError: + pass + except KeyError: + pass + + options["--plug"] = plug + for retry in range(retry_attempts): + if reboot_cycle_fn(connection, options): + break + if retry == retry_attempts-1: + success = False + time.sleep(int(options["--power-wait"])) + + return success + +def show_docs(options, docs=None): + device_opt = options["device_opt"] + + if docs == None: + docs = {} + docs["shortdesc"] = "Fence agent" + docs["longdesc"] = "" + + if "--help" in options: + usage(device_opt) + sys.exit(0) + + if options.get("--action", "") in ["metadata", "manpage"]: + if "port_as_ip" in device_opt: + device_opt.remove("separator") + metadata(options, device_opt, docs) + sys.exit(0) + + if "--version" in options: + print(RELEASE_VERSION) + sys.exit(0) + +def fence_action(connection, options, set_power_fn, get_power_fn, get_outlet_list=None, reboot_cycle_fn=None, sync_set_power_fn=None): + result = 0 + + try: + if "--plug" in options: + options["--plugs"] = options["--plug"].split(options["--plug-separator"]) + + ## Process options that manipulate fencing device + ##### + if (options["--action"] in ["list", "list-status"]) or \ + ((options["--action"] == "monitor") and 1 == options["device_opt"].count("port") and \ + 0 == options["device_opt"].count("port_as_ip")): + + if 0 == options["device_opt"].count("port"): + print("N/A") + elif get_outlet_list == None: + ## @todo: exception? + ## This is just temporal solution, we will remove default value + ## None as soon as all existing agent will support this operation + print("NOTICE: List option is not working on this device yet") + else: + options["--original-action"] = options["--action"] + options["--action"] = "list" + outlets = get_outlet_list(connection, options) + options["--action"] = options["--original-action"] + del options["--original-action"] + + ## keys can be numbers (port numbers) or strings (names of VM, UUID) + for outlet_id in list(outlets.keys()): + (alias, status) = outlets[outlet_id] + if status is None or (not status.upper() in ["ON", "OFF"]): + status = "UNKNOWN" + status = status.upper() + + if options["--action"] == "list": + try: + print(outlet_id + options["--separator"] + alias) + except UnicodeEncodeError as e: + print((outlet_id + options["--separator"] + alias).encode("utf-8")) + elif options["--action"] == "list-status": + try: + print(outlet_id + options["--separator"] + alias + options["--separator"] + status) + except UnicodeEncodeError as e: + print((outlet_id + options["--separator"] + alias).encode("utf-8") + options["--separator"] + status) + + return + + if options["--action"] == "monitor" and not "port" in options["device_opt"] and "no_status" in options["device_opt"]: + # Unable to do standard monitoring because 'status' action is not available + return 0 + + status = None + if not "no_status" in options["device_opt"]: + status = get_multi_power_fn(connection, options, get_power_fn) + if status != "on" and status != "off": + fail(EC_STATUS) + + if options["--action"] == status: + if not (status == "on" and "force_on" in options["device_opt"]): + print("Success: Already %s" % (status.upper())) + return 0 + + if options["--action"] == "on": + if set_multi_power_fn(connection, options, set_power_fn, get_power_fn, sync_set_power_fn, 1 + int(options["--retry-on"])): + print("Success: Powered ON") + else: + fail(EC_WAITING_ON) + elif options["--action"] == "off": + if set_multi_power_fn(connection, options, set_power_fn, get_power_fn, sync_set_power_fn): + print("Success: Powered OFF") + else: + fail(EC_WAITING_OFF) + elif options["--action"] == "reboot": + power_on = False + if options.get("--method", "").lower() == "cycle" and reboot_cycle_fn is not None: + try: + power_on = multi_reboot_cycle_fn(connection, options, reboot_cycle_fn, 1 + int(options["--retry-on"])) + except Exception as ex: + # an error occured during reboot action + logging.warning("%s", str(ex)) + + if not power_on: + fail(EC_TIMED_OUT) + + else: + if status != "off": + options["--action"] = "off" + if not set_multi_power_fn(connection, options, set_power_fn, get_power_fn, sync_set_power_fn): + fail(EC_WAITING_OFF) + + options["--action"] = "on" + + try: + power_on = set_multi_power_fn(connection, options, set_power_fn, get_power_fn, sync_set_power_fn, int(options["--retry-on"])) + except Exception as ex: + # an error occured during power ON phase in reboot + # fence action was completed succesfully even in that case + logging.warning("%s", str(ex)) + + # switch back to original action for the case it is used lateron + options["--action"] = "reboot" + + if power_on == False: + # this should not fail as node was fenced succesfully + logging.error('Timed out waiting to power ON\n') + + print("Success: Rebooted") + elif options["--action"] == "status": + print("Status: " + status.upper()) + if status.upper() == "OFF": + result = 2 + elif options["--action"] == "monitor": + pass + except pexpect.EOF: + fail(EC_CONNECTION_LOST) + except pexpect.TIMEOUT: + fail(EC_TIMED_OUT) + except pycurl.error as ex: + logging.error("%s\n", str(ex)) + fail(EC_TIMED_OUT) + except socket.timeout as ex: + logging.error("%s\n", str(ex)) + fail(EC_TIMED_OUT) + + return result + +def fence_login(options, re_login_string=r"(login\s*: )|((?!Last )Login Name: )|(username: )|(User Name :)"): + run_delay(options) + + if "eol" not in options: + options["eol"] = "\r\n" + + if "--command-prompt" in options and type(options["--command-prompt"]) is not list: + options["--command-prompt"] = [options["--command-prompt"]] + + try: + if "--ssl" in options: + conn = _open_ssl_connection(options) + elif "--ssh" in options and "--identity-file" not in options: + conn = _login_ssh_with_password(options, re_login_string) + elif "--ssh" in options and "--identity-file" in options: + conn = _login_ssh_with_identity_file(options) + else: + conn = _login_telnet(options, re_login_string) + except pexpect.EOF as exception: + logging.debug("%s", str(exception)) + fail(EC_LOGIN_DENIED) + except pexpect.TIMEOUT as exception: + logging.debug("%s", str(exception)) + fail(EC_LOGIN_DENIED) + return conn + +def is_executable(path): + if os.path.exists(path): + stats = os.stat(path) + if stat.S_ISREG(stats.st_mode) and os.access(path, os.X_OK): + return True + return False + +def run_commands(options, commands, timeout=None, env=None, log_command=None): + # inspired by psutils.wait_procs (BSD License) + def check_gone(proc, timeout): + try: + returncode = proc.wait(timeout=timeout) + except subprocess.TimeoutExpired: + pass + else: + if returncode is not None or not proc.is_running(): + proc.returncode = returncode + gone.add(proc) + + if timeout is None and "--power-timeout" in options: + timeout = options["--power-timeout"] + if timeout == 0: + timeout = None + if timeout is not None: + timeout = float(timeout) + + time_start = time.time() + procs = [] + status = None + pipe_stdout = "" + pipe_stderr = "" + + for command in commands: + logging.info("Executing: %s\n", log_command or command) + + try: + process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + # decodes newlines and in python3 also converts bytes to str + universal_newlines=(sys.version_info[0] > 2)) + except OSError: + fail_usage("Unable to run %s\n" % command) + + procs.append(process) + + gone = set() + alive = set(procs) + + while True: + if alive: + max_timeout = 2.0 / len(alive) + for proc in alive: + if timeout is not None: + if time.time()-time_start >= timeout: + # quickly go over the rest + max_timeout = 0 + check_gone(proc, max_timeout) + alive = alive - gone + + if not alive: + break + + if time.time()-time_start < 5.0: + # give it at least 5s to get a complete answer + # afterwards we're OK with a quorate answer + continue + + if len(gone) > len(alive): + good_cnt = 0 + for proc in gone: + if proc.returncode == 0: + good_cnt += 1 + # a positive result from more than half is fine + if good_cnt > len(procs)/2: + break + + if timeout is not None: + if time.time() - time_start >= timeout: + logging.debug("Stop waiting after %s\n", str(timeout)) + break + + logging.debug("Done: %d gone, %d alive\n", len(gone), len(alive)) + + for proc in gone: + if (status != 0): + status = proc.returncode + # hand over the best status we have + # but still collect as much stdout/stderr feedback + # avoid communicate as we know already process + # is gone and it seems to block when there + # are D state children we don't get rid off + os.set_blocking(proc.stdout.fileno(), False) + os.set_blocking(proc.stderr.fileno(), False) + try: + pipe_stdout += proc.stdout.read() + except: + pass + try: + pipe_stderr += proc.stderr.read() + except: + pass + proc.stdout.close() + proc.stderr.close() + + for proc in alive: + proc.kill() + + if status is None: + fail(EC_TIMED_OUT, stop=(int(options.get("retry", 0)) < 1)) + status = EC_TIMED_OUT + pipe_stdout = "" + pipe_stderr = "timed out" + + logging.debug("%s %s %s\n", str(status), str(pipe_stdout), str(pipe_stderr)) + + return (status, pipe_stdout, pipe_stderr) + +def run_command(options, command, timeout=None, env=None, log_command=None): + if timeout is None and "--power-timeout" in options: + timeout = options["--power-timeout"] + if timeout is not None: + timeout = float(timeout) + + logging.info("Executing: %s\n", log_command or command) + + try: + process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + # decodes newlines and in python3 also converts bytes to str + universal_newlines=(sys.version_info[0] > 2)) + except OSError: + fail_usage("Unable to run %s\n" % command) + + thread = threading.Thread(target=process.wait) + thread.start() + thread.join(timeout if timeout else None) + if thread.is_alive(): + process.kill() + fail(EC_TIMED_OUT, stop=(int(options.get("retry", 0)) < 1)) + + status = process.wait() + + (pipe_stdout, pipe_stderr) = process.communicate() + process.stdout.close() + process.stderr.close() + + logging.debug("%s %s %s\n", str(status), str(pipe_stdout), str(pipe_stderr)) + + return (status, pipe_stdout, pipe_stderr) + +def run_delay(options, reserve=0, result=0): + ## Delay is important for two-node clusters fencing + ## but we do not need to delay 'status' operations + ## and get us out quickly if we already know that we are gonna fail + ## still wanna do something right before fencing? - reserve some time + if options["--action"] in ["off", "reboot"] \ + and options["--delay"] != "0" \ + and result == 0 \ + and reserve >= 0: + time_left = 1 + int(options["--delay"]) - (time.time() - run_delay.time_start) - reserve + if time_left > 0: + logging.info("Delay %d second(s) before logging in to the fence device", time_left) + time.sleep(time_left) +# mark time when fence-agent is started +run_delay.time_start = time.time() + +def fence_logout(conn, logout_string, sleep=0): + # Logout is not required part of fencing but we should attempt to do it properly + # In some cases our 'exit' command is faster and we can not close connection as it + # was already closed by fencing device + try: + conn.send_eol(logout_string) + time.sleep(sleep) + conn.close() + except OSError: + pass + except pexpect.ExceptionPexpect: + pass + +def source_env(env_file): + # POSIX: name shall not contain '=', value doesn't contain '\0' + output = subprocess.check_output("source {} && env -0".format(env_file), shell=True, + executable="/bin/sh") + # replace env + os.environ.clear() + os.environ.update(line.partition('=')[::2] for line in output.decode("utf-8").split('\0') if not re.match("^\s*$", line)) + +# Convert array of format [[key1, value1], [key2, value2], ... [keyN, valueN]] to dict, where key is +# in format a.b.c.d...z and returned dict has key only z +def array_to_dict(array): + return dict([[x[0].split(".")[-1], x[1]] for x in array]) + +## Own logger handler that uses old-style syslog handler as otherwise everything is sourced +## from /dev/syslog +class SyslogLibHandler(logging.StreamHandler): + """ + A handler class that correctly push messages into syslog + """ + def emit(self, record): + syslog_level = { + logging.CRITICAL:syslog.LOG_CRIT, + logging.ERROR:syslog.LOG_ERR, + logging.WARNING:syslog.LOG_WARNING, + logging.INFO:syslog.LOG_INFO, + logging.DEBUG:syslog.LOG_DEBUG, + logging.NOTSET:syslog.LOG_DEBUG, + }[record.levelno] + + msg = self.format(record) + + # syslos.syslog can not have 0x00 character inside or exception is thrown + syslog.syslog(syslog_level, msg.replace("\x00", "\n")) + return + +def _open_ssl_connection(options): + gnutls_opts = "" + ssl_opts = "" + + if "--notls" in options: + gnutls_opts = "--priority \"NORMAL:-VERS-TLS1.2:-VERS-TLS1.1:-VERS-TLS1.0:+VERS-SSL3.0\"" + elif "--tls1.0" in options: + gnutls_opts = "--priority \"NORMAL:-VERS-TLS1.2:-VERS-TLS1.1:+VERS-TLS1.0:%LATEST_RECORD_VERSION\"" + + # --ssl is same as the --ssl-secure; it means we want to verify certificate in these cases + if "--ssl-insecure" in options: + ssl_opts = "--insecure" + + command = '%s %s %s --crlf -p %s %s' % \ + (options["--gnutlscli-path"], gnutls_opts, ssl_opts, options["--ipport"], options["--ip"]) + try: + conn = fspawn(options, command) + except pexpect.ExceptionPexpect as ex: + logging.error("%s\n", str(ex)) + sys.exit(EC_GENERIC_ERROR) + + return conn + +def _login_ssh_with_identity_file(options): + if "--inet6-only" in options: + force_ipvx = "-6 " + elif "--inet4-only" in options: + force_ipvx = "-4 " + else: + force_ipvx = "" + + command = '%s %s %s@%s -i %s -p %s' % \ + (options["--ssh-path"], force_ipvx, options["--username"], options["--ip"], \ + options["--identity-file"], options["--ipport"]) + if "--ssh-options" in options: + command += ' ' + options["--ssh-options"] + + conn = fspawn(options, command) + + result = conn.log_expect(["Enter passphrase for key '" + options["--identity-file"] + "':", \ + "Are you sure you want to continue connecting (yes/no)?"] + \ + options["--command-prompt"], int(options["--login-timeout"])) + if result == 1: + conn.sendline("yes") + result = conn.log_expect( + ["Enter passphrase for key '" + options["--identity-file"]+"':"] + \ + options["--command-prompt"], int(options["--login-timeout"])) + if result == 0: + if "--password" in options: + conn.sendline(options["--password"]) + conn.log_expect(options["--command-prompt"], int(options["--login-timeout"])) + else: + fail_usage("Failed: You have to enter passphrase (-p) for identity file") + + return conn + +def _login_telnet(options, re_login_string): + re_login = re.compile(re_login_string, re.IGNORECASE) + re_pass = re.compile("(password)|(pass phrase)", re.IGNORECASE) + + conn = fspawn(options, options["--telnet-path"]) + conn.send("set binary\n") + conn.send("open %s -%s\n"%(options["--ip"], options["--ipport"])) + + conn.log_expect(re_login, int(options["--login-timeout"])) + conn.send_eol(options["--username"]) + + ## automatically change end of line separator + screen = conn.read_nonblocking(size=100, timeout=int(options["--shell-timeout"])) + if re_login.search(screen) != None: + options["eol"] = "\n" + conn.send_eol(options["--username"]) + conn.log_expect(re_pass, int(options["--login-timeout"])) + elif re_pass.search(screen) == None: + conn.log_expect(re_pass, int(options["--shell-timeout"])) + + try: + conn.send_eol(options["--password"]) + valid_password = conn.log_expect([re_login] + \ + options["--command-prompt"], int(options["--shell-timeout"])) + if valid_password == 0: + ## password is invalid or we have to change EOL separator + options["eol"] = "\r" + conn.send_eol("") + screen = conn.read_nonblocking(size=100, timeout=int(options["--shell-timeout"])) + ## after sending EOL the fence device can either show 'Login' or 'Password' + if re_login.search(conn.after + screen) != None: + conn.send_eol("") + conn.send_eol(options["--username"]) + conn.log_expect(re_pass, int(options["--login-timeout"])) + conn.send_eol(options["--password"]) + conn.log_expect(options["--command-prompt"], int(options["--login-timeout"])) + except KeyError: + fail(EC_PASSWORD_MISSING) + + return conn + +def _login_ssh_with_password(options, re_login_string): + re_login = re.compile(re_login_string, re.IGNORECASE) + re_pass = re.compile("(password)|(pass phrase)", re.IGNORECASE) + + if "--inet6-only" in options: + force_ipvx = "-6 " + elif "--inet4-only" in options: + force_ipvx = "-4 " + else: + force_ipvx = "" + + command = '%s %s %s@%s -p %s -o PubkeyAuthentication=no' % \ + (options["--ssh-path"], force_ipvx, options["--username"], options["--ip"], options["--ipport"]) + if "--ssh-options" in options: + command += ' ' + options["--ssh-options"] + + conn = fspawn(options, command) + + if "telnet_over_ssh" in options: + # This is for stupid ssh servers (like ALOM) which behave more like telnet + # (ignore name and display login prompt) + result = conn.log_expect( \ + [re_login, "Are you sure you want to continue connecting (yes/no)?"], + int(options["--login-timeout"])) + if result == 1: + conn.sendline("yes") # Host identity confirm + conn.log_expect(re_login, int(options["--login-timeout"])) + + conn.sendline(options["--username"]) + conn.log_expect(re_pass, int(options["--login-timeout"])) + else: + result = conn.log_expect( \ + ["ssword:", "Are you sure you want to continue connecting (yes/no)?"], + int(options["--login-timeout"])) + if result == 1: + conn.sendline("yes") + conn.log_expect("ssword:", int(options["--login-timeout"])) + + conn.sendline(options["--password"]) + conn.log_expect(options["--command-prompt"], int(options["--login-timeout"])) + + return conn + +# +# To update metadata, we change values in all_opt +def _update_metadata(options): + device_opt = options["device_opt"] + + if device_opt.count("login") and device_opt.count("no_login") == 0: + all_opt["login"]["required"] = "1" + else: + all_opt["login"]["required"] = "0" + + if device_opt.count("port_as_ip"): + all_opt["ipaddr"]["required"] = "0" + all_opt["port"]["required"] = "0" + + (available_actions, default_value) = _get_available_actions(device_opt) + all_opt["action"]["default"] = default_value + + actions_with_default = \ + [x if not x == all_opt["action"]["default"] else x + " (default)" for x in available_actions] + all_opt["action"]["help"] = \ + "-o, --action=[action] Action: %s" % (_join_wrap(actions_with_default, last_separator=" or ")) + + if device_opt.count("ipport"): + default_value = None + default_string = None + + if "default" in all_opt["ipport"]: + default_value = all_opt["ipport"]["default"] + elif device_opt.count("web") and device_opt.count("ssl"): + default_value = "80" + default_string = "(default 80, 443 if --ssl option is used)" + elif device_opt.count("telnet") and device_opt.count("secure"): + default_value = "23" + default_string = "(default 23, 22 if --ssh option is used)" + else: + tcp_ports = {"community" : "161", "secure" : "22", "telnet" : "23", "web" : "80", "ssl" : "443"} + # all cases where next command returns multiple results are covered by previous blocks + protocol = [x for x in ["community", "secure", "ssl", "web", "telnet"] if device_opt.count(x)][0] + default_value = tcp_ports[protocol] + + if default_string is None: + all_opt["ipport"]["help"] = "-u, --ipport=[port] TCP/UDP port to use (default %s)" % \ + (default_value) + else: + all_opt["ipport"]["help"] = "-u, --ipport=[port] TCP/UDP port to use\n" + " "*40 + default_string + +def _set_default_values(options): + if "ipport" in options["device_opt"]: + if not "--ipport" in options: + if "default" in all_opt["ipport"]: + options["--ipport"] = all_opt["ipport"]["default"] + elif "community" in options["device_opt"]: + options["--ipport"] = "161" + elif "--ssh" in options or all_opt["secure"].get("default", "0") == "1": + options["--ipport"] = "22" + elif "--ssl" in options or all_opt["ssl"].get("default", "0") == "1": + options["--ipport"] = "443" + elif "--ssl-secure" in options or all_opt["ssl_secure"].get("default", "0") == "1": + options["--ipport"] = "443" + elif "--ssl-insecure" in options or all_opt["ssl_insecure"].get("default", "0") == "1": + options["--ipport"] = "443" + elif "web" in options["device_opt"]: + options["--ipport"] = "80" + elif "telnet" in options["device_opt"]: + options["--ipport"] = "23" + + if "--ipport" in options: + all_opt["ipport"]["default"] = options["--ipport"] + + for opt in options["device_opt"]: + if "default" in all_opt[opt] and not opt == "ipport": + getopt_long = "--" + all_opt[opt]["longopt"] + if getopt_long not in options: + options[getopt_long] = all_opt[opt]["default"] + + return options + +# stop = True/False : exit fence agent when problem is encountered +def _validate_input(options, stop = True): + device_opt = options["device_opt"] + valid_input = True + + if "--username" not in options and \ + device_opt.count("login") and (device_opt.count("no_login") == 0): + valid_input = False + fail_usage("Failed: You have to set login name", stop) + + if device_opt.count("ipaddr") and "--ip" not in options and "--managed" not in options and "--target" not in options: + valid_input = False + fail_usage("Failed: You have to enter fence address", stop) + + if device_opt.count("no_password") == 0: + if 0 == device_opt.count("identity_file"): + if not ("--password" in options or "--password-script" in options): + valid_input = False + fail_usage("Failed: You have to enter password or password script", stop) + else: + if not ("--password" in options or \ + "--password-script" in options or "--identity-file" in options): + valid_input = False + fail_usage("Failed: You have to enter password, password script or identity file", stop) + + if "--ssh" not in options and "--identity-file" in options: + valid_input = False + fail_usage("Failed: You have to use identity file together with ssh connection (-x)", stop) + + if "--identity-file" in options and not os.path.isfile(options["--identity-file"]): + valid_input = False + fail_usage("Failed: Identity file " + options["--identity-file"] + " does not exist", stop) + + if (0 == ["list", "list-status", "monitor"].count(options["--action"])) and \ + "--plug" not in options and device_opt.count("port") and \ + device_opt.count("no_port") == 0 and not device_opt.count("port_as_ip"): + valid_input = False + fail_usage("Failed: You have to enter plug number or machine identification", stop) + + for failed_opt in _get_opts_with_invalid_choices(options): + valid_input = False + fail_usage("Failed: You have to enter a valid choice for %s from the valid values: %s" % \ + ("--" + all_opt[failed_opt]["longopt"], str(all_opt[failed_opt]["choices"])), stop) + + for failed_opt in _get_opts_with_invalid_types(options): + valid_input = False + if all_opt[failed_opt]["type"] == "second": + fail_usage("Failed: The value you have entered for %s is not a valid time in seconds" % \ + ("--" + all_opt[failed_opt]["longopt"]), stop) + else: + fail_usage("Failed: The value you have entered for %s is not a valid %s" % \ + ("--" + all_opt[failed_opt]["longopt"], all_opt[failed_opt]["type"]), stop) + + return valid_input + +def _encode_html_entities(text): + return text.replace("&", "&").replace('"', """).replace('<', "<"). \ + replace('>', ">").replace("'", "'") + +def _prepare_getopt_args(options): + getopt_string = "" + longopt_list = [] + for k in options: + if k in all_opt and all_opt[k]["getopt"] != ":": + # getopt == ":" means that opt is without short getopt, but has value + getopt_string += all_opt[k]["getopt"] + elif k not in all_opt: + fail_usage("Parse error: unknown option '"+k+"'") + + if k in all_opt and "longopt" in all_opt[k]: + if all_opt[k]["getopt"].endswith(":"): + longopt_list.append(all_opt[k]["longopt"] + "=") + else: + longopt_list.append(all_opt[k]["longopt"]) + + return (getopt_string, longopt_list) + +def _parse_input_stdin(avail_opt): + opt = {} + name = "" + + mapping_longopt_names = dict([(all_opt[o].get("longopt"), o) for o in avail_opt]) + + for line in sys.stdin.readlines(): + line = line.strip() + if (line.startswith("#")) or (len(line) == 0): + continue + + (name, value) = (line + "=").split("=", 1) + value = value[:-1] + value = re.sub("^\"(.*)\"$", "\\1", value) + + if name.replace("-", "_") in mapping_longopt_names: + name = mapping_longopt_names[name.replace("-", "_")] + elif name.replace("_", "-") in mapping_longopt_names: + name = mapping_longopt_names[name.replace("_", "-")] + + if avail_opt.count(name) == 0 and name in ["nodename"]: + continue + elif avail_opt.count(name) == 0: + logging.warning("Parse error: Ignoring unknown option '%s'\n", line) + continue + + if all_opt[name]["getopt"].endswith(":"): + opt["--"+all_opt[name]["longopt"].rstrip(":")] = value + elif value.lower() in ["1", "yes", "on", "true"]: + opt["--"+all_opt[name]["longopt"]] = "1" + elif value.lower() in ["0", "no", "off", "false"]: + opt["--"+all_opt[name]["longopt"]] = "0" + else: + logging.warning("Parse error: Ignoring option '%s' because it does not have value\n", name) + + opt.setdefault("--verbose-level", opt.get("--verbose", 0)) + + return opt + +def _parse_input_cmdline(avail_opt): + filtered_opts = {} + _verify_unique_getopt(avail_opt) + (getopt_string, longopt_list) = _prepare_getopt_args(avail_opt) + + try: + (entered_opt, left_arg) = getopt.gnu_getopt(sys.argv[1:], getopt_string, longopt_list) + if len(left_arg) > 0: + logging.warning("Unused arguments on command line: %s" % (str(left_arg))) + except getopt.GetoptError as error: + fail_usage("Parse error: " + error.msg) + + for opt in avail_opt: + filtered_opts.update({opt : all_opt[opt]}) + + # Short and long getopt names are changed to consistent "--" + long name (e.g. --username) + long_opts = {} + verbose_count = 0 + for arg_name in [k for (k, v) in entered_opt]: + all_key = [key for (key, value) in list(filtered_opts.items()) \ + if "--" + value.get("longopt", "") == arg_name or "-" + value.get("getopt", "").rstrip(":") == arg_name][0] + long_opts["--" + filtered_opts[all_key]["longopt"]] = dict(entered_opt)[arg_name] + if all_key == "verbose": + verbose_count += 1 + + long_opts.setdefault("--verbose-level", verbose_count) + + # This test is specific because it does not apply to input on stdin + if "port_as_ip" in avail_opt and not "--port-as-ip" in long_opts and "--plug" in long_opts: + fail_usage("Parser error: option -n/--plug is not recognized") + + return long_opts + +# for ["John", "Mary", "Eli"] returns "John, Mary and Eli" +def _join2(words, normal_separator=", ", last_separator=" and "): + if len(words) <= 1: + return "".join(words) + else: + return last_separator.join([normal_separator.join(words[:-1]), words[-1]]) + +def _join_wrap(words, normal_separator=", ", last_separator=" and ", first_indent=42): + x = _join2(words, normal_separator, last_separator) + wrapper = textwrap.TextWrapper() + wrapper.initial_indent = " "*first_indent + wrapper.subsequent_indent = " "*40 + wrapper.width = 85 + wrapper.break_on_hyphens = False + wrapper.break_long_words = False + wrapped_text = "" + for line in wrapper.wrap(x): + wrapped_text += line + "\n" + return wrapped_text.lstrip().rstrip("\n") + +def _get_opts_with_invalid_choices(options): + options_failed = [] + device_opt = options["device_opt"] + + for opt in device_opt: + if "choices" in all_opt[opt]: + longopt = "--" + all_opt[opt]["longopt"] + possible_values_upper = [y.upper() for y in all_opt[opt]["choices"]] + if longopt in options: + options[longopt] = options[longopt].upper() + if not options["--" + all_opt[opt]["longopt"]] in possible_values_upper: + options_failed.append(opt) + return options_failed + +def _get_opts_with_invalid_types(options): + options_failed = [] + device_opt = options["device_opt"] + + for opt in device_opt: + if "type" in all_opt[opt]: + longopt = "--" + all_opt[opt]["longopt"] + if longopt in options: + if all_opt[opt]["type"] in ["integer", "second"]: + try: + number = int(options["--" + all_opt[opt]["longopt"]]) + except ValueError: + options_failed.append(opt) + return options_failed + +def _verify_unique_getopt(avail_opt): + used_getopt = set() + + for opt in avail_opt: + getopt_value = all_opt[opt].get("getopt", "").rstrip(":") + if getopt_value and getopt_value in used_getopt: + fail_usage("Short getopt for %s (-%s) is not unique" % (opt, getopt_value)) + else: + used_getopt.add(getopt_value) + +def _get_available_actions(device_opt): + available_actions = ["on", "off", "reboot", "status", "list", "list-status", \ + "monitor", "metadata", "manpage", "validate-all"] + default_value = "reboot" + + if device_opt.count("fabric_fencing"): + available_actions.remove("reboot") + default_value = "off" + if device_opt.count("no_status"): + available_actions.remove("status") + if device_opt.count("no_on"): + available_actions.remove("on") + if device_opt.count("no_off"): + available_actions.remove("off") + if not device_opt.count("separator"): + available_actions.remove("list") + available_actions.remove("list-status") + if device_opt.count("diag"): + available_actions.append("diag") + + return (available_actions, default_value) diff --git a/lib/fencing_snmp.py.py b/lib/fencing_snmp.py.py new file mode 100644 index 0000000..f9e5768 --- /dev/null +++ b/lib/fencing_snmp.py.py @@ -0,0 +1,128 @@ +#!@PYTHON@ -tt + +# For example of use please see fence_cisco_mds + +import re, pexpect +import logging +from fencing import * +from fencing import fail, fail_usage, EC_TIMED_OUT, run_delay, frun + +__all__ = ['FencingSnmp'] + +## do not add code here. +class FencingSnmp: + def __init__(self, options): + self.options = options + run_delay(options) + + def quote_for_run(self, string): + return string.replace(r"'", "'\\''") + + def complete_missed_params(self): + mapping = [[ + ['snmp-priv-passwd', 'password', '!snmp-sec-level'], + 'self.options["--snmp-sec-level"]="authPriv"' + ], [ + ['!snmp-version', 'community', '!username', '!snmp-priv-passwd', '!password'], + 'self.options["--snmp-version"]="2c"' + ]] + + for val in mapping: + e = val[0] + + res = True + + for item in e: + if item[0] == '!' and "--" + item[1:] in self.options: + res = False + break + + if item[0] != '!' and "--" + item[0:] not in self.options: + res = False + break + + if res: + exec(val[1]) + + def prepare_cmd(self, command): + cmd = "%s -m '' -Oeqn "% (command) + + self.complete_missed_params() + + #mapping from our option to snmpcmd option + mapping = (('snmp-version', 'v'), ('community', 'c')) + + for item in mapping: + if "--" + item[0] in self.options: + cmd += " -%s '%s'"% (item[1], self.quote_for_run(self.options["--" + item[0]])) + + # Some options make sense only for v3 (and for v1/2c can cause "problems") + if ("--snmp-version" in self.options) and (self.options["--snmp-version"] == "3"): + # Mapping from our options to snmpcmd options for v3 + mapping_v3 = (('snmp-auth-prot', 'a'), ('snmp-sec-level', 'l'), ('snmp-priv-prot', 'x'), \ + ('snmp-priv-passwd', 'X'), ('password', 'A'), ('username', 'u')) + for item in mapping_v3: + if "--"+item[0] in self.options: + cmd += " -%s '%s'"% (item[1], self.quote_for_run(self.options["--" + item[0]])) + + force_ipvx = "" + + if "--inet6-only" in self.options: + force_ipvx = "udp6:" + + if "--inet4-only" in self.options: + force_ipvx = "udp:" + + cmd += " '%s%s%s'"% (force_ipvx, self.quote_for_run(self.options["--ip"]), + "--ipport" in self.options and self.quote_for_run(":" + str(self.options["--ipport"])) or "") + return cmd + + def run_command(self, command, additional_timeout=0): + try: + logging.debug("%s\n", command) + + (res_output, res_code) = frun(command, + int(self.options["--shell-timeout"]) + + int(self.options["--login-timeout"]) + + additional_timeout, True) + + if res_code == None: + fail(EC_TIMED_OUT) + + logging.debug("%s\n", res_output) + + if (res_code != 0) or (re.search("^Error ", res_output, re.MULTILINE) != None): + fail_usage("Returned %d: %s"% (res_code, res_output)) + except pexpect.ExceptionPexpect: + fail_usage("Cannot run command %s"%(command)) + + return res_output + + def get(self, oid, additional_timeout=0): + cmd = "%s '%s'"% (self.prepare_cmd(self.options["--snmpget-path"]), self.quote_for_run(oid)) + + output = self.run_command(cmd, additional_timeout).splitlines() + + return output[len(output)-1].split(None, 1) + + def set(self, oid, value, additional_timeout=0): + mapping = ((int, 'i'), (str, 's')) + + type_of_value = '' + + for item in mapping: + if isinstance(value, item[0]): + type_of_value = item[1] + break + + cmd = "%s '%s' %s '%s'" % (self.prepare_cmd(self.options["--snmpset-path"]), + self.quote_for_run(oid), type_of_value, self.quote_for_run(str(value))) + + self.run_command(cmd, additional_timeout) + + def walk(self, oid, additional_timeout=0): + cmd = "%s '%s'"% (self.prepare_cmd(self.options["--snmpwalk-path"]), self.quote_for_run(oid)) + + output = self.run_command(cmd, additional_timeout).splitlines() + + return [x.split(None, 1) for x in output if x.startswith(".")] diff --git a/lib/metadata.rng b/lib/metadata.rng new file mode 100644 index 0000000..e0cd441 --- /dev/null +++ b/lib/metadata.rng @@ -0,0 +1,80 @@ +<grammar xmlns="http://relaxng.org/ns/structure/1.0"> + +<start><element name="resource-agent"> + <attribute name="name" /> + <attribute name="shortdesc" /> + + <zeroOrMore> + <element name="symlink"> + <attribute name="name" /> + <attribute name="shortdesc" /> + </element> + </zeroOrMore> + + <element name="longdesc"> <text /> </element> + <element name="vendor-url"> <text /> </element> + + <element name="parameters"> <oneOrMore> + <element name="parameter"> + <attribute name="name" /> + <attribute name="unique"> <ref name="boolean-values" /> </attribute> + <attribute name="required"> <ref name="boolean-values" /> </attribute> + <optional><attribute name="deprecated"> <ref name="boolean-values" /></attribute></optional> + <optional><attribute name="obsoletes" /> </optional> + <element name="getopt"> + <attribute name="mixed" /> + </element> + <element name="content"> + <choice> + <attribute name="type"> + <choice> + <value>boolean</value> + <value>string</value> + <value>second</value> + <value>integer</value> + </choice> + </attribute> + <group> + <attribute name="type"> + <value>select</value> + </attribute> + <zeroOrMore> + <element name="option"> + <attribute name="value" /> + </element> + </zeroOrMore> + </group> + </choice> + <optional> + <attribute name="default"> <text /> </attribute> + </optional> + </element> + + <oneOrMore> <element name="shortdesc"> + <attribute name="lang" /> + <text /> + </element> </oneOrMore> + </element> + </oneOrMore> </element> + + <element name="actions"> <oneOrMore> + <element name="action"> + <attribute name="name" /> + <optional> + <attribute name="on_target"> <ref name="boolean-values" /> </attribute> + </optional> + <optional> + <attribute name="automatic"> <ref name="boolean-values" /> </attribute> + </optional> + </element> + </oneOrMore> </element> +</element></start> + +<define name="boolean-values"> + <choice> + <value>0</value> + <value>1</value> + </choice> +</define> + +</grammar> diff --git a/lib/tests/test_fencing.py b/lib/tests/test_fencing.py new file mode 100644 index 0000000..6ee9385 --- /dev/null +++ b/lib/tests/test_fencing.py @@ -0,0 +1,123 @@ +#!/usr/bin/python + +import unittest +import sys +sys.path.append("..") +import fencing +import copy + +class Test_join2(unittest.TestCase): + def test_single(self): + words = ["Mike"] + self.assertEqual(fencing._join2(words), "Mike") + self.assertEqual(fencing._join2(words, last_separator=" xor "), "Mike") + self.assertEqual(fencing._join2(words, normal_separator=" xor "), "Mike") + + def test_double(self): + words = ["Mike", "John"] + self.assertEqual(fencing._join2(words), "Mike and John") + self.assertEqual(fencing._join2(words, last_separator=" xor "), "Mike xor John") + self.assertEqual(fencing._join2(words, normal_separator=" xor "), "Mike and John") + + def test_triple(self): + words = ["Mike", "John", "Adam"] + self.assertEqual(fencing._join2(words), "Mike, John and Adam") + self.assertEqual(fencing._join2(words, last_separator=" xor "), "Mike, John xor Adam") + self.assertEqual(fencing._join2(words, normal_separator=" xor "), "Mike xor John and Adam") + + def test_quadruple(self): + words = ["Eve", "Mike", "John", "Adam"] + self.assertEqual(fencing._join2(words), "Eve, Mike, John and Adam") + self.assertEqual(fencing._join2(words, last_separator=" xor "), "Eve, Mike, John xor Adam") + self.assertEqual(fencing._join2(words, normal_separator=" xor "), "Eve xor Mike xor John and Adam") + +class Test_add_dependency_options(unittest.TestCase): + basic_set = fencing.DEPENDENCY_OPT["default"] + + def test_add_nothing(self): + self.assertEqual(set(fencing._add_dependency_options([])), set(self.basic_set)) + self.assertEqual(set(fencing._add_dependency_options(["not-exist"])), set(self.basic_set)) + + def test_add_single(self): + self.assertEqual(set(fencing._add_dependency_options(["passwd"])), set(self.basic_set + ["passwd_script"])) + + def test_add_tuple(self): + self.assertEqual(set(fencing._add_dependency_options(["ssl", "passwd"])), \ + set(self.basic_set + ["passwd_script", "ssl_secure", "ssl_insecure", "gnutlscli_path"])) + +class Test_set_default_values(unittest.TestCase): + original_all_opt = None + + def setUp(self): + # all_opt[*]["default"] can be changed during tests + self.original_all_opt = copy.deepcopy(fencing.all_opt) + + def tearDown(self): + fencing.all_opt = copy.deepcopy(self.original_all_opt) + + def _prepare_options(self, device_opts, args = {}): + device_opts = fencing._add_dependency_options(device_opts) + device_opts + + arg_opts = args + options = dict(arg_opts) + options["device_opt"] = device_opts + fencing._update_metadata(options) + return fencing._set_default_values(options) + + def test_status_io(self): + options = self._prepare_options([]) + + self.assertEqual(options["--action"], "reboot") + self.assertIsNone(options.get("--not-exist", None)) + + def test_status_fabric(self): + options = self._prepare_options(["fabric_fencing"]) + self.assertEqual(options["--action"], "off") + + def test_ipport_nothing(self): + # should fail because connection method (telnet/ssh/...) is not set at all + self.assertRaises(IndexError, self._prepare_options, ["ipaddr"]) + + def test_ipport_set(self): + options = self._prepare_options(["ipaddr", "telnet"], {"--ipport" : "999"}) + self.assertEqual(options["--ipport"], "999") + + def test_ipport_telnet(self): + options = self._prepare_options(["ipaddr", "telnet"]) + self.assertEqual(options["--ipport"], "23") + + def test_ipport_ssh(self): + options = self._prepare_options(["ipaddr", "secure"], {"--ssh" : "1"}) + self.assertEqual(options["--ipport"], "22") + + def test_ipport_sshtelnet_use_telnet(self): + options = self._prepare_options(["ipaddr", "secure", "telnet"]) + self.assertEqual(options["--ipport"], "23") + + def test_ipport_sshtelnet_use_ssh(self): + options = self._prepare_options(["ipaddr", "secure", "telnet"], {"--ssh" : "1"}) + self.assertEqual(options["--ipport"], "22") + + def test_ipport_ssl(self): + options = self._prepare_options(["ipaddr", "ssl"], {"--ssl-secure" : "1"}) + self.assertEqual(options["--ipport"], "443") + + def test_ipport_ssl_insecure_as_default(self): + fencing.all_opt["ssl_insecure"]["default"] = "1" + options = self._prepare_options(["ipaddr", "ssl"]) + self.assertEqual(options["--ipport"], "443") + + def test_ipport_snmp(self): + options = self._prepare_options(["ipaddr", "community"]) + self.assertEqual(options["--ipport"], "161") + + def test_ipport_web(self): + options = self._prepare_options(["ipaddr", "web", "ssl"]) + self.assertEqual(options["--ipport"], "80") + + def test_path_telnet(self): + options = self._prepare_options(["ipaddr", "telnet"]) + self.assertTrue("--telnet-path" in options) + +if __name__ == '__main__': + unittest.main() |