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