Skip to content
Snippets Groups Projects
ssapi.py 15.7 KiB
Newer Older
Chad Kerner's avatar
Chad Kerner committed
#!/usr/bin/env python
#=====================================================================================
# Chad Kerner, Systems Engineer
# Storage Enabling Technologies 
# National Center for Supercomputing Applications
# ckerner@illinois.edu     chad.kerner@gmail.com
#=====================================================================================
Chad Kerner's avatar
Chad Kerner committed
#
# This was born out of a need to programmatically interface with IBM Spectrum
# Scale or the software formerly knows as GPFS.
Chad Kerner's avatar
Chad Kerner committed
#
# This was written in Python 2.7 with Spectrum Scale 4.2.  That is what we currently
# have and so that is what I will keep it updated with until other needs arise.
Chad Kerner's avatar
Chad Kerner committed
#
# There is NO support, use it at your own risk.  Although I have not coded anything
# too awfully dramatic in here.
#
# If you find a bug, fix it.  Then send me the diff and I will merge it into the code.
Chad Kerner's avatar
Chad Kerner committed
#
# You may want to pull often because this is being updated quite frequently as our 
# needs arise in our clusters.
#
#=====================================================================================
Chad Kerner's avatar
Chad Kerner committed


from __future__ import print_function
from subprocess import Popen, PIPE
import sys
import shlex
Chad Kerner's avatar
Chad Kerner committed
def run_cmd( cmdstr=None ):
    """
    Wrapper around subprocess module calls.
    """
    if not cmdstr:
        return None
    cmd = shlex.split(cmdstr)
    subp = Popen(cmd, stdout=PIPE, stderr=PIPE)
    (outdata, errdata) = subp.communicate()
    if subp.returncode != 0:
        msg = "Error\n  Command: {0}\n  Message: {1}".format(cmdstr,errdata)
Chad Kerner's avatar
Chad Kerner committed
        raise UserWarning( msg )
Chad Kerner's avatar
Chad Kerner committed
    return( outdata )


Chad Kerner's avatar
Chad Kerner committed
def replace_encoded_strings( mystring ):
    """
    The mmlsfileset command returns encoded strings for special characters. 
    This will replace those encoded strings and return a true string.
    """
    tempstring = mystring.replace('%2F', '/')
    mystring = tempstring
    tempstring = mystring.replace('%3A', ':')
    return tempstring


def remove_special_characters( mystring ):
Chad Kerner's avatar
Chad Kerner committed
    """
    The mmlspool command has returns strings with special characters.       
    This will remove those encoded strings.
    """
    tempstring = mystring.replace('%', '')
    mystring = tempstring
    tempstring = mystring.replace('(', '')
    mystring = tempstring
    tempstring = mystring.replace(')', '')
    return tempstring


class Nsds:
    """
    This class contains all of the information about the NSDs in the cluster.  It
    includes the NSD name, servers they are attached to, gpfs device they serve, etc.

    gpfsdevs - a list unique gpfs devices

    nsds[name]['usage'] = The gpfs device the specified name is a part of
    nsds[name]['servers'] = The storage servers the specified name is hosted by

Chad Kerner's avatar
Chad Kerner committed
    """
    def __init__( self ):
        self.collect_nsd_info()
    def dump( self ):
        print("GPFS Devices: {}".format(self.gpfsdevs))
        nsd_keys = self.nsds.keys()
        for nsd in sorted(nsd_keys):
            print("{:<10s}  {:<10s}  {:<s}".format(nsd, self.nsds[nsd]['usage'], self.nsds[nsd]['servers']))

    def return_gpfs_devices( self ):
        """
        Return an iterable list of uniq GPFS devices.
        """
        return self.gpfsdevs
    
    def collect_nsd_info( self ):
Chad Kerner's avatar
Chad Kerner committed
        """
        Process the mmlsnsd command output to build the necessary structures.
Chad Kerner's avatar
Chad Kerner committed
        """
Chad Kerner's avatar
Chad Kerner committed
        fsdevs = {}
        cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlsnsd")

        for line in cmd_out.splitlines():
            line.rstrip()

            # Ignore blank lines
            if not line:
               continue

            # Ignore dashed lines
            if '----------' in line:
               continue

            # Ignore header lines
            if 'File system' in line:
               continue

root's avatar
root committed
            if '(local cache)' in line:
               nsd_name = line.split()[2]
               fsname = 'lroc'
               servers = (line.split()[3]).split(',')
            elif 'free disk' in line:
Chad Kerner's avatar
Chad Kerner committed
               nsd_name = line.split()[2]
               fsname = 'free'
               servers = (line.split()[3]).split(',')
            else:
               nsd_name = line.split()[1]
               fsname = line.split()[0]
               servers = (line.split()[2]).split(',')
               fsdevs[fsname] = 1

            self.nsds[nsd_name] = {}
            self.nsds[nsd_name]['usage'] = fsname
            self.nsds[nsd_name]['servers'] = servers

            try:
               del fsname
               del servers
            except NameError:
               pass

Chad Kerner's avatar
Chad Kerner committed
        self.gpfsdevs = fsdevs.keys()



class Cluster:
    """
    This class will collect the information about the cluster.
    """
    def __init__( self ):
        self.debug = 0
        self.get_cluster_info()
        self.nsds = Nsds()
        self.gpfsdevs = self.nsds.return_gpfs_devices()

Chad Kerner's avatar
Chad Kerner committed
    def get_cluster_info( self ):
        """
        This routine parses the mmlscluster command.
        """
        self.cluster_info = {}
        cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlscluster")
        found_nodes = 0
        for line in cmd_out.splitlines():
            line.rstrip()

            # Ignore blank lines
            if not line:
               continue

            # Ignore dashed lines
            if '----------' in line:
               continue
            if '==========' in line:
               continue

            if found_nodes >= 1:
               nodeid = line.split()[0]
               daemonname = line.split()[1]
               ipaddr = line.split()[2]
               adminname = line.split()[3]
               self.cluster_info['nodes'][nodeid] = {}
               self.cluster_info['nodes'][nodeid]['daemon_name'] = daemonname
               self.cluster_info['nodes'][nodeid]['ip'] = ipaddr
               self.cluster_info['nodes'][nodeid]['admin_name'] = adminname
 
            if 'Node  Daemon' in line:
               found_nodes = found_nodes + 1
               self.cluster_info['nodes'] = {}
            
            if 'GPFS cluster name' in line:
               self.cluster_info['name'] = line.split()[3]
            if 'GPFS cluster id' in line:
               self.cluster_info['id'] = line.split()[3]
            if 'GPFS UID domain' in line:
               self.cluster_info['uid'] = line.split()[3]
            if 'Remote shell command' in line:
               self.cluster_info['rsh'] = line.split()[3]
            if 'Remote file copy command' in line:
               self.cluster_info['rcp'] = line.split()[4]
            if 'Primary server' in line:
               self.cluster_info['primary'] = line.split()[2]
            if 'Secondary server' in line:
               self.cluster_info['secondary'] = line.split()[2]
            
    
    def dump( self ):
        if self.debug >= 1:
Chad Kerner's avatar
Chad Kerner committed
           print("Cluster Information")
           for key in self.cluster_info.keys():
               print("{0} -> {1}".format(key, self.cluster_info[key]))

        if self.debug >= 2:
Chad Kerner's avatar
Chad Kerner committed
           print("\nNSD Information")
           for key in self.nsd_info.keys():
               print("{0} -> FS: {1}   Servers: {2}".format(key, self.nsd_info[key]['usage'], self.nsd_info[key]['servers']))

Chad Kerner's avatar
Chad Kerner committed




class StoragePool:
    def __init__( self, gpfsdev ):
        self.gpfsdev = gpfsdev
        self.pools = {}

        cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlspool {}".format( self.gpfsdev))

        for line in cmd_out.splitlines()[2:]:
            line.rstrip()

            # Ignore blank lines
            if not line:
               continue

            newline = remove_special_characters( line )
            vals = newline.split()
            poolname = vals[0]
            self.pools[poolname] = {}
            self.pools[poolname]['id'] = vals[1]
            self.pools[poolname]['blksize'] = vals[2]
            self.pools[poolname]['blkmod'] = vals[3]
            self.pools[poolname]['data'] = vals[4]
            self.pools[poolname]['metadata'] = vals[5]
            self.pools[poolname]['datasize'] = vals[6]
            self.pools[poolname]['datafree'] = vals[7]
            self.pools[poolname]['datapctfree'] = vals[8]
            self.pools[poolname]['metasize'] = vals[9]
            self.pools[poolname]['metafree'] = vals[10]
            self.pools[poolname]['metapctfree'] = vals[11]

        self.pool_list = self.pools.keys()


    def dump( self ):
        print("{}".format(self.pools))


    def __getitem__( self, key ):
        return self.pools[key]
                     

class Snapshots:
    def __init__( self, gpfsdev, fileset='' ):
        self.gpfsdev = gpfsdev
        self.fileset = fileset
        self.snapshots = {}

        if self.fileset == '':
           cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlssnapshot {0} -Y".format( self.gpfsdev ))
        else:
           cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlssnapshot {0} -j {1} -Y".format( self.gpfsdev, self.fileset ))

        # Process the HEADER line
        if 'No snapshots in file system' in cmd_out.splitlines()[0]:
           self.snap_count = 0
           return

        keys = cmd_out.splitlines()[0].split(':')[6:]

        for line in cmd_out.splitlines()[1:]:
            line.rstrip()

            # Ignore blank lines
            if not line:
               continue

            vals = line.split(':')[6:]
            sname = vals[1]
            self.snapshots[sname] = {}
            for idx in range(len(vals)-1):
Chad Kerner's avatar
Chad Kerner committed
                self.snapshots[sname][keys[idx]] = replace_encoded_strings( vals[idx] )

        snaplist = self.snapshots.keys()
        self.snaplist = sorted( snaplist )
        self.snap_count = len( snaplist )

Chad Kerner's avatar
Chad Kerner committed
    def get_delete_list( self, max_to_keep ):
        """
        Given an integer of how many snapshots you want to keep, this will return a list of snapshot
        names that are to be purged.  It does not purge them, only a list of what needs to be purged
        based on how many you want to keep.
        """
Chad Kerner's avatar
Chad Kerner committed
        self.dellist = []
root's avatar
root committed
        if self.snap_count > max_to_keep:
           self.dellist = list(self.snaplist)[ : -( max_to_keep ) ]

Chad Kerner's avatar
Chad Kerner committed
        return self.dellist


    def delsnap( self, snap_name ):
        """
        Given a specific snapshot name, this routine will execute mmdelsnapshot and return you the output
        from the command.  The object already knows if it is a filesystem or a fileset snapshot, so you just
        need to specify the snapshot name.
        """
Chad Kerner's avatar
Chad Kerner committed
        if self.fileset == '':
           cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmdelsnapshot {} {}".format(self.gpfsdev, snap_name))
Chad Kerner's avatar
Chad Kerner committed
        else:
           cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmdelsnapshot {} {} -j {}".format(self.gpfsdev, snap_name, self.fileset))
        return cmd_out 
        """
        This code will create a snapshot of the specified filesystem or fileset.  

        NOTE: You can NOT mix filesystem and fileset snapshots on the same GPFS device.

        Filesystem snapshots are named: CCYYMMDD==HHMM for easy sorting / processing.

        Filesystem snapshots are named: <Fileset>==CCYYMMDD==HHMM for easy processing again.
        """
           snapname = time.strftime("%Y%m%d") + self.snap_name_separator + time.strftime("%H%M")
           cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmcrsnapshot {0} {1}".format( self.gpfsdev, snapname ))
        else:
Chad Kerner's avatar
Chad Kerner committed
           snapname = self.fileset + self.snap_name_separator + time.strftime("%Y%m%d") + self.snap_name_separator + time.strftime("%H%M")
           cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmcrsnapshot {0} {1} -j {2}".format( self.gpfsdev, snapname, self.fileset ))

Chad Kerner's avatar
Chad Kerner committed
class Filesystem:
    """
    This class will collect the information about the specified GPFS device.
    """

    filesystem_defaults = { 'automaticMountOption': 'yes', 
                            'defaultMetadataReplicas': '1', 
                            'maxMetadataReplicas': '2', 
                            'defaultDataReplicas': '1', 
                            'maxDataReplicas': '2', 
                            'blockAllocationType': '',
                            'fileLockingSemantics': '',
                          }

Chad Kerner's avatar
Chad Kerner committed
    def __init__( self, gpfsdev ):
        if not gpfsdev:
           raise ValueError('NoDevice')
        else:
           self.gpfsdev = gpfsdev
           self.get_filesystem_information()
           self.get_fileset_information()
Chad Kerner's avatar
Chad Kerner committed
           self.get_pool_information()
    def print_keys( self ):
        keys = self.filesys.keys()
        return keys


Chad Kerner's avatar
Chad Kerner committed
    def get_pool_information( self ):
        self.pools = StoragePool( self.gpfsdev )

    def get_filesystem_information( self ):
Chad Kerner's avatar
Chad Kerner committed
        self.filesys = {}
        cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlsfs {0} -Y".format(self.gpfsdev))
        for line in cmd_out.splitlines():
            line.rstrip()

            # Ignore blank lines
            if not line:
               continue

            # Ignore HEADER line
            if 'HEADER' in line:
               continue

            key = line.split(':')[7]  
            value = line.split(':')[8]  
Chad Kerner's avatar
Chad Kerner committed
            self.filesys[key] = replace_encoded_strings( value )

    def fileset_list( self ):
        """
        Return all of the fileset names in the file system.
        """
        return self.filesets.keys()


    def independent_inode_fileset( self, fname ):
        if self.filesets[fname]['fstype'] == 'Independent':
           return True
        else:
           return False


    def get_fileset_information( self ):
        self.filesets = {}
        cmd_out = run_cmd("/usr/lpp/mmfs/bin/mmlsfileset {0} -Y".format(self.gpfsdev))

        # Process the HEADER line
        keys = cmd_out.splitlines()[0].split(':')[7:]

        for line in cmd_out.splitlines()[1:]:
            line.rstrip()

            # Ignore blank lines
            if not line:
               continue

            vals = line.split(':')[7:]
            fname = vals[0]
            self.filesets[fname] = {}
            for idx in range(len(vals)-1):
Chad Kerner's avatar
Chad Kerner committed
                self.filesets[fname][keys[idx]] = replace_encoded_strings( vals[idx] )
            # Set the fileset type. independent inode or dependent inode
            if self.filesets[fname]['filesetName'] == 'root' and self.filesets[fname]['inodeSpace'] == '0':
               self.filesets[fname]['fstype'] = 'Independent'
            elif self.filesets[fname]['inodeSpace'] >= '1':
               self.filesets[fname]['fstype'] = 'Independent'
            elif self.filesets[fname]['inodeSpace'] == '0':
               self.filesets[fname]['fstype'] = 'Dependent'
            else:
               self.filesets[fname]['fstype'] = 'Unknown'




    @classmethod
    def Create( self, gpfsdev, fsname ):
        """ 
        This function will create a new filesystem.

        Input 1: A dictionary containing the nsd's and their parameters.
           mydisks['nsd1']['usage']='dataAndMetadata'
           mydisks['nsd1']['failuregroup']='-1'
           mydisks['nsd1']['pool']='system'

        Input 2: A dictionary containing parameters for the file system.
        """
        print("Creating {0} on {1}".format(fsname, gpfsdev))

        self = Filesystem( gpfsdev )
        return self

Chad Kerner's avatar
Chad Kerner committed
    def __getitem__( self, key ):
        return self.filesys[key]
                     

if __name__ == '__main__':
   #
   # This is just where I do my testing of stuff.
   #

   sys.exit(0)

   snap = Snapshots( 'condo', 'root' )

   #myFS = Filesystem( 'condo' )
   #fslist = myFS.fileset_list()
   #print("{}".format(myFS.filesets))
Chad Kerner's avatar
Chad Kerner committed
   #Clstr.dump()

   #print(Clstr.gpfsdevs)
   #myFs = Filesystem( 'wvu' )
   #FSa = Filesystem( 'des003' )
   #try:
   #  F = Filesystem('')
   #except:
   #  print("No Filesystem device specified.")
   #else:
   #  print(F[disks])