elesai 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright (c) 2012 by Evernote Corporation, All rights reserved.
3
+ *
4
+ * Use of the source code and binary libraries included in this package
5
+ * is permitted under the following terms:
6
+ *
7
+ * Redistribution and use in source and binary forms, with or without
8
+ * modification, are permitted provided that the following conditions
9
+ * are met:
10
+ *
11
+ * 1. Redistributions of source code must retain the above copyright
12
+ * notice, this list of conditions and the following disclaimer.
13
+ * 2. Redistributions in binary form must reproduce the above copyright
14
+ * notice, this list of conditions and the following disclaimer in the
15
+ * documentation and/or other materials provided with the distribution.
16
+ *
17
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ *
28
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
29
+ * not use this file except in compliance with the License. You may obtain a
30
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
31
+ */
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # OVERVIEW
2
+
3
+ *Elesai* is a wrapper around LSI's `MegaCli` utility that provides access to common types of information about the RAID controllers via a `show` action without the need to speak martian. It is a line-oriented tool so that it can be combined with other Unix command-line tools to process and manipulate the data (i.e., `sed`, `awk`, and friends). It also provides a `check` action (currently as a Nagios plugin in both active and passive modes) which monitors the health of the array and its components and reports it accordingly (this is not yet configurable).
4
+
5
+ # SYNOPSIS
6
+
7
+ elesai [global-options] <action> [action-options] [<component>]
8
+
9
+ where:
10
+
11
+ * `<action>` is one of `show` or `check`
12
+ * `<component>` is one of `virtualdrive` (or `vd`) or `physicaldrive` (or `pd`) (for `show` action)
13
+
14
+ Global options include:
15
+
16
+ * `-d`, `--debug`: enable *debug* mode
17
+ * `-f`, `--fake DIRECTORY`: specifies path to directory containing output of MegaCli invocations:
18
+ * `ldlist_aall`: output from `MegaCli -pdlist -aall`
19
+ * `ldpdinfo_aall`: output from `MegaCli -ldpdinfo -aall`
20
+ * `-m`, `--megacli MEGACLI`: path to `MegaCli` binary (if noth in `$PATH`)
21
+ * `-a`, `--about`: display general information about `elesai`
22
+ * `-V`, `--version`: display `elesai`'s version
23
+
24
+ The `<check>` can have options specific to itself:
25
+
26
+ * `-M`, `--monitor MONITOR`: Monitoring system (default: `nagios`)
27
+ * `-m`, `--mode [active|passive]`: Monitoring mode
28
+ * `-H`, `--nsca_hostname HOSTNAME`: NSCA hostname to send passive checks
29
+ * `-c`, `--config CONFIG`: Path to Senedsa (send_nsca) configuration
30
+ * `-S`, `--svc_descr SVC_DESCR`: Nagios service description
31
+
32
+ *Elesai* uses [Senedsa](https://rubygems.org/gems/senedsa "Senedsa") for Nagios passive check submission, which can use a configuration file to set options.
33
+
34
+ # Invocations
35
+
36
+ ## Normal Invocation
37
+
38
+ root@boxie:~# elesai show pd | sort -k 4
39
+ [PD] e253s3 27 online:spunup 1.82TB HDD SATA 0 0 9WM1B7VDST32000644NS SN11
40
+ [PD] e252s7 23 online:spunup 1.82TB HDD SATA 0 0 9WM1FX1LST32000644NS SN11
41
+ [PD] e253s0 24 online:spunup 1.82TB HDD SATA 0 0 9WM1GF2NST32000644NS SN11
42
+ [PD] e253s1 25 online:spunup 1.82TB HDD SATA 0 0 9WM1GY85ST32000644NS SN11
43
+ [PD] e252s6 22 online:spunup 1.82TB HDD SATA 0 0 9WM1GYKJST32000644NS SN11
44
+ [PD] e253s2 26 online:spunup 1.82TB HDD SATA 0 0 9WM1HA0NST32000644NS SN11
45
+ [PD] e253s5 29 online:spunup 1.82TB HDD SATA 0 0 9WM7L834ST32000644NS SN12
46
+ [PD] e252s4 12 online:spunup 223.06GB SSD SATA 0 0 CVPR138405AQ300EGN INTEL SSDSA2CW300G3 4PC10362
47
+ [PD] e252s2 9 online:spunup 223.06GB SSD SATA 0 0 CVPR140100MQ300EGN INTEL SSDSA2CW300G3 4PC10362
48
+ [PD] e252s0 11 online:spunup 223.06GB SSD SATA 0 0 CVPR140201KZ300EGN INTEL SSDSA2CW300G3 4PC10362
49
+ [PD] e252s1 10 online:spunup 223.06GB SSD SATA 0 0 CVPR141300K9300EGN INTEL SSDSA2CW300G3 4PC10362
50
+ [PD] e252s3 8 online:spunup 223.06GB SSD SATA 0 0 CVPR141301KG300EGN INTEL SSDSA2CW300G3 4PC10362
51
+ [PD] e253s4 30 failed:spunup 1.82TB HDD SATA 0 0 9WM5Y4AEST32000644NS SN12
52
+ [PD] e252s5 13 hotspare:spunup 223.06GB SSD SATA 0 0 CVPR140100TT300EGN INTEL SSDSA2CW300G3 4PC10362
53
+
54
+ ## Remote Invocation
55
+
56
+ Pipe output to `elesai`:
57
+
58
+ root@boxie:~# ssh server.example.com sudo MegaCli -pdlist -aall | elesai show pd
59
+ [PD] e252s0 16 online:spunup 278.46GB HDD SAS 0 0 a0 SEAGATE ST3300657SS 00066SJ01W4G
60
+ [PD] e252s1 17 online:spunup 278.46GB HDD SAS 0 0 a0 SEAGATE ST3300657SS 00066SJ01TTR
61
+ [PD] e252s2 20 online:spunup 278.46GB HDD SAS 0 0 a0 SEAGATE ST3300657SS 00066SJ02BCX
62
+ [PD] e252s3 21 online:spunup 278.46GB HDD SAS 0 0 a0 SEAGATE ST3300657SS 00066SJ025LP
63
+ [PD] e252s4 18 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1G6MXST32000644NS SN11
64
+ [PD] e252s5 28 online:spunup 1.82TB HDD SATA 0 0 a0 9WM0FFYCST32000644NS SN11
65
+ [PD] e252s6 22 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1GYKJST32000644NS SN11
66
+ [PD] e252s7 23 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1FX1LST32000644NS SN11
67
+ [PD] e253s0 24 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1GF2NST32000644NS SN11
68
+ [PD] e253s1 25 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1GY85ST32000644NS SN11
69
+ [PD] e253s2 26 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1HA0NST32000644NS SN11
70
+ [PD] e253s3 27 online:spunup 1.82TB HDD SATA 0 0 a0 9WM1B7VDST32000644NS SN11
71
+ [PD] e253s4 30 online:spunup 1.82TB HDD SATA 0 0 a0 9WM5Y4AEST32000644NS SN12
72
+ [PD] e253s5 29 online:spunup 1.82TB HDD SATA 0 0 a0 9WM7L834ST32000644NS SN12
73
+ [PD] e253s6 31 unconfigured(bad): 0.00KB HDD SAS 0 0 a0 SEAGATE ST33000650SS 0003Z29182VM
74
+ [PD] e253s7 32 unconfigured(bad): 0.00KB HDD SAS 0 0 a0 SEAGATE ST33000650SS 0003Z291A3QV
75
+
76
+ # STATUS
77
+
78
+ Very much in progress. The tool does not yet poke MegaCli itself.
79
+
80
+
81
+
data/bin/check_elesai ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'socket'
7
+ require 'rubygems'
8
+ require 'elesai'
9
+ require 'elesai/cli'
10
+
11
+ module Elesai
12
+
13
+ ID = File.basename($PROGRAM_NAME).to_sym
14
+
15
+ app = CLI.new(ARGV)
16
+ app.run
17
+
18
+ end
data/bin/elesai ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'socket'
7
+ require 'rubygems'
8
+ require 'elesai'
9
+ require 'elesai/cli'
10
+
11
+ module Elesai
12
+
13
+ ID = File.basename($PROGRAM_NAME).to_sym
14
+
15
+ app = CLI.new(ARGV)
16
+ app.run
17
+
18
+ end
@@ -0,0 +1,5 @@
1
+ require 'elesai/version'
2
+ module Elesai
3
+ ME = :elesai
4
+ ABOUT = "#{ME} v#{VERSION}\nhttps://github.com/evernote/ops-#{ME}\nCopyright (c) 2012 by Evernote Corporation\nLicensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)"
5
+ end
data/lib/elesai/cli.rb ADDED
@@ -0,0 +1,203 @@
1
+ require 'optparse'
2
+ require 'senedsa'
3
+ include Senedsa
4
+ require 'elesai/version'
5
+ require 'elesai/about'
6
+
7
+ module Elesai
8
+
9
+ class CLI
10
+
11
+ COMMANDS = %w(show check)
12
+ COMPONENTS = %w(virtualdrive vd physicaldrive pd)
13
+ DEFAULT_CONFIG_FILE = File.join(ENV['HOME'],"/.senedsa/config")
14
+
15
+ def initialize(arguments)
16
+ @arguments = arguments
17
+
18
+ @global_options = { :debug => false, :megacli => 'MegaCli' }
19
+ @action_options = { :monitor => 'nagios', :mode => 'active' }
20
+ @action = nil
21
+ end
22
+
23
+ def run
24
+ begin
25
+ parsed_options?
26
+
27
+ @log = Elesai::Logger.instance.log
28
+ @log.level = Log4r::INFO unless @global_options[:debug]
29
+
30
+ config_options?
31
+ arguments_valid?
32
+ options_valid?
33
+ process_options
34
+ process_arguments
35
+ process_command
36
+
37
+ rescue => e #ArgumentError, OptionParser::MissingArgument, Senedsa::SendNsca::ConfigurationError => e
38
+ output_message e.message, 1
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def parsed_options?
45
+ opts = OptionParser.new
46
+
47
+ opts.banner = "Usage: #{ID} [options] <action> [options]"
48
+ opts.separator ""
49
+ opts.separator "Actions:"
50
+ opts.separator " show Displays component information"
51
+ opts.separator " check Performs health checks"
52
+ opts.separator ""
53
+ opts.separator "General options:"
54
+ opts.on('-m', '--megacli MEGACLI', String, "Path to MegaCli binary") { |megacli| @global_options[:megacli] = megacli }
55
+ opts.on('-f', '--fake DIRECTORY', String, "Path to directory with Megacli output") { |dir| @global_options[:fake] = dir }
56
+ opts.on('-d', '--debug', "Enable debug mode") { @global_options[:debug] = true}
57
+ opts.on('-a', '--about', "Display #{ID} information") { output_message ABOUT, 0 }
58
+ opts.on('-V', '--version', "Display #{ID} version") { output_message VERSION, 0 }
59
+ opts.on_tail('--help', "Show this message") { @global_options[:HELP] = true }
60
+
61
+ actions = {
62
+ :show => OptionParser.new do |aopts|
63
+ aopts.banner = "Usage: #{ID} [options] show <component>"
64
+ aopts.separator ""
65
+ aopts.separator " <component> is physicaldisk|pd, virtualdisk|vd"
66
+ end,
67
+ :check => OptionParser.new do |aopts|
68
+ aopts.banner = "Usage: #{ID} [options] check [check_options]"
69
+ aopts.separator ""
70
+ aopts.separator "Check Options"
71
+ aopts.on('-M', '--monitor [nagios]', [:nagios], "Monitoring system") { |monitor| @action_options[:monitor] = monitor }
72
+ aopts.on('-m', '--mode [active|passive]', [:active, :passive], "Monitoring mode") { |mode| @action_options[:mode] = mode }
73
+ aopts.on('-H', '--nsca_hostname HOSTNAME', String, "NSCA hostname to send passive checks") { |nsca_hostname| @action_options[:nsca_hostame] = nsca_hostname }
74
+ aopts.on('-c', '--config CONFIG', String, "Path to Senedsa (send_nsca) configuration" ) { |config| @action_options[:senedsa_config] = config }
75
+ aopts.on('-S', '--svc_descr SVC_DESR', String, "Nagios service description") { |svc_descr| @action_options[:svc_descr] = svc_descr }
76
+ end
77
+ }
78
+
79
+ opts.order!
80
+ output_message opts, 0 if @arguments.size == 0 or @global_options[:HELP]
81
+
82
+ @action = ARGV.shift.to_sym
83
+ actions[@action].order!
84
+ end
85
+
86
+ def config_options?
87
+ cfg_file = nil
88
+ cfg_file = @action_options[:senedsa_config] unless @action_options[:senedsa_config].nil?
89
+ cfg_file = DEFAULT_CONFIG_FILE if @action_options[:senedsa_config].nil? and File.readable? DEFAULT_CONFIG_FILE
90
+
91
+ unless cfg_file.nil?
92
+ @action_options.merge!(Senedsa::SendNsca.configure(cfg_file))
93
+ @action_options[:senedsa_config] = cfg_file
94
+ end
95
+ end
96
+
97
+ def arguments_valid?
98
+ true
99
+ end
100
+
101
+ def options_valid?
102
+ case @action
103
+ when :check
104
+ raise OptionParser::MissingArgument, "NSCA hostname (-H) must be specified" if @action_options[:nsca_hostname].nil?
105
+ raise OptionParser::MissingArgument, "service description (-S) must be specified" if @action_options[:svc_descr].nil?
106
+ end
107
+ end
108
+
109
+ def process_options
110
+ true
111
+ end
112
+
113
+ def process_arguments
114
+ @action_options[:hint] = @arguments[0].nil? ? nil : @arguments[0].to_sym
115
+ true
116
+ end
117
+
118
+ def process_command
119
+
120
+ @lsi = LSIArray.new(:megacli => @global_options[:megacli], :fake => @global_options[:fake], :hint => @action_options[:hint])
121
+
122
+ case @action
123
+ when :show then run_show
124
+ when :check then run_check
125
+ end
126
+
127
+ end
128
+
129
+ def run_show
130
+
131
+ raise ArgumentError, "missing component" if @arguments.size == 0
132
+ component = @arguments[0]
133
+
134
+ case component
135
+ when 'virtualdrive', 'vd'
136
+ @lsi.virtualdrives.each do |virtualdrive|
137
+ print "#{virtualdrive}\n"
138
+ end
139
+ when 'physicaldrive', 'pd'
140
+ @lsi.physicaldrives.each do |id,physicaldrive|
141
+ print "#{physicaldrive}\n"
142
+ end
143
+ else
144
+ raise ArgumentError, "invalid component #{component}"
145
+ end
146
+ end
147
+
148
+ def run_check
149
+
150
+ plugin_output = ""
151
+ plugin_status = ""
152
+
153
+ @lsi.physicaldrives.each do |id,physicaldrive|
154
+ drive_plugin_string = "[PD:#{physicaldrive._id}:#{physicaldrive[:size]}:#{physicaldrive[:mediatype]}:#{physicaldrive[:pdtype]}]"
155
+ unless physicaldrive[:firmwarestate].state == :online or physicaldrive[:firmwarestate].state == :hotspare
156
+ plugin_output += " #{drive_plugin_string}:#{physicaldrive[:firmwarestate].state}"
157
+ plugin_status = :warning if plugin_status.empty?
158
+ end
159
+ unless physicaldrive[:mediaerrorcount].to_i == 0
160
+ plugin_output += " #{drive_plugin_string}:me:#{physicaldrive[:mediaerrorcount]}"
161
+ plugin_status = :warning if plugin_status.empty?
162
+ end
163
+ unless physicaldrive[:predictivefailurecount].to_i == 0
164
+ plugin_output += " #{drive_plugin_string}:pf:#{physicaldrive[:predictivefailurecount]}"
165
+ plugin_status = :warning if plugin_status.empty?
166
+ end
167
+ end
168
+
169
+ plugin_output = "no RAID subsystems errors found" if plugin_output.empty? and plugin_status.empty?
170
+ plugin_status = :ok if plugin_status.empty?
171
+
172
+ case @action_options[:monitor]
173
+ when 'nagios'
174
+ case @action_options[:mode]
175
+ when 'active'
176
+ puts "#{plugin_status}: #{plugin_output}"
177
+ exit SendNsca::STATUS[plugin_status]
178
+ when 'passive'
179
+ sn = SendNsca.new Socket.gethostname,'raid/lsi'
180
+ sn.nsca_hostname = @command_opts[:nsca_hostname]
181
+ begin
182
+ sn.send plugin_status , plugin_output
183
+ rescue SendNsca::SendNscaError => e
184
+ $stderr.write "#{ME}: error: send_nsca failed: #{e.message}\n"
185
+ exit
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def output_message(message, exitstatus=nil)
192
+ m = (! exitstatus.nil? and exitstatus > 0) ? "%s: error: %s" % [ID, message] : message
193
+ $stderr.write "#{m}\n"
194
+ exit exitstatus unless exitstatus.nil?
195
+ end
196
+
197
+ end
198
+ end
199
+
200
+
201
+
202
+
203
+
@@ -0,0 +1,20 @@
1
+ require 'singleton'
2
+ require 'log4r'
3
+
4
+ module Elesai
5
+
6
+ class Logger
7
+
8
+ include Singleton
9
+
10
+ attr_reader :log
11
+
12
+ def initialize
13
+ @log = Log4r::Logger.new("elesai")
14
+ @log.add Log4r::StderrOutputter.new('console', :formatter => Log4r::PatternFormatter.new(:pattern => "%c [%l] %m"), :level => Log4r::DEBUG)
15
+ end
16
+
17
+
18
+
19
+ end
20
+ end
data/lib/elesai/lsi.rb ADDED
@@ -0,0 +1,210 @@
1
+ module Elesai
2
+
3
+ class LSIArray
4
+
5
+ attr_reader :adapters, :virtualdrives, :physicaldrives, :enclosures
6
+
7
+ def initialize(opts)
8
+ @adapters = []
9
+ @virtualdrives = []
10
+ @physicaldrives = {}
11
+ @enclosures = []
12
+ @spans = []
13
+
14
+ case opts[:hint]
15
+ when :pd,:physicaldrive
16
+ PDlist_aAll.new.parse!(self,opts)
17
+ when :vd, :virtualdrive
18
+ puts "vd!"
19
+ LDPDinfo_aAll.new.parse!(self,opts)
20
+ else
21
+ PDlist_aAll.new.parse!(self,opts)
22
+ LDPDinfo_aAll.new.parse!(self,opts)
23
+ end
24
+ end
25
+
26
+ def add_adapter(a)
27
+ @adapters[a[:id]] = a if @adapters[a[:id]].nil?
28
+ end
29
+
30
+ def add_virtualdrive(vd)
31
+ @virtualdrives.push(vd)
32
+ end
33
+
34
+ def add_physicaldrive(pd)
35
+ @physicaldrives[pd._id] = pd if @physicaldrives[pd._id].nil?
36
+ @physicaldrives[pd._id]
37
+ end
38
+
39
+ def to_s
40
+ lsiarrayout = "LSI Array\n"
41
+ @adapters.each do |adapter|
42
+ lsiarrayout += " adapter #{adapter.id}\n"
43
+ adapter.virtualdrives.each do |virtualdrive|
44
+ lsiarrayout += " +--+ #{virtualdrive.to_str}\n"
45
+ virtualdrive.physicaldrives.each do |id,physicaldrive|
46
+ lsiarrayout += " | |-- pd #{physicaldrive.to_str}\n"
47
+ end
48
+ end
49
+ end
50
+ lsiarrayout
51
+ end
52
+
53
+ ### Adapter
54
+
55
+ class Adapter < Hash
56
+
57
+ def initialize
58
+ self[:virtualdrives] = []
59
+ self[:physicaldrives] = {}
60
+ super
61
+ end
62
+
63
+ def _id
64
+ "#{self[:id]}"
65
+ end
66
+
67
+ def type
68
+ :adapter
69
+ end
70
+
71
+ def type_of?(type)
72
+ self.type == type
73
+ end
74
+
75
+ def inspect
76
+ "#{self.class}:#{self.__id__}"
77
+ end
78
+
79
+ def add_physicaldrive(pd)
80
+ self[:physicaldrives][pd._id] = pd unless self[:physicaldrives][pd._id].nil?
81
+ end
82
+
83
+ end
84
+
85
+ ### Virtual Drive
86
+
87
+ class VirtualDrive < Hash
88
+
89
+ STATES = {
90
+ :optimal => 'Optimal',
91
+ :degraded => 'Degraded',
92
+ :partial_degraded => 'Partial Degraded',
93
+ :failed => 'Failed',
94
+ :offline => 'Offline'
95
+ }
96
+
97
+ class Size < Struct.new(:number, :unit)
98
+ def to_s ; "%8.2f%s" % [self.number,self.unit] end
99
+ end
100
+ class RaidLevel < Struct.new(:primary, :secondary)
101
+ def to_s ; "raid%s:raid%s" % [self.primary,self.secondary] end
102
+ end
103
+
104
+ def initialize
105
+ self[:physicaldrives] = []
106
+ end
107
+
108
+ def _id
109
+ self[:targetid]
110
+ end
111
+
112
+ def type
113
+ :virtualdrive
114
+ end
115
+
116
+ def type_of?(type)
117
+ self.type == type
118
+ end
119
+
120
+ def inspect
121
+ "#{self.class}:#{self.__id__}"
122
+ end
123
+
124
+ def add_physicaldrive(pd)
125
+
126
+ end
127
+
128
+ def to_s
129
+ "[VD] %4s %18s %s %s %d" % [ self._id, self[:state], self[:size], self[:raidlevel], self[:physicaldrives].size ]
130
+ end
131
+
132
+ end
133
+
134
+ ### Physical Drive
135
+
136
+ class PhysicalDrive < Hash
137
+
138
+ STATES = {
139
+ :online => 'Online',
140
+ :unconfigured_good => 'Unconfigured(good)',
141
+ :hotspare => 'Hotspare',
142
+ :failed => 'Failed',
143
+ :rebuild => 'Rebuild',
144
+ :unconfigured_bad => 'Unconfigured(bad)',
145
+ :missing => 'Missing',
146
+ :offline => 'Offline'
147
+ }
148
+
149
+ SPINS = {
150
+ :spun_up => 'Spun up'
151
+ }
152
+
153
+ class Size < Struct.new(:number, :unit)
154
+ def to_s ; "%8.2f%s" % [self.number,self.unit] end
155
+ end
156
+ class FirmwareState < Struct.new(:state, :spin)
157
+ def to_s
158
+ "#{self.state}:#{self.spin}"
159
+ end
160
+ end
161
+
162
+ def initialize
163
+ self[:_adapter] = nil
164
+ self[:_virtualdrives] = []
165
+ end
166
+
167
+ def _id
168
+ "e#{self[:enclosuredeviceid].to_s}s#{self[:slotnumber].to_s}".to_sym
169
+ end
170
+
171
+ def type
172
+ :physicaldrive
173
+ end
174
+
175
+ def type_of?(type)
176
+ self.type == type
177
+ end
178
+
179
+ def to_s
180
+ keys = [:deviceid, :firmwarestate, :coercedsize, :mediatype, :pdtype, :mediaerrorcount, :predictivefailurecount,:inquirydata]
181
+ #"[PD] %8s %4s %19s %8.2f%s %5s %5s %3d %3d %s" % [ self.id, @deviceid, "#{@state}:#{@spin}", @_size.number, @_size.unit, @mediatype, @pdtype, @mediaerrors, @predictivefailure, @inquirydata ]
182
+ "[PD] %8s %4s %19s %s %5s %5s %3d %3d a%s %s" % [ self._id, self[:deviceid], self[:firmwarestate], self[:coercedsize], self[:mediatype], self[:pdtype], self[:mediaerrorcount], self[:predictivefailurecount], self[:_adapter]._id, self[:inquirydata] ]
183
+ end
184
+
185
+ def inspect
186
+ "#{self.class}:#{self.__id__}"
187
+ end
188
+
189
+ def add_adapter(a)
190
+ self[:_adapter] = a
191
+ end
192
+
193
+ def get_adapter
194
+ self[:_adapter]
195
+ end
196
+
197
+ def add_virtualdrive(vd)
198
+ self[:_virtualdrives][vd._id] = vd if self[:_virtualdrives][vd._id].nil?
199
+ end
200
+
201
+ def get_virtualdrive(vd_id)
202
+ self[:_virtualdrives][vd_id]
203
+ end
204
+
205
+ def get_virtualdrives
206
+ self[:_virtualdrives]
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,393 @@
1
+ require 'workflow'
2
+ require 'open3'
3
+
4
+ module Elesai
5
+
6
+ class Megacli
7
+
8
+ include Workflow
9
+
10
+ ADAPTER_RE = /^Adapter\s+#*(?<value>\d+)/
11
+ VIRTUALDRIVE_RE = /^Virtual\s+Drive:\s+\d+\s+\((?<key>Target\s+Id):\s+(?<value>\d+)\)/
12
+ SPAN_RE = /^Span:\s+(?<value>\d+)/
13
+ PHYSICALDRIVE_RE = /^(?<key>Enclosure\s+Device\s+ID):\s+(?<value>\d+)/
14
+ ATTRIBUTE_RE = /^(?<key>[A-Za-z0-9()\s#]+):(?<value>.*)/
15
+ EXIT_RE = /^Exit Code: /
16
+
17
+ ### Context
18
+
19
+ class Context
20
+
21
+ def initialize(current_state,lsi)
22
+ current_state.meta[:context] = { :stack => [], :adapter => nil, :virtualdrive => nil, :physicaldrive => nil }
23
+ @context = current_state.meta[:context]
24
+ @lsi = lsi
25
+ @log = Elesai::Logger.instance.log
26
+ end
27
+
28
+ def open(component)
29
+ @log.debug " * Open #{component.inspect}"
30
+ @context[:stack].push(component)
31
+ @context[component.type] = component
32
+ @log.debug " + context: #{@context[:stack]}"
33
+ end
34
+
35
+ def flash!(new_state)
36
+ new_state.meta[:context] = @context
37
+ @context = nil
38
+ @context = new_state.meta[:context]
39
+ @log.debug " + Flash context: #{@context[:stack]}"
40
+ end
41
+
42
+ def close
43
+ component = @context[:stack].pop
44
+ @context[component.type] = nil
45
+ @log.debug " * Close #{component.inspect}"
46
+ if component.type_of? :physicaldrive
47
+ pd = @lsi.add_physicaldrive(component)
48
+ pd.add_adapter(adapter)
49
+ pd.add_virtualdrive(virtualdrive) unless virtualdrive.nil?
50
+ adapter.add_physicaldrive(pd)
51
+ elsif component.type_of? :virtualdrive
52
+ vd = @lsi.add_virtualdrive(component)
53
+ elsif component.type_of? :adapter
54
+ @lsi.add_adapter(component)
55
+ end
56
+ @log.debug " + context: #{@context[:stack]}"
57
+ end
58
+
59
+ def current
60
+ @context[:stack][-1]
61
+ end
62
+
63
+ def adapter
64
+ @context[:adapter]
65
+ end
66
+
67
+ def virtualdrive
68
+ @context[:virtualdrive]
69
+ end
70
+
71
+ def physicaldrive
72
+ @context[:physicaldrive]
73
+ end
74
+
75
+ end
76
+
77
+ ### State Machine Handlers
78
+
79
+ # Start
80
+
81
+ def on_start_exit(new_state, event, *args)
82
+ @log.debug " [#{current_state}]: on_exit : #{event} -> #{new_state}; args: #{args}"
83
+ @context = Context.new(current_state,@lsi)
84
+ end
85
+
86
+ # Adapter
87
+
88
+ def adapter_line(adapter,key,value)
89
+ @log.debug " [#{current_state}] event adapter_line: new #{adapter.inspect}"
90
+ adapter[key.to_sym] = value.to_i
91
+ end
92
+
93
+ def on_adapter_entry(old_state, event, *args)
94
+ @log.debug " [#{current_state}] on_entry: leaving #{old_state}; args: #{args}"
95
+
96
+ @context.close unless @context.current.nil? or @context.current.type_of? :adapter
97
+ @context.open args[0]
98
+
99
+ end
100
+
101
+ def on_adapter_exit(new_state, event, *args)
102
+ @log.debug " [#{current_state}] on_exit: entering #{new_state}; args: #{args}"
103
+ @context.flash!(new_state)
104
+ end
105
+
106
+ # Virtual Drive
107
+
108
+ def virtualdrive_line(virtualdrive,key,value)
109
+ @log.debug " [#{current_state}] event: virtualdrive_line: new #{virtualdrive.inspect}"
110
+ virtualdrive[key.to_sym] = value.to_i
111
+ end
112
+
113
+ def on_virtualdrive_entry(old_state, event, *args)
114
+ @log.debug " [#{current_state}] on_entry: leaving #{old_state}; args: #{args}"
115
+
116
+ unless @context.current.nil?
117
+ if @context.current.type_of? :virtualdrive
118
+ @context.close
119
+ end
120
+ end
121
+ virtualdrive = args[0]
122
+ @context.open virtualdrive
123
+ end
124
+
125
+ def on_virtualdrive_exit(new_state, event, *args)
126
+ @log.debug " [#{current_state}] on_exit: entering #{new_state}; args: #{args}"
127
+ @context.flash!(new_state)
128
+ end
129
+
130
+ # Physical Drive
131
+
132
+ def physicaldrive_line(physicaldrive,key,value)
133
+ @log.debug " [#{current_state}] event: physicaldrive_line: new #{physicaldrive.inspect}"
134
+ physicaldrive[key.to_sym] = value.to_i
135
+ end
136
+
137
+ def on_physicaldrive_entry(old_state, event, *args)
138
+ @log.debug " [#{current_state}] on_entry: leaving #{old_state}; args: #{args}"
139
+ @context.open args[0]
140
+ end
141
+
142
+ def on_physicaldrive_exit(new_state, event, *args)
143
+ @log.debug " [#{current_state}] on_exit: entering #{new_state}; args: #{args}"
144
+ @context.flash!(new_state)
145
+ end
146
+
147
+ # Attribute
148
+
149
+ def attribute_line(key,value)
150
+ @log.debug " [#{current_state}] event: attribute_line: #{key} => #{value}"
151
+ end
152
+
153
+ def on_attribute_entry(old_state, event, *args)
154
+ @log.debug " [#{current_state}] entry: leaving #{old_state}; args: #{args}"
155
+
156
+
157
+ c = @context.current
158
+ k = args[0].to_sym
159
+ v = args[1]
160
+
161
+ # Some attributes require special treatment for our purposes
162
+
163
+ case k
164
+ when :coercedsize, :noncoercedsize, :rawsize, :size
165
+ m = /(?<number>[0-9\.]+)\s+(?<unit>[A-Z]+)/.match(v)
166
+ v = LSIArray::PhysicalDrive::Size.new(m[:number],m[:unit])
167
+ when :raidlevel
168
+ m = /Primary-(?<primary>\d+),\s+Secondary-(?<secondary>\d+)/.match(v)
169
+ v = LSIArray::VirtualDrive::RaidLevel.new(m[:primary],m[:secondary])
170
+ when :firmwarestate
171
+ st,sp = v.gsub(/\s/,'').split(/,/)
172
+ state = st.gsub(/\s/,'_').downcase.to_sym
173
+ spin = sp.gsub(/\s/,'_').downcase.to_sym unless sp.nil?
174
+ v = LSIArray::PhysicalDrive::FirmwareState.new(state,spin)
175
+ when :state
176
+ v = v.gsub(/\s/,'_').downcase.to_sym
177
+ when :mediatype
178
+ v = v.scan(/[A-Z]/).join
179
+ when :inquirydata
180
+ v = v.gsub(/\s+/,' ')
181
+ end
182
+ c[k] = v
183
+ end
184
+
185
+ def on_attribute_exit(new_state, event, *args)
186
+ @log.debug " [#{current_state}] exit: entering #{new_state} throught event #{event}; args: #{args}"
187
+ @context.close if @context.current.type_of? :physicaldrive and event != :attribute_line
188
+
189
+ @context.flash!(new_state)
190
+ end
191
+
192
+ # Exit
193
+
194
+ def exit_line
195
+ @log.debug " [#{current_state}] event: exit_line"
196
+ end
197
+
198
+ def on_exit_entry(new_state, event, *args)
199
+ @log.debug " [#{current_state}] exit: entering #{new_state} throught event #{event}; args: #{args}"
200
+ until @context.current.nil? do
201
+ @context.close
202
+ end
203
+ end
204
+
205
+ ### Regular Expression Match Handlers
206
+
207
+ # Adapter
208
+
209
+ def adapter_match(match)
210
+ @log.debug "ADAPTER! #{match.string}"
211
+ key = 'id'
212
+ value = match[:value]
213
+ adapter_line!(LSIArray::Adapter.new,key,value)
214
+ end
215
+
216
+ # Virtual Drive
217
+
218
+ def virtualdrive_match(match)
219
+ @log.debug "VIRTUALDRIVE! #{match.string}"
220
+ key = match[:key].gsub(/\s+/,"").downcase
221
+ value = match[:value]
222
+ virtualdrive_line!(LSIArray::VirtualDrive.new,key,value)
223
+ end
224
+
225
+ # Physical Drive
226
+
227
+ def physicaldrive_match(match)
228
+ @log.debug "PHYSICALDRIVE! #{match.string}"
229
+ key = match[:key].gsub(/\s+/,"").downcase
230
+ value = match[:value]
231
+ physicaldrive_line!(LSIArray::PhysicalDrive.new,key,value)
232
+ end
233
+
234
+ # Attribute
235
+
236
+ def attribute_match(match)
237
+ @log.debug "ATTRIBUTE! #{match.string}"
238
+ key = match[:key].gsub(/\s+/,"").downcase
239
+ value = match[:value].strip
240
+ attribute_line!(key,value)
241
+ end
242
+
243
+ # Exit
244
+
245
+ def exit_match(match)
246
+ @log.debug "EXIT! #{match.string}"
247
+ exit_line!
248
+ end
249
+
250
+ ### Parse!
251
+
252
+ def parse!(lsi,opts)
253
+
254
+ @lsi = lsi
255
+ @log = Elesai::Logger.instance.log
256
+
257
+ if STDIN.tty?
258
+ if opts[:fake].start_with? '-'
259
+ megacli = opts[:megacli].nil? ? "Megacli" : opts[:megacli]
260
+ command = "#{megacli} #{opts[:fake]}"
261
+ output = Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
262
+ stdin.close
263
+ raise RuntimeError, stderr.gets.chomp unless wait_thr.value.exitstatus == 0
264
+ stdout.gets
265
+ end
266
+ else
267
+ output = File.read(opts[:fake])
268
+ end
269
+ else
270
+ output = STDIN.read
271
+ end
272
+
273
+ output.each_line do |line|
274
+ line.strip!
275
+ next if line == ''
276
+
277
+ case line
278
+ when ADAPTER_RE then adapter_match(ADAPTER_RE.match(line))
279
+ when VIRTUALDRIVE_RE then virtualdrive_match(VIRTUALDRIVE_RE.match(line))
280
+ when PHYSICALDRIVE_RE then physicaldrive_match(PHYSICALDRIVE_RE.match(line))
281
+ when EXIT_RE then exit_match(EXIT_RE.match(line))
282
+ when ATTRIBUTE_RE then attribute_match(ATTRIBUTE_RE.match(line))
283
+ else raise StandardError, "cannot parse '#{line}'"
284
+ end
285
+
286
+ @log.debug "\n\n"
287
+ end
288
+ end
289
+
290
+ end
291
+
292
+ class PDlist_aAll < Megacli
293
+
294
+ def parse!(lsi,opts)
295
+ fake = opts[:fake].nil? ? "-pdlist -aall" : File.join(opts[:fake],"pdlist_aall")
296
+ super lsi, :fake => fake, :megacli => opts[:megacli]
297
+ end
298
+
299
+ workflow do
300
+
301
+ state :start do
302
+ event :adapter_line, :transitions_to => :adapter
303
+ event :exit_line, :transitions_to => :exit
304
+ end
305
+
306
+ state :adapter do
307
+ event :adapter_line, :transitions_to => :adapter # empty adapter
308
+ event :physicaldrive_line, :transitions_to => :physicaldrive
309
+ event :exit_line, :transitions_to => :exit
310
+ end
311
+
312
+ state :physicaldrive do
313
+ event :attribute_line, :transitions_to => :physicaldrive
314
+ event :exit_line, :transitions_to => :exit
315
+ event :adapter_line, :transitions_to => :adapter
316
+ event :physicaldrive_line, :transitions_to => :physicaldrive
317
+ event :attribute_line, :transitions_to => :attribute
318
+ end
319
+
320
+ state :attribute do
321
+ event :attribute_line, :transitions_to => :attribute
322
+ event :physicaldrive_line, :transitions_to => :physicaldrive
323
+ event :adapter_line, :transitions_to => :adapter
324
+ event :exit_line, :transitions_to => :exit
325
+ end
326
+
327
+ state :exit
328
+
329
+ on_transition do |from, to, triggering_event, *event_args|
330
+ #puts self.spec.states[to].class
331
+ # puts " transition: #{from} >> #{triggering_event}! >> #{to}: #{event_args.join(' ')}"
332
+ #puts " #{current_state.meta}"
333
+ end
334
+ end
335
+
336
+ end
337
+
338
+ class LDPDinfo_aAll < Megacli
339
+
340
+ def parse!(lsi,opts)
341
+ fake = opts[:fake].nil? ? "-ldpdinfo -aall" : File.join(opts[:fake],"ldpdinfo_aall")
342
+ super lsi, :fake => fake, :megacli => opts[:megacli]
343
+ end
344
+
345
+
346
+ workflow do
347
+
348
+ state :start do
349
+ event :adapter_line, :transitions_to => :adapter
350
+ event :exit_line, :transitions_to => :exit
351
+ end
352
+
353
+ state :adapter do
354
+ event :adapter_line, :transitions_to => :adapter
355
+ event :attribute_line, :transitions_to => :attribute
356
+ event :virtualdrive_line, :transitions_to => :virtualdrive
357
+ event :exit_line, :transitions_to => :exit
358
+ end
359
+
360
+ state :physicaldrive do
361
+ event :attribute_line, :transitions_to => :physicaldrive
362
+ event :exit_line, :transitions_to => :exit
363
+ event :adapter_line, :transitions_to => :adapter
364
+ event :physicaldrive_line, :transitions_to => :physicaldrive
365
+ event :attribute_line, :transitions_to => :attribute
366
+ end
367
+
368
+ state :virtualdrive do
369
+ event :physicaldrive_line, :transitions_to => :physicaldrive
370
+ event :attribute_line, :transitions_to => :attribute
371
+ end
372
+
373
+ state :attribute do
374
+ event :attribute_line, :transitions_to => :attribute
375
+ event :virtualdrive_line, :transitions_to => :virtualdrive
376
+ event :physicaldrive_line, :transitions_to => :physicaldrive
377
+ event :adapter_line, :transitions_to => :adapter
378
+ event :exit_line, :transitions_to => :exit
379
+ end
380
+
381
+ state :exit
382
+
383
+ on_transition do |from, to, triggering_event, *event_args|
384
+ #puts self.spec.states[to].class
385
+ # puts " transition: #{from} >> #{triggering_event}! >> #{to}: #{event_args.join(' ')}"
386
+ #puts " #{current_state.meta}"
387
+ end
388
+ end
389
+
390
+ end
391
+
392
+
393
+ end
@@ -0,0 +1,3 @@
1
+ module Elesai
2
+ VERSION = "0.4.0"
3
+ end
data/lib/elesai.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'elesai/logger'
2
+ require 'elesai/lsi'
3
+ require 'elesai/megacli'
4
+
5
+ module Elesai
6
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elesai
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Gerardo López-Fernádez
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: log4r
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.9
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.9
30
+ - !ruby/object:Gem::Dependency
31
+ name: senedsa
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.1.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.1.0
46
+ description: Senedsa is a small utility and library wrapper for the Nagios send_nsca.
47
+ email: gerir@evernote.com
48
+ executables:
49
+ - elesai
50
+ - check_elesai
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/elesai/about.rb
55
+ - lib/elesai/cli.rb
56
+ - lib/elesai/logger.rb
57
+ - lib/elesai/lsi.rb
58
+ - lib/elesai/megacli.rb
59
+ - lib/elesai/version.rb
60
+ - lib/elesai.rb
61
+ - bin/check_elesai
62
+ - bin/elesai
63
+ - LICENSE
64
+ - README.md
65
+ homepage: https://github.com/evernote/ops-elesai
66
+ licenses:
67
+ - Apache License, Version 2.0
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: 1.3.5
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 1.8.23
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Utility and library wrapper for Nagios send_nsca utility
90
+ test_files: []