diff --git a/pyarcconf/arcconf.py b/pyarcconf/arcconf.py index 6c5310f..9dc850c 100644 --- a/pyarcconf/arcconf.py +++ b/pyarcconf/arcconf.py @@ -13,45 +13,16 @@ https://wiki.miko.ru/kb:sysadm:arcconf """ -import logging -import subprocess +from . import runner -from . import parser -logger = logging.getLogger('pyArcconf') - - -class Arcconf(): +class Arcconf(runner.CMDRunner): """Arcconf wrapper class.""" + def __init__(self, cmdrunner=None): + self.runner = cmdrunner or runner.CMDRunner() - def __init__(self, path=''): - """Initialize a new Arcconf object. - - Args: - path (str): path to arcconf binary - """ - self.path = path or self._exec(['which', 'arcconf'])[0].split('\n')[0] - - def _exec(self, cmd): - """Execute a command using arcconf. - - Args: - cmd (list): - Returns: - str: arcconf output - Raises: - RuntimeError: if command fails - """ - proc = subprocess.Popen(cmd, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - _stdout, _stderr = proc.communicate() - if isinstance(_stdout, bytes): - _stdout = _stdout.decode('utf8').strip() - _stderr = _stderr.decode('utf8').strip() - return _stdout, proc.returncode - - def _execute(self, cmd, args=[]): - """Execute a command using arcconf. + def _execute(self, cmd, args=None): + """Execute a command Return codes: 0x00: SUCCESS 0x01: FAILURE - The requested command failed @@ -61,28 +32,20 @@ def _execute(self, cmd, args=[]): Args: args (list): Returns: - str: arcconf output + str: output Raises: RuntimeError: if command fails """ + args = args or [] if type(cmd) == str: cmd = [cmd] - out, rc = self._exec([self.path] + cmd + args) + out, err, rc = self.runner.run(args=[self.runner.path] + cmd + args, universal_newlines=True) for arg in cmd + args: if '>' in arg: # out was redirected return out, rc - out = out.split('\n') - while out and not out[-1]: - # remove empty lines in the end of out - del out[-1] - if 'Command completed successfully' not in out[-1]: - logger.error(f'{cmd} {out[-1]}') - del out[-1] - while out and not out[-1]: - # remove empty lines in the end of out - del out[-1] - return '\n'.join(out), rc + out = runner.sanitize_stdout(out, 'Command ') + return out, rc def get_version(self): """Check the versions of all connected controllers. @@ -92,7 +55,7 @@ def get_version(self): """ versions = {} result = self._execute('GETVERSION')[0] - result = parser.cut_lines(result, 1) + result = runner.cut_lines(result, 1) for part in result.split('\n\n'): lines = part.split('\n') id_ = lines[0].split('#')[1] @@ -104,26 +67,26 @@ def get_version(self): return versions def list(self): - """List all adapter by their ids. + """List all controllers by their ids. Returns: - list: list of adapter ids + list: list of controller ids """ - adapters = [] + controllers = [] result = self._execute('LIST')[0] - result = parser.cut_lines(result, 6) + result = runner.cut_lines(result, 6) for line in list(filter(None, result.split('\n'))): - adapters.append(line.split(':')[0].strip().split()[1]) - return adapters + controllers.append(line.split(':')[0].strip().split()[1]) + return controllers - def get_adapters(self): - """Get all adapter objects for further interaction. + def get_controllers(self): + """Get all controller objects for further interaction. Returns: - list: list of adapter objects. + list: list of controller objects. """ from common.pyarcconf.controller import Controller - adapters = [] + controllers = [] for idx in self.list(): - adapters.append(Controller(idx, self)) - return adapters + controllers.append(Controller(idx, self)) + return controllers diff --git a/pyarcconf/array.py b/pyarcconf/array.py index a245e2c..f89f230 100644 --- a/pyarcconf/array.py +++ b/pyarcconf/array.py @@ -1,6 +1,6 @@ -"""Pyarcconf submodule, which provides a logical drive representing class.""" +"""Logical (Virtual) array class""" -from . import parser +from . import runner from .arcconf import Arcconf from .physical_drive import PhysicalDrive @@ -8,31 +8,31 @@ class Array(): """Object which represents an Array of logical\physical drives""" - def __init__(self, adapter_obj, id_, arcconf=None): + def __init__(self, controller_obj, id_, cmdrunner=None): """Initialize a new LogicalDrive object.""" - self.arcconf = arcconf or Arcconf() - self.adapter = adapter_obj - self.adapter_id = str(adapter_obj.id) + self.runner = cmdrunner or Arcconf() + self.controller = controller_obj + self.controller_id = str(controller_obj.id) self.id = str(id_) # pystorcli compliance self.facts = {} def _execute(self, cmd, args=[]): - """Execute a command using arcconf. + """Execute a command Args: args (list): Returns: - str: arcconf output + str: output Raises: RuntimeError: if command fails """ if cmd == 'GETCONFIG': - base_cmd = [cmd, self.adapter_id] + base_cmd = [cmd, self.controller_id] else: - base_cmd = [cmd, self.adapter_id, 'ARRAY', self.id_] - return self.arcconf._execute(base_cmd + args) + base_cmd = [cmd, self.controller_id, 'ARRAY', self.id_] + return self.runner._execute(base_cmd + args) def __repr__(self): """Define a basic representation of the class object.""" @@ -46,31 +46,31 @@ def update(self, config=''): if config and type(config) == list: config = '\n'.join(config) config = config or self._get_config() - config = config.split(parser.SEPARATOR_SECTION)[0] + config = config.split(runner.SEPARATOR_SECTION)[0] for line in config.split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) self.__setattr__(key, value) # pystorcli compliance - key = parser.convert_key_dict(line) + key = runner.convert_key_dict(line) self.facts[key] = value def _get_config(self): result = self._execute('GETCONFIG', ['AR', self.id])[0] - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) return result @property def drives(self): config = self._get_config() - config = config.split(parser.SEPARATOR_SECTION)[-1] + config = config.split(runner.SEPARATOR_SECTION)[-1] drives = [] for line in config.split('\n'): if not line: continue serial = line.split(')')[1].strip() # TODO: create new objects instead of getting them from the controller ? - for d in self.adapter.drives: + for d in self.controller.drives: if serial == d.serial: d.update() drives.append(d) @@ -79,12 +79,12 @@ def drives(self): @property def lgs(self): config = self._get_config() - config = config.split(parser.SEPARATOR_SECTION)[-4] + config = config.split(runner.SEPARATOR_SECTION)[-4] drives = [] for line in config.split('\n'): - serial = line.split(parser.SEPARATOR_ATTRIBUTE)[0].strip() + serial = line.split(runner.SEPARATOR_ATTRIBUTE)[0].strip() # TODO: create new objects instead of getting them from the controller ? - for d in self.adapter.drives: + for d in self.controller.drives: if serial == d.serial: d.update() drives.append(d) @@ -101,7 +101,7 @@ def set_name(self, name): result, rc = self._execute('SETNAME', [name]) if not rc: result = self._execute('GETCONFIG', ['LD', self.id]) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for line in result.split('\n'): if line.strip().startswith('Logical Device Name'): self.logical_device_name = line.split(':')[1].strip().lower() @@ -119,7 +119,7 @@ def set_state(self, state='OPTIMAL'): result, rc = self._execute('SETSTATE', [state]) if not rc: result = self._execute('GETCONFIG', ['LD', self.id]) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for line in result.split('\n'): if line.strip().startswith('Status'): self.status_of_logical_device = line.split(':')[1].strip().lower() @@ -144,10 +144,10 @@ def set_cache(self, mode): result, rc = self._execute('SETCACHE', [mode]) if not rc: result = self._execute('GETCONFIG', ['LD', self.id]) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for line in result.split('\n'): if line.split(':')[0].strip() in ['Read-cache', 'Write-cache']: - key, value = parser.convert_property(line) + key, value = runner.convert_property(line) self.__setattr__(key, value) return True return False diff --git a/pyarcconf/controller.py b/pyarcconf/controller.py index 809f45a..93d156f 100644 --- a/pyarcconf/controller.py +++ b/pyarcconf/controller.py @@ -1,7 +1,7 @@ -"""Pyarcconf submodule, which provides a raidcontroller representing Adapter class.""" +"""Pyarcconf submodule, which provides a raidcontroller representing controller class.""" import re -from . import parser +from . import runner from .arcconf import Arcconf from .enclosure import Enclosure from .array import Array @@ -11,12 +11,12 @@ class Controller(): - """Object which represents an adapter.""" + """Object which represents an controller.""" - def __init__(self, adapter_id, arcconf=None): - """Initialize a new Adapter object.""" - self.id = str(adapter_id) - self.arcconf = arcconf or Arcconf() + def __init__(self, controller_id, cmdrunner=None): + """Initialize a new controller object.""" + self.id = str(controller_id) + self.runner = cmdrunner or Arcconf() self.model = '' self.mode = '' self.channel_description = '' @@ -44,16 +44,16 @@ def __repr__(self): ) def _execute(self, cmd, args=[]): - """Execute a command using arcconf. + """Execute a command Args: args (list): Returns: - str: arcconf output + str: output Raises: RuntimeError: if command fails """ - return self.arcconf._execute([cmd, self.id] + args)[0] + return self.runner._execute([cmd, self.id] + args)[0] def initialize(self): self.update() @@ -91,14 +91,14 @@ def hba(self): @property def phyerrorcounters(self): result = self._execute('PHYERRORLOG') - result = parser.cut_lines(result, 8) + result = runner.cut_lines(result, 8) data = {} for phy in result.split('\n\n'): phy = phy.split('\n') - _, phyid = parser.convert_property(phy[0]) + _, phyid = runner.convert_property(phy[0]) data[phyid] = {} for attr in phy[1:]: - key, value = parser.convert_property(attr) + key, value = runner.convert_property(attr) data[phyid][key] = value return data @@ -106,26 +106,26 @@ def phyerrorcounters(self): def connectors(self): data = {} result = self._execute('GETCONFIG', ['CN']) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for part in result.split('\n\n'): lines = part.split('\n') cnid = lines[0].split('#')[-1].strip() data[cnid] = {} for line in lines[1:]: - key, value = parser.convert_property(line) + key, value = runner.convert_property(line) data[cnid][key] = value return data def update(self): - """Parse adapter info""" + """Parse controller info""" result = self._execute('GETCONFIG', ['AD']) - result = parser.cut_lines(result, 4) - section = list(filter(None, result.split(parser.SEPARATOR_SECTION))) + result = runner.cut_lines(result, 4) + section = list(filter(None, result.split(runner.SEPARATOR_SECTION))) info = section[0] for line in info.split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) if key == 'pci_address': #changing from 0:d9:0:0 to lspci format 0:d9:00.0 @@ -137,7 +137,7 @@ def update(self): # pystorcli compliance self.__setattr__(key.replace('controller_', ''), value) - key = parser.convert_key_dict(line) + key = runner.convert_key_dict(line) # TODO: did not decide about naming, adding both. the second one is better for pystorcli self.facts[key] = value self.facts[key.replace('Controller ', '')] = value @@ -145,20 +145,20 @@ def update(self): for idx in range(1, len(section), 2): if not section[idx].replace(' ', ''): print('NO SECTION') # TODO: remove it later - attr = parser.convert_key_dict(section[idx]) + attr = runner.convert_key_dict(section[idx]) # pystorcli compliance attr = attr.replace('Information', '') attr = attr.replace('Controller', '').strip() if 'temperature sensors' in attr.lower(): props = {} for sub_section in section[idx + 1].split('\n\n'): - sub_props = parser.get_properties(sub_section) + sub_props = runner.get_properties(sub_section) if sub_props: props[sub_props['Sensor ID']] = sub_props else: - props = parser.get_properties(section[idx + 1]) + props = runner.get_properties(section[idx + 1]) if props: - self.__setattr__(parser.convert_key_attribute(attr), props) + self.__setattr__(runner.convert_key_attribute(attr), props) # pystorcli compliance self.facts[attr] = props @@ -168,16 +168,16 @@ def update(self): versions = section[4] battery = section[6] for line in raid_props.split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) self.raid_properties[key] = value for line in versions.split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) self.versions[key] = value for line in battery.split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) self.battery[key] = value # TODO: remove it later print('print(self.facts):') @@ -192,13 +192,13 @@ def get_lds(self): if 'No logical devices configured' in result: return [] self.lds = [] - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for part in result.split('\n\n'): - sections = part.split(parser.SEPARATOR_SECTION) + sections = part.split(runner.SEPARATOR_SECTION) options = sections[0] lines = list(filter(None, options.split('\n'))) ldid = lines[0].split()[-1] - ld = LogicalDrive(self, ldid, arcconf=self.arcconf) + ld = LogicalDrive(self, ldid, runner=self.runner) ld.update(lines) if 'Logical Device segment information' in part: @@ -228,13 +228,13 @@ def get_arrays(self): if 'No arrays configured' in result: return [] self.arrays = [] - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for part in result.split('\n\n'): - sections = part.split(parser.SEPARATOR_SECTION) + sections = part.split(runner.SEPARATOR_SECTION) options = sections[0] lines = list(filter(None, options.split('\n'))) ldid = lines[0].split()[-1] - ld = Array(self, ldid, arcconf=self.arcconf) + ld = Array(self, ldid, runner=self.runner) ld.update(lines) self.arrays.append(ld) return self.arrays @@ -244,7 +244,7 @@ def get_pds(self): """ self._drives = [] result = self._execute('GETCONFIG', ['PD']) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) result = re.split('.*Channel #\d+:', result) result = [re.split('.*Device #\d+\n', r) for r in result] result = [item for sublist in result for item in sublist] @@ -259,12 +259,12 @@ def get_pds(self): if 'Device is a Hard drive' not in part: # this is an expander\enclosure case - enc = Enclosure(self, channel, device, arcconf=self.arcconf) + enc = Enclosure(self, channel, device, runner=self.runner) self.enclosures.append(enc) enc.update(lines) continue - drive = PhysicalDrive(self, channel, device, arcconf=self.arcconf) + drive = PhysicalDrive(self, channel, device, runner=self.runner) drive.update(lines) self._drives.append(drive) return self._drives @@ -275,11 +275,11 @@ def get_tasks(self): if 'Current operation : None' in result: return [] self.tasks = [] - result = parser.cut_lines(result, 1) + result = runner.cut_lines(result, 1) for part in result.split('\n\n'): task = Task() for line in part.split('\n')[1:]: - key, value = parser.convert_property(line) + key, value = runner.convert_property(line) task.__setattr__(key, value) self.tasks.append(task) return self.tasks diff --git a/pyarcconf/datasets/mvcli/info b/pyarcconf/datasets/mvcli/info new file mode 100644 index 0000000..5d14523 --- /dev/null +++ b/pyarcconf/datasets/mvcli/info @@ -0,0 +1,49 @@ +SG driver version 3.5.36. +CLI Version: 4.1.13.31 RaidAPI Version: 5.0.13.1071 +Welcome to RAID Command Line Interface. + +> info -o hba + +controller ID: 0 +Product: 1b4b-9230 +Sub Product: 1b4b-9230 +Chip revision: A1 +slot number: 0 +Max PCIe speed: 5Gb/s +Current PCIe speed: 5Gb/s +Max PCIe link: 2 +Current PCIe link: 2 +Rom version: 0.0.0.0000 +Firmware version: 2.3.99.1009 +Boot loader version: 2.1.0.1009 +# of ports: 3 +Buzzer: Not supported +Supported port type: SATA +Supported RAID mode: RAID0 RAID1 JBOD +Maximum disk in one VD: 2 +PM: Not supported +Expander: Not supported +Rebuild: Supported +Background init: Not supported +Sync: Not supported +Migrate: Not supported +Media patrol: Supported +Foreground init: Not supported +Copy back: Not supported +Maximum supported disk: 2 +Maximum supported VD: 1 +Max total blocks: 128 +Features: rebuild,media patrol +Advanced features: event sense code,multi VD,spc 4,image health,timer,ata pass through,oem data +Advanced features 2: scsi pass through,flash +Max buffer size: 3 +Stripe size supported: 32K 64K +Image health: Healthy +Autoload image health: Healthy +Boot loader image health: Healthy +Firmware image health: Healthy +Boot ROM image health: Healthy +HBA info image health: Healthy + +> exit + diff --git a/pyarcconf/enclosure.py b/pyarcconf/enclosure.py index f029914..0370650 100644 --- a/pyarcconf/enclosure.py +++ b/pyarcconf/enclosure.py @@ -1,4 +1,4 @@ -"""Pyarcconf submodule, which provides a logical drive representing class.""" +"""Enclosure\Expander class""" from .physical_drive import PhysicalDrive @@ -50,12 +50,12 @@ class Enclosure(PhysicalDrive): """ #TODO: this method is not really needed for now def _execute(self, cmd, args=[]): - """Execute a command using arcconf. + """Execute a command Args: args (list): Returns: - str: arcconf output + str: output Raises: RuntimeError: if command fails """ diff --git a/pyarcconf/logical_drive.py b/pyarcconf/logical_drive.py index 142cf3a..0592bb6 100644 --- a/pyarcconf/logical_drive.py +++ b/pyarcconf/logical_drive.py @@ -1,6 +1,6 @@ -"""Pyarcconf submodule, which provides a logical drive representing class.""" +"""Logical (Virtual) drive class""" -from . import parser +from . import runner from .arcconf import Arcconf from .physical_drive import PhysicalDrive @@ -8,11 +8,11 @@ class LogicalDrive(): """Object which represents a logical drive.""" - def __init__(self, adapter_obj, id_, arcconf=None): + def __init__(self, controller_obj, id_, cmdrunner=None): """Initialize a new LogicalDrive object.""" - self.arcconf = arcconf or Arcconf() - self.adapter = adapter_obj - self.adapter_id = str(adapter_obj.id) + self.runner = cmdrunner or Arcconf() + self.controller = controller_obj + self.controller_id = str(controller_obj.id) self.id = str(id_) self.raid_level = None self.size = None @@ -50,43 +50,43 @@ def __repr__(self): ) def _execute(self, cmd, args=[]): - """Execute a command using arcconf. + """Execute a command Args: args (list): Returns: - str: arcconf output + str: output Raises: RuntimeError: if command fails """ if cmd == 'GETCONFIG': - base_cmd = [cmd, self.adapter_id] + base_cmd = [cmd, self.controller_id] else: - base_cmd = [cmd, self.adapter_id, 'LOGICALDRIVE', self.id] - return self.arcconf._execute(base_cmd + args) + base_cmd = [cmd, self.controller_id, 'LOGICALDRIVE', self.id] + return self.runner._execute(base_cmd + args) def update(self, config=''): if config and type(config) == list: config = '\n'.join(config) config = config or self._get_config() - config = config.split(parser.SEPARATOR_SECTION)[0] + config = config.split(runner.SEPARATOR_SECTION)[0] for line in config.split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) self.__setattr__(key, value) # pystorcli compliance - key = parser.convert_key_dict(line) + key = runner.convert_key_dict(line) self.facts[key] = value def _get_config(self): result = self._execute('GETCONFIG', ['LD', self.id])[0] - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) return result @property def drives(self): config = self._get_config() - config = config.split(parser.SEPARATOR_SECTION)[-1] + config = config.split(runner.SEPARATOR_SECTION)[-1] drives = [] print(config) for line in config.split('\n'): @@ -94,7 +94,7 @@ def drives(self): continue serial = line.split(')')[1].strip() # TODO: create new objects instead of getting them from the controller ? - for d in self.adapter.drives: + for d in self.controller.drives: if serial == d.serial: d.update() drives.append(d) @@ -111,7 +111,7 @@ def set_name(self, name): result, rc = self._execute('SETNAME', [name]) if not rc: result = self._execute('GETCONFIG', ['LD', self.id]) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for line in result.split('\n'): if line.strip().startswith('Logical Device Name'): self.logical_device_name = line.split(':')[1].strip().lower() @@ -129,7 +129,7 @@ def set_state(self, state='OPTIMAL'): result, rc = self._execute('SETSTATE', [state]) if not rc: result = self._execute('GETCONFIG', ['LD', self.id]) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for line in result.split('\n'): if line.strip().startswith('Status'): self.status_of_logical_device = line.split(':')[1].strip().lower() @@ -154,10 +154,10 @@ def set_cache(self, mode): result, rc = self._execute('SETCACHE', [mode]) if not rc: result = self._execute('GETCONFIG', ['LD', self.id]) - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) for line in result.split('\n'): if line.split(':')[0].strip() in ['Read-cache', 'Write-cache']: - key, value = parser.convert_property(line) + key, value = runner.convert_property(line) self.__setattr__(key, value) return True return False diff --git a/pyarcconf/mvcli.py b/pyarcconf/mvcli.py new file mode 100644 index 0000000..a3f51ca --- /dev/null +++ b/pyarcconf/mvcli.py @@ -0,0 +1,162 @@ +from . import runner + + +class MVCLI(): + """MVCLI wrapper class.""" + def __init__(self, cmdrunner=None): + self.runner = cmdrunner or runner.CMDRunner() + + def _execute(self, cmd, args=None): + """Execute a command using mvcli + Args: + args (list): + Returns: + str: arcconf output + Raises: + RuntimeError: if command fails + """ + args = args or [] + if type(cmd) == str: + cmd = [cmd] + cmd = cmd + args + cmd = f'(echo "{" ".join(cmd)}"; echo "exit") | {self.runner.path}' + out, err, rc = self.runner.run(args=cmd, universal_newlines=True) + if '>' in cmd: + # out was redirected + return out, rc + out = out.split('\n') + out = runner.cut_lines(out, 6) + out = runner.sanitize_stdout(out, '> exit') + if '(error ' in out[-1]: + err = out[-1] + rc = err.split('(error ')[1].split(runner.SEPARATOR_ATTRIBUTE)[0] + out = '' + else: + rc = 0 + #out = runner.cut_lines(out, 6) + return '\n'.join(out), rc + + def get_controllers(self): + """Get all controller objects for further interaction. + + Returns: + list: list of controller objects. + """ + #from common.pyarcconf.controller import Controller + result = self._execute('info -o hba')[0] + #result = runner.cut_lines(result, 6) + result = result.split('\n\n') + controllers = [] + for info in result: + controllers.append(Controller(info, self)) + return controllers + + + + +class Controller(): + """Object which represents an controller.""" + + def __init__(self, info, cmdrunner=None): + """Initialize a new controller object.""" + if type(info) == str and 'Adapter ID' in info: + self.id = info.split('\n')[0].split(runner.SEPARATOR_ATTRIBUTE)[1] + else: + self.id = str(info) + info = '' + self.id = self.id.strip() + self.runner = cmdrunner or MVCLI() + self.mode = '' + + self._drives = [] + self.lds = [] + self.enclosures = [] + + # pystorcli compliance + self.facts = {} + self.name = self.id + + self.update(info) + + def __repr__(self): + """Define a basic representation of the class object.""" + return ''.format( + self.id, self.mode, self.model + ) + + def _execute(self, cmd, args=[]): + """Execute a command using arcconf. + + Args: + args (list): + Returns: + str: arcconf output + Raises: + RuntimeError: if command fails + """ + return self.runner._execute([cmd] + args)[0] + + @property + def model(self): + return getattr(self, 'product', '') + + @property + def drives(self): + if not self._drives: + self._drives = self.get_pds() + return self._drives + + @property + def hba(self): + """ + Return: + bool: True if card is HBA + """ + return not getattr(self, 'supported_raid_mode', '').upper() + + def update(self, info=''): + """Parse controller info""" + result = info or self._execute(f'info -o hba -i {self.id}') + if not result: + print('Command failed, aborting') + return + print(result) + #result = runner.cut_lines(result, 4) + section = list(filter(None, result.split('\n\n'))) + info = section[0] + for line in info.split('\n'): + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) + self.__setattr__(key, value) + + # pystorcli compliance + self.__setattr__(key.replace('controller_', ''), value) + key = runner.convert_key_dict(line) + # TODO: did not decide about naming, adding both. the second one is better for pystorcli + self.facts[key] = value + self.facts[key.replace('Controller ', '')] = value + self.id = self.facts['Adapter ID'] + + for idx in range(1, len(section), 2): + if not section[idx].replace(' ', ''): + print('NO SECTION') # TODO: remove it later + attr = runner.convert_key_dict(section[idx]) + # pystorcli compliance + attr = attr.replace('Information', '') + attr = attr.replace('Controller', '').strip() + if 'temperature sensors' in attr.lower(): + props = {} + for sub_section in section[idx + 1].split('\n\n'): + sub_props = runner.get_properties(sub_section) + if sub_props: + props[sub_props['Sensor ID']] = sub_props + else: + props = runner.get_properties(section[idx + 1]) + if props: + self.__setattr__(runner.convert_key_attribute(attr), props) + # pystorcli compliance + self.facts[attr] = props + + # TODO: remove it later + print('print(self.facts):') + print(self.facts) diff --git a/pyarcconf/physical_drive.py b/pyarcconf/physical_drive.py index 99082d0..946c8c4 100644 --- a/pyarcconf/physical_drive.py +++ b/pyarcconf/physical_drive.py @@ -1,6 +1,6 @@ -"""Pyarcconf submodule, which provides a logical drive representing class.""" +"""Physical drive \ device class""" -from . import parser +from . import runner from .arcconf import Arcconf SEPARATOR_SECTION = 64 * '-' @@ -9,11 +9,11 @@ class PhysicalDrive(): """Object which represents a physical drive.""" - def __init__(self, adapter_obj, channel, device, arcconf=None): + def __init__(self, controller_obj, channel, device, cmdrunner=None): """Initialize a new LogicalDriveSegment object.""" - self.arcconf = arcconf or Arcconf() - self.adapter = adapter_obj - self.adapter_id = str(adapter_obj.id) + self.runner = cmdrunner or Arcconf() + self.controller = controller_obj + self.controller_id = str(controller_obj.id) self.channel = str(channel).strip() self.device = str(device).strip() # pystorcli compliance @@ -25,20 +25,20 @@ def encl_id(self): return getattr(self, 'raid_level', '') def _execute(self, cmd, args=[]): - """Execute a command using arcconf. + """Execute a command Args: args (list): Returns: - str: arcconf output + str: output Raises: RuntimeError: if command fails """ if cmd == 'GETCONFIG': - base_cmd = [cmd, self.adapter_id] + base_cmd = [cmd, self.controller_id] else: - base_cmd = [cmd, self.adapter_id, 'DEVICE', self.channel, self.device] - return self.arcconf._execute(base_cmd + args) + base_cmd = [cmd, self.controller_id, 'DEVICE', self.channel, self.device] + return self.runner._execute(base_cmd + args) def __repr__(self): """Define a basic representation of the class object.""" @@ -59,18 +59,18 @@ def update(self, config=''): section = config or self._get_config() section = section.split(SEPARATOR_SECTION) for line in section[0].split('\n'): - if parser.SEPARATOR_ATTRIBUTE in line: - key, value = parser.convert_property(line) + if runner.SEPARATOR_ATTRIBUTE in line: + key, value = runner.convert_property(line) self.__setattr__(key, value) # pystorcli compliance - key = parser.convert_key_dict(line) + key = runner.convert_key_dict(line) self.facts[key] = value if len(section) == 1: return for idx in range(1, len(section), 2): - props = parser.get_properties(section[idx + 1]) + props = runner.get_properties(section[idx + 1]) if props: - attr = parser.convert_key_attribute(section[idx]) + attr = runner.convert_key_attribute(section[idx]) # pystorcli compliance attr = attr.replace('device_', '') self.__setattr__(attr, props) @@ -87,7 +87,7 @@ def name(self): def _get_config(self): result = self._execute('GETCONFIG', ['PD', self.channel, self.device])[0] - result = parser.cut_lines(result, 4) + result = runner.cut_lines(result, 4) return result def set_state(self, state): @@ -143,7 +143,7 @@ def phyerrorcounters(self): if rc == 2: return {} sata = 'SATA' in result - result = parser.cut_lines(result, 16 if sata else 15) + result = runner.cut_lines(result, 16 if sata else 15) data = {} #TODO: add sata logic if not sata: @@ -151,9 +151,9 @@ def phyerrorcounters(self): if 'No device attached' in phy: continue phy = phy.split('\n') - _, phyid = parser.convert_property(phy[0]) + _, phyid = runner.convert_property(phy[0]) data[phyid] = {} for attr in phy[7:]: - key, value = parser.convert_property(attr) + key, value = runner.convert_property(attr) data[phyid][key] = value return data diff --git a/pyarcconf/parser.py b/pyarcconf/runner.py similarity index 63% rename from pyarcconf/parser.py rename to pyarcconf/runner.py index 3debecf..3f8e301 100644 --- a/pyarcconf/parser.py +++ b/pyarcconf/runner.py @@ -1,21 +1,72 @@ -"""Pyarcconf submodule, which provides methods for easier output parsing.""" +"""Command execute and output parse methods""" +import os +import re +import shutil +from subprocess import Popen, PIPE -SEPARATOR_ATTRIBUTE = ' : ' +SEPARATOR_ATTRIBUTE = ': ' SEPARATOR_SECTION = 56 * '-' +class CMDRunner(): + """This is a simple wrapper for subprocess.Popen()/subprocess.run(). The main idea is to inherit this class and create easy mockable tests. + """ + def __init__(self, path=''): + """Initialize a new MVCLI object. + + Args: + path (str): path to mvcli binary + """ + self.path = self.binaryCheck(path) + + def run(self, args, **kwargs): + """Runs a command and returns the output. + """ + proc = Popen(args, stdout=PIPE, stderr=PIPE, **kwargs) + + _stdout, _stderr = [i.decode('utf8') for i in proc.communicate()] + + return _stdout, _stderr, proc.returncode + + def binaryCheck(self, binary) -> str: + """Verify and return full binary path + """ + _bin = shutil.which(binary) + if not _bin: + raise Exception( + "Cannot find storcli binary '%s'" % (binary)) + return _bin + + def cut_lines(output, start, end=0): """Cut a number of lines from the start and the end. Args: - output (str): command output from arcconf + output (str|list): command output from arcconf start (int): offset from start end (int): offset from end Returns: - str: cutted output + str|list: cutted output """ - output = output.split('\n') - return '\n'.join(output[start:len(output) - end]) + islist = type(output) == list + if not islist: + output = output.split('\n') + output = output[start:len(output) - end] + return output if islist else '\n'.join(output) + + +def sanitize_stdout(output, last_line=True): + islist = type(output) == list + output = output if islist else output.split('\n') + while output and not output[-1]: + del output[-1] + if not output: + return [] if islist else '' + if last_line and last_line in output[-1]: + del output[-1] + while output and not output[-1]: + del output[-1] + return output if islist else '\n'.join(output) def convert_property(key, value=None): @@ -62,11 +113,11 @@ def convert_key_attribute(key): return key for char in [' ', '-', ',', '/']: key = key.replace(char, '_') - for char in ['.']: - key = key.replace(char, '') if key[0].isnumeric(): - # some properties might have a number in first char + # first char might be a number key = '_' + key + # clear special chars + key = re.sub('[^a-zA-Z0-9\_]', '', key) return key.strip() diff --git a/pyarcconf/task.py b/pyarcconf/task.py index 01b30a4..5edf9c5 100644 --- a/pyarcconf/task.py +++ b/pyarcconf/task.py @@ -1,4 +1,4 @@ -"""Pyarcconf submodule, which provides a task representing class.""" +"""Arcconf Task class""" class Task():