├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README ├── README.md ├── bin └── check_megacli ├── pymegacli ├── __init__.py ├── components.py └── parser.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | env/ 3 | dist/ 4 | sdist/ 5 | build/ 6 | *.egg-info/ 7 | *.egg_info/ 8 | .*.un~ 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | pymegacli 2 | Copyright (c) 2014 Uber Technologies, Inc. 3 | The MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include README.md 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MegaCli64 is the command line utility provided by LSI for configuring raid 2 | cards. While all of the functionality is there, the interface can be 3 | challenging to deal with. Here are some examples: 4 | 5 | - case sensitivity + inconsistent case (example: `/opt/MegaRAID/MegaCli/MegaCli64 -LDInfo -Lall -aALL`) 6 | - inconsistent, hard-to-parse output (`BBU GasGauge Status: 0x0128`) 7 | - misleading summary information (components like BBUs will often simultaneously report `State: Optimal` and `Pack is about to fail & should be replaced`, which seems like they should never occur at the same time) 8 | 9 | This library seeks to wrap MegaCli and MegaCli64 and provide an object-oriented interface 10 | to see what the heck is actually going on with your controller. 11 | 12 | At this time, it doesn't support CHANGING anything, just displaying data. This makes it 13 | suitable to use in, e.g., nagios checks, but you still have to remember how to actually 14 | change settings. That might change in the future. 15 | -------------------------------------------------------------------------------- /bin/check_megacli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Example script using pymegacli which is suitable for invocation by nagios 4 | 5 | import argparse 6 | import os 7 | import sys 8 | 9 | import pymegacli 10 | 11 | 12 | OK = 0 13 | WARNING = 1 14 | CRITICAL = 2 15 | UNKNOWN = 3 16 | 17 | 18 | def main(): 19 | CHECKS = {'PD', 'LD', 'BBU'} 20 | 21 | parser = argparse.ArgumentParser() 22 | 23 | parser.add_argument( 24 | '--megacli-path', 25 | default='/opt/MegaRAID/MegaCli/MegaCli64', 26 | help='Path to MegaCli or MegaCli64 (default %(default)s)' 27 | ) 28 | parser.add_argument( 29 | '--check', 30 | choices=CHECKS, 31 | action='append', 32 | help='Subsystems to check. If not passed, defaults to all' 33 | ) 34 | args = parser.parse_args() 35 | 36 | if os.geteuid() != 0: 37 | parser.error('Must run as root!') 38 | 39 | checks = {} 40 | for check in CHECKS: 41 | checks[check] = (not args.check or check in args.check) 42 | 43 | connection = pymegacli.MegaCLIBase(args.megacli_path) 44 | 45 | messages = { 46 | OK: [], 47 | WARNING: [], 48 | CRITICAL: [], 49 | UNKNOWN: [] 50 | } 51 | 52 | def check_component(component): 53 | if component.healthy: 54 | messages[OK].append('%s is healthy' % component.identifier) 55 | else: 56 | for message in component.health_messages: 57 | messages[CRITICAL].append('%s %s' % ( 58 | component.identifier, 59 | message 60 | )) 61 | 62 | controllers = list(connection.controllers) 63 | if not controllers: 64 | messages[OK].append('No MegaRAID controllers found on this host') 65 | else: 66 | for controller in controllers: 67 | if checks['PD']: 68 | for disk in controller.PDs: 69 | check_component(disk) 70 | if checks['LD']: 71 | for logical_device in controller.LDs: 72 | check_component(disk) 73 | if 'WriteBack' not in logical_device['Current Cache Policy']: 74 | messages[WARNING].append('%s has cache policy %s, which does not include WriteBack' % ( 75 | logical_device.identifier, 76 | logical_device['Current Cache Policy'] 77 | )) 78 | if checks['BBU']: 79 | for bbu in controller.BBUs: 80 | check_component(bbu) 81 | if bbu['Learn Cycle Active']: 82 | messages[WARNING].append('%s is in learn cycle' % bbu.identifier) 83 | 84 | if messages[CRITICAL]: 85 | print 'CRITICAL: %s' % '; '.join(messages[CRITICAL]) 86 | return CRITICAL 87 | elif messages[WARNING]: 88 | print 'WARNING: %s' % '; '.join(messages[WARNING]) 89 | return WARNING 90 | elif messages[UNKNOWN]: 91 | print 'UNKNOWN: %s' % '; '.join(messages[UNKNOWN]) 92 | return UNKNOWN 93 | else: 94 | print 'OK: %s' % '; '.join(messages[OK]) 95 | return OK 96 | 97 | 98 | if __name__ == '__main__': 99 | sys.exit(main()) 100 | -------------------------------------------------------------------------------- /pymegacli/__init__.py: -------------------------------------------------------------------------------- 1 | from .components import MegaCLIBase 2 | 3 | __all__ = ['MegaCLIBase'] 4 | 5 | version_info = (0, 1, 5, 5) 6 | __version__ = '.'.join(str(c) for c in version_info) 7 | __author__ = 'James Brown ' 8 | -------------------------------------------------------------------------------- /pymegacli/components.py: -------------------------------------------------------------------------------- 1 | import pipes 2 | import subprocess 3 | import re 4 | 5 | from .parser import BlockParser 6 | from .parser import bail_on 7 | from .parser import colon_field 8 | from .parser import ignore_rule 9 | from .parser import int_or_na 10 | from .parser import not_found_rule 11 | from .parser import oknokbool 12 | from .parser import once_per_block 13 | from .parser import parse_bytes 14 | from .parser import parse_time 15 | from .parser import regexp_match 16 | from .parser import rule 17 | from .parser import yesnobool 18 | 19 | 20 | class MegaCLIBase(object): 21 | def __init__(self, megacli_path, log=None): 22 | self.megacli_path = megacli_path 23 | self.log = log 24 | 25 | def run_command(self, *args): 26 | exit_re = re.compile('^Exit Code: (.*)$') 27 | cmd = [self.megacli_path] + list(args) 28 | if self.log: 29 | self.log.debug('executing: ' + ' '.join(map(pipes.quote, cmd))) 30 | p = subprocess.Popen( 31 | cmd, 32 | shell=False, 33 | stdout=subprocess.PIPE 34 | ) 35 | for line in p.communicate()[0].split('\n'): 36 | emd = exit_re.match(line) 37 | if emd: 38 | # exit code is usually meaningless 39 | rv = int(emd.groups()[0], 16) 40 | rv = rv 41 | else: 42 | yield line 43 | 44 | def extract_by_regex(self, regex, lines, one_only=False): 45 | if isinstance(regex, basestring): 46 | regex = re.compile(regex) 47 | result = [] 48 | for line in lines: 49 | md = regex.match(line) 50 | if md: 51 | if one_only: 52 | return md.groups() 53 | else: 54 | result.append(md.groups) 55 | if one_only: 56 | raise Exception('Expected one match for %s, got %d. Input was %s' % ( 57 | regex, 58 | len(result), 59 | '\n'.join(lines) 60 | )) 61 | return result 62 | 63 | @property 64 | def controller_count(self): 65 | return int(self.extract_by_regex( 66 | r'^Controller Count: ([^.]+)\.$', 67 | self.run_command('-adpCount'), 68 | one_only=True 69 | )[0]) 70 | 71 | @property 72 | def controllers(self): 73 | for i in range(self.controller_count): 74 | yield MegaCLIController(i, self) 75 | 76 | 77 | class Component(object): 78 | REQUIRED_FIELDS = tuple() 79 | 80 | def __init__(self, parent, props=None): 81 | if props is None: 82 | self.props = {} 83 | else: 84 | self.props = props 85 | self.parent = parent 86 | 87 | def __setitem__(self, key, value): 88 | self.props[key] = value 89 | 90 | def __getitem__(self, key): 91 | return self.props[key] 92 | 93 | def get(self, key, default=None): 94 | return self.props.get(key, default) 95 | 96 | @property 97 | def identifier(self): 98 | return NotImplementedError() 99 | 100 | @property 101 | def health_status(self): 102 | return NotImplementedError() 103 | 104 | @property 105 | def health_messages(self): 106 | for bad_key, bad_value in sorted(self.health_status[0].items()): 107 | yield '%s was unexpectedly %r' % (bad_key, bad_value) 108 | 109 | @property 110 | def healthy(self): 111 | return self.health_status[1] 112 | 113 | def __str__(self): 114 | return self.identifier 115 | 116 | @classmethod 117 | def from_output(kls, lines, parent): 118 | lds = [] 119 | for d in kls.PARSER.parse(lines): 120 | args = [] 121 | for f in kls.REQUIRED_FIELDS: 122 | args.append(d.pop(f)) 123 | args.extend([parent, d]) 124 | lds.append(kls(*args)) 125 | return lds 126 | 127 | 128 | class Disk(Component): 129 | ERROR_COUNT_KEYS = ( 130 | 'Media Error Count', 131 | 'Predictive Failure Count', 132 | ) 133 | ERROR_BOOL_KEYS = ('Drive has flagged a S.M.A.R.T alert', ) 134 | REQUIRED_FIELDS = ('Enclosure Device ID', 'Slot Number') 135 | 136 | PARSER = BlockParser(rules=[ 137 | once_per_block(colon_field('Enclosure Device ID', int_or_na)), 138 | rule(colon_field('Slot Number', int)), 139 | rule(colon_field('Other Error Count', int)), 140 | rule(colon_field('Predictive Failure Count', int)), 141 | rule(colon_field('Media Error Count', int)), 142 | rule(colon_field('Drive has flagged a S.M.A.R.T alert', yesnobool)), 143 | ], default_constructor=colon_field(None, str)) 144 | 145 | def __init__(self, enclosure_id, slot_number, parent, props=None): 146 | self.enclosure_id = enclosure_id 147 | self.slot_number = slot_number 148 | self.thresholds = dict( 149 | (k, 0) 150 | for k 151 | in self.ERROR_COUNT_KEYS 152 | ) 153 | super(Disk, self).__init__(parent, props) 154 | 155 | def set_threshold(self, key, value): 156 | self.thresholds[key] = value 157 | 158 | @property 159 | def identifier(self): 160 | return 'PhysDrv [%d:%d]' % (self.enclosure_id, self.slot_number) 161 | 162 | @property 163 | def health_status(self): 164 | status = {} 165 | overall_status = True 166 | for key, value in self.thresholds.items(): 167 | if self.props.get(key, 0) > value: 168 | status[key] = self.props[key] 169 | overall_status = False 170 | for key in self.ERROR_BOOL_KEYS: 171 | if self.props.get(key, 0) != 0: 172 | status[key] = self.props[key] 173 | overall_status = False 174 | # need to allow for JBOD as well 175 | if 'Online' not in self['Firmware state'] and 'JBOD' not in self['Firmware state']: 176 | status['Firmware state'] = self['Firmware state'] 177 | overall_status = False 178 | return status, overall_status 179 | 180 | 181 | class LogicalDevice(Component): 182 | PARSER = BlockParser(rules=[ 183 | once_per_block(colon_field('Virtual Drive', lambda s: int(s.split(' ')[0]))), 184 | rule(colon_field('Bad Blocks Exist', yesnobool)), 185 | rule(colon_field('Size', parse_bytes)), 186 | bail_on('No Virtual Drive Configured'), 187 | ], default_constructor=colon_field(None, str)) 188 | 189 | REQUIRED_FIELDS = ('Name', ) 190 | 191 | def __init__(self, name, parent, props=None): 192 | self.name = name 193 | super(LogicalDevice, self).__init__(parent, props) 194 | 195 | @property 196 | def identifier(self): 197 | return 'VD %r' % self.name 198 | 199 | @property 200 | def health_status(self): 201 | status = {} 202 | if self['State'] != 'Optimal': 203 | status['State'] = self['State'] 204 | return status, not bool(status) 205 | 206 | 207 | class BBU(Component): 208 | BAD_KEYS = ( 209 | 'Pack is about to fail & should be replaced', 210 | 'Remaining Capacity Low', 211 | 'Battery Pack Missing', 212 | 'Battery Replacement required', 213 | 'I2c Errors Detected', 214 | ) 215 | UNEXPECTED_KEYS = ('Temperature',) 216 | 217 | PARSER = BlockParser(rules=[ 218 | ignore_rule(colon_field('BBU status for Adapter')), 219 | not_found_rule(regexp_match(r'\s*The required hardware component is not present.')), 220 | once_per_block(colon_field('BatteryType')), 221 | rule(colon_field('Voltage', oknokbool)), 222 | rule(colon_field('Temperature', oknokbool)), 223 | rule(colon_field('Learn Cycle Requested', yesnobool)), 224 | rule(colon_field('Learn Cycle Active', yesnobool)), 225 | rule(colon_field('Learn Cycle Status', oknokbool)), 226 | rule(colon_field('Learn Cycle Timeout', yesnobool)), 227 | rule(colon_field('I2c Errors Detected', yesnobool)), 228 | rule(colon_field('Battery Pack Missing', yesnobool)), 229 | rule(colon_field('Battery Replacement required', yesnobool)), 230 | rule(colon_field('Remaining Capacity Low', yesnobool)), 231 | rule(colon_field('Periodic Learn Required', yesnobool)), 232 | rule(colon_field('Transparent Learn', yesnobool)), 233 | rule(colon_field('No space to cache offload', yesnobool)), 234 | rule(colon_field('Pack is about to fail & should be replaced', yesnobool)), 235 | rule(colon_field('Cache Offload premium feature required', yesnobool)), 236 | rule(colon_field('Module microcode update required', yesnobool)), 237 | ], default_constructor=colon_field(None, str)) 238 | 239 | @property 240 | def identifier(self): 241 | return self.props['BatteryType'] 242 | 243 | @property 244 | def health_status(self): 245 | status = {} 246 | if self['Battery State'] != 'Optimal': 247 | status['Battery State'] = self['Battery State'] 248 | for key in self.BAD_KEYS: 249 | if self.get(key): 250 | status[key] = self[key] 251 | for key in self.UNEXPECTED_KEYS: 252 | if not self.get(key): 253 | status[key] = 'not ok' 254 | return status, not bool(status) 255 | 256 | 257 | class MegaCLIController(object): 258 | def __init__(self, controller_number, parent): 259 | self.controller_number = controller_number 260 | self.parent = parent 261 | 262 | @property 263 | def patrol_read_status(self): 264 | """patrol reads are the background disk re-reads that constantly 265 | happen to detect failed blocks.""" 266 | parser = BlockParser(rules=[ 267 | rule(colon_field('Patrol Read Mode')), 268 | rule(colon_field('Patrol Read Execution Delay', parse_time)), 269 | rule(colon_field('Number of iterations completed', int)), 270 | rule(colon_field('Current State')), 271 | ]) 272 | return parser.parse( 273 | self.parent.run_command('-AdpPR', '-Info', '-a%d' % self.controller_number) 274 | )[0] 275 | 276 | @property 277 | def PDs(self): 278 | return Disk.from_output(self.parent.run_command( 279 | '-PDList', 'a%d' % self.controller_number 280 | ), self) 281 | 282 | @property 283 | def LDs(self): 284 | return LogicalDevice.from_output(self.parent.run_command( 285 | '-LDInfo', '-Lall', '-a%d' % self.controller_number 286 | ), self) 287 | 288 | @property 289 | def BBUs(self): 290 | return BBU.from_output(self.parent.run_command( 291 | '-AdpBbuCmd -GetBbuStatus', '-a%d' % self.controller_number 292 | ), self) 293 | -------------------------------------------------------------------------------- /pymegacli/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | # utilities for parsing the weird output of megacli 4 | 5 | import functools 6 | import re 7 | 8 | NewBlock = object() 9 | 10 | IGNORE = object() 11 | BAIL = object() 12 | 13 | # return this sigil when the output contains a line 14 | # indicating that no object was found 15 | NOT_FOUND = object() 16 | 17 | 18 | def colon_field(expected_key, ty=str): 19 | COLON_SEPARATED_RE = re.compile(r'\s*(?P.*\w)\s*:\s*(?P.*)$') 20 | 21 | def parser(line): 22 | md = COLON_SEPARATED_RE.match(line) 23 | if not md: 24 | return 25 | key, value = md.groups() 26 | if expected_key is None or key == expected_key: 27 | return key, ty(value) 28 | else: 29 | return None 30 | 31 | return parser 32 | 33 | 34 | def regexp_match(regex): 35 | if not hasattr(regex, 'match'): 36 | regex = re.compile(regex) 37 | 38 | def parser(line): 39 | return regex.match(line) 40 | 41 | return parser 42 | 43 | 44 | def yesnobool(s): 45 | return s.lower() in ('yes', 'true') 46 | 47 | 48 | def oknokbool(s): 49 | return s.lower() == 'ok' 50 | 51 | 52 | def int_or_na(s): 53 | if s.upper() == 'N/A': 54 | return -1 55 | else: 56 | return int(s) 57 | 58 | 59 | def parse_bytes(s): 60 | size, units = s.strip().split(' ') 61 | size = float(size) 62 | multiplier = { 63 | 'PB': 1000 * 1000 * 1000 * 1000 * 1000, 64 | 'TB': 1000 * 1000 * 1000 * 1000, 65 | 'GB': 1000 * 1000 * 1000, 66 | 'MB': 1000 * 1000, 67 | 'KB': 1000, 68 | 'B': 1, 69 | }.get(units, 1) 70 | return int(size * multiplier) 71 | 72 | 73 | def parse_time(s): 74 | size, units = s.strip().split(' ') 75 | size = int(size) 76 | multiplier = { 77 | 'days': 86400, 78 | 'hours': 3600, 79 | 'minutes': 60, 80 | 'seconds': 1, 81 | }.get(units, 1) 82 | return int(size * multiplier) 83 | 84 | 85 | def once_per_block(line_parser): 86 | @functools.wraps(line_parser) 87 | def parse(line): 88 | rv = line_parser(line) 89 | if rv is not None: 90 | return rv, NewBlock 91 | else: 92 | return rv, None 93 | return parse 94 | 95 | 96 | def bail_on(substring): 97 | def parse(line): 98 | if substring in line: 99 | return BAIL, None 100 | else: 101 | return None, None 102 | return parse 103 | 104 | 105 | def rule(line_parser): 106 | @functools.wraps(line_parser) 107 | def parse(line): 108 | return line_parser(line), None 109 | return parse 110 | 111 | 112 | def ignore_rule(line_parser): 113 | @functools.wraps(line_parser) 114 | def parse(line): 115 | resp = line_parser(line) 116 | if resp is not None: 117 | return IGNORE, None 118 | else: 119 | return resp, None 120 | return parse 121 | 122 | 123 | def not_found_rule(line_parser): 124 | @functools.wraps(line_parser) 125 | def parse(line): 126 | resp = line_parser(line) 127 | if resp is not None: 128 | return NOT_FOUND, None 129 | else: 130 | return resp, None 131 | return parse 132 | 133 | 134 | class BlockParser(object): 135 | def __init__(self, rules, default_constructor=lambda s: None): 136 | self.rules = rules 137 | self.default_constructor = default_constructor 138 | 139 | def parse(self, lines): 140 | rv = [] 141 | current_state = {} 142 | for line in lines: 143 | for a_rule in self.rules: 144 | resp, create_new_block = a_rule(line) 145 | if resp is BAIL: 146 | lines = [] 147 | current_state = {} 148 | break 149 | if current_state and create_new_block: 150 | rv.append(current_state) 151 | current_state = {} 152 | if resp is NOT_FOUND: 153 | return [] 154 | if resp is IGNORE: 155 | break 156 | if resp is not None: 157 | current_state[resp[0]] = resp[1] 158 | break 159 | else: 160 | resp = self.default_constructor(line) 161 | if resp is not None: 162 | current_state[resp[0]] = resp[1] 163 | if current_state: 164 | rv.append(current_state) 165 | return rv 166 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=100 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='pymegacli', 8 | version='0.1.5.6', 9 | author='James Brown', 10 | author_email='jbrown@uber.com', 11 | url='http://github.com/uber/pymegacli', 12 | description='object-oriented API around the MegaCLI tool for administrating LSI RAID cards', 13 | license='MIT', 14 | classifiers=[ 15 | 'Programming Language :: Python', 16 | 'Operating System :: OS Independent', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Topic :: System :: Hardware', 19 | 'Topic :: System :: Monitoring', 20 | 'Topic :: System :: Systems Administration', 21 | 'Intended Audience :: System Administrators', 22 | 'Development Status :: 4 - Beta', 23 | ], 24 | packages=['pymegacli'], 25 | scripts=['bin/check_megacli'], 26 | long_description=open('README.md', 'r').read(), 27 | ) 28 | --------------------------------------------------------------------------------