openbmctool: Add system dump operations support

Added support for system dump create,delete,retreive
and list operations

Testing:
python3 openbmctool.py -U root -P password -H <9.xx.xx.x> dump -t SystemDump list
python3 openbmctool.py -U root -P password -H <9.xx.xx.x> dump -t SystemDump retrieve -s /tmp/verifydump -n 2
python3 openbmctool.py -U root -P password -H <9.xx.xx.x> dump -t SystemDump delete -n 2
python3 openbmctool.py -U root -P password -H <9.xx.xx.x> dump -t SystemDump delete all

Change-Id: I0a0d0dfea3d1cbe72bc6f8d3b8adbddc42ee29d7
Signed-off-by: Ravi Teja <raviteja28031990@gmail.com>
diff --git a/thalerj/openbmctool.py b/thalerj/openbmctool.py
index 715284f..2710ba3 100755
--- a/thalerj/openbmctool.py
+++ b/thalerj/openbmctool.py
@@ -31,7 +31,14 @@
 import hashlib
 import re
 import uuid
+import ssl
+import socket
+import select
+import http.client
+from subprocess import check_output
 
+
+MAX_NBD_PACKET_SIZE = 131088
 jsonHeader = {'Content-Type' : 'application/json'}
 xAuthHeader = {}
 baseTimeout = 60
@@ -40,6 +47,163 @@
         'OpenLDAP' : 'openldap'
         }
 
+class NBDPipe:
+
+    def openHTTPSocket(self,args):
+
+        try:
+            _create_unverified_https_context = ssl._create_unverified_context
+        except AttributeError:
+            # Legacy Python that doesn't verify HTTPS certificates by default
+            pass
+        else:
+            # Handle target environment that doesn't support HTTPS verification
+            ssl._create_default_https_context = _create_unverified_https_context
+
+
+        token = gettoken(args)
+        self.conn = http.client.HTTPSConnection(args.host,port=443)
+        URI = "/redfish/v1/Systems/system/LogServices/SystemDump/Entries/"+str(args.dumpNum)+"/Actions/Oem/OpenBmc/LogEntry.DownloadLog"
+        self.conn.request("POST",URI, headers={"X-Auth-Token":token})
+
+    def openTCPSocket(self):
+        # Create a TCP/IP socket
+        self.tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        # Connect the socket to the port where the server is listening
+        server_address = ('localhost', 1043)
+        self.tcp.connect(server_address)
+
+    def waitformessage(self):
+        inputs = [self.conn.sock,self.tcp]
+        outputs = []
+        message_queues = {}
+        while True:
+            readable, writable, exceptional = select.select(
+                    inputs, outputs, inputs)
+
+            for s in readable:
+                if s is self.conn.sock:
+
+                    data = self.conn.sock.recv(MAX_NBD_PACKET_SIZE)
+                    print("<<HTTP")
+                    #print(len(data))
+                    if data:
+                        self.tcp.send(data)
+                    else:
+                        print ("BMC Closed the connection")
+                        self.conn.close()
+                        self.tcp.close()
+                        sys.exit(1)
+                elif s is self.tcp:
+                    data = self.tcp.recv(MAX_NBD_PACKET_SIZE)
+                    print(">>TCP")
+                    #print(len(data));
+                    if data:
+                        self.conn.sock.send(data)
+                    else:
+                        print("NBD server closed the connection")
+                        self.conn.sock.close()
+                        self.tcp.close()
+                        sys.exit(1)
+            for s in exceptional:
+                inputs.remove(s)
+                print("Exceptional closing the socket")
+                s.close()
+
+def getsize(host,args,session):
+    url = "https://"+host+"/redfish/v1/Systems/system/LogServices/SystemDump/Entries/"+str(args.dumpNum)
+    print(url)
+    try:
+        resp = session.get(url, headers=jsonHeader, verify=False, timeout=baseTimeout)
+        if resp.status_code==200:
+            size = resp.json()["Oem"]["OpenBmc"]['SizeInB']
+            print(size)
+            return size
+        else:
+            return "Failed get Size"
+    except(requests.exceptions.Timeout):
+        return connectionErrHandler(args.json, "Timeout", None)
+
+    except(requests.exceptions.ConnectionError) as err:
+        return connectionErrHandler(args.json, "ConnectionError", err)
+
+def gettoken(args):
+   mysess = requests.session()
+   resp = mysess.post('https://'+args.host+'/login', headers=jsonHeader,json={"data":[args.user,args.PW]},verify=False)
+   if resp.status_code == 200:
+       cookie = resp.headers['Set-Cookie']
+       match = re.search('SESSION=(\w+);', cookie)
+       return match.group(1)
+
+
+
+def get_pid(name):
+    try:
+        pid = map(int, check_output(["pidof", "-s",name]))
+    except Exception:
+        pid = 0
+
+    return pid
+
+def findThisProcess( process_name ):
+  ps     = subprocess.Popen("ps -eaf | grep "+process_name, shell=True, stdout=subprocess.PIPE)
+  output = ps.stdout.read()
+  ps.stdout.close()
+  ps.wait()
+  pid = get_pid(process_name)
+  print(pid)
+  return output
+
+def isThisProcessRunning( process_name ):
+  pid = get_pid(process_name)
+  if (pid == 0 ):
+    return False
+  else:
+    return True
+
+def NBDSetup(host,args,session):
+    user=os.getenv("SUDO_USER")
+    if user is None:
+        path = os.getcwd()
+        nbdServerPath = path + "/nbd-server"
+        print(nbdServerPath,os.path.exists(nbdServerPath))
+        if not os.path.exists(nbdServerPath):
+            print("Error: this program did not run as sudo!\nplease copy nbd-server to  current directory and run script again")
+            exit()
+
+    if isThisProcessRunning('nbd-server') == True:
+        print("nbd-server already Running! killing the nbd-server")
+        os.system('killall nbd-server')
+
+    if (args.dumpSaveLoc is not None):
+        if(os.path.exists(args.dumpSaveLoc)):
+            print("Error: File already exists.")
+            exit()
+
+    fp= open(args.dumpSaveLoc,"w")
+    sizeInBytes = getsize(host,args,session)
+    #Round off size to mutiples of 1024
+    size = int(sizeInBytes)
+    print(size)
+    mod = size % 1024
+    if mod :
+        roundoff = 1024 - mod
+        size = size + roundoff
+
+    cmd = 'chmod 777 ' + args.dumpSaveLoc
+    os.system(cmd)
+
+    #Run truncate to create file with given size
+    cmd = 'truncate -s ' + str(size) + ' '+ args.dumpSaveLoc
+    os.system(cmd)
+
+    if user is None:
+        cmd = './nbd-server 1043 '+ args.dumpSaveLoc
+    else:
+        cmd = 'nbd-server 1043 '+ args.dumpSaveLoc
+    os.system(cmd)
+
+
 def hilight(textToColor, color, bold):
     """
          Used to add highlights to various text for displaying in a terminal
@@ -1319,6 +1483,83 @@
         return "This feature is not yet implemented"
     return result
 
+def dumpRetrieve(host, args, session):
+    """
+         Downloads dump of given dump type
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    dumpType = args.dumpType
+    if (args.dumpType=="SystemDump"):
+        dumpResp=systemDumpRetrieve(host,args,session)
+    elif(args.dumpType=="bmc"):
+        dumpResp=bmcDumpRetrieve(host,args,session)
+    return dumpResp
+
+def dumpList(host, args, session):
+    """
+         Lists dump of the given dump type
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    if (args.dumpType=="SystemDump"):
+        dumpResp=systemDumpList(host,args,session)
+    elif(args.dumpType=="bmc"):
+        dumpResp=bmcDumpList(host,args,session)
+    return dumpResp
+
+def dumpDelete(host, args, session):
+    """
+         Deletes dump of the given dump type
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    if (args.dumpType=="SystemDump"):
+        dumpResp=systemDumpDelete(host,args,session)
+    elif(args.dumpType=="bmc"):
+        dumpResp=bmcDumpDelete(host,args,session)
+    return dumpResp
+
+def dumpDeleteAll(host, args, session):
+    """
+         Deletes all dumps of the given dump type
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    if (args.dumpType=="SystemDump"):
+        dumpResp=systemDumpDeleteAll(host,args,session)
+    elif(args.dumpType=="bmc"):
+        dumpResp=bmcDumpDeleteAll(host,args,session)
+    return dumpResp
+
+def dumpCreate(host, args, session):
+    """
+         Creates dump for the given dump type
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    if (args.dumpType=="SystemDump"):
+        dumpResp=systemDumpCreate(host,args,session)
+    elif(args.dumpType=="bmc"):
+        dumpResp=bmcDumpDelete(host,args,session)
+    return dumpResp
+
+
 def bmcDumpRetrieve(host, args, session):
     """
          Downloads a dump file from the bmc
@@ -1462,6 +1703,124 @@
     except(requests.exceptions.ConnectionError) as err:
         return connectionErrHandler(args.json, "ConnectionError", err)
 
+def systemDumpRetrieve(host, args, session):
+    """
+         Downloads system dump
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    NBDSetup(host,args,session)
+    pipe = NBDPipe()
+    pipe.openHTTPSocket(args)
+    pipe.openTCPSocket()
+    pipe.waitformessage()
+
+def systemDumpList(host, args, session):
+    """
+         Lists system dumps
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    print("in systemDumpList")
+    url = "https://"+host+"/redfish/v1/Systems/system/LogServices/"+args.dumpType+"/Entries"
+    try:
+        r = session.get(url, headers=jsonHeader, verify=False, timeout=baseTimeout)
+        dumpList = r.json()
+        return dumpList
+    except(requests.exceptions.Timeout):
+        return connectionErrHandler(args.json, "Timeout", None)
+
+    except(requests.exceptions.ConnectionError) as err:
+        return connectionErrHandler(args.json, "ConnectionError", err)
+
+
+def systemDumpDelete(host, args, session):
+    """
+         Deletes system dump
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    dumpList = []
+    successList = []
+    failedList = []
+    if args.dumpNum is not None:
+        if isinstance(args.dumpNum, list):
+            dumpList = args.dumpNum
+        else:
+            dumpList.append(args.dumpNum)
+        for dumpNum in dumpList:
+            url = 'https://'+host+'/redfish/v1/Systems/system/LogServices/'+args.dumpType+'/Entries/'+ str(dumpNum)
+            try:
+                r = session.delete(url, headers=jsonHeader, json = {"data": []}, verify=False, timeout=baseTimeout)
+                if r.status_code == 200:
+                    successList.append(str(dumpNum))
+                else:
+                    failedList.append(str(dumpNum))
+            except(requests.exceptions.Timeout):
+                return connectionErrHandler(args.json, "Timeout", None)
+            except(requests.exceptions.ConnectionError) as err:
+                return connectionErrHandler(args.json, "ConnectionError", err)
+        output = "Successfully deleted dumps: " + ', '.join(successList)
+        if(len(failedList)>0):
+            output+= '\nFailed to delete dumps: ' + ', '.join(failedList)
+        return output
+    else:
+        return 'You must specify an entry number to delete'
+
+def systemDumpDeleteAll(host, args, session):
+    """
+         Deletes All system dumps
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    url = 'https://'+host+'/redfish/v1/Systems/system/LogServices/'+args.dumpType+'/Actions/LogService.ClearLog'
+    try:
+        r = session.post(url, headers=jsonHeader, json = {"data": []}, verify=False, timeout=baseTimeout)
+        if(r.status_code == 200 and not args.json):
+            return ('Dumps successfully cleared')
+        elif(args.json):
+            return r.json()
+        else:
+            return ('Failed to clear dumps')
+    except(requests.exceptions.Timeout):
+        return connectionErrHandler(args.json, "Timeout", None)
+    except(requests.exceptions.ConnectionError) as err:
+        return connectionErrHandler(args.json, "ConnectionError", err)
+
+def systemDumpCreate(host, args, session):
+    """
+         Creates a system dump
+
+         @param host: string, the hostname or IP address of the bmc
+         @param args: contains additional arguments used by the collectServiceData sub command
+         @param session: the active session to use
+         @param args.json: boolean, if this flag is set to true, the output will be provided in json format for programmatic consumption
+    """
+    url =  'https://'+host+'/redfish/v1/Systems/system/LogServices/'+args.dumpType+'/Actions/Oem/Openbmc/LogService.CreateLog'
+    try:
+        r = session.post(url, headers=jsonHeader, json = {"data": []}, verify=False, timeout=baseTimeout)
+        if(r.status_code == 200 and not args.json):
+            return ('Dump successfully created')
+        elif(args.json):
+            return r.json()
+        else:
+            return ('Failed to create dump')
+    except(requests.exceptions.Timeout):
+        return connectionErrHandler(args.json, "Timeout", None)
+    except(requests.exceptions.ConnectionError) as err:
+        return connectionErrHandler(args.json, "ConnectionError", err)
 
 def csdDumpInitiate(host, args, session):
     """
@@ -4295,28 +4654,29 @@
     parser_healthChk = subparsers.add_parser("health_check", help="Work with platform sensors")
     parser_healthChk.set_defaults(func=healthCheck)
 
-    #work with bmc dumps
-    parser_bmcdump = subparsers.add_parser("dump", help="Work with bmc dump files")
+    #work with dumps
+    parser_bmcdump = subparsers.add_parser("dump", help="Work with dumps")
+    parser_bmcdump.add_argument("-t", "--dumpType", default='bmc', choices=['bmc','SystemDump'],help="Type of dump")
     bmcDump_sub = parser_bmcdump.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
     bmcDump_sub.required = True
-    dump_Create = bmcDump_sub.add_parser('create', help="Create a bmc dump")
-    dump_Create.set_defaults(func=bmcDumpCreate)
+    dump_Create = bmcDump_sub.add_parser('create', help="Create a dump of given type")
+    dump_Create.set_defaults(func=dumpCreate)
 
-    dump_list = bmcDump_sub.add_parser('list', help="list all bmc dump files")
-    dump_list.set_defaults(func=bmcDumpList)
+    dump_list = bmcDump_sub.add_parser('list', help="list all dumps")
+    dump_list.set_defaults(func=dumpList)
 
-    parserdumpdelete = bmcDump_sub.add_parser('delete', help="Delete bmc dump files")
+    parserdumpdelete = bmcDump_sub.add_parser('delete', help="Delete dump")
     parserdumpdelete.add_argument("-n", "--dumpNum", nargs='*', type=int, help="The Dump entry to delete")
-    parserdumpdelete.set_defaults(func=bmcDumpDelete)
+    parserdumpdelete.set_defaults(func=dumpDelete)
 
     bmcDumpDelsub = parserdumpdelete.add_subparsers(title='subcommands', description='valid subcommands',help="sub-command help", dest='command')
-    deleteAllDumps = bmcDumpDelsub.add_parser('all', help='Delete all bmc dump files')
-    deleteAllDumps.set_defaults(func=bmcDumpDeleteAll)
+    deleteAllDumps = bmcDumpDelsub.add_parser('all', help='Delete all dumps')
+    deleteAllDumps.set_defaults(func=dumpDeleteAll)
 
     parser_dumpretrieve = bmcDump_sub.add_parser('retrieve', help='Retrieve a dump file')
-    parser_dumpretrieve.add_argument("dumpNum", type=int, help="The Dump entry to delete")
-    parser_dumpretrieve.add_argument("-s", "--dumpSaveLoc", help="The location to save the bmc dump file")
-    parser_dumpretrieve.set_defaults(func=bmcDumpRetrieve)
+    parser_dumpretrieve.add_argument("-n,", "--dumpNum", help="The Dump entry to retrieve")
+    parser_dumpretrieve.add_argument("-s", "--dumpSaveLoc", help="The location to save the bmc dump file or file path for system dump")
+    parser_dumpretrieve.set_defaults(func=dumpRetrieve)
 
     #bmc command for reseting the bmc
     parser_bmc = subparsers.add_parser('bmc', help="Work with the bmc")