blob: ee6d2fa42688a15b58974068297f03516a9ab04c [file] [log] [blame]
Justin Thalerb8807ce2018-05-25 19:16:20 -05001#!/usr/bin/python3
Justin Thalere412dc22018-01-12 16:28:24 -06002"""
3 Copyright 2017 IBM Corporation
Justin Thalerf9aee3e2017-12-05 12:11:09 -06004
Justin Thalere412dc22018-01-12 16:28:24 -06005 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16"""
Justin Thalerf9aee3e2017-12-05 12:11:09 -060017import argparse
18import requests
19import getpass
20import json
21import os
22import urllib3
23import time, datetime
Justin Thalerf9aee3e2017-12-05 12:11:09 -060024import binascii
25import subprocess
26import platform
27import zipfile
Justin Thaler22b1bb52018-03-15 13:31:32 -050028import tarfile
29import tempfile
30import hashlib
Justin Thalera6b5df72018-07-16 11:10:07 -050031import re
Justin Thalerf9aee3e2017-12-05 12:11:09 -060032
Justin Thalerf9aee3e2017-12-05 12:11:09 -060033def hilight(textToColor, color, bold):
Justin Thalere412dc22018-01-12 16:28:24 -060034 """
35 Used to add highlights to various text for displaying in a terminal
36
37 @param textToColor: string, the text to be colored
38 @param color: string, used to color the text red or green
39 @param bold: boolean, used to bold the textToColor
40 @return: Buffered reader containing the modified string.
41 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -060042 if(sys.platform.__contains__("win")):
43 if(color == "red"):
44 os.system('color 04')
45 elif(color == "green"):
46 os.system('color 02')
47 else:
48 os.system('color') #reset to default
49 return textToColor
50 else:
51 attr = []
52 if(color == "red"):
53 attr.append('31')
54 elif(color == "green"):
55 attr.append('32')
56 else:
57 attr.append('0')
58 if bold:
59 attr.append('1')
60 else:
61 attr.append('0')
62 return '\x1b[%sm%s\x1b[0m' % (';'.join(attr),textToColor)
63
Justin Thalere412dc22018-01-12 16:28:24 -060064
Justin Thalerf9aee3e2017-12-05 12:11:09 -060065def connectionErrHandler(jsonFormat, errorStr, err):
Justin Thalere412dc22018-01-12 16:28:24 -060066 """
67 Error handler various connection errors to bmcs
68
69 @param jsonFormat: boolean, used to output in json format with an error code.
70 @param errorStr: string, used to color the text red or green
71 @param err: string, the text from the exception
72 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -060073 if errorStr == "Timeout":
74 if not jsonFormat:
75 return("FQPSPIN0000M: Connection timed out. Ensure you have network connectivity to the bmc")
76 else:
Justin Thaler115bca72018-05-25 19:29:08 -050077 conerror = {}
78 conerror['CommonEventID'] = 'FQPSPIN0000M'
79 conerror['sensor']="N/A"
80 conerror['state']="N/A"
81 conerror['additionalDetails'] = "N/A"
82 conerror['Message']="Connection timed out. Ensure you have network connectivity to the BMC"
83 conerror['LengthyDescription'] = "While trying to establish a connection with the specified BMC, the BMC failed to respond in adequate time. Verify the BMC is functioning properly, and the network connectivity to the BMC is stable."
84 conerror['Serviceable']="Yes"
85 conerror['CallHomeCandidate']= "No"
86 conerror['Severity'] = "Critical"
87 conerror['EventType'] = "Communication Failure/Timeout"
88 conerror['VMMigrationFlag'] = "Yes"
89 conerror["AffectedSubsystem"] = "Interconnect (Networking)"
90 conerror["timestamp"] = str(int(time.time()))
91 conerror["UserAction"] = "Verify network connectivity between the two systems and the bmc is functional."
92 eventdict = {}
93 eventdict['event0'] = conerror
94 eventdict['numAlerts'] = '1'
95
96 errorMessageStr = errorMessageStr = json.dumps(eventdict, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False)
Justin Thalerf9aee3e2017-12-05 12:11:09 -060097 return(errorMessageStr)
98 elif errorStr == "ConnectionError":
99 if not jsonFormat:
100 return("FQPSPIN0001M: " + str(err))
101 else:
Justin Thaler115bca72018-05-25 19:29:08 -0500102 conerror = {}
103 conerror['CommonEventID'] = 'FQPSPIN0001M'
104 conerror['sensor']="N/A"
105 conerror['state']="N/A"
106 conerror['additionalDetails'] = str(err)
107 conerror['Message']="Connection Error. View additional details for more information"
108 conerror['LengthyDescription'] = "A connection error to the specified BMC occurred and additional details are provided. Review these details to resolve the issue."
109 conerror['Serviceable']="Yes"
110 conerror['CallHomeCandidate']= "No"
111 conerror['Severity'] = "Critical"
112 conerror['EventType'] = "Communication Failure/Timeout"
113 conerror['VMMigrationFlag'] = "Yes"
114 conerror["AffectedSubsystem"] = "Interconnect (Networking)"
115 conerror["timestamp"] = str(int(time.time()))
116 conerror["UserAction"] = "Correct the issue highlighted in additional details and try again"
117 eventdict = {}
118 eventdict['event0'] = conerror
119 eventdict['numAlerts'] = '1'
120
121 errorMessageStr = json.dumps(eventdict, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False)
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600122 return(errorMessageStr)
Justin Thaler115bca72018-05-25 19:29:08 -0500123
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600124 else:
125 return("Unknown Error: "+ str(err))
126
Justin Thalere412dc22018-01-12 16:28:24 -0600127
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600128def setColWidth(keylist, numCols, dictForOutput, colNames):
Justin Thalere412dc22018-01-12 16:28:24 -0600129 """
130 Sets the output width of the columns to display
131
132 @param keylist: list, list of strings representing the keys for the dictForOutput
133 @param numcols: the total number of columns in the final output
134 @param dictForOutput: dictionary, contains the information to print to the screen
135 @param colNames: list, The strings to use for the column headings, in order of the keylist
136 @return: A list of the column widths for each respective column.
137 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600138 colWidths = []
139 for x in range(0, numCols):
140 colWidths.append(0)
141 for key in dictForOutput:
142 for x in range(0, numCols):
143 colWidths[x] = max(colWidths[x], len(str(dictForOutput[key][keylist[x]])))
144
145 for x in range(0, numCols):
146 colWidths[x] = max(colWidths[x], len(colNames[x])) +2
147
148 return colWidths
149
150def loadPolicyTable(pathToPolicyTable):
Justin Thalere412dc22018-01-12 16:28:24 -0600151 """
152 loads a json based policy table into a dictionary
153
154 @param value: boolean, the value to convert
155 @return: A string of "Yes" or "No"
156 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600157 policyTable = {}
158 if(os.path.exists(pathToPolicyTable)):
159 with open(pathToPolicyTable, 'r') as stream:
160 try:
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600161 contents =json.load(stream)
162 policyTable = contents['events']
Justin Thalere412dc22018-01-12 16:28:24 -0600163 except Exception as err:
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600164 print(err)
165 return policyTable
166
Justin Thalere412dc22018-01-12 16:28:24 -0600167
168def boolToString(value):
169 """
170 converts a boolean value to a human readable string value
171
172 @param value: boolean, the value to convert
173 @return: A string of "Yes" or "No"
174 """
175 if(value):
176 return "Yes"
177 else:
178 return "No"
179
Justin Thalera6b5df72018-07-16 11:10:07 -0500180def stringToInt(text):
181 """
182 returns an integer if the string can be converted, otherwise returns the string
183
184 @param text: the string to try to convert to an integer
185 """
186 if text.isdigit():
187 return int(text)
188 else:
189 return text
Justin Thalere412dc22018-01-12 16:28:24 -0600190
Justin Thalera6b5df72018-07-16 11:10:07 -0500191def naturalSort(text):
192 """
193 provides a way to naturally sort a list
194
195 @param text: the key to convert for sorting
196 @return list containing the broken up string parts by integers and strings
197 """
198 stringPartList = []
199 for c in re.split('(\d+)', text):
200 stringPartList.append(stringToInt(c))
201 return stringPartList
202
Justin Thalere412dc22018-01-12 16:28:24 -0600203def tableDisplay(keylist, colNames, output):
204 """
205 Logs into the BMC and creates a session
206
207 @param keylist: list, keys for the output dictionary, ordered by colNames
208 @param colNames: Names for the Table of the columns
209 @param output: The dictionary of data to display
210 @return: Session object
211 """
212 colWidth = setColWidth(keylist, len(colNames), output, colNames)
213 row = ""
214 outputText = ""
215 for i in range(len(colNames)):
216 if (i != 0): row = row + "| "
217 row = row + colNames[i].ljust(colWidth[i])
218 outputText += row + "\n"
Justin Thalera6b5df72018-07-16 11:10:07 -0500219
220 output_keys = list(output.keys())
221 output_keys.sort(key=naturalSort)
222 for key in output_keys:
Justin Thalere412dc22018-01-12 16:28:24 -0600223 row = ""
Justin Thaler8fe0c732018-07-24 14:32:35 -0500224 for i in range(len(keylist)):
Justin Thalere412dc22018-01-12 16:28:24 -0600225 if (i != 0): row = row + "| "
226 row = row + output[key][keylist[i]].ljust(colWidth[i])
227 outputText += row + "\n"
228
229 return outputText
230
Justin Thaler22b1bb52018-03-15 13:31:32 -0500231def checkFWactivation(host, args, session):
232 """
233 Checks the software inventory for an image that is being activated.
234
235 @return: True if an image is being activated, false is no activations are happening
236 """
237 url="https://"+host+"/xyz/openbmc_project/software/enumerate"
238 httpHeader = {'Content-Type':'application/json'}
239 try:
240 resp = session.get(url, headers=httpHeader, verify=False, timeout=30)
241 except(requests.exceptions.Timeout):
242 print(connectionErrHandler(args.json, "Timeout", None))
243 return(True)
244 except(requests.exceptions.ConnectionError) as err:
245 print( connectionErrHandler(args.json, "ConnectionError", err))
246 return True
247 fwInfo = json.loads(resp.text)['data']
248 for key in fwInfo:
249 if 'Activation' in fwInfo[key]:
250 if 'Activating' in fwInfo[key]['Activation'] or 'Activating' in fwInfo[key]['RequestedActivation']:
251 return True
252 return False
253
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600254def login(host, username, pw,jsonFormat):
Justin Thalere412dc22018-01-12 16:28:24 -0600255 """
256 Logs into the BMC and creates a session
257
258 @param host: string, the hostname or IP address of the bmc to log into
259 @param username: The user name for the bmc to log into
260 @param pw: The password for the BMC to log into
261 @param jsonFormat: boolean, flag that will only allow relevant data from user command to be display. This function becomes silent when set to true.
262 @return: Session object
263 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600264 if(jsonFormat==False):
265 print("Attempting login...")
266 httpHeader = {'Content-Type':'application/json'}
267 mysess = requests.session()
268 try:
269 r = mysess.post('https://'+host+'/login', headers=httpHeader, json = {"data": [username, pw]}, verify=False, timeout=30)
270 loginMessage = json.loads(r.text)
271 if (loginMessage['status'] != "ok"):
272 print(loginMessage["data"]["description"].encode('utf-8'))
273 sys.exit(1)
274# if(sys.version_info < (3,0)):
275# urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
276# if sys.version_info >= (3,0):
277# requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
278 return mysess
279 except(requests.exceptions.Timeout):
Justin Thaler115bca72018-05-25 19:29:08 -0500280 return (connectionErrHandler(jsonFormat, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600281 except(requests.exceptions.ConnectionError) as err:
Justin Thaler115bca72018-05-25 19:29:08 -0500282 return (connectionErrHandler(jsonFormat, "ConnectionError", err))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600283
Justin Thalere412dc22018-01-12 16:28:24 -0600284
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600285def logout(host, username, pw, session, jsonFormat):
Justin Thalere412dc22018-01-12 16:28:24 -0600286 """
287 Logs out of the bmc and terminates the session
288
289 @param host: string, the hostname or IP address of the bmc to log out of
290 @param username: The user name for the bmc to log out of
291 @param pw: The password for the BMC to log out of
292 @param session: the active session to use
293 @param jsonFormat: boolean, flag that will only allow relevant data from user command to be display. This function becomes silent when set to true.
294 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600295 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -0600296 try:
297 r = session.post('https://'+host+'/logout', headers=httpHeader,json = {"data": [username, pw]}, verify=False, timeout=10)
298 except(requests.exceptions.Timeout):
299 print(connectionErrHandler(jsonFormat, "Timeout", None))
300
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600301 if(jsonFormat==False):
302 if('"message": "200 OK"' in r.text):
303 print('User ' +username + ' has been logged out')
304
Justin Thalere412dc22018-01-12 16:28:24 -0600305
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600306def fru(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -0600307 """
308 prints out the system inventory. deprecated see fruPrint and fruList
309
310 @param host: string, the hostname or IP address of the bmc
311 @param args: contains additional arguments used by the fru sub command
312 @param session: the active session to use
313 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
314 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600315 #url="https://"+host+"/org/openbmc/inventory/system/chassis/enumerate"
316
317 #print(url)
318 #res = session.get(url, headers=httpHeader, verify=False)
319 #print(res.text)
320 #sample = res.text
321
322 #inv_list = json.loads(sample)["data"]
323
324 url="https://"+host+"/xyz/openbmc_project/inventory/enumerate"
325 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -0600326 try:
327 res = session.get(url, headers=httpHeader, verify=False, timeout=40)
328 except(requests.exceptions.Timeout):
329 return(connectionErrHandler(args.json, "Timeout", None))
330
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600331 sample = res.text
332# inv_list.update(json.loads(sample)["data"])
333#
334# #determine column width's
335# colNames = ["FRU Name", "FRU Type", "Has Fault", "Is FRU", "Present", "Version"]
336# colWidths = setColWidth(["FRU Name", "fru_type", "fault", "is_fru", "present", "version"], 6, inv_list, colNames)
337#
338# print("FRU Name".ljust(colWidths[0])+ "FRU Type".ljust(colWidths[1]) + "Has Fault".ljust(colWidths[2]) + "Is FRU".ljust(colWidths[3])+
339# "Present".ljust(colWidths[4]) + "Version".ljust(colWidths[5]))
340# format the output
341# for key in sorted(inv_list.keys()):
342# keyParts = key.split("/")
343# isFRU = "True" if (inv_list[key]["is_fru"]==1) else "False"
344#
345# fruEntry = (keyParts[len(keyParts) - 1].ljust(colWidths[0]) + inv_list[key]["fru_type"].ljust(colWidths[1])+
346# inv_list[key]["fault"].ljust(colWidths[2])+isFRU.ljust(colWidths[3])+
347# inv_list[key]["present"].ljust(colWidths[4])+ inv_list[key]["version"].ljust(colWidths[5]))
348# if(isTTY):
349# if(inv_list[key]["is_fru"] == 1):
350# color = "green"
351# bold = True
352# else:
353# color='black'
354# bold = False
355# fruEntry = hilight(fruEntry, color, bold)
356# print (fruEntry)
357 return sample
Justin Thalere412dc22018-01-12 16:28:24 -0600358
359def fruPrint(host, args, session):
360 """
361 prints out all inventory
362
363 @param host: string, the hostname or IP address of the bmc
364 @param args: contains additional arguments used by the fru sub command
365 @param session: the active session to use
366 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
367 @return returns the total fru list.
368 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600369 url="https://"+host+"/xyz/openbmc_project/inventory/enumerate"
370 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -0600371 try:
372 res = session.get(url, headers=httpHeader, verify=False, timeout=40)
373 except(requests.exceptions.Timeout):
374 return(connectionErrHandler(args.json, "Timeout", None))
375
376
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600377# print(res.text)
378 frulist = res.text
379 url="https://"+host+"/xyz/openbmc_project/software/enumerate"
Justin Thalere412dc22018-01-12 16:28:24 -0600380 try:
381 res = session.get(url, headers=httpHeader, verify=False, timeout=40)
382 except(requests.exceptions.Timeout):
383 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600384# print(res.text)
385 frulist = frulist +"\n" + res.text
386
387 return frulist
388
Justin Thalere412dc22018-01-12 16:28:24 -0600389
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600390def fruList(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -0600391 """
392 prints out all inventory or only a specific specified item
393
394 @param host: string, the hostname or IP address of the bmc
395 @param args: contains additional arguments used by the fru sub command
396 @param session: the active session to use
397 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
398 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600399 if(args.items==True):
400 return fruPrint(host, args, session)
401 else:
Justin Thalere412dc22018-01-12 16:28:24 -0600402 return fruPrint(host, args, session)
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600403
404
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600405
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600406def fruStatus(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -0600407 """
408 prints out the status of all FRUs
409
410 @param host: string, the hostname or IP address of the bmc
411 @param args: contains additional arguments used by the fru sub command
412 @param session: the active session to use
413 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
414 """
415 url="https://"+host+"/xyz/openbmc_project/inventory/enumerate"
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600416 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -0600417 try:
418 res = session.get(url, headers=httpHeader, verify=False)
419 except(requests.exceptions.Timeout):
420 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600421# print(res.text)
Justin Thalere412dc22018-01-12 16:28:24 -0600422 frulist = json.loads(res.text)['data']
423 frus = {}
424 for key in frulist:
425 component = frulist[key]
426 isFru = False
427 present = False
428 func = False
429 hasSels = False
430 keyPieces = key.split('/')
431 fruName = keyPieces[-1]
432 if 'core' in fruName: #associate cores to cpus
433 fruName = keyPieces[-2] + '-' + keyPieces[-1]
434 if 'Functional' in component:
435 if('Present' in component):
436
437 if 'FieldReplaceable' in component:
438 if component['FieldReplaceable'] == 1:
439 isFru = True
440 if "fan" in fruName:
441 isFru = True;
442 if component['Present'] == 1:
443 present = True
444 if component['Functional'] == 1:
445 func = True
446 if ((key + "/fault") in frulist):
447 hasSels = True;
448 if args.verbose:
449 if hasSels:
450 loglist = []
451 faults = frulist[key+"/fault"]['endpoints']
452 for item in faults:
453 loglist.append(item.split('/')[-1])
454 frus[fruName] = {"compName": fruName, "Functional": boolToString(func), "Present":boolToString(present), "IsFru": boolToString(isFru), "selList": ', '.join(loglist).strip() }
455 else:
456 frus[fruName] = {"compName": fruName, "Functional": boolToString(func), "Present":boolToString(present), "IsFru": boolToString(isFru), "selList": "None" }
457 else:
458 frus[fruName] = {"compName": fruName, "Functional": boolToString(func), "Present":boolToString(present), "IsFru": boolToString(isFru), "hasSEL": boolToString(hasSels) }
Justin Thalerfb9c81c2018-07-16 11:14:37 -0500459 elif "power_supply" in fruName or "powersupply" in fruName:
Justin Thalere412dc22018-01-12 16:28:24 -0600460 if component['Present'] ==1:
461 present = True
462 isFru = True
463 if ((key + "/fault") in frulist):
464 hasSels = True;
465 if args.verbose:
466 if hasSels:
467 loglist = []
468 faults = frulist[key+"/fault"]['endpoints']
469 for key in faults:
470 loglist.append(faults[key].split('/')[-1])
471 frus[fruName] = {"compName": fruName, "Functional": "No", "Present":boolToString(present), "IsFru": boolToString(isFru), "selList": ', '.join(loglist).strip() }
472 else:
473 frus[fruName] = {"compName": fruName, "Functional": "Yes", "Present":boolToString(present), "IsFru": boolToString(isFru), "selList": "None" }
474 else:
475 frus[fruName] = {"compName": fruName, "Functional": boolToString(not hasSels), "Present":boolToString(present), "IsFru": boolToString(isFru), "hasSEL": boolToString(hasSels) }
476 if not args.json:
477 if not args.verbose:
478 colNames = ["Component", "Is a FRU", "Present", "Functional", "Has Logs"]
479 keylist = ["compName", "IsFru", "Present", "Functional", "hasSEL"]
480 else:
481 colNames = ["Component", "Is a FRU", "Present", "Functional", "Assoc. Log Number(s)"]
482 keylist = ["compName", "IsFru", "Present", "Functional", "selList"]
483 return tableDisplay(keylist, colNames, frus)
484 else:
485 return str(json.dumps(frus, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False))
486
487def sensor(host, args, session):
488 """
489 prints out all sensors
490
491 @param host: string, the hostname or IP address of the bmc
492 @param args: contains additional arguments used by the sensor sub command
493 @param session: the active session to use
494 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
495 """
496 httpHeader = {'Content-Type':'application/json'}
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600497 url="https://"+host+"/xyz/openbmc_project/sensors/enumerate"
Justin Thalere412dc22018-01-12 16:28:24 -0600498 try:
499 res = session.get(url, headers=httpHeader, verify=False, timeout=30)
500 except(requests.exceptions.Timeout):
501 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600502
503 #Get OCC status
504 url="https://"+host+"/org/open_power/control/enumerate"
Justin Thalere412dc22018-01-12 16:28:24 -0600505 try:
506 occres = session.get(url, headers=httpHeader, verify=False, timeout=30)
507 except(requests.exceptions.Timeout):
508 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600509 if not args.json:
510 colNames = ['sensor', 'type', 'units', 'value', 'target']
511 sensors = json.loads(res.text)["data"]
512 output = {}
513 for key in sensors:
514 senDict = {}
515 keyparts = key.split("/")
516 senDict['sensorName'] = keyparts[-1]
517 senDict['type'] = keyparts[-2]
Justin Thalere412dc22018-01-12 16:28:24 -0600518 try:
519 senDict['units'] = sensors[key]['Unit'].split('.')[-1]
520 except KeyError:
Justin Thaler22b1bb52018-03-15 13:31:32 -0500521 senDict['units'] = "N/A"
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600522 if('Scale' in sensors[key]):
523 scale = 10 ** sensors[key]['Scale']
524 else:
525 scale = 1
Justin Thaler22b1bb52018-03-15 13:31:32 -0500526 try:
527 senDict['value'] = str(sensors[key]['Value'] * scale)
528 except KeyError:
529 if 'value' in sensors[key]:
530 senDict['value'] = sensors[key]['value']
531 else:
532 senDict['value'] = "N/A"
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600533 if 'Target' in sensors[key]:
534 senDict['target'] = str(sensors[key]['Target'])
535 else:
536 senDict['target'] = 'N/A'
537 output[senDict['sensorName']] = senDict
538
539 occstatus = json.loads(occres.text)["data"]
540 if '/org/open_power/control/occ0' in occstatus:
541 occ0 = occstatus["/org/open_power/control/occ0"]['OccActive']
542 if occ0 == 1:
543 occ0 = 'Active'
544 else:
545 occ0 = 'Inactive'
546 output['OCC0'] = {'sensorName':'OCC0', 'type': 'Discrete', 'units': 'N/A', 'value': occ0, 'target': 'Active'}
547 occ1 = occstatus["/org/open_power/control/occ1"]['OccActive']
548 if occ1 == 1:
549 occ1 = 'Active'
550 else:
551 occ1 = 'Inactive'
552 output['OCC1'] = {'sensorName':'OCC1', 'type': 'Discrete', 'units': 'N/A', 'value': occ0, 'target': 'Active'}
553 else:
554 output['OCC0'] = {'sensorName':'OCC0', 'type': 'Discrete', 'units': 'N/A', 'value': 'Inactive', 'target': 'Inactive'}
555 output['OCC1'] = {'sensorName':'OCC1', 'type': 'Discrete', 'units': 'N/A', 'value': 'Inactive', 'target': 'Inactive'}
556 keylist = ['sensorName', 'type', 'units', 'value', 'target']
Justin Thalere412dc22018-01-12 16:28:24 -0600557
558 return tableDisplay(keylist, colNames, output)
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600559 else:
560 return res.text + occres.text
Justin Thalere412dc22018-01-12 16:28:24 -0600561
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600562def sel(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -0600563 """
564 prints out the bmc alerts
565
566 @param host: string, the hostname or IP address of the bmc
567 @param args: contains additional arguments used by the sel sub command
568 @param session: the active session to use
569 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
570 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600571
572 url="https://"+host+"/xyz/openbmc_project/logging/entry/enumerate"
573 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -0600574 try:
575 res = session.get(url, headers=httpHeader, verify=False, timeout=60)
576 except(requests.exceptions.Timeout):
577 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600578 return res.text
Justin Thalere412dc22018-01-12 16:28:24 -0600579
580
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600581def parseESEL(args, eselRAW):
Justin Thalere412dc22018-01-12 16:28:24 -0600582 """
583 parses the esel data and gets predetermined search terms
584
585 @param eselRAW: string, the raw esel string from the bmc
586 @return: A dictionary containing the quick snapshot data unless args.fullEsel is listed then a full PEL log is returned
587 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600588 eselParts = {}
589 esel_bin = binascii.unhexlify(''.join(eselRAW.split()[16:]))
590 #search terms contains the search term as the key and the return dictionary key as it's value
591 searchTerms = { 'Signature Description':'signatureDescription', 'devdesc':'devdesc',
Justin Thaler22b1bb52018-03-15 13:31:32 -0500592 'Callout type': 'calloutType', 'Procedure':'procedure', 'Sensor Type': 'sensorType'}
Justin Thalercf1deae2018-05-25 19:35:21 -0500593 eselBinPath = tempfile.gettempdir() + os.sep + 'esel.bin'
594 with open(eselBinPath, 'wb') as f:
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600595 f.write(esel_bin)
596 errlPath = ""
597 #use the right errl file for the machine architecture
598 arch = platform.machine()
599 if(arch =='x86_64' or arch =='AMD64'):
600 if os.path.exists('/opt/ibm/ras/bin/x86_64/errl'):
601 errlPath = '/opt/ibm/ras/bin/x86_64/errl'
602 elif os.path.exists('errl/x86_64/errl'):
603 errlPath = 'errl/x86_64/errl'
604 else:
605 errlPath = 'x86_64/errl'
606 elif (platform.machine()=='ppc64le'):
607 if os.path.exists('/opt/ibm/ras/bin/ppc64le/errl'):
608 errlPath = '/opt/ibm/ras/bin/ppc64le/errl'
609 elif os.path.exists('errl/ppc64le/errl'):
610 errlPath = 'errl/ppc64le/errl'
611 else:
612 errlPath = 'ppc64le/errl'
613 else:
614 print("machine architecture not supported for parsing eSELs")
615 return eselParts
616
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600617 if(os.path.exists(errlPath)):
Justin Thalercf1deae2018-05-25 19:35:21 -0500618 output= subprocess.check_output([errlPath, '-d', '--file='+eselBinPath]).decode('utf-8')
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600619# output = proc.communicate()[0]
620 lines = output.split('\n')
621
622 if(hasattr(args, 'fullEsel')):
623 return output
624
625 for i in range(0, len(lines)):
626 lineParts = lines[i].split(':')
627 if(len(lineParts)>1): #ignore multi lines, output formatting lines, and other information
628 for term in searchTerms:
629 if(term in lineParts[0]):
630 temp = lines[i][lines[i].find(':')+1:].strip()[:-1].strip()
631 if lines[i+1].find(':') != -1:
632 if (len(lines[i+1].split(':')[0][1:].strip())==0):
633 while(len(lines[i][:lines[i].find(':')].strip())>2):
634 if((i+1) <= len(lines)):
635 i+=1
636 else:
637 i=i-1
638 break
639 temp = temp + lines[i][lines[i].find(':'):].strip()[:-1].strip()[:-1].strip()
Justin Thaler22b1bb52018-03-15 13:31:32 -0500640 if(searchTerms[term] in eselParts):
641 eselParts[searchTerms[term]] = eselParts[searchTerms[term]] + ", " + temp
642 else:
643 eselParts[searchTerms[term]] = temp
Justin Thalercf1deae2018-05-25 19:35:21 -0500644 os.remove(eselBinPath)
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600645 else:
646 print("errl file cannot be found")
647
648 return eselParts
649
Justin Thalere412dc22018-01-12 16:28:24 -0600650
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600651def sortSELs(events):
Justin Thalere412dc22018-01-12 16:28:24 -0600652 """
653 sorts the sels by timestamp, then log entry number
654
655 @param events: Dictionary containing events
656 @return: list containing a list of the ordered log entries, and dictionary of keys
657 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600658 logNumList = []
659 timestampList = []
660 eventKeyDict = {}
661 eventsWithTimestamp = {}
662 logNum2events = {}
663 for key in events:
664 if key == 'numAlerts': continue
665 if 'callout' in key: continue
666 timestamp = (events[key]['timestamp'])
667 if timestamp not in timestampList:
668 eventsWithTimestamp[timestamp] = [events[key]['logNum']]
669 else:
670 eventsWithTimestamp[timestamp].append(events[key]['logNum'])
671 #map logNumbers to the event dictionary keys
672 eventKeyDict[str(events[key]['logNum'])] = key
673
674 timestampList = list(eventsWithTimestamp.keys())
675 timestampList.sort()
676 for ts in timestampList:
677 if len(eventsWithTimestamp[ts]) > 1:
678 tmplist = eventsWithTimestamp[ts]
679 tmplist.sort()
680 logNumList = logNumList + tmplist
681 else:
682 logNumList = logNumList + eventsWithTimestamp[ts]
683
684 return [logNumList, eventKeyDict]
685
Justin Thalere412dc22018-01-12 16:28:24 -0600686
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600687def parseAlerts(policyTable, selEntries, args):
Justin Thalere412dc22018-01-12 16:28:24 -0600688 """
689 parses alerts in the IBM CER format, using an IBM policy Table
690
691 @param policyTable: dictionary, the policy table entries
692 @param selEntries: dictionary, the alerts retrieved from the bmc
693 @return: A dictionary of the parsed entries, in chronological order
694 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600695 eventDict = {}
696 eventNum =""
697 count = 0
698 esel = ""
699 eselParts = {}
700 i2cdevice= ""
701
702 'prepare and sort the event entries'
703 for key in selEntries:
704 if 'callout' not in key:
705 selEntries[key]['logNum'] = key.split('/')[-1]
706 selEntries[key]['timestamp'] = selEntries[key]['Timestamp']
707 sortedEntries = sortSELs(selEntries)
708 logNumList = sortedEntries[0]
709 eventKeyDict = sortedEntries[1]
710
711 for logNum in logNumList:
712 key = eventKeyDict[logNum]
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600713 hasEsel=False
714 i2creadFail = False
715 if 'callout' in key:
716 continue
717 else:
718 messageID = str(selEntries[key]['Message'])
719 addDataPiece = selEntries[key]['AdditionalData']
720 calloutIndex = 0
721 calloutFound = False
722 for i in range(len(addDataPiece)):
723 if("CALLOUT_INVENTORY_PATH" in addDataPiece[i]):
724 calloutIndex = i
725 calloutFound = True
726 fruCallout = str(addDataPiece[calloutIndex]).split('=')[1]
727 if("CALLOUT_DEVICE_PATH" in addDataPiece[i]):
728 i2creadFail = True
729 i2cdevice = str(addDataPiece[i]).strip().split('=')[1]
730 i2cdevice = '/'.join(i2cdevice.split('/')[-4:])
Justin Thalere34c43a2018-05-25 19:37:55 -0500731 if 'fsi' in str(addDataPiece[calloutIndex]).split('=')[1]:
732 fruCallout = 'FSI'
733 else:
734 fruCallout = 'I2C'
735 calloutFound = True
736 if("CALLOUT_GPIO_NUM" in addDataPiece[i]):
737 if not calloutFound:
738 fruCallout = 'GPIO'
739 calloutFound = True
740 if("CALLOUT_IIC_BUS" in addDataPiece[i]):
741 if not calloutFound:
742 fruCallout = "I2C"
743 calloutFound = True
744 if("CALLOUT_IPMI_SENSOR_NUM" in addDataPiece[i]):
745 if not calloutFound:
746 fruCallout = "IPMI"
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600747 calloutFound = True
748 if("ESEL" in addDataPiece[i]):
749 esel = str(addDataPiece[i]).strip().split('=')[1]
750 if args.devdebug:
751 eselParts = parseESEL(args, esel)
752 hasEsel=True
753 if("GPU" in addDataPiece[i]):
754 fruCallout = '/xyz/openbmc_project/inventory/system/chassis/motherboard/gpu' + str(addDataPiece[i]).strip()[-1]
755 calloutFound = True
756 if("PROCEDURE" in addDataPiece[i]):
757 fruCallout = str(hex(int(str(addDataPiece[i]).split('=')[1])))[2:]
758 calloutFound = True
Justin Thalere412dc22018-01-12 16:28:24 -0600759 if("RAIL_NAME" in addDataPiece[i]):
760 calloutFound=True
761 fruCallout = str(addDataPiece[i]).split('=')[1].strip()
762 if("INPUT_NAME" in addDataPiece[i]):
763 calloutFound=True
764 fruCallout = str(addDataPiece[i]).split('=')[1].strip()
765 if("SENSOR_TYPE" in addDataPiece[i]):
766 calloutFound=True
767 fruCallout = str(addDataPiece[i]).split('=')[1].strip()
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600768
769 if(calloutFound):
Justin Thaler22b1bb52018-03-15 13:31:32 -0500770 if fruCallout != "":
771 policyKey = messageID +"||" + fruCallout
Justin Thalere34c43a2018-05-25 19:37:55 -0500772 if policyKey not in policyTable:
773 policyKey = messageID
Justin Thaler22b1bb52018-03-15 13:31:32 -0500774 else:
775 policyKey = messageID
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600776 else:
777 policyKey = messageID
778 event = {}
779 eventNum = str(count)
780 if policyKey in policyTable:
781 for pkey in policyTable[policyKey]:
782 if(type(policyTable[policyKey][pkey])== bool):
783 event[pkey] = boolToString(policyTable[policyKey][pkey])
784 else:
785 if (i2creadFail and pkey == 'Message'):
786 event[pkey] = policyTable[policyKey][pkey] + ' ' +i2cdevice
787 else:
788 event[pkey] = policyTable[policyKey][pkey]
789 event['timestamp'] = selEntries[key]['Timestamp']
790 event['resolved'] = bool(selEntries[key]['Resolved'])
791 if(hasEsel):
792 if args.devdebug:
793 event['eselParts'] = eselParts
794 event['raweSEL'] = esel
795 event['logNum'] = key.split('/')[-1]
796 eventDict['event' + eventNum] = event
797
798 else:
799 severity = str(selEntries[key]['Severity']).split('.')[-1]
800 if severity == 'Error':
801 severity = 'Critical'
802 eventDict['event'+eventNum] = {}
803 eventDict['event' + eventNum]['error'] = "error: Not found in policy table: " + policyKey
804 eventDict['event' + eventNum]['timestamp'] = selEntries[key]['Timestamp']
805 eventDict['event' + eventNum]['Severity'] = severity
806 if(hasEsel):
807 if args.devdebug:
808 eventDict['event' +eventNum]['eselParts'] = eselParts
809 eventDict['event' +eventNum]['raweSEL'] = esel
810 eventDict['event' +eventNum]['logNum'] = key.split('/')[-1]
811 eventDict['event' +eventNum]['resolved'] = bool(selEntries[key]['Resolved'])
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600812 count += 1
813 return eventDict
814
815
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600816def selDisplay(events, args):
Justin Thalere412dc22018-01-12 16:28:24 -0600817 """
818 displays alerts in human readable format
819
820 @param events: Dictionary containing events
821 @return:
822 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600823 activeAlerts = []
824 historyAlerts = []
825 sortedEntries = sortSELs(events)
826 logNumList = sortedEntries[0]
827 eventKeyDict = sortedEntries[1]
828 keylist = ['Entry', 'ID', 'Timestamp', 'Serviceable', 'Severity','Message']
829 if(args.devdebug):
830 colNames = ['Entry', 'ID', 'Timestamp', 'Serviceable', 'Severity','Message', 'eSEL contents']
831 keylist.append('eSEL')
832 else:
833 colNames = ['Entry', 'ID', 'Timestamp', 'Serviceable', 'Severity', 'Message']
834 for log in logNumList:
835 selDict = {}
836 alert = events[eventKeyDict[str(log)]]
837 if('error' in alert):
838 selDict['Entry'] = alert['logNum']
839 selDict['ID'] = 'Unknown'
840 selDict['Timestamp'] = datetime.datetime.fromtimestamp(int(alert['timestamp']/1000)).strftime("%Y-%m-%d %H:%M:%S")
841 msg = alert['error']
842 polMsg = msg.split("policy table:")[0]
843 msg = msg.split("policy table:")[1]
844 msgPieces = msg.split("||")
845 err = msgPieces[0]
846 if(err.find("org.open_power.")!=-1):
847 err = err.split("org.open_power.")[1]
848 elif(err.find("xyz.openbmc_project.")!=-1):
849 err = err.split("xyz.openbmc_project.")[1]
850 else:
851 err = msgPieces[0]
852 callout = ""
853 if len(msgPieces) >1:
854 callout = msgPieces[1]
855 if(callout.find("/org/open_power/")!=-1):
856 callout = callout.split("/org/open_power/")[1]
857 elif(callout.find("/xyz/openbmc_project/")!=-1):
858 callout = callout.split("/xyz/openbmc_project/")[1]
859 else:
860 callout = msgPieces[1]
861 selDict['Message'] = polMsg +"policy table: "+ err + "||" + callout
862 selDict['Serviceable'] = 'Unknown'
863 selDict['Severity'] = alert['Severity']
864 else:
865 selDict['Entry'] = alert['logNum']
866 selDict['ID'] = alert['CommonEventID']
867 selDict['Timestamp'] = datetime.datetime.fromtimestamp(int(alert['timestamp']/1000)).strftime("%Y-%m-%d %H:%M:%S")
868 selDict['Message'] = alert['Message']
869 selDict['Serviceable'] = alert['Serviceable']
870 selDict['Severity'] = alert['Severity']
871
872
873 eselOrder = ['refCode','signatureDescription', 'eselType', 'devdesc', 'calloutType', 'procedure']
874 if ('eselParts' in alert and args.devdebug):
875 eselOutput = ""
876 for item in eselOrder:
877 if item in alert['eselParts']:
878 eselOutput = eselOutput + item + ": " + alert['eselParts'][item] + " | "
879 selDict['eSEL'] = eselOutput
880 else:
881 if args.devdebug:
882 selDict['eSEL'] = "None"
883
884 if not alert['resolved']:
885 activeAlerts.append(selDict)
886 else:
887 historyAlerts.append(selDict)
888 mergedOutput = activeAlerts + historyAlerts
889 colWidth = setColWidth(keylist, len(colNames), dict(enumerate(mergedOutput)), colNames)
890
891 output = ""
892 if(len(activeAlerts)>0):
893 row = ""
894 output +="----Active Alerts----\n"
895 for i in range(0, len(colNames)):
896 if i!=0: row =row + "| "
897 row = row + colNames[i].ljust(colWidth[i])
898 output += row + "\n"
899
900 for i in range(0,len(activeAlerts)):
901 row = ""
902 for j in range(len(activeAlerts[i])):
903 if (j != 0): row = row + "| "
904 row = row + activeAlerts[i][keylist[j]].ljust(colWidth[j])
905 output += row + "\n"
906
907 if(len(historyAlerts)>0):
908 row = ""
909 output+= "----Historical Alerts----\n"
910 for i in range(len(colNames)):
911 if i!=0: row =row + "| "
912 row = row + colNames[i].ljust(colWidth[i])
913 output += row + "\n"
914
915 for i in range(0, len(historyAlerts)):
916 row = ""
917 for j in range(len(historyAlerts[i])):
918 if (j != 0): row = row + "| "
919 row = row + historyAlerts[i][keylist[j]].ljust(colWidth[j])
920 output += row + "\n"
921# print(events[eventKeyDict[str(log)]])
922 return output
923
Justin Thalere412dc22018-01-12 16:28:24 -0600924
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600925def selPrint(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -0600926 """
927 prints out all bmc alerts
928
929 @param host: string, the hostname or IP address of the bmc
930 @param args: contains additional arguments used by the fru sub command
931 @param session: the active session to use
932 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
933 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600934 if(args.policyTableLoc is None):
935 if os.path.exists('policyTable.json'):
936 ptableLoc = "policyTable.json"
937 elif os.path.exists('/opt/ibm/ras/lib/policyTable.json'):
938 ptableLoc = '/opt/ibm/ras/lib/policyTable.json'
939 else:
940 ptableLoc = 'lib/policyTable.json'
941 else:
942 ptableLoc = args.policyTableLoc
943 policyTable = loadPolicyTable(ptableLoc)
944 rawselEntries = ""
945 if(hasattr(args, 'fileloc') and args.fileloc is not None):
946 if os.path.exists(args.fileloc):
947 with open(args.fileloc, 'r') as selFile:
948 selLines = selFile.readlines()
949 rawselEntries = ''.join(selLines)
950 else:
951 print("Error: File not found")
952 sys.exit(1)
953 else:
954 rawselEntries = sel(host, args, session)
955 loadFailed = False
956 try:
957 selEntries = json.loads(rawselEntries)
958 except ValueError:
959 loadFailed = True
960 if loadFailed:
961 cleanSels = json.dumps(rawselEntries).replace('\\n', '')
962 #need to load json twice as original content was string escaped a second time
963 selEntries = json.loads(json.loads(cleanSels))
964 selEntries = selEntries['data']
Justin Thalere412dc22018-01-12 16:28:24 -0600965
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600966 if 'description' in selEntries:
967 if(args.json):
968 return("{\n\t\"numAlerts\": 0\n}")
969 else:
970 return("No log entries found")
971
972 else:
973 if(len(policyTable)>0):
974 events = parseAlerts(policyTable, selEntries, args)
975 if(args.json):
976 events["numAlerts"] = len(events)
977 retValue = str(json.dumps(events, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False))
978 return retValue
979 elif(hasattr(args, 'fullSel')):
980 return events
981 else:
982 #get log numbers to order event entries sequentially
983 return selDisplay(events, args)
984 else:
985 if(args.json):
986 return selEntries
987 else:
988 print("error: Policy Table not found.")
989 return selEntries
Justin Thalere412dc22018-01-12 16:28:24 -0600990
Justin Thalerf9aee3e2017-12-05 12:11:09 -0600991def selList(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -0600992 """
993 prints out all all bmc alerts, or only prints out the specified alerts
994
995 @param host: string, the hostname or IP address of the bmc
996 @param args: contains additional arguments used by the fru sub command
997 @param session: the active session to use
998 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
999 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001000 return(sel(host, args, session))
1001
Justin Thalere412dc22018-01-12 16:28:24 -06001002
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001003def selClear(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001004 """
1005 clears all alerts
1006
1007 @param host: string, the hostname or IP address of the bmc
1008 @param args: contains additional arguments used by the fru sub command
1009 @param session: the active session to use
1010 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1011 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001012 url="https://"+host+"/xyz/openbmc_project/logging/action/deleteAll"
1013 httpHeader = {'Content-Type':'application/json'}
1014 data = "{\"data\": [] }"
Justin Thalere412dc22018-01-12 16:28:24 -06001015
1016 try:
1017 res = session.post(url, headers=httpHeader, data=data, verify=False, timeout=30)
1018 except(requests.exceptions.Timeout):
1019 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001020 if res.status_code == 200:
1021 return "The Alert Log has been cleared. Please allow a few minutes for the action to complete."
1022 else:
1023 print("Unable to clear the logs, trying to clear 1 at a time")
1024 sels = json.loads(sel(host, args, session))['data']
1025 for key in sels:
1026 if 'callout' not in key:
1027 logNum = key.split('/')[-1]
1028 url = "https://"+ host+ "/xyz/openbmc_project/logging/entry/"+logNum+"/action/Delete"
1029 try:
1030 session.post(url, headers=httpHeader, data=data, verify=False, timeout=30)
1031 except(requests.exceptions.Timeout):
1032 return connectionErrHandler(args.json, "Timeout", None)
1033 sys.exit(1)
1034 except(requests.exceptions.ConnectionError) as err:
1035 return connectionErrHandler(args.json, "ConnectionError", err)
1036 sys.exit(1)
1037 return ('Sel clearing complete')
1038
1039def selSetResolved(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001040 """
1041 sets a sel entry to resolved
1042
1043 @param host: string, the hostname or IP address of the bmc
1044 @param args: contains additional arguments used by the fru sub command
1045 @param session: the active session to use
1046 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1047 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001048 url="https://"+host+"/xyz/openbmc_project/logging/entry/" + str(args.selNum) + "/attr/Resolved"
1049 httpHeader = {'Content-Type':'application/json'}
1050 data = "{\"data\": 1 }"
Justin Thalere412dc22018-01-12 16:28:24 -06001051 try:
1052 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1053 except(requests.exceptions.Timeout):
1054 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001055 if res.status_code == 200:
1056 return "Sel entry "+ str(args.selNum) +" is now set to resolved"
1057 else:
1058 return "Unable to set the alert to resolved"
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001059
Justin Thalere412dc22018-01-12 16:28:24 -06001060def selResolveAll(host, args, session):
1061 """
1062 sets a sel entry to resolved
1063
1064 @param host: string, the hostname or IP address of the bmc
1065 @param args: contains additional arguments used by the fru sub command
1066 @param session: the active session to use
1067 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1068 """
1069 rawselEntries = sel(host, args, session)
1070 loadFailed = False
1071 try:
1072 selEntries = json.loads(rawselEntries)
1073 except ValueError:
1074 loadFailed = True
1075 if loadFailed:
1076 cleanSels = json.dumps(rawselEntries).replace('\\n', '')
1077 #need to load json twice as original content was string escaped a second time
1078 selEntries = json.loads(json.loads(cleanSels))
1079 selEntries = selEntries['data']
1080
1081 if 'description' in selEntries:
1082 if(args.json):
1083 return("{\n\t\"selsResolved\": 0\n}")
1084 else:
1085 return("No log entries found")
1086 else:
1087 d = vars(args)
1088 successlist = []
1089 failedlist = []
1090 for key in selEntries:
1091 if 'callout' not in key:
1092 d['selNum'] = key.split('/')[-1]
1093 resolved = selSetResolved(host,args,session)
1094 if 'Sel entry' in resolved:
1095 successlist.append(d['selNum'])
1096 else:
1097 failedlist.append(d['selNum'])
1098 output = ""
1099 successlist.sort()
1100 failedlist.sort()
1101 if len(successlist)>0:
1102 output = "Successfully resolved: " +', '.join(successlist) +"\n"
1103 if len(failedlist)>0:
1104 output += "Failed to resolve: " + ', '.join(failedlist) + "\n"
1105 return output
1106
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001107def chassisPower(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001108 """
1109 called by the chassis function. Controls the power state of the chassis, or gets the status
1110
1111 @param host: string, the hostname or IP address of the bmc
1112 @param args: contains additional arguments used by the fru sub command
1113 @param session: the active session to use
1114 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1115 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001116 if(args.powcmd == 'on'):
Justin Thaler22b1bb52018-03-15 13:31:32 -05001117 if checkFWactivation(host, args, session):
1118 return ("Chassis Power control disabled during firmware activation")
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001119 print("Attempting to Power on...:")
1120 url="https://"+host+"/xyz/openbmc_project/state/host0/attr/RequestedHostTransition"
1121 httpHeader = {'Content-Type':'application/json',}
1122 data = '{"data":"xyz.openbmc_project.State.Host.Transition.On"}'
Justin Thalere412dc22018-01-12 16:28:24 -06001123 try:
1124 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1125 except(requests.exceptions.Timeout):
1126 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001127 return res.text
Justin Thalere412dc22018-01-12 16:28:24 -06001128 elif(args.powcmd == 'softoff'):
Justin Thaler22b1bb52018-03-15 13:31:32 -05001129 if checkFWactivation(host, args, session):
1130 return ("Chassis Power control disabled during firmware activation")
Justin Thalere412dc22018-01-12 16:28:24 -06001131 print("Attempting to Power off gracefully...:")
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001132 url="https://"+host+"/xyz/openbmc_project/state/host0/attr/RequestedHostTransition"
1133 httpHeader = {'Content-Type':'application/json'}
1134 data = '{"data":"xyz.openbmc_project.State.Host.Transition.Off"}'
Justin Thalere412dc22018-01-12 16:28:24 -06001135 try:
1136 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1137 except(requests.exceptions.Timeout):
1138 return(connectionErrHandler(args.json, "Timeout", None))
1139 return res.text
1140 elif(args.powcmd == 'hardoff'):
Justin Thaler22b1bb52018-03-15 13:31:32 -05001141 if checkFWactivation(host, args, session):
1142 return ("Chassis Power control disabled during firmware activation")
Justin Thalere412dc22018-01-12 16:28:24 -06001143 print("Attempting to Power off immediately...:")
1144 url="https://"+host+"/xyz/openbmc_project/state/chassis0/attr/RequestedPowerTransition"
1145 httpHeader = {'Content-Type':'application/json'}
1146 data = '{"data":"xyz.openbmc_project.State.Chassis.Transition.Off"}'
1147 try:
1148 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1149 except(requests.exceptions.Timeout):
1150 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001151 return res.text
1152 elif(args.powcmd == 'status'):
1153 url="https://"+host+"/xyz/openbmc_project/state/chassis0/attr/CurrentPowerState"
1154 httpHeader = {'Content-Type':'application/json'}
1155# print(url)
Justin Thalere412dc22018-01-12 16:28:24 -06001156 try:
1157 res = session.get(url, headers=httpHeader, verify=False, timeout=30)
1158 except(requests.exceptions.Timeout):
1159 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001160 chassisState = json.loads(res.text)['data'].split('.')[-1]
1161 url="https://"+host+"/xyz/openbmc_project/state/host0/attr/CurrentHostState"
Justin Thalere412dc22018-01-12 16:28:24 -06001162 try:
1163 res = session.get(url, headers=httpHeader, verify=False, timeout=30)
1164 except(requests.exceptions.Timeout):
1165 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001166 hostState = json.loads(res.text)['data'].split('.')[-1]
1167 url="https://"+host+"/xyz/openbmc_project/state/bmc0/attr/CurrentBMCState"
Justin Thalere412dc22018-01-12 16:28:24 -06001168 try:
1169 res = session.get(url, headers=httpHeader, verify=False, timeout=30)
1170 except(requests.exceptions.Timeout):
1171 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001172 bmcState = json.loads(res.text)['data'].split('.')[-1]
1173 if(args.json):
1174 outDict = {"Chassis Power State" : chassisState, "Host Power State" : hostState, "BMC Power State":bmcState}
1175 return json.dumps(outDict, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False)
1176 else:
1177 return "Chassis Power State: " +chassisState + "\nHost Power State: " + hostState + "\nBMC Power State: " + bmcState
1178 else:
1179 return "Invalid chassis power command"
1180
Justin Thalere412dc22018-01-12 16:28:24 -06001181
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001182def chassisIdent(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001183 """
1184 called by the chassis function. Controls the identify led of the chassis. Sets or gets the state
1185
1186 @param host: string, the hostname or IP address of the bmc
1187 @param args: contains additional arguments used by the fru sub command
1188 @param session: the active session to use
1189 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1190 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001191 if(args.identcmd == 'on'):
1192 print("Attempting to turn identify light on...:")
1193 url="https://"+host+"/xyz/openbmc_project/led/groups/enclosure_identify/attr/Asserted"
1194 httpHeader = {'Content-Type':'application/json',}
1195 data = '{"data":true}'
Justin Thalere412dc22018-01-12 16:28:24 -06001196 try:
1197 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1198 except(requests.exceptions.Timeout):
1199 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001200 return res.text
1201 elif(args.identcmd == 'off'):
1202 print("Attempting to turn identify light off...:")
1203 url="https://"+host+"/xyz/openbmc_project/led/groups/enclosure_identify/attr/Asserted"
1204 httpHeader = {'Content-Type':'application/json'}
1205 data = '{"data":false}'
Justin Thalere412dc22018-01-12 16:28:24 -06001206 try:
1207 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1208 except(requests.exceptions.Timeout):
1209 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001210 return res.text
1211 elif(args.identcmd == 'status'):
1212 url="https://"+host+"/xyz/openbmc_project/led/groups/enclosure_identify"
1213 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -06001214 try:
1215 res = session.get(url, headers=httpHeader, verify=False, timeout=30)
1216 except(requests.exceptions.Timeout):
1217 return(connectionErrHandler(args.json, "Timeout", None))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001218 status = json.loads(res.text)['data']
1219 if(args.json):
1220 return status
1221 else:
1222 if status['Asserted'] == 0:
1223 return "Identify light is off"
1224 else:
1225 return "Identify light is blinking"
1226 else:
1227 return "Invalid chassis identify command"
1228
Justin Thalere412dc22018-01-12 16:28:24 -06001229
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001230def chassis(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001231 """
1232 controls the different chassis commands
1233
1234 @param host: string, the hostname or IP address of the bmc
1235 @param args: contains additional arguments used by the fru sub command
1236 @param session: the active session to use
1237 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1238 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001239 if(hasattr(args, 'powcmd')):
1240 result = chassisPower(host,args,session)
1241 elif(hasattr(args, 'identcmd')):
1242 result = chassisIdent(host, args, session)
1243 else:
Justin Thaler22b1bb52018-03-15 13:31:32 -05001244 return "This feature is not yet implemented"
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001245 return result
Justin Thalere412dc22018-01-12 16:28:24 -06001246
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001247def bmcDumpRetrieve(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001248 """
1249 Downloads a dump file from the bmc
1250
1251 @param host: string, the hostname or IP address of the bmc
1252 @param args: contains additional arguments used by the collectServiceData sub command
1253 @param session: the active session to use
1254 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1255 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001256 httpHeader = {'Content-Type':'application/json'}
1257 dumpNum = args.dumpNum
1258 if (args.dumpSaveLoc is not None):
1259 saveLoc = args.dumpSaveLoc
1260 else:
Justin Thalercf1deae2018-05-25 19:35:21 -05001261 saveLoc = tempfile.gettempdir()
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001262 url ='https://'+host+'/download/dump/' + str(dumpNum)
1263 try:
Justin Thalere412dc22018-01-12 16:28:24 -06001264 r = session.get(url, headers=httpHeader, stream=True, verify=False, timeout=30)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001265 if (args.dumpSaveLoc is not None):
1266 if os.path.exists(saveLoc):
1267 if saveLoc[-1] != os.path.sep:
1268 saveLoc = saveLoc + os.path.sep
1269 filename = saveLoc + host+'-dump' + str(dumpNum) + '.tar.xz'
1270
1271 else:
1272 return 'Invalid save location specified'
1273 else:
Justin Thalercf1deae2018-05-25 19:35:21 -05001274 filename = tempfile.gettempdir()+os.sep + host+'-dump' + str(dumpNum) + '.tar.xz'
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001275
1276 with open(filename, 'wb') as f:
1277 for chunk in r.iter_content(chunk_size =1024):
1278 if chunk:
1279 f.write(chunk)
1280 return 'Saved as ' + filename
1281
1282 except(requests.exceptions.Timeout):
1283 return connectionErrHandler(args.json, "Timeout", None)
Justin Thalere412dc22018-01-12 16:28:24 -06001284
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001285 except(requests.exceptions.ConnectionError) as err:
1286 return connectionErrHandler(args.json, "ConnectionError", err)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001287
Justin Thalere412dc22018-01-12 16:28:24 -06001288def bmcDumpList(host, args, session):
1289 """
1290 Lists the number of dump files on the bmc
1291
1292 @param host: string, the hostname or IP address of the bmc
1293 @param args: contains additional arguments used by the collectServiceData sub command
1294 @param session: the active session to use
1295 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1296 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001297 httpHeader = {'Content-Type':'application/json'}
1298 url ='https://'+host+'/xyz/openbmc_project/dump/list'
1299 try:
1300 r = session.get(url, headers=httpHeader, verify=False, timeout=20)
1301 dumpList = json.loads(r.text)
1302 return r.text
1303 except(requests.exceptions.Timeout):
1304 return connectionErrHandler(args.json, "Timeout", None)
Justin Thalere412dc22018-01-12 16:28:24 -06001305
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001306 except(requests.exceptions.ConnectionError) as err:
Justin Thalere412dc22018-01-12 16:28:24 -06001307 return connectionErrHandler(args.json, "ConnectionError", err)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001308
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001309def bmcDumpDelete(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001310 """
1311 Deletes BMC dump files from the bmc
1312
1313 @param host: string, the hostname or IP address of the bmc
1314 @param args: contains additional arguments used by the collectServiceData sub command
1315 @param session: the active session to use
1316 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1317 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001318 httpHeader = {'Content-Type':'application/json'}
1319 dumpList = []
1320 successList = []
1321 failedList = []
1322 if args.dumpNum is not None:
1323 if isinstance(args.dumpNum, list):
1324 dumpList = args.dumpNum
1325 else:
1326 dumpList.append(args.dumpNum)
1327 for dumpNum in dumpList:
1328 url ='https://'+host+'/xyz/openbmc_project/dump/entry/'+str(dumpNum)+'/action/Delete'
1329 try:
1330 r = session.post(url, headers=httpHeader, json = {"data": []}, verify=False, timeout=30)
1331 if r.status_code == 200:
1332 successList.append(str(dumpNum))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001333 else:
1334 failedList.append(str(dumpNum))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001335 except(requests.exceptions.Timeout):
1336 return connectionErrHandler(args.json, "Timeout", None)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001337 except(requests.exceptions.ConnectionError) as err:
1338 return connectionErrHandler(args.json, "ConnectionError", err)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001339 output = "Successfully deleted dumps: " + ', '.join(successList)
1340 if(len(failedList)>0):
1341 output+= '\nFailed to delete dumps: ' + ', '.join(failedList)
1342 return output
1343 else:
1344 return 'You must specify an entry number to delete'
1345
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001346def bmcDumpDeleteAll(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001347 """
1348 Deletes All BMC dump files from the bmc
1349
1350 @param host: string, the hostname or IP address of the bmc
1351 @param args: contains additional arguments used by the collectServiceData sub command
1352 @param session: the active session to use
1353 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1354 """
1355 dumpResp = bmcDumpList(host, args, session)
1356 if 'FQPSPIN0000M' in dumpResp or 'FQPSPIN0001M'in dumpResp:
1357 return dumpResp
1358 dumpList = json.loads(dumpResp)['data']
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001359 d = vars(args)
1360 dumpNums = []
1361 for dump in dumpList:
1362 if '/xyz/openbmc_project/dump/internal/manager' not in dump:
1363 dumpNums.append(int(dump.strip().split('/')[-1]))
1364 d['dumpNum'] = dumpNums
1365
1366 return bmcDumpDelete(host, args, session)
1367
Justin Thalere412dc22018-01-12 16:28:24 -06001368
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001369def bmcDumpCreate(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001370 """
1371 Creates a bmc dump file
1372
1373 @param host: string, the hostname or IP address of the bmc
1374 @param args: contains additional arguments used by the collectServiceData sub command
1375 @param session: the active session to use
1376 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1377 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001378 httpHeader = {'Content-Type':'application/json'}
1379 url = 'https://'+host+'/xyz/openbmc_project/dump/action/CreateDump'
1380 try:
1381 r = session.post(url, headers=httpHeader, json = {"data": []}, verify=False, timeout=30)
1382 if('"message": "200 OK"' in r.text and not args.json):
1383 return ('Dump successfully created')
1384 else:
1385 return ('Failed to create dump')
1386 except(requests.exceptions.Timeout):
1387 return connectionErrHandler(args.json, "Timeout", None)
1388 except(requests.exceptions.ConnectionError) as err:
1389 return connectionErrHandler(args.json, "ConnectionError", err)
1390
1391
1392
Justin Thalere412dc22018-01-12 16:28:24 -06001393
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001394def collectServiceData(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001395 """
1396 Collects all data needed for service from the BMC
1397
1398 @param host: string, the hostname or IP address of the bmc
1399 @param args: contains additional arguments used by the collectServiceData sub command
1400 @param session: the active session to use
1401 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1402 """
Justin Thaler22b1bb52018-03-15 13:31:32 -05001403
1404 global toolVersion
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001405 #create a bmc dump
1406 dumpcount = len(json.loads(bmcDumpList(host, args, session))['data'])
1407 try:
1408 dumpcreated = bmcDumpCreate(host, args, session)
1409 except Exception as e:
1410 print('failed to create a bmc dump')
1411
1412
1413 #Collect Inventory
1414 try:
1415 args.silent = True
Justin Thalercf1deae2018-05-25 19:35:21 -05001416 myDir = tempfile.gettempdir()+os.sep + host + "--" + datetime.datetime.now().strftime("%Y-%m-%d_%H.%M.%S")
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001417 os.makedirs(myDir)
1418 filelist = []
1419 frulist = fruPrint(host, args, session)
1420 with open(myDir +'/inventory.txt', 'w') as f:
1421 f.write(frulist)
1422 print("Inventory collected and stored in " + myDir + "/inventory.txt")
1423 filelist.append(myDir+'/inventory.txt')
1424 except Exception as e:
1425 print("Failed to collect inventory")
1426
1427 #Read all the sensor and OCC status
1428 try:
1429 sensorReadings = sensor(host, args, session)
1430 with open(myDir +'/sensorReadings.txt', 'w') as f:
1431 f.write(sensorReadings)
1432 print("Sensor readings collected and stored in " +myDir + "/sensorReadings.txt")
1433 filelist.append(myDir+'/sensorReadings.txt')
1434 except Exception as e:
1435 print("Failed to collect sensor readings")
1436
1437 #Collect all of the LEDs status
1438 try:
1439 url="https://"+host+"/xyz/openbmc_project/led/enumerate"
1440 httpHeader = {'Content-Type':'application/json'}
1441 leds = session.get(url, headers=httpHeader, verify=False, timeout=20)
1442 with open(myDir +'/ledStatus.txt', 'w') as f:
1443 f.write(leds.text)
1444 print("System LED status collected and stored in "+myDir +"/ledStatus.txt")
1445 filelist.append(myDir+'/ledStatus.txt')
1446 except Exception as e:
1447 print("Failed to collect LED status")
1448
1449 #Collect the bmc logs
1450 try:
1451 sels = selPrint(host,args,session)
1452 with open(myDir +'/SELshortlist.txt', 'w') as f:
1453 f.write(str(sels))
1454 print("sel short list collected and stored in "+myDir +"/SELshortlist.txt")
1455 filelist.append(myDir+'/SELshortlist.txt')
1456 time.sleep(2)
1457
1458 d = vars(args)
1459 d['json'] = True
1460 d['fullSel'] = True
1461 parsedfullsels = json.loads(selPrint(host, args, session))
1462 d['fullEsel'] = True
1463 sortedSELs = sortSELs(parsedfullsels)
1464 with open(myDir +'/parsedSELs.txt', 'w') as f:
1465 for log in sortedSELs[0]:
1466 esel = ""
1467 parsedfullsels[sortedSELs[1][str(log)]]['timestamp'] = datetime.datetime.fromtimestamp(int(parsedfullsels[sortedSELs[1][str(log)]]['timestamp']/1000)).strftime("%Y-%m-%d %H:%M:%S")
1468 if ('raweSEL' in parsedfullsels[sortedSELs[1][str(log)]] and args.devdebug):
1469 esel = parsedfullsels[sortedSELs[1][str(log)]]['raweSEL']
1470 del parsedfullsels[sortedSELs[1][str(log)]]['raweSEL']
1471 f.write(json.dumps(parsedfullsels[sortedSELs[1][str(log)]],sort_keys=True, indent=4, separators=(',', ': ')))
1472 if(args.devdebug and esel != ""):
1473 f.write(parseESEL(args, esel))
1474 print("fully parsed sels collected and stored in "+myDir +"/parsedSELs.txt")
1475 filelist.append(myDir+'/parsedSELs.txt')
1476 except Exception as e:
1477 print("Failed to collect system event logs")
1478 print(e)
1479
1480 #collect RAW bmc enumeration
1481 try:
1482 url="https://"+host+"/xyz/openbmc_project/enumerate"
1483 print("Attempting to get a full BMC enumeration")
1484 fullDump = session.get(url, headers=httpHeader, verify=False, timeout=120)
1485 with open(myDir +'/bmcFullRaw.txt', 'w') as f:
1486 f.write(fullDump.text)
1487 print("RAW BMC data collected and saved into "+myDir +"/bmcFullRaw.txt")
1488 filelist.append(myDir+'/bmcFullRaw.txt')
1489 except Exception as e:
1490 print("Failed to collect bmc full enumeration")
1491
1492 #collect the dump files
1493 waitingForNewDump = True
1494 count = 0;
1495 while(waitingForNewDump):
1496 dumpList = json.loads(bmcDumpList(host, args, session))['data']
1497 if len(dumpList) > dumpcount:
1498 waitingForNewDump = False
1499 break;
1500 elif(count>30):
1501 print("Timed out waiting for bmc to make a new dump file. Dump space may be full.")
1502 break;
1503 else:
1504 time.sleep(2)
1505 count += 1
1506 try:
1507 print('Collecting bmc dump files')
1508 d['dumpSaveLoc'] = myDir
1509 dumpList = json.loads(bmcDumpList(host, args, session))['data']
1510 for dump in dumpList:
1511 if '/xyz/openbmc_project/dump/internal/manager' not in dump:
1512 d['dumpNum'] = int(dump.strip().split('/')[-1])
1513 print('retrieving dump file ' + str(d['dumpNum']))
1514 filename = bmcDumpRetrieve(host, args, session).split('Saved as ')[-1]
1515 filelist.append(filename)
1516 time.sleep(2)
1517 except Exception as e:
1518 print("Failed to collect bmc dump files")
1519 print(e)
1520
1521 #create the zip file
1522 try:
Justin Thalercf1deae2018-05-25 19:35:21 -05001523 filename = myDir.split(tempfile.gettempdir()+os.sep)[-1] + "_" + toolVersion + '_openbmc.zip'
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001524 zf = zipfile.ZipFile(myDir+'/' + filename, 'w')
1525 for myfile in filelist:
1526 zf.write(myfile, os.path.basename(myfile))
1527 zf.close()
1528 except Exception as e:
1529 print("Failed to create zip file with collected information")
1530 return "data collection complete"
1531
Justin Thalere412dc22018-01-12 16:28:24 -06001532
1533def healthCheck(host, args, session):
1534 """
1535 runs a health check on the platform
1536
1537 @param host: string, the hostname or IP address of the bmc
1538 @param args: contains additional arguments used by the bmc sub command
1539 @param session: the active session to use
1540 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1541 """
1542 #check fru status and get as json to easily work through
1543 d = vars(args)
1544 useJson = d['json']
1545 d['json'] = True
1546 d['verbose']= False
1547
1548 frus = json.loads(fruStatus(host, args, session))
1549
1550 hwStatus= "OK"
1551 performanceStatus = "OK"
1552 for key in frus:
1553 if frus[key]["Functional"] == "No" and frus[key]["Present"] == "Yes":
1554 hwStatus= "Degraded"
Justin Thalerfb9c81c2018-07-16 11:14:37 -05001555 if("power_supply" in key or "powersupply" in key):
1556 gpuCount =0
1557 for comp in frus:
Justin Thalere412dc22018-01-12 16:28:24 -06001558 if "gv100card" in comp:
1559 gpuCount +=1
1560 if gpuCount > 4:
1561 hwStatus = "Critical"
1562 performanceStatus="Degraded"
1563 break;
1564 elif("fan" in key):
1565 hwStatus = "Degraded"
1566 else:
1567 performanceStatus = "Degraded"
1568 if useJson:
1569 output = {"Hardware Status": hwStatus, "Performance": performanceStatus}
1570 output = json.dumps(output, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False)
1571 else:
1572 output = ("Hardware Status: " + hwStatus +
1573 "\nPerformance: " +performanceStatus )
1574
1575
1576 #SW407886: Clear the duplicate entries
1577 #collect the dups
1578 d['devdebug'] = False
1579 sels = json.loads(selPrint(host, args, session))
1580 logNums2Clr = []
1581 oldestLogNum={"logNum": "bogus" ,"key" : ""}
1582 count = 0
1583 if sels['numAlerts'] > 0:
1584 for key in sels:
1585 if "numAlerts" in key:
1586 continue
1587 try:
1588 if "slave@00:00/00:00:00:06/sbefifo1-dev0/occ1-dev0" in sels[key]['Message']:
1589 count += 1
1590 if count > 1:
1591 #preserve first occurrence
1592 if sels[key]['timestamp'] < sels[oldestLogNum['key']]['timestamp']:
1593 oldestLogNum['key']=key
1594 oldestLogNum['logNum'] = sels[key]['logNum']
1595 else:
1596 oldestLogNum['key']=key
1597 oldestLogNum['logNum'] = sels[key]['logNum']
1598 logNums2Clr.append(sels[key]['logNum'])
1599 except KeyError:
1600 continue
1601 if(count >0):
1602 logNums2Clr.remove(oldestLogNum['logNum'])
1603 #delete the dups
1604 if count >1:
1605 httpHeader = {'Content-Type':'application/json'}
1606 data = "{\"data\": [] }"
1607 for logNum in logNums2Clr:
1608 url = "https://"+ host+ "/xyz/openbmc_project/logging/entry/"+logNum+"/action/Delete"
1609 try:
1610 session.post(url, headers=httpHeader, data=data, verify=False, timeout=30)
1611 except(requests.exceptions.Timeout):
1612 deleteFailed = True
1613 except(requests.exceptions.ConnectionError) as err:
1614 deleteFailed = True
1615 #End of defect resolve code
1616 d['json'] = useJson
1617 return output
1618
1619
1620
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001621def bmc(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001622 """
1623 handles various bmc level commands, currently bmc rebooting
1624
1625 @param host: string, the hostname or IP address of the bmc
1626 @param args: contains additional arguments used by the bmc sub command
1627 @param session: the active session to use
1628 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1629 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001630 if(args.type is not None):
1631 return bmcReset(host, args, session)
Justin Thalere412dc22018-01-12 16:28:24 -06001632 if(args.info):
1633 return "Not implemented at this time"
1634
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001635
Justin Thalere412dc22018-01-12 16:28:24 -06001636
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001637def bmcReset(host, args, session):
Justin Thalere412dc22018-01-12 16:28:24 -06001638 """
1639 controls resetting the bmc. warm reset reboots the bmc, cold reset removes the configuration and reboots.
1640
1641 @param host: string, the hostname or IP address of the bmc
1642 @param args: contains additional arguments used by the bmcReset sub command
1643 @param session: the active session to use
1644 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
1645 """
Justin Thaler22b1bb52018-03-15 13:31:32 -05001646 if checkFWactivation(host, args, session):
1647 return ("BMC reset control disabled during firmware activation")
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001648 if(args.type == "warm"):
1649 print("\nAttempting to reboot the BMC...:")
1650 url="https://"+host+"/xyz/openbmc_project/state/bmc0/attr/RequestedBMCTransition"
1651 httpHeader = {'Content-Type':'application/json'}
Justin Thalere412dc22018-01-12 16:28:24 -06001652 data = '{"data":"xyz.openbmc_project.State.BMC.Transition.Reboot"}'
1653 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=20)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001654 return res.text
1655 elif(args.type =="cold"):
Justin Thalere412dc22018-01-12 16:28:24 -06001656 print("\nAttempting to reboot the BMC...:")
1657 url="https://"+host+"/xyz/openbmc_project/state/bmc0/attr/RequestedBMCTransition"
1658 httpHeader = {'Content-Type':'application/json'}
1659 data = '{"data":"xyz.openbmc_project.State.BMC.Transition.Reboot"}'
1660 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=20)
1661 return res.text
Justin Thalerf9aee3e2017-12-05 12:11:09 -06001662 else:
1663 return "invalid command"
Justin Thalere412dc22018-01-12 16:28:24 -06001664
1665def gardClear(host, args, session):
1666 """
1667 clears the gard records from the bmc
1668
1669 @param host: string, the hostname or IP address of the bmc
1670 @param args: contains additional arguments used by the gardClear sub command
1671 @param session: the active session to use
1672 """
1673 url="https://"+host+"/org/open_power/control/gard/action/Reset"
1674 httpHeader = {'Content-Type':'application/json'}
1675 data = '{"data":[]}'
1676 try:
1677
1678 res = session.post(url, headers=httpHeader, data=data, verify=False, timeout=30)
1679 if res.status_code == 404:
1680 return "Command not supported by this firmware version"
1681 else:
1682 return res.text
1683 except(requests.exceptions.Timeout):
1684 return connectionErrHandler(args.json, "Timeout", None)
1685 except(requests.exceptions.ConnectionError) as err:
1686 return connectionErrHandler(args.json, "ConnectionError", err)
1687
1688def activateFWImage(host, args, session):
1689 """
1690 activates a firmware image on the bmc
1691
1692 @param host: string, the hostname or IP address of the bmc
1693 @param args: contains additional arguments used by the fwflash sub command
1694 @param session: the active session to use
1695 @param fwID: the unique ID of the fw image to activate
1696 """
1697 fwID = args.imageID
1698
1699 #determine the existing versions
1700 httpHeader = {'Content-Type':'application/json'}
1701 url="https://"+host+"/xyz/openbmc_project/software/enumerate"
1702 try:
1703 resp = session.get(url, headers=httpHeader, verify=False, timeout=30)
1704 except(requests.exceptions.Timeout):
1705 return connectionErrHandler(args.json, "Timeout", None)
1706 except(requests.exceptions.ConnectionError) as err:
1707 return connectionErrHandler(args.json, "ConnectionError", err)
1708 existingSoftware = json.loads(resp.text)['data']
1709 altVersionID = ''
1710 versionType = ''
1711 imageKey = '/xyz/openbmc_project/software/'+fwID
1712 if imageKey in existingSoftware:
1713 versionType = existingSoftware[imageKey]['Purpose']
1714 for key in existingSoftware:
1715 if imageKey == key:
1716 continue
1717 if 'Purpose' in existingSoftware[key]:
1718 if versionType == existingSoftware[key]['Purpose']:
1719 altVersionID = key.split('/')[-1]
1720
1721
1722
1723
1724 url="https://"+host+"/xyz/openbmc_project/software/"+ fwID + "/attr/Priority"
1725 url1="https://"+host+"/xyz/openbmc_project/software/"+ altVersionID + "/attr/Priority"
1726 data = "{\"data\": 0}"
1727 data1 = "{\"data\": 1 }"
1728 try:
1729 resp = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1730 resp1 = session.put(url1, headers=httpHeader, data=data1, verify=False, timeout=30)
1731 except(requests.exceptions.Timeout):
1732 return connectionErrHandler(args.json, "Timeout", None)
1733 except(requests.exceptions.ConnectionError) as err:
1734 return connectionErrHandler(args.json, "ConnectionError", err)
1735 if(not args.json):
1736 if resp.status_code == 200 and resp1.status_code == 200:
Justin Thaler22b1bb52018-03-15 13:31:32 -05001737 return 'Firmware flash and activation completed. Please reboot the bmc and then boot the host OS for the changes to take effect. '
Justin Thalere412dc22018-01-12 16:28:24 -06001738 else:
1739 return "Firmware activation failed."
1740 else:
1741 return resp.text + resp1.text
Justin Thaler22b1bb52018-03-15 13:31:32 -05001742
1743def activateStatus(host, args, session):
1744 if checkFWactivation(host, args, session):
1745 return("Firmware is currently being activated. Do not reboot the BMC or start the Host OS")
1746 else:
1747 return("No firmware activations are pending")
1748
1749def extractFWimage(path, imageType):
1750 """
1751 extracts the bmc image and returns information about the package
1752
1753 @param path: the path and file name of the firmware image
1754 @param imageType: The type of image the user is trying to flash. Host or BMC
1755 @return: the image id associated with the package. returns an empty string on error.
1756 """
1757 f = tempfile.TemporaryFile()
1758 tmpDir = tempfile.gettempdir()
1759 newImageID = ""
1760 if os.path.exists(path):
1761 try:
1762 imageFile = tarfile.open(path,'r')
1763 contents = imageFile.getmembers()
1764 for tf in contents:
1765 if 'MANIFEST' in tf.name:
1766 imageFile.extract(tf.name, path=tmpDir)
1767 with open(tempfile.gettempdir() +os.sep+ tf.name, 'r') as imageInfo:
1768 for line in imageInfo:
1769 if 'purpose' in line:
1770 purpose = line.split('=')[1]
1771 if imageType not in purpose.split('.')[-1]:
1772 print('The specified image is not for ' + imageType)
1773 print('Please try again with the image for ' + imageType)
1774 return ""
1775 if 'version' == line.split('=')[0]:
1776 version = line.split('=')[1].strip().encode('utf-8')
1777 m = hashlib.sha512()
1778 m.update(version)
1779 newImageID = m.hexdigest()[:8]
1780 break
1781 try:
1782 os.remove(tempfile.gettempdir() +os.sep+ tf.name)
1783 except OSError:
1784 pass
1785 return newImageID
1786 except tarfile.ExtractError as e:
1787 print('Unable to extract information from the firmware file.')
1788 print('Ensure you have write access to the directory: ' + tmpDir)
1789 return newImageID
1790 except tarfile.TarError as e:
1791 print('This is not a valid firmware file.')
1792 return newImageID
1793 print("This is not a valid firmware file.")
1794 return newImageID
1795 else:
1796 print('The filename and path provided are not valid.')
1797 return newImageID
1798
1799def getAllFWImageIDs(fwInvDict):
1800 """
1801 gets a list of all the firmware image IDs
1802
1803 @param fwInvDict: the dictionary to search for FW image IDs
1804 @return: list containing string representation of the found image ids
1805 """
1806 idList = []
1807 for key in fwInvDict:
1808 if 'Version' in fwInvDict[key]:
1809 idList.append(key.split('/')[-1])
1810 return idList
1811
Justin Thalere412dc22018-01-12 16:28:24 -06001812def fwFlash(host, args, session):
1813 """
1814 updates the bmc firmware and pnor firmware
1815
1816 @param host: string, the hostname or IP address of the bmc
1817 @param args: contains additional arguments used by the fwflash sub command
1818 @param session: the active session to use
1819 """
Justin Thaler22b1bb52018-03-15 13:31:32 -05001820 d = vars(args)
Justin Thalere412dc22018-01-12 16:28:24 -06001821 if(args.type == 'bmc'):
1822 purp = 'BMC'
1823 else:
1824 purp = 'Host'
Justin Thaler22b1bb52018-03-15 13:31:32 -05001825
1826 #check power state of the machine. No concurrent FW updates allowed
1827 d['powcmd'] = 'status'
1828 powerstate = chassisPower(host, args, session)
1829 if 'Chassis Power State: On' in powerstate:
1830 return("Aborting firmware update. Host is powered on. Please turn off the host and try again.")
1831
1832 #determine the existing images on the bmc
Justin Thalere412dc22018-01-12 16:28:24 -06001833 httpHeader = {'Content-Type':'application/json'}
1834 url="https://"+host+"/xyz/openbmc_project/software/enumerate"
1835 try:
1836 resp = session.get(url, headers=httpHeader, verify=False, timeout=30)
1837 except(requests.exceptions.Timeout):
1838 return connectionErrHandler(args.json, "Timeout", None)
1839 except(requests.exceptions.ConnectionError) as err:
1840 return connectionErrHandler(args.json, "ConnectionError", err)
1841 oldsoftware = json.loads(resp.text)['data']
1842
Justin Thaler22b1bb52018-03-15 13:31:32 -05001843 #Extract the tar and get information from the manifest file
1844 newversionID = extractFWimage(args.fileloc, purp)
1845 if newversionID == "":
1846 return "Unable to verify FW image."
1847
1848
1849 #check if the new image is already on the bmc
1850 if newversionID not in getAllFWImageIDs(oldsoftware):
1851
1852 #upload the file
1853 httpHeader = {'Content-Type':'application/octet-stream'}
1854 url="https://"+host+"/upload/image"
1855 data=open(args.fileloc,'rb').read()
1856 print("Uploading file to BMC")
Justin Thalere412dc22018-01-12 16:28:24 -06001857 try:
Justin Thaler22b1bb52018-03-15 13:31:32 -05001858 resp = session.post(url, headers=httpHeader, data=data, verify=False)
Justin Thalere412dc22018-01-12 16:28:24 -06001859 except(requests.exceptions.Timeout):
1860 return connectionErrHandler(args.json, "Timeout", None)
1861 except(requests.exceptions.ConnectionError) as err:
1862 return connectionErrHandler(args.json, "ConnectionError", err)
Justin Thaler22b1bb52018-03-15 13:31:32 -05001863 if resp.status_code != 200:
1864 return "Failed to upload the file to the bmc"
Justin Thalere412dc22018-01-12 16:28:24 -06001865 else:
Justin Thaler22b1bb52018-03-15 13:31:32 -05001866 print("Upload complete.")
1867
1868 #verify bmc processed the image
1869 software ={}
1870 for i in range(0, 5):
1871 httpHeader = {'Content-Type':'application/json'}
1872 url="https://"+host+"/xyz/openbmc_project/software/enumerate"
1873 try:
1874 resp = session.get(url, headers=httpHeader, verify=False, timeout=30)
1875 except(requests.exceptions.Timeout):
1876 return connectionErrHandler(args.json, "Timeout", None)
1877 except(requests.exceptions.ConnectionError) as err:
1878 return connectionErrHandler(args.json, "ConnectionError", err)
1879 software = json.loads(resp.text)['data']
1880 #check if bmc is done processing the new image
1881 if (newversionID in getAllFWImageIDs(software)):
Justin Thalere412dc22018-01-12 16:28:24 -06001882 break
Justin Thaler22b1bb52018-03-15 13:31:32 -05001883 else:
1884 time.sleep(15)
1885
1886 #activate the new image
1887 print("Activating new image: "+newversionID)
1888 url="https://"+host+"/xyz/openbmc_project/software/"+ newversionID + "/attr/RequestedActivation"
1889 data = '{"data":"xyz.openbmc_project.Software.Activation.RequestedActivations.Active"}'
1890 try:
1891 resp = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
1892 except(requests.exceptions.Timeout):
1893 return connectionErrHandler(args.json, "Timeout", None)
1894 except(requests.exceptions.ConnectionError) as err:
1895 return connectionErrHandler(args.json, "ConnectionError", err)
1896
1897 #wait for the activation to complete, timeout after ~1 hour
1898 i=0
1899 while i < 360:
1900 url="https://"+host+"/xyz/openbmc_project/software/"+ newversionID
1901 data = '{"data":"xyz.openbmc_project.Software.Activation.RequestedActivations.Active"}'
1902 try:
1903 resp = session.get(url, headers=httpHeader, verify=False, timeout=30)
1904 except(requests.exceptions.Timeout):
1905 return connectionErrHandler(args.json, "Timeout", None)
1906 except(requests.exceptions.ConnectionError) as err:
1907 return connectionErrHandler(args.json, "ConnectionError", err)
1908 fwInfo = json.loads(resp.text)['data']
1909 if 'Activating' not in fwInfo['Activation'] and 'Activating' not in fwInfo['RequestedActivation']:
1910 print('')
1911 break
1912 else:
1913 sys.stdout.write('.')
1914 sys.stdout.flush()
1915 time.sleep(10) #check every 10 seconds
1916 return "Firmware flash and activation completed. Please reboot the bmc and then boot the host OS for the changes to take effect. "
1917 else:
1918 print("This image has been found on the bmc. Activating image: " + newversionID)
1919
1920 d['imageID'] = newversionID
1921 return activateFWImage(host, args, session)
Justin Thalere412dc22018-01-12 16:28:24 -06001922
Justin Thaler3d71d402018-07-24 14:35:39 -05001923def getFWInventoryAttributes(rawFWInvItem, ID):
1924 """
1925 gets and lists all of the firmware in the system.
1926
1927 @return: returns a dictionary containing the image attributes
1928 """
1929 reqActivation = rawFWInvItem["RequestedActivation"].split('.')[-1]
1930 pendingActivation = ""
1931 if reqActivation == "None":
1932 pendingActivation = "No"
1933 else:
1934 pendingActivation = "Yes"
1935 firmwareAttr = {ID: {
1936 "Purpose": rawFWInvItem["Purpose"].split('.')[-1],
1937 "Version": rawFWInvItem["Version"],
1938 "RequestedActivation": pendingActivation,
1939 "ID": ID}}
1940
1941 if "ExtendedVersion" in rawFWInvItem:
1942 firmwareAttr[ID]['ExtendedVersion'] = rawFWInvItem['ExtendedVersion'].split(',')
1943 else:
1944 firmwareAttr[ID]['ExtendedVersion'] = ""
1945 return firmwareAttr
1946
1947def parseFWdata(firmwareDict):
1948 """
1949 creates a dictionary with parsed firmware data
1950
1951 @return: returns a dictionary containing the image attributes
1952 """
1953 firmwareInfoDict = {"Functional": {}, "Activated":{}, "NeedsActivated":{}}
1954 for key in firmwareDict['data']:
1955 #check for valid endpoint
1956 if "Purpose" in firmwareDict['data'][key]:
1957 id = key.split('/')[-1]
1958 if firmwareDict['data'][key]['Activation'].split('.')[-1] == "Active":
1959 fwActivated = True
1960 else:
1961 fwActivated = False
1962 if firmwareDict['data'][key]['Priority'] == 0:
1963 firmwareInfoDict['Functional'].update(getFWInventoryAttributes(firmwareDict['data'][key], id))
1964 elif firmwareDict['data'][key]['Priority'] >= 0 and fwActivated:
1965 firmwareInfoDict['Activated'].update(getFWInventoryAttributes(firmwareDict['data'][key], id))
1966 else:
1967 firmwareInfoDict['Activated'].update(getFWInventoryAttributes(firmwareDict['data'][key], id))
1968 emptySections = []
1969 for key in firmwareInfoDict:
1970 if len(firmwareInfoDict[key])<=0:
1971 emptySections.append(key)
1972 for key in emptySections:
1973 del firmwareInfoDict[key]
1974 return firmwareInfoDict
1975
1976def displayFWInvenory(firmwareInfoDict, args):
1977 """
1978 gets and lists all of the firmware in the system.
1979
1980 @return: returns a string containing all of the firmware information
1981 """
1982 output = ""
1983 if not args.json:
1984 for key in firmwareInfoDict:
1985 for subkey in firmwareInfoDict[key]:
1986 firmwareInfoDict[key][subkey]['ExtendedVersion'] = str(firmwareInfoDict[key][subkey]['ExtendedVersion'])
1987 if not args.verbose:
1988 output = "---Running Images---\n"
1989 colNames = ["Purpose", "Version", "ID"]
1990 keylist = ["Purpose", "Version", "ID"]
1991 output += tableDisplay(keylist, colNames, firmwareInfoDict["Functional"])
1992 if "Activated" in firmwareInfoDict:
1993 output += "\n---Available Images---\n"
1994 output += tableDisplay(keylist, colNames, firmwareInfoDict["Activated"])
1995 if "NeedsActivated" in firmwareInfoDict:
1996 output += "\n---Needs Activated Images---\n"
1997 output += tableDisplay(keylist, colNames, firmwareInfoDict["NeedsActivated"])
1998
1999 else:
2000 output = "---Running Images---\n"
2001 colNames = ["Purpose", "Version", "ID", "Pending Activation", "Extended Version"]
2002 keylist = ["Purpose", "Version", "ID", "RequestedActivation", "ExtendedVersion"]
2003 output += tableDisplay(keylist, colNames, firmwareInfoDict["Functional"])
2004 if "Activated" in firmwareInfoDict:
2005 output += "\n---Available Images---\n"
2006 output += tableDisplay(keylist, colNames, firmwareInfoDict["Activated"])
2007 if "NeedsActivated" in firmwareInfoDict:
2008 output += "\n---Needs Activated Images---\n"
2009 output += tableDisplay(keylist, colNames, firmwareInfoDict["NeedsActivated"])
2010 return output
2011 else:
2012 return str(json.dumps(firmwareInfoDict, sort_keys=True, indent=4, separators=(',', ': '), ensure_ascii=False))
2013
2014def firmwareList(host, args, session):
2015 """
2016 gets and lists all of the firmware in the system.
2017
2018 @return: returns a string containing all of the firmware information
2019 """
2020 httpHeader = {'Content-Type':'application/json'}
2021 url="https://{hostname}/xyz/openbmc_project/software/enumerate".format(hostname=host)
2022 try:
2023 res = session.get(url, headers=httpHeader, verify=False, timeout=40)
2024 except(requests.exceptions.Timeout):
2025 return(connectionErrHandler(args.json, "Timeout", None))
2026 firmwareDict = json.loads(res.text)
2027
2028 #sort the received information
2029 firmwareInfoDict = parseFWdata(firmwareDict)
2030
2031 #display the information
2032 return displayFWInvenory(firmwareInfoDict, args)
2033
Deepak Kodihalli22d4df02018-09-18 06:52:43 -05002034
2035def restLogging(host, args, session):
2036 """
2037 Called by the logging function. Turns REST API logging on/off.
2038
2039 @param host: string, the hostname or IP address of the bmc
2040 @param args: contains additional arguments used by the logging sub command
2041 @param session: the active session to use
2042 @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
2043 """
2044
2045 url="https://"+host+"/xyz/openbmc_project/logging/rest_api_logs/attr/Enabled"
2046 httpHeader = {'Content-Type':'application/json'}
2047
2048 if(args.rest_logging == 'on'):
2049 data = '{"data": 1}'
2050 elif(args.rest_logging == 'off'):
2051 data = '{"data": 0}'
2052 else:
2053 return "Invalid logging rest_api command"
2054
2055 try:
2056 res = session.put(url, headers=httpHeader, data=data, verify=False, timeout=30)
2057 except(requests.exceptions.Timeout):
2058 return(connectionErrHandler(args.json, "Timeout", None))
2059 return res.text
2060
2061
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002062def createCommandParser():
Justin Thalere412dc22018-01-12 16:28:24 -06002063 """
2064 creates the parser for the command line along with help for each command and subcommand
2065
2066 @return: returns the parser for the command line
2067 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002068 parser = argparse.ArgumentParser(description='Process arguments')
Justin Thalere412dc22018-01-12 16:28:24 -06002069 parser.add_argument("-H", "--host", help='A hostname or IP for the BMC')
2070 parser.add_argument("-U", "--user", help='The username to login with')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002071 group = parser.add_mutually_exclusive_group()
2072 group.add_argument("-A", "--askpw", action='store_true', help='prompt for password')
2073 group.add_argument("-P", "--PW", help='Provide the password in-line')
2074 parser.add_argument('-j', '--json', action='store_true', help='output json data only')
2075 parser.add_argument('-t', '--policyTableLoc', help='The location of the policy table to parse alerts')
2076 parser.add_argument('-c', '--CerFormat', action='store_true', help=argparse.SUPPRESS)
2077 parser.add_argument('-T', '--procTime', action='store_true', help= argparse.SUPPRESS)
Justin Thalere412dc22018-01-12 16:28:24 -06002078 parser.add_argument('-V', '--version', action='store_true', help='Display the version number of the openbmctool')
2079 subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002080
2081 #fru command
2082 parser_inv = subparsers.add_parser("fru", help='Work with platform inventory')
Justin Thalere412dc22018-01-12 16:28:24 -06002083 inv_subparser = parser_inv.add_subparsers(title='subcommands', description='valid inventory actions', help="valid inventory actions", dest='command')
Justin Thaler53bf2f12018-07-16 14:05:32 -05002084 inv_subparser.required = True
2085 #fru print
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002086 inv_print = inv_subparser.add_parser("print", help="prints out a list of all FRUs")
2087 inv_print.set_defaults(func=fruPrint)
2088 #fru list [0....n]
2089 inv_list = inv_subparser.add_parser("list", help="print out details on selected FRUs. Specifying no items will list the entire inventory")
2090 inv_list.add_argument('items', nargs='?', help="print out details on selected FRUs. Specifying no items will list the entire inventory")
2091 inv_list.set_defaults(func=fruList)
2092 #fru status
2093 inv_status = inv_subparser.add_parser("status", help="prints out the status of all FRUs")
Justin Thalere412dc22018-01-12 16:28:24 -06002094 inv_status.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002095 inv_status.set_defaults(func=fruStatus)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002096
2097 #sensors command
2098 parser_sens = subparsers.add_parser("sensors", help="Work with platform sensors")
Justin Thalere412dc22018-01-12 16:28:24 -06002099 sens_subparser=parser_sens.add_subparsers(title='subcommands', description='valid sensor actions', help='valid sensor actions', dest='command')
Justin Thaler53bf2f12018-07-16 14:05:32 -05002100 sens_subparser.required = True
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002101 #sensor print
2102 sens_print= sens_subparser.add_parser('print', help="prints out a list of all Sensors.")
2103 sens_print.set_defaults(func=sensor)
2104 #sensor list[0...n]
2105 sens_list=sens_subparser.add_parser("list", help="Lists all Sensors in the platform. Specify a sensor for full details. ")
2106 sens_list.add_argument("sensNum", nargs='?', help="The Sensor number to get full details on" )
2107 sens_list.set_defaults(func=sensor)
2108
2109
2110 #sel command
2111 parser_sel = subparsers.add_parser("sel", help="Work with platform alerts")
Justin Thalere412dc22018-01-12 16:28:24 -06002112 sel_subparser = parser_sel.add_subparsers(title='subcommands', description='valid SEL actions', help = 'valid SEL actions', dest='command')
Justin Thaler53bf2f12018-07-16 14:05:32 -05002113 sel_subparser.required = True
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002114 #sel print
2115 sel_print = sel_subparser.add_parser("print", help="prints out a list of all sels in a condensed list")
2116 sel_print.add_argument('-d', '--devdebug', action='store_true', help=argparse.SUPPRESS)
2117 sel_print.add_argument('-v', '--verbose', action='store_true', help="Changes the output to being very verbose")
2118 sel_print.add_argument('-f', '--fileloc', help='Parse a file instead of the BMC output')
2119 sel_print.set_defaults(func=selPrint)
Justin Thaler22b1bb52018-03-15 13:31:32 -05002120
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002121 #sel list
2122 sel_list = sel_subparser.add_parser("list", help="Lists all SELs in the platform. Specifying a specific number will pull all the details for that individual SEL")
2123 sel_list.add_argument("selNum", nargs='?', type=int, help="The SEL entry to get details on")
2124 sel_list.set_defaults(func=selList)
2125
2126 sel_get = sel_subparser.add_parser("get", help="Gets the verbose details of a specified SEL entry")
2127 sel_get.add_argument('selNum', type=int, help="the number of the SEL entry to get")
2128 sel_get.set_defaults(func=selList)
2129
2130 sel_clear = sel_subparser.add_parser("clear", help="Clears all entries from the SEL")
2131 sel_clear.set_defaults(func=selClear)
2132
2133 sel_setResolved = sel_subparser.add_parser("resolve", help="Sets the sel entry to resolved")
Justin Thalere412dc22018-01-12 16:28:24 -06002134 sel_setResolved.add_argument('-n', '--selNum', type=int, help="the number of the SEL entry to resolve")
2135 sel_ResolveAll_sub = sel_setResolved.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
2136 sel_ResolveAll = sel_ResolveAll_sub.add_parser('all', help='Resolve all SEL entries')
2137 sel_ResolveAll.set_defaults(func=selResolveAll)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002138 sel_setResolved.set_defaults(func=selSetResolved)
2139
2140 parser_chassis = subparsers.add_parser("chassis", help="Work with chassis power and status")
Justin Thalere412dc22018-01-12 16:28:24 -06002141 chas_sub = parser_chassis.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002142
2143 parser_chassis.add_argument('status', action='store_true', help='Returns the current status of the platform')
2144 parser_chassis.set_defaults(func=chassis)
2145
2146 parser_chasPower = chas_sub.add_parser("power", help="Turn the chassis on or off, check the power state")
Justin Thalere412dc22018-01-12 16:28:24 -06002147 parser_chasPower.add_argument('powcmd', choices=['on','softoff', 'hardoff', 'status'], help='The value for the power command. on, off, or status')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002148 parser_chasPower.set_defaults(func=chassisPower)
2149
2150 #control the chassis identify led
2151 parser_chasIdent = chas_sub.add_parser("identify", help="Control the chassis identify led")
2152 parser_chasIdent.add_argument('identcmd', choices=['on', 'off', 'status'], help='The control option for the led: on, off, blink, status')
2153 parser_chasIdent.set_defaults(func=chassisIdent)
2154
2155 #collect service data
2156 parser_servData = subparsers.add_parser("collect_service_data", help="Collect all bmc data needed for service")
2157 parser_servData.add_argument('-d', '--devdebug', action='store_true', help=argparse.SUPPRESS)
2158 parser_servData.set_defaults(func=collectServiceData)
2159
Justin Thalere412dc22018-01-12 16:28:24 -06002160 #system quick health check
2161 parser_healthChk = subparsers.add_parser("health_check", help="Work with platform sensors")
2162 parser_healthChk.set_defaults(func=healthCheck)
2163
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002164 #work with bmc dumps
2165 parser_bmcdump = subparsers.add_parser("dump", help="Work with bmc dump files")
Justin Thalere412dc22018-01-12 16:28:24 -06002166 bmcDump_sub = parser_bmcdump.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
Justin Thaler53bf2f12018-07-16 14:05:32 -05002167 bmcDump_sub.required = True
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002168 dump_Create = bmcDump_sub.add_parser('create', help="Create a bmc dump")
2169 dump_Create.set_defaults(func=bmcDumpCreate)
2170
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002171 dump_list = bmcDump_sub.add_parser('list', help="list all bmc dump files")
2172 dump_list.set_defaults(func=bmcDumpList)
2173
2174 parserdumpdelete = bmcDump_sub.add_parser('delete', help="Delete bmc dump files")
2175 parserdumpdelete.add_argument("-n", "--dumpNum", nargs='*', type=int, help="The Dump entry to delete")
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002176 parserdumpdelete.set_defaults(func=bmcDumpDelete)
2177
Justin Thalere412dc22018-01-12 16:28:24 -06002178 bmcDumpDelsub = parserdumpdelete.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002179 deleteAllDumps = bmcDumpDelsub.add_parser('all', help='Delete all bmc dump files')
2180 deleteAllDumps.set_defaults(func=bmcDumpDeleteAll)
2181
2182 parser_dumpretrieve = bmcDump_sub.add_parser('retrieve', help='Retrieve a dump file')
2183 parser_dumpretrieve.add_argument("dumpNum", type=int, help="The Dump entry to delete")
2184 parser_dumpretrieve.add_argument("-s", "--dumpSaveLoc", help="The location to save the bmc dump file")
2185 parser_dumpretrieve.set_defaults(func=bmcDumpRetrieve)
2186
Justin Thaler22b1bb52018-03-15 13:31:32 -05002187 #bmc command for reseting the bmc
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002188 parser_bmc = subparsers.add_parser('bmc', help="Work with the bmc")
Justin Thalere412dc22018-01-12 16:28:24 -06002189 bmc_sub = parser_bmc.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002190 parser_BMCReset = bmc_sub.add_parser('reset', help='Reset the bmc' )
2191 parser_BMCReset.add_argument('type', choices=['warm','cold'], help="Warm: Reboot the BMC, Cold: CLEAR config and reboot bmc")
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002192 parser_bmc.add_argument('info', action='store_true', help="Displays information about the BMC hardware, including device revision, firmware revision, IPMI version supported, manufacturer ID, and information on additional device support.")
2193 parser_bmc.set_defaults(func=bmc)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002194
2195 #add alias to the bmc command
2196 parser_mc = subparsers.add_parser('mc', help="Work with the management controller")
Justin Thalere412dc22018-01-12 16:28:24 -06002197 mc_sub = parser_mc.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002198 parser_MCReset = mc_sub.add_parser('reset', help='Reset the bmc' )
2199 parser_MCReset.add_argument('type', choices=['warm','cold'], help="Reboot the BMC")
2200 #parser_MCReset.add_argument('cold', action='store_true', help="Reboot the BMC and CLEAR the configuration")
2201 parser_mc.add_argument('info', action='store_true', help="Displays information about the BMC hardware, including device revision, firmware revision, IPMI version supported, manufacturer ID, and information on additional device support.")
Justin Thalere412dc22018-01-12 16:28:24 -06002202 parser_MCReset.set_defaults(func=bmcReset)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002203 parser_mc.set_defaults(func=bmc)
Justin Thalere412dc22018-01-12 16:28:24 -06002204
2205 #gard clear
2206 parser_gc = subparsers.add_parser("gardclear", help="Used to clear gard records")
2207 parser_gc.set_defaults(func=gardClear)
2208
2209 #firmware_flash
2210 parser_fw = subparsers.add_parser("firmware", help="Work with the system firmware")
2211 fwflash_subproc = parser_fw.add_subparsers(title='subcommands', description='valid firmware commands', help='sub-command help', dest='command')
Justin Thaler53bf2f12018-07-16 14:05:32 -05002212 fwflash_subproc.required = True
2213
Justin Thalere412dc22018-01-12 16:28:24 -06002214 fwflash = fwflash_subproc.add_parser('flash', help="Flash the system firmware")
2215 fwflash.add_argument('type', choices=['bmc', 'pnor'], help="image type to flash")
2216 fwflash.add_argument('-f', '--fileloc', required=True, help="The absolute path to the firmware image")
2217 fwflash.set_defaults(func=fwFlash)
2218
Justin Thaler22b1bb52018-03-15 13:31:32 -05002219 fwActivate = fwflash_subproc.add_parser('activate', help="Activate existing image on the bmc")
Justin Thalere412dc22018-01-12 16:28:24 -06002220 fwActivate.add_argument('imageID', help="The image ID to activate from the firmware list. Ex: 63c95399")
2221 fwActivate.set_defaults(func=activateFWImage)
2222
Justin Thaler22b1bb52018-03-15 13:31:32 -05002223 fwActivateStatus = fwflash_subproc.add_parser('activation_status', help="Check Status of activations")
2224 fwActivateStatus.set_defaults(func=activateStatus)
2225
Justin Thaler3d71d402018-07-24 14:35:39 -05002226 fwList = fwflash_subproc.add_parser('list', help="List all of the installed firmware")
2227 fwList.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
2228 fwList.set_defaults(func=firmwareList)
2229
2230 fwprint = fwflash_subproc.add_parser('print', help="List all of the installed firmware")
2231 fwprint.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
2232 fwprint.set_defaults(func=firmwareList)
Justin Thaler22b1bb52018-03-15 13:31:32 -05002233
Deepak Kodihalli22d4df02018-09-18 06:52:43 -05002234 #logging
2235 parser_logging = subparsers.add_parser("logging", help="logging controls")
2236 logging_sub = parser_logging.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
2237
2238 #turn rest api logging on/off
2239 parser_rest_logging = logging_sub.add_parser("rest_api", help="turn rest api logging on/off")
2240 parser_rest_logging.add_argument('rest_logging', choices=['on', 'off'], help='The control option for rest logging: on, off')
2241 parser_rest_logging.set_defaults(func=restLogging)
2242
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002243 return parser
2244
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002245def main(argv=None):
Justin Thalere412dc22018-01-12 16:28:24 -06002246 """
2247 main function for running the command line utility as a sub application
2248 """
Justin Thaler22b1bb52018-03-15 13:31:32 -05002249 global toolVersion
Justin Thaler14c98d32018-07-24 14:37:42 -05002250 toolVersion = "1.06"
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002251 parser = createCommandParser()
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002252 args = parser.parse_args(argv)
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002253
2254 totTimeStart = int(round(time.time()*1000))
2255
2256 if(sys.version_info < (3,0)):
2257 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
2258 if sys.version_info >= (3,0):
2259 requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
Justin Thalere412dc22018-01-12 16:28:24 -06002260 if (args.version):
Justin Thaler22b1bb52018-03-15 13:31:32 -05002261 print("Version: "+ toolVersion)
Justin Thalere412dc22018-01-12 16:28:24 -06002262 sys.exit(0)
2263 if (hasattr(args, 'fileloc') and args.fileloc is not None and 'print' in args.command):
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002264 mysess = None
Justin Thalere412dc22018-01-12 16:28:24 -06002265 print(selPrint('N/A', args, mysess))
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002266 else:
Justin Thalere412dc22018-01-12 16:28:24 -06002267 if(hasattr(args, 'host') and hasattr(args,'user')):
2268 if (args.askpw):
2269 pw = getpass.getpass()
2270 elif(args.PW is not None):
2271 pw = args.PW
2272 else:
2273 print("You must specify a password")
2274 sys.exit()
2275 logintimeStart = int(round(time.time()*1000))
2276 mysess = login(args.host, args.user, pw, args.json)
Justin Thalera9415b42018-05-25 19:40:13 -05002277 if(sys.version_info < (3,0)):
2278 if isinstance(mysess, basestring):
2279 print(mysess)
2280 sys.exit(1)
2281 elif sys.version_info >= (3,0):
2282 if isinstance(mysess, str):
2283 print(mysess)
2284 sys.exit(1)
Justin Thalere412dc22018-01-12 16:28:24 -06002285 logintimeStop = int(round(time.time()*1000))
2286
2287 commandTimeStart = int(round(time.time()*1000))
2288 output = args.func(args.host, args, mysess)
2289 commandTimeStop = int(round(time.time()*1000))
2290 print(output)
2291 if (mysess is not None):
2292 logout(args.host, args.user, pw, mysess, args.json)
2293 if(args.procTime):
2294 print("Total time: " + str(int(round(time.time()*1000))- totTimeStart))
2295 print("loginTime: " + str(logintimeStop - logintimeStart))
2296 print("command Time: " + str(commandTimeStop - commandTimeStart))
2297 else:
2298 print("usage: openbmctool.py [-h] -H HOST -U USER [-A | -P PW] [-j]\n" +
2299 "\t[-t POLICYTABLELOC] [-V]\n" +
Deepak Kodihalli22d4df02018-09-18 06:52:43 -05002300 "\t{fru,sensors,sel,chassis,collect_service_data, \
2301 health_check,dump,bmc,mc,gardclear,firmware,logging}\n" +
Justin Thalere412dc22018-01-12 16:28:24 -06002302 "\t...\n" +
2303 "openbmctool.py: error: the following arguments are required: -H/--host, -U/--user")
2304 sys.exit()
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002305
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002306if __name__ == '__main__':
Justin Thalere412dc22018-01-12 16:28:24 -06002307 """
2308 main function when called from the command line
2309
2310 """
Justin Thalerf9aee3e2017-12-05 12:11:09 -06002311 import sys
2312
2313 isTTY = sys.stdout.isatty()
2314 assert sys.version_info >= (2,7)
2315 main()