1    	#!@PYTHON@ -tt
2    	
3    	# Copyright (c) 2020 IBM Corp.
4    	#
5    	# This library is free software; you can redistribute it and/or
6    	# modify it under the terms of the GNU Lesser General Public
7    	# License as published by the Free Software Foundation; either
8    	# version 2.1 of the License, or (at your option) any later version.
9    	#
10   	# This library is distributed in the hope that it will be useful,
11   	# but WITHOUT ANY WARRANTY; without even the implied warranty of
12   	# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   	# Lesser General Public License for more details.
14   	#
15   	# You should have received a copy of the GNU Lesser General Public
16   	# License along with this library.  If not, see
17   	# <http://www.gnu.org/licenses/>.
18   	
19   	import atexit
20   	import logging
21   	import time
22   	import sys
23   	
24   	import requests
25   	from requests.packages import urllib3
26   	
27   	sys.path.append("@FENCEAGENTSLIBDIR@")
28   	from fencing import *
29   	from fencing import fail_usage, run_delay, EC_GENERIC_ERROR
30   	
31   	DEFAULT_POWER_TIMEOUT = '300'
32   	ERROR_NOT_FOUND = ("{obj_type} {obj_name} not found in this HMC. "
33   	                   "Attention: names are case-sensitive.")
34   	
35   	class ApiClientError(Exception):
36   	    """
37   	    Base exception for all API Client related errors.
38   	    """
39   	
40   	class ApiClientRequestError(ApiClientError):
41   	    """
42   	    Raised when an API request ends in error
43   	    """
44   	
45   	    def __init__(self, req_method, req_uri, status, reason, message):
46   	        self.req_method = req_method
47   	        self.req_uri = req_uri
48   	        self.status = status
49   	        self.reason = reason
50   	        self.message = message
51   	        super(ApiClientRequestError, self).__init__()
52   	
53   	    def __str__(self):
54   	        return (
55   	            "API request failed, details:\n"
56   	            "HTTP Request : {req_method} {req_uri}\n"
57   	            "HTTP Response status: {status}\n"
58   	            "Error reason: {reason}\n"
59   	            "Error message: {message}\n".format(
60   	                req_method=self.req_method, req_uri=self.req_uri,
61   	                status=self.status, reason=self.reason, message=self.message)
62   	        )
63   	
64   	class APIClient(object):
65   	    DEFAULT_CONFIG = {
66   	        # how many connection-related errors to retry on
67   	        'connect_retries': 3,
68   	        # how many times to retry on read errors (after request was sent to the
69   	        # server)
70   	        'read_retries': 3,
71   	        # http methods that should be retried
72   	        'method_whitelist': ['HEAD', 'GET', 'OPTIONS'],
73   	        # limit of redirects to perform to avoid loops
74   	        'redirect': 5,
75   	        # how long to wait while establishing a connection
76   	        'connect_timeout': 30,
77   	        # how long to wait for asynchronous operations (jobs) to complete
78   	        'operation_timeout': 900,
79   	        # how long to wait between bytes sent by the remote side
80   	        'read_timeout': 300,
81   	        # default API port
82   	        'port': 6794,
83   	        # validate ssl certificates
84   	        'ssl_verify': False,
85   	        # load on activate is set in the HMC activation profile and therefore
86   	        # no additional load is executed by the fence agent
87   	        'load_on_activate': False
88   	    }
89   	    LABEL_BY_OP_MODE = {
90   	        'classic': {
91   	            'nodes': 'logical-partitions',
92   	            'state-on': 'operating',
93   	            'start': 'load',
94   	            'stop': 'deactivate'
95   	        },
96   	        'dpm': {
97   	            'nodes': 'partitions',
98   	            'state-on': 'active',
99   	            'start': 'start',
100  	            'stop': 'stop'
101  	        }
102  	    }
103  	    def __init__(self, host, user, passwd, config=None):
104  	        self.host = host
105  	        if not passwd:
106  	            raise ValueError('Password cannot be empty')
107  	        self.passwd = passwd
108  	        if not user:
109  	            raise ValueError('Username cannot be empty')
110  	        self.user = user
111  	        self._cpc_cache = {}
112  	        self._session = None
113  	        self._config = self.DEFAULT_CONFIG.copy()
114  	        # apply user defined values
115  	        if config:
116  	            self._config.update(config)
117  	
118  	    def _create_session(self):
119  	        """
120  	        Create a new requests session and apply config values
121  	        """
122  	        session = requests.Session()
123  	        retry_obj = urllib3.Retry(
124  	            # setting a total is necessary to cover SSL related errors
125  	            total=max(self._config['connect_retries'],
126  	                      self._config['read_retries']),
127  	            connect=self._config['connect_retries'],
128  	            read=self._config['read_retries'],
129  	            method_whitelist=self._config['method_whitelist'],
130  	            redirect=self._config['redirect']
131  	        )
132  	        session.mount('http://', requests.adapters.HTTPAdapter(
133  	            max_retries=retry_obj))
134  	        session.mount('https://', requests.adapters.HTTPAdapter(
135  	            max_retries=retry_obj))
136  	        return session
137  	
138  	    def _get_mode_labels(self, cpc):
139  	        """
140  	        Return the map of labels that corresponds to the cpc operation mode
141  	        """
142  	        if self.is_dpm_enabled(cpc):
143  	            return self.LABEL_BY_OP_MODE['dpm']
144  	        return self.LABEL_BY_OP_MODE['classic']
145  	
146  	    def _get_partition(self, cpc, partition):
147  	        """
148  	        Return the properties of the specified partition. Raises ValueError if
149  	        it cannot be found.
150  	        """
151  	        # HMC API's documentation says it'll return an empty array when no
152  	        # matches are found but for a CPC in classic mode it returns in fact
153  	        # 404, so we handle this accordingly. Remove the extra handling below
154  	        # once this behavior has been fixed on the API's side.
155  	        label_map = self._get_mode_labels(cpc)
156  	        resp = self._request('get', '{}/{}?name={}'.format(
157  	            self._cpc_cache[cpc]['object-uri'], label_map['nodes'], partition),
158  	                             valid_codes=[200, 404])
159  	
160  	        if label_map['nodes'] not in resp or not resp[label_map['nodes']]:
161  	            raise ValueError(ERROR_NOT_FOUND.format(
162  	                obj_type='LPAR/Partition', obj_name=partition))
163  	        return resp[label_map['nodes']][0]
164  	
165  	    def _partition_switch_power(self, cpc, partition, action):
166  	        """
167  	        Perform the API request to start (power on) or stop (power off) the
168  	        target partition and wait for the job to finish.
169  	        """
170  	        # retrieve partition's uri
171  	        part_uri = self._get_partition(cpc, partition)['object-uri']
172  	        label_map = self._get_mode_labels(cpc)
173  	
174  	        # in dpm mode the request must have empty body
175  	        if self.is_dpm_enabled(cpc):
176  	            body = None
177  	        # in classic mode we make sure the operation is executed
178  	        # even if the partition is already on
179  	        else:
180  	            body = {'force': True}
181  	            # when powering on the partition must be activated first
182  	            if action == 'start':
183  	                op_uri = '{}/operations/activate'.format(part_uri)
184  	                job_resp = self._request(
185  	                    'post', op_uri, body=body, valid_codes=[202])
186  	                # always wait for activate otherwise the load (start)
187  	                # operation will fail
188  	                if self._config['operation_timeout'] == 0:
189  	                    timeout = self.DEFAULT_CONFIG['operation_timeout']
190  	                else:
191  	                    timeout = self._config['operation_timeout']
192  	                logging.debug(
193  	                    'waiting for activate (timeout %s secs)', timeout)
194  	                self._wait_for_job('post', op_uri, job_resp['job-uri'],
195  	                                   timeout=timeout)
196  	                if self._config['load_on_activate']:
197  	                    return
198  	
199  	        # trigger the start job
200  	        op_uri = '{}/operations/{}'.format(part_uri, label_map[action])
201  	        job_resp = self._request('post', op_uri, body=body, valid_codes=[202])
202  	        if self._config['operation_timeout'] == 0:
203  	            return
204  	        logging.debug('waiting for %s (timeout %s secs)',
205  	                      label_map[action], self._config['operation_timeout'])
206  	        self._wait_for_job('post', op_uri, job_resp['job-uri'],
207  	                           timeout=self._config['operation_timeout'])
208  	
209  	    def _request(self, method, uri, body=None, headers=None, valid_codes=None):
210  	        """
211  	        Perform a request to the HMC API
212  	        """
213  	        assert method in ('delete', 'head', 'get', 'post', 'put')
214  	
215  	        url = 'https://{host}:{port}{uri}'.format(
216  	            host=self.host, port=self._config['port'], uri=uri)
217  	        if not headers:
218  	            headers = {}
219  	
220  	        if self._session is None:
221  	            raise ValueError('You need to log on first')
222  	        method = getattr(self._session, method)
223  	        timeout = (
224  	            self._config['connect_timeout'], self._config['read_timeout'])
225  	        response = method(url, json=body, headers=headers,
226  	                          verify=self._config['ssl_verify'], timeout=timeout)
227  	
228  	        if valid_codes and response.status_code not in valid_codes:
229  	            reason = '(no reason)'
230  	            message = '(no message)'
231  	            if response.headers.get('content-type') == 'application/json':
232  	                try:
233  	                    json_resp = response.json()
234  	                except ValueError:
235  	                    pass
236  	                else:
237  	                    reason = json_resp.get('reason', reason)
238  	                    message = json_resp.get('message', message)
239  	            else:
240  	                message = '{}...'.format(response.text[:500])
241  	            raise ApiClientRequestError(
242  	                response.request.method, response.request.url,
243  	                response.status_code, reason, message)
244  	
245  	        if response.status_code == 204:
246  	            return dict()
247  	        try:
248  	            json_resp = response.json()
249  	        except ValueError:
250  	            raise ApiClientRequestError(
251  	                response.request.method, response.request.url,
252  	                response.status_code, '(no reason)',
253  	                'Invalid JSON content in response')
254  	
255  	        return json_resp
256  	
257  	    def _update_cpc_cache(self, cpc_props):
258  	        self._cpc_cache[cpc_props['name']] = {
259  	            'object-uri': cpc_props['object-uri'],
260  	            'dpm-enabled': cpc_props.get('dpm-enabled', False)
261  	        }
262  	
263  	    def _wait_for_job(self, req_method, req_uri, job_uri, timeout):
264  	        """
265  	        Perform API requests to check for job status until it has completed
266  	        or the specified timeout is reached
267  	        """
268  	        op_timeout = time.time() + timeout
269  	        while time.time() < op_timeout:
270  	            job_resp = self._request("get", job_uri)
271  	            if job_resp['status'] == 'complete':
272  	                if job_resp['job-status-code'] in (200, 201, 204):
273  	                    return
274  	                raise ApiClientRequestError(
275  	                    req_method, req_uri,
276  	                    job_resp.get('job-status-code', '(no status)'),
277  	                    job_resp.get('job-reason-code', '(no reason)'),
278  	                    job_resp.get('job-results', {}).get(
279  	                        'message', '(no message)')
280  	                )
281  	            time.sleep(1)
282  	        raise ApiClientError('Timed out while waiting for job completion')
283  	
284  	    def cpc_list(self):
285  	        """
286  	        Return a list of CPCs in the format {'name': 'cpc-name', 'status':
287  	        'operating'}
288  	        """
289  	        list_resp = self._request("get", "/api/cpcs", valid_codes=[200])
290  	        ret = []
291  	        for cpc_props in list_resp['cpcs']:
292  	            self._update_cpc_cache(cpc_props)
293  	            ret.append({
294  	                'name': cpc_props['name'], 'status': cpc_props['status']})
295  	        return ret
296  	
297  	    def is_dpm_enabled(self, cpc):
298  	        """
299  	        Return True if CPC is in DPM mode, False for classic mode
300  	        """
301  	        if cpc in self._cpc_cache:
302  	            return self._cpc_cache[cpc]['dpm-enabled']
303  	        list_resp = self._request("get", "/api/cpcs?name={}".format(cpc),
304  	                                  valid_codes=[200])
305  	        if not list_resp['cpcs']:
306  	            raise ValueError(ERROR_NOT_FOUND.format(
307  	                obj_type='CPC', obj_name=cpc))
308  	        self._update_cpc_cache(list_resp['cpcs'][0])
309  	        return self._cpc_cache[cpc]['dpm-enabled']
310  	
311  	    def logon(self):
312  	        """
313  	        Open a session with the HMC API and store its ID
314  	        """
315  	        self._session = self._create_session()
316  	        logon_body = {"userid": self.user, "password": self.passwd}
317  	        logon_resp = self._request("post", "/api/sessions", body=logon_body,
318  	                                   valid_codes=[200, 201])
319  	        self._session.headers["X-API-Session"] = logon_resp['api-session']
320  	
321  	    def logoff(self):
322  	        """
323  	        Close/delete the HMC API session
324  	        """
325  	        if self._session is None:
326  	            return
327  	        self._request("delete", "/api/sessions/this-session",
328  	                      valid_codes=[204])
329  	        self._cpc_cache = {}
330  	        self._session = None
331  	
332  	    def partition_list(self, cpc):
333  	        """
334  	        Return a list of partitions in the format {'name': 'part-name',
335  	        'status': 'on'}
336  	        """
337  	        label_map = self._get_mode_labels(cpc)
338  	        list_resp = self._request(
339  	            'get', '{}/{}'.format(
340  	                self._cpc_cache[cpc]['object-uri'], label_map['nodes']),
341  	            valid_codes=[200])
342  	        status_map = {label_map['state-on']: 'on'}
343  	        return [{'name': part['name'],
344  	                 'status': status_map.get(part['status'].lower(), 'off')}
345  	                for part in list_resp[label_map['nodes']]]
346  	
347  	    def partition_start(self, cpc, partition):
348  	        """
349  	        Power on a partition
350  	        """
351  	        self._partition_switch_power(cpc, partition, 'start')
352  	
353  	    def partition_status(self, cpc, partition):
354  	        """
355  	        Return the current status of a partition (on or off)
356  	        """
357  	        label_map = self._get_mode_labels(cpc)
358  	
359  	        part_props = self._get_partition(cpc, partition)
360  	        if part_props['status'].lower() == label_map['state-on']:
361  	            return 'on'
362  	        return 'off'
363  	
364  	    def partition_stop(self, cpc, partition):
365  	        """
366  	        Power off a partition
367  	        """
368  	        self._partition_switch_power(cpc, partition, 'stop')
369  	
370  	def parse_plug(options):
371  	    """
372  	    Extract cpc and partition from specified plug value
373  	    """
374  	    try:
375  	        cpc, partition = options['--plug'].strip().split('/', 1)
376  	    except ValueError:
377  	        fail_usage('Please specify nodename in format cpc/partition')
378  	    cpc = cpc.strip()
379  	    if not cpc or not partition:
380  	        fail_usage('Please specify nodename in format cpc/partition')
381  	    return cpc, partition
382  	
383  	def get_power_status(conn, options):
384  	    logging.debug('executing get_power_status')
385  	    status = conn.partition_status(*parse_plug(options))
386  	    return status
387  	
388  	def set_power_status(conn, options):
389  	    logging.debug('executing set_power_status')
390  	    if options['--action'] == 'on':
391  	        conn.partition_start(*parse_plug(options))
392  	    elif options['--action'] == 'off':
393  	        conn.partition_stop(*parse_plug(options))
394  	    else:
395  	        fail_usage('Invalid action {}'.format(options['--action']))
396  	
397  	def get_outlet_list(conn, options):
398  	    logging.debug('executing get_outlet_list')
399  	    result = {}
400  	    for cpc in conn.cpc_list():
401  	        for part in conn.partition_list(cpc['name']):
402  	            result['{}/{}'.format(cpc['name'], part['name'])] = (
403  	                part['name'], part['status'])
404  	    return result
405  	
406  	def disconnect(conn):
407  	    """
408  	    Close the API session
409  	    """
410  	    try:
411  	        conn.logoff()
412  	    except Exception as exc:
413  	        logging.exception('Logoff failed: ')
414  	        sys.exit(str(exc))
415  	
416  	def set_opts():
417  	    """
418  	    Define the options supported by this agent
419  	    """
420  	    device_opt = [
421  	        "ipaddr",
422  	        "ipport",
423  	        "login",
424  	        "passwd",
425  	        "port",
426  	        "connect_retries",
427  	        "connect_timeout",
428  	        "operation_timeout",
429  	        "read_retries",
430  	        "read_timeout",
431  	        "ssl_secure",
432  	        "load_on_activate",
433  	    ]
434  	
435  	    all_opt["ipport"]["default"] = APIClient.DEFAULT_CONFIG['port']
436  	    all_opt["power_timeout"]["default"] = DEFAULT_POWER_TIMEOUT
437  	    port_desc = ("Physical plug id in the format cpc-name/partition-name "
438  	                 "(case-sensitive)")
439  	    all_opt["port"]["shortdesc"] = port_desc
440  	    all_opt["port"]["help"] = (
441  	        "-n, --plug=[id]                {}".format(port_desc))
442  	    all_opt["connect_retries"] = {
443  	        "getopt" : ":",
444  	        "longopt" : "connect-retries",
445  	        "help" : "--connect-retries=[number]     How many times to "
446  	                 "retry on connection errors",
447  	        "default" : APIClient.DEFAULT_CONFIG['connect_retries'],
448  	        "type" : "integer",
449  	        "required" : "0",
450  	        "shortdesc" : "How many times to retry on connection errors",
451  	        "order" : 2
452  	    }
453  	    all_opt["read_retries"] = {
454  	        "getopt" : ":",
455  	        "longopt" : "read-retries",
456  	        "help" : "--read-retries=[number]        How many times to "
457  	                 "retry on errors related to reading from server",
458  	        "default" : APIClient.DEFAULT_CONFIG['read_retries'],
459  	        "type" : "integer",
460  	        "required" : "0",
461  	        "shortdesc" : "How many times to retry on read errors",
462  	        "order" : 2
463  	    }
464  	    all_opt["connect_timeout"] = {
465  	        "getopt" : ":",
466  	        "longopt" : "connect-timeout",
467  	        "help" : "--connect-timeout=[seconds]    How long to wait to "
468  	                 "establish a connection",
469  	        "default" : APIClient.DEFAULT_CONFIG['connect_timeout'],
470  	        "type" : "second",
471  	        "required" : "0",
472  	        "shortdesc" : "How long to wait to establish a connection",
473  	        "order" : 2
474  	    }
475  	    all_opt["operation_timeout"] = {
476  	        "getopt" : ":",
477  	        "longopt" : "operation-timeout",
478  	        "help" : "--operation-timeout=[seconds]  How long to wait for "
479  	                 "power operation to complete (0 = do not wait)",
480  	        "default" : APIClient.DEFAULT_CONFIG['operation_timeout'],
481  	        "type" : "second",
482  	        "required" : "0",
483  	        "shortdesc" : "How long to wait for power operation to complete",
484  	        "order" : 2
485  	    }
486  	    all_opt["read_timeout"] = {
487  	        "getopt" : ":",
488  	        "longopt" : "read-timeout",
489  	        "help" : "--read-timeout=[seconds]       How long to wait "
490  	                 "to read data from server",
491  	        "default" : APIClient.DEFAULT_CONFIG['read_timeout'],
492  	        "type" : "second",
493  	        "required" : "0",
494  	        "shortdesc" : "How long to wait for server data",
495  	        "order" : 2
496  	    }
497  	    all_opt["load_on_activate"] = {
498  	        "getopt" : "",
499  	        "longopt" : "load-on-activate",
500  	        "help" : "--load-on-activate             Rely on the HMC to perform "
501  	                 "a load operation on activation",
502  	        "required" : "0",
503  	        "order" : 3
504  	    }
505  	    return device_opt
506  	
507  	def main():
508  	    """
509  	    Agent entry point
510  	    """
511  	    # register exit handler used by pacemaker
512  	    atexit.register(atexit_handler)
513  	
514  	    # prepare accepted options
515  	    device_opt = set_opts()
516  	
517  	    # parse options provided on input
518  	    options = check_input(device_opt, process_input(device_opt))
519  	
520  	    docs = {
521  	        "shortdesc": "Fence agent for IBM z LPARs",
522  	        "longdesc": (
523  	            "fence_ibmz is a Power Fencing agent which uses the HMC Web "
524  	            "Services API to fence IBM z LPARs."),
525  	        "vendorurl": "http://www.ibm.com"
526  	    }
527  	    show_docs(options, docs)
528  	
529  	    run_delay(options)
530  	
531  	    # set underlying library's logging and ssl config according to specified
532  	    # options
533  	    requests_log = logging.getLogger("urllib3")
534  	    requests_log.propagate = True
535  	    if "--verbose" in options:
CID (unavailable; MK=354ca6af1d07ca5b074403b4e8c363c4) (#1 of 1): Excessive log level (SIGMA.debug_logging_enabled):
(1) Event Sigma main event: The Python application has been configured to create excessive logs using a `DEBUG` log level. Excessive logging can expose sensitive information in log files.
(2) Event remediation: The log level of a production Python application should be set to `ERROR`, `WARN`, or `INFO`, instead of `DEBUG`.
536  	        requests_log.setLevel(logging.DEBUG)
537  	    if "--ssl-insecure" in options:
538  	        urllib3.disable_warnings(
539  	            category=urllib3.exceptions.InsecureRequestWarning)
540  	
541  	    hmc_address = options["--ip"]
542  	    hmc_userid = options["--username"]
543  	    hmc_password = options["--password"]
544  	    config = {
545  	        'connect_retries': int(options['--connect-retries']),
546  	        'read_retries': int(options['--read-retries']),
547  	        'operation_timeout': int(options['--operation-timeout']),
548  	        'connect_timeout': int(options['--connect-timeout']),
549  	        'read_timeout': int(options['--read-timeout']),
550  	        'port': int(options['--ipport']),
551  	        'ssl_verify': bool('--ssl-insecure' not in options),
552  	        'load_on_activate': bool('--load-on-activate' in options),
553  	    }
554  	    try:
555  	        conn = APIClient(hmc_address, hmc_userid, hmc_password, config)
556  	        conn.logon()
557  	        atexit.register(disconnect, conn)
558  	        result = fence_action(conn, options, set_power_status,
559  	                              get_power_status, get_outlet_list)
560  	    except Exception:
561  	        logging.exception('Exception occurred: ')
562  	        result = EC_GENERIC_ERROR
563  	    sys.exit(result)
564  	
565  	if __name__ == "__main__":
566  	    main()
567