#!/usr/bin/python2 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2018 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import optparse import os import signal import sys try: signal.signal(signal.SIGPIPE, signal.SIG_DFL) except AttributeError: pass path = os.path.dirname(os.path.realpath(__file__)) path = os.path.realpath(os.path.join(path, '..', 'lib', 'python')) if path.startswith('/opt'): sys.path.append(path) import confluent.client as client exitcode = 0 class OptParser(optparse.OptionParser): def format_epilog(self, formatter): return self.expand_prog_name(self.epilog) def mbtohuman(mb): if mb > 1000000: return '{0:.3f} TB'.format(mb/1000000.0) if mb > 1000: return '{0:.3f} GB'.format(mb/1000.0) return '{0:.3f} MB'.format(mb) def showstorage(noderange, options, args): global exitcode session = client.Command() disks = {} arrays = {} volumes = {} scfg = session.read('/noderange/{0}/configuration/storage/all'.format( noderange)) _print_cfg(scfg) def _print_cfg(scfg): global exitcode storagebynode = {} for e in scfg: if 'error' in e: sys.stderr.write(e['error'] + '\n') exitcode = e.get('errorcode', 1) for node in e.get('databynode', {}): curr = e['databynode'][node] if 'error' in curr: if 'no available drives' in curr['error']: curr['error'] += ' (drives must be in unconfigured state to be available, they must not be in jbod or online state)' sys.stderr.write('{0}: {1}\n'.format(node, curr['error'])) exitcode = curr.get('errorcode', 1) continue if node not in storagebynode: storagebynode[node] = {'disks': [], 'arrays': [], 'volumes': []} storagebynode[node][curr['type'] + 's'].append(curr) for node in storagebynode: for disk in sorted(storagebynode[node]['disks'], key=lambda x: x['name']): print('{0}: Disk {1} Description: {2}'.format( node, disk['name'], disk['description'])) print('{0}: Disk {1} State: {2}'.format(node, disk['name'], disk['state'])) print('{0}: Disk {1} FRU: {2}'.format(node, disk['name'], disk['fru'])) print('{0}: Disk {1} Serial Number: {2}'.format(node, disk['name'], disk['serial'])) if disk['array']: print('{0}: Disk {1} Array: {2}'.format(node, disk['name'], disk['array'])) for arr in storagebynode[node]['arrays']: print('{0}: Array {1} Available Capacity: {2}'.format( node, arr['id'], mbtohuman(arr['available']))) print('{0}: Array {1} Total Capacity: {2}'.format( node, arr['id'], mbtohuman(arr['capacity']))) print('{0}: Array {1} RAID: {2}'.format(node, arr['id'], arr['raid'])) print('{0}: Array {1} Disks: {2}'.format(node, arr['id'], ','.join( arr['disks']))) print( '{0}: Array {1} Volumes: {2}'.format(node, arr['id'], ','.join( arr['volumes']))) for vol in storagebynode[node]['volumes']: print('{0}: Volume {1}: Size: {2}'.format(node, vol['name'], mbtohuman(vol['size']))) print('{0}: Volume {1}: State: {2}'.format(node, vol['name'], vol['state'])) print('{0}: Volume {1}: Array {2}'.format(node, vol['name'], vol['array'])) def createstorage(noderange, options, args): if options.raidlevel is None or options.disks is None: sys.stderr.write('-r and -d are required arguments to create array\n') sys.exit(1) session = client.Command() session.stop_if_noderange_over(noderange, options.maxnodes) names = options.name if names is None: names = ''.join(args) parms = {'disks': options.disks, 'raidlevel': options.raidlevel, 'name': names} if options.size: parms['size'] = options.size if options.stripsizes: parms['stripsizes'] = options.stripsizes _print_cfg(session.create( '/noderange/{0}/configuration/storage/volumes/{1}'.format( noderange, names), parms)) def deletestorage(noderange, options, args): if options.name is None: if len(args) == 1: names = args[0] else: sys.stderr.write('-n is required to indicate volume(s) to delete\n') sys.exit(1) else: names = options.name session = client.Command() session.stop_if_noderange_over(noderange, options.maxnodes) for rsp in session.delete( '/noderange/{0}/configuration/storage/volumes/{1}'.format( noderange, names)): if 'deleted' in rsp: print('Deleted: {0}'.format(rsp['deleted'])) elif 'databynode' in rsp: for node in rsp['databynode']: if 'error' in rsp['databynode'][node]: sys.stderr.write('{0}: {1}\n'.format( node, rsp['databynode'][node]['error'])) else: sys.stderr.write('{0}: {1}\n'.format( node, repr(rsp['databynode'][node]))) else: print(repr(rsp)) def setdisk(noderange, options, args): if options.disks is None: if len(args): names = args.pop(0) else: sys.stderr.write('-d is required to indicate disk to modify\n') sys.exit(1) else: names = options.disks if not len(args) or args[0] not in ('hotspare', 'jbod', 'unconfigured'): sys.stderr.write('diskset requires valid state as argument (hotspare, jbod, unconfigured)\n') sys.exit(1) session = client.Command() session.stop_if_noderange_over(noderange, options.maxnodes) scfg = session.update('/noderange/{0}/configuration/storage/disks/{1}'.format(noderange, names), {'state': args[0]}) _print_cfg(scfg) funmap = { 'create': createstorage, 'show': showstorage, 'diskset': setdisk, 'delete': deletestorage, 'rm': deletestorage, } def main(): argparser = OptParser( usage='Usage: %prog [show|create|delete|diskset] [hotspare|jbod|unconfigured] [options]', epilog='', ) argparser.add_option('-r', '--raidlevel', type='int', help='RAID level to use when creating an array') argparser.add_option('-d', '--disks', type='str', help='Comma separated list of disks to use, or the ' 'word "rest" to indicate use of all available ' 'disks') argparser.add_option('-s', '--size', type='str', help='Comma separated list of sizes to use when ' 'creating volumes. The sizes may be absolute ' 'size (e.g. 16gb), percentage (10%) or the word ' '"rest" to use remaining capacity, default ' 'behavior is to use all capacity to make a ' 'volume') argparser.add_option('-n', '--name', type='str', help='Comma separated list of names to use when ' 'naming volumes, or selecting a volume for ' 'delete. Default behavior is to use ' 'implementation provided default names.') argparser.add_option('-z', '--stripsizes', type='str', help='Comma separated list of stripsizes to use when creating volumes. ' 'This value is in kilobytes. The default behavior is to allow the ' 'storage controller to decide.') argparser.add_option('-m', '--maxnodes', type='int', help='Specify a maximum number of ' 'nodes to configure storage on, ' 'prompting if over the threshold') (options, args) = argparser.parse_args() if len(args) == 1: args.append('show') try: noderange = args[0] operation = args[1] except IndexError: argparser.print_help() sys.exit(1) client.check_globbing(noderange) try: handler = funmap[operation] except KeyError: argparser.print_help() sys.exit(1) handler(noderange, options, args[2:]) if __name__ == '__main__': main()