From 4048a7ea2160100bc8771ced686be5d8edb3404b Mon Sep 17 00:00:00 2001 From: Robert Gauld Date: Sat, 3 Jun 2017 05:42:33 +0100 Subject: [PATCH 1/4] Prepare for more development --- CHANGELOG.md | 4 ++++ version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a4d6f..0908209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 0.0.5 + + * + ## Version 0.0.4 * Rename gem to snmp_table_viewer diff --git a/version.rb b/version.rb index 8df6ffd..7f638a6 100644 --- a/version.rb +++ b/version.rb @@ -1,3 +1,3 @@ module SNMPTableViewer - VERSION = "0.0.4" + VERSION = "0.0.5.dev" end From 618851a6bfee85e364e2873a86a0b67517bad7cf Mon Sep 17 00:00:00 2001 From: Robert Gauld Date: Sat, 3 Jun 2017 17:26:53 +0100 Subject: [PATCH 2/4] Tidy gemspec file --- snmp_table_viewer.gemspec | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/snmp_table_viewer.gemspec b/snmp_table_viewer.gemspec index bc4653e..7c90876 100644 --- a/snmp_table_viewer.gemspec +++ b/snmp_table_viewer.gemspec @@ -1,21 +1,22 @@ # -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path(File.join('..', 'lib'), __FILE__) require File.join(File.dirname(__FILE__), 'version') Gem::Specification.new do |s| - s.name = "snmp_table_viewer" + s.name = 'snmp_table_viewer' s.license = 'BSD 3 clause' s.version = SNMPTableViewer::VERSION s.authors = ['Robert Gauld'] s.email = ['robert@robertgauld.co.uk'] - s.homepage = 'https://github.com/robertgauld/snmp-table-viewer' + s.homepage = 'https://github.com/robertgauld/snmp_table_viewer' s.summary = %q{Easily view SNMP tables.} s.description = %q{Easily view SNMP tables in a variety of different formats including as a table in the terminal, json or csv.} s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - s.require_paths = ["lib"] + s.require_paths = ['lib'] + s.bindir = 'bin' s.add_runtime_dependency 'netsnmp', '~> 0.1.3' s.add_runtime_dependency 'terminal-table', '~> 1.8.0' From dca0e16d16ff446ebeeacc1c47b0661406818b29 Mon Sep 17 00:00:00 2001 From: Robert Gauld Date: Sat, 3 Jun 2017 20:07:57 +0100 Subject: [PATCH 3/4] Add new script - table-from-stdin --- CHANGELOG.md | 3 +- README.md | 18 ++++++- bin/{snmp-table-viewer => table-from-snmp} | 33 ++++-------- bin/table-from-stdin | 62 ++++++++++++++++++++++ lib/snmp_table_viewer.rb | 20 +++++++ lib/snmp_table_viewer/fetcher.rb | 48 ++++++++++++++++- spec/snmp_table_viewer/fetcher_spec.rb | 44 ++++++++++++++- 7 files changed, 198 insertions(+), 30 deletions(-) rename bin/{snmp-table-viewer => table-from-snmp} (75%) create mode 100755 bin/table-from-stdin diff --git a/CHANGELOG.md b/CHANGELOG.md index 0908209..bd80383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Version 0.0.5 - * + * Rename snmp-table-viewer script to table-from-snmp + * Add script table-from-stdin ## Version 0.0.4 diff --git a/README.md b/README.md index 469e08d..f8ef717 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,11 @@ gem 'snmp_table_viewer', '~> 0.0' ``` -## Executable +## Executables +### Create table from doing an SNMP walk ``` -Usage: snmp-table-viewer [options] +Usage: table-from-snmp [options] -h, -?, --help Prints this help. --headings HEADINGS Headings to use in the table (e.g. "A, Bc, D"). --headings-for TYPE Use headings for this table type (ifTable). @@ -75,6 +76,19 @@ SNMP version 3 options: Table formatter options: --[no-]transpose-table Transpose the output table (swap rows and columns). ``` +### Create table from piped in data +``` +Usage: table-from-stdin [options] + -h, -?, --help Prints this help. + --headings HEADINGS Headings to use in the table (e.g. "A, Bc, D"). + --headings-for TYPE Use headings for this table type (ifTable). + --format FORMAT How to format the output (table|csv|json|raw) (default table). + --converter CONVERTER A converter to run on the data before formatting (iftable). + +Table formatter options: + --[no-]transpose-table Transpose the output table (swap rows and columns). +``` + ## Documentation & Versioning diff --git a/bin/snmp-table-viewer b/bin/table-from-snmp similarity index 75% rename from bin/snmp-table-viewer rename to bin/table-from-snmp index 67c17ab..9c787bc 100755 --- a/bin/snmp-table-viewer +++ b/bin/table-from-snmp @@ -29,25 +29,10 @@ def parse_command_line headings: [], formatter: SNMPTableViewer::Formatter::Table, } - headings_for = { - 'ifTable' => ['Index', 'Descr', 'Type', 'Mtu', 'Speed', 'PhysAddress', 'AdminStatus', 'OperStatus', 'LastChange', 'InOctets', 'InUcastPkts', 'InNUcastkts', 'InDiscards', 'InErrors', 'InUnknownPrortos', 'OutOctets', 'OutUcastPkts', 'OutNUcastPkts', 'OutDiscards', 'OutErrors', 'OutQLen', 'Specific'], - } - formatters = { - 'table' => SNMPTableViewer::Formatter::Table, - 'csv' => SNMPTableViewer::Formatter::CSV, - 'json' => SNMPTableViewer::Formatter::JSON, - 'raw' => SNMPTableViewer::Formatter::Raw, - } - converters = { - 'iftable' => SNMPTableViewer::Converter::IfTable, - } - base_oids = { - 'iftable' => '1.3.6.1.2.1.2.2', - } snmp_version = 'v3' parser = OptionParser.new do |opts| - opts.banner = "Usage: snmp-table-viewer [options]" + opts.banner = "Usage: table-from-snmp [options]" opts.on('-h', '--help', '-?', 'Prints this help.') do puts opts @@ -58,16 +43,16 @@ def parse_command_line options[:headings] += v.map(&:strip) end - opts.on('--headings-for TYPE', String, "Use headings for this table type (#{headings_for.keys.join('|')}).") do |v| - options[:headings] += headings_for[v] || [] + opts.on('--headings-for TYPE', String, "Use headings for this table type (#{SNMPTableViewer::HEADINGS_FOR.keys.join('|')}).") do |v| + options[:headings] += SNMPTableViewer::HEADINGS_FOR[v] || [] end - opts.on('--format FORMAT', String, "How to format the output (#{formatters.keys.join('|')}) (default table).") do |v| - options[:formatter] = formatters[v.downcase] + opts.on('--format FORMAT', String, "How to format the output (#{SNMPTableViewer::FORMATTERS.keys.join('|')}) (default table).") do |v| + options[:formatter] = SNMPTableViewer::FORMATTERS[v.downcase] end - opts.on('--converter CONVERTER', String, "A converter to run on the data before formatting (#{converters.keys.join('|')}).") do |v| - options[:converter] = converters[v.downcase] + opts.on('--converter CONVERTER', String, "A converter to run on the data before formatting (#{SNMPTableViewer::CONVERTERS.keys.join('|')}).") do |v| + options[:converter] = SNMPTableViewer::CONVERTERS[v.downcase] end @@ -87,7 +72,7 @@ def parse_command_line end opts.on('--base-oid OID', 'The oid at the start of the table. Can by dotted numbers or ifTable') do |v| - options[:base_oid] = base_oids[v.downcase] || v + options[:base_oid] = SNMPTableViewer::BASE_OIDS[v.downcase] || v end @@ -139,7 +124,7 @@ options = parse_command_line snmp_options = options[:snmp][:common].clone snmp_options.merge!(options[:snmp][snmp_options[:version]]) -data = SNMPTableViewer::Fetcher.fetch(base_oid: options[:base_oid], **snmp_options) +data = SNMPTableViewer::Fetcher.from_snmp(base_oid: options[:base_oid], **snmp_options) converter = options[:converter] data = converter.convert(data) unless converter.nil? diff --git a/bin/table-from-stdin b/bin/table-from-stdin new file mode 100755 index 0000000..9e04d22 --- /dev/null +++ b/bin/table-from-stdin @@ -0,0 +1,62 @@ +#!/usr/bin/env ruby + +require 'optparse' +require_relative File.join('..', 'lib', 'snmp_table_viewer') + +def parse_command_line + options = { + SNMPTableViewer::Formatter::Table => { + transpose: false, + }, + headings: [], + formatter: SNMPTableViewer::Formatter::Table, + } + + parser = OptionParser.new do |opts| + opts.banner = "Usage: table-from-stdin [options]" + + opts.on('-h', '--help', '-?', 'Prints this help.') do + puts opts + exit + end + + opts.on('--headings HEADINGS', Array, 'Headings to use in the table (e.g. "A, Bc, D").') do |v| + options[:headings] += v.map(&:strip) + end + + opts.on('--headings-for TYPE', String, "Use headings for this table type (#{SNMPTableViewer::HEADINGS_FOR.keys.join('|')}).") do |v| + options[:headings] += SNMPTableViewer::HEADINGS_FOR[v] || [] + end + + opts.on('--format FORMAT', String, "How to format the output (#{SNMPTableViewer::FORMATTERS.keys.join('|')}) (default table).") do |v| + options[:formatter] = SNMPTableViewer::FORMATTERS[v.downcase] + end + + opts.on('--converter CONVERTER', String, "A converter to run on the data before formatting (#{SNMPTableViewer::CONVERTERS.keys.join('|')}).") do |v| + options[:converter] = SNMPTableViewer::CONVERTERS[v.downcase] + end + + + opts.separator "\nTable formatter options:" + opts.on('--[no-]transpose-table', 'Transpose the output table (swap rows and columns).') do |v| + options[SNMPTableViewer::Formatter::Table][:transpose] = v + end + + end # create parser + + parser.parse!(ARGV) + options +end + + +options = parse_command_line +data = STDIN.readlines.map(&:strip) +data = SNMPTableViewer::Fetcher.from_array(data) + +converter = options[:converter] +data = converter.convert(data) unless converter.nil? + +formatter = options[:formatter] +formatter_options = options[formatter] || {} +formatter = formatter.new(data: data, headings: options[:headings]) +puts formatter.output(**formatter_options) diff --git a/lib/snmp_table_viewer.rb b/lib/snmp_table_viewer.rb index 931e8ff..af509bb 100644 --- a/lib/snmp_table_viewer.rb +++ b/lib/snmp_table_viewer.rb @@ -11,4 +11,24 @@ Dir["#{File.dirname(__FILE__)}/**/*.rb"].select{ |f| f != __FILE__}.sort.each { |f| load(f) } module SNMPTableViewer + + HEADINGS_FOR = { + 'ifTable' => ['Index', 'Descr', 'Type', 'Mtu', 'Speed', 'PhysAddress', 'AdminStatus', 'OperStatus', 'LastChange', 'InOctets', 'InUcastPkts', 'InNUcastkts', 'InDiscards', 'InErrors', 'InUnknownPrortos', 'OutOctets', 'OutUcastPkts', 'OutNUcastPkts', 'OutDiscards', 'OutErrors', 'OutQLen', 'Specific'], + }.freeze + + FORMATTERS = { + 'table' => SNMPTableViewer::Formatter::Table, + 'csv' => SNMPTableViewer::Formatter::CSV, + 'json' => SNMPTableViewer::Formatter::JSON, + 'raw' => SNMPTableViewer::Formatter::Raw, + }.freeze + + CONVERTERS = { + 'iftable' => SNMPTableViewer::Converter::IfTable, + }.freeze + + BASE_OIDS = { + 'iftable' => '1.3.6.1.2.1.2.2', + }.freeze + end # module SNMPTableViewer diff --git a/lib/snmp_table_viewer/fetcher.rb b/lib/snmp_table_viewer/fetcher.rb index 69191f2..8174cdd 100644 --- a/lib/snmp_table_viewer/fetcher.rb +++ b/lib/snmp_table_viewer/fetcher.rb @@ -6,19 +6,63 @@ class Fetcher # Fetch the data using SNMP. # @param base_oid [String] The OID to start the SNMP walk from # @param **snmp_options [Hash] The options to pass to NETSNMP::Client.new - # @return [Array>] A two dimensional array containing objects in each cell (at 'address' data[row][col]) + # @return [Array>] A two dimensional array containing objects in each cell (at 'address' data\[row\]\[col\]) def self.from_snmp(base_oid:, **snmp_options) data = Array.new NETSNMP::Client.new(snmp_options) do |manager| manager.walk(oid: base_oid).each do |oid, value| col, row = oid.split('.')[-2..-1].map{ |i| i.to_i - 1} - data[row] ||= [] + data[row] ||= Array.new data[row][col] = value end end data end + # Build the data from an Array. + # Each String of the format returned by the snmpwalk command (" = : "). + # @param data [Array] The Strings to get the data from + # @return [Array>] A two dimensional array containing objects in each cell (at 'address' data\[row\]\[col\]) + def self.from_array(data_in) + data_out = Array.new + regexp_general = Regexp.compile(/\A(?:iso)?[\.0-9]+\.([0-9]+)\.([0-9]+)\s+=\s+([A-Za-z0-9 \-]+):\s+(.+)\Z/) + regexp_nodata = Regexp.compile(/\A(?:iso)?[\.0-9]+\.([0-9]+)\.([0-9]+)\s+=\s+""\Z/) + data_in.each.with_index do |line, index| + # Try to get a match on the various valid frmats of line + match = line.match(regexp_general) + match ||= line.match(regexp_nodata) + if match.nil? + STDERR.puts "Could not parse data on line #{index+1}: #{line}" + next + end + + col, row, type, value = match.captures + # Convert value + case type && type.downcase + when nil + when 'string' + value = value[1..-2] # Strip enclosing quotes + when 'oid', 'hex-string' + when 'integer', 'integer32', 'uinteger32', 'gauge32', 'counter32', 'counter64' + value = value.to_i + when 'ipaddress' + value = IPAddr.new(value) + when 'timeticks' + match = value.match(/\A\((\d+)\)/) + value = NETSNMP::Timetick.new(match.nil? ? 0 : match[1].to_i) + else + STDERR.puts "Unknown SNMP type (#{type}) on line #{index+1}: #{line}" + end + + # Save value + row = row.to_i - 1 + col = col.to_i - 1 + data_out[row] ||= Array.new + data_out[row][col] = value + end # each line of data_in + data_out + end + end # class Fetcher end # module SNMPTableViewer diff --git a/spec/snmp_table_viewer/fetcher_spec.rb b/spec/snmp_table_viewer/fetcher_spec.rb index 417aeb2..a3acd31 100644 --- a/spec/snmp_table_viewer/fetcher_spec.rb +++ b/spec/snmp_table_viewer/fetcher_spec.rb @@ -1,6 +1,6 @@ describe SNMPTableViewer::Fetcher do - it 'Fetches' do + it '#from_snmp' do snmp_options = {opt_a: :a} manager = double("manager") expect(manager).to receive(:walk).with(oid: '1.2.3.4.5').once.and_return([ @@ -20,4 +20,46 @@ ] end + describe '#from_array' do + it 'Given array of strings " = : "' do + data = [ + 'iso.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1', + 'iso.3.6.1.2.1.2.2.1.2.1 = STRING: "lo"', + 'iso.3.6.1.2.1.2.2.1.3.1 = Gauge32: 2', + 'iso.3.6.1.2.1.2.2.1.4.1 = Hex-STRING: 01 23 45 67 89 AB', + 'iso.3.6.1.2.1.2.2.1.5.1 = Timeticks: (10) 0:00:10.00', + 'iso.3.6.1.2.1.2.2.1.6.1 = Counter32: 3', + 'iso.3.6.1.2.1.2.2.1.7.1 = OID: ccitt.0', + 'iso.3.6.1.2.1.2.2.1.8.1 = IpAddress: 1.2.3.4', + 'iso.3.6.1.2.1.2.2.1.10.1 = ""', + ] + expect(described_class.from_array(data)).to eq [[ + 1, + 'lo', + 2, + '01 23 45 67 89 AB', + NETSNMP::Timetick.new(10), + 3, + 'ccitt.0', + IPAddr.new('1.2.3.4'), + nil, + nil + ]] + end + + it 'Errors but survives on a bad line' do + data = ['This is a bad line', 'This is another bad line'] + expect(STDERR).to receive(:puts).with('Could not parse data on line 1: This is a bad line').once + expect(STDERR).to receive(:puts).with('Could not parse data on line 2: This is another bad line').once + expect(described_class.from_array(data)).to eq [] + end + + it 'Errors but survives on a bad type' do + data = ['1.2.3.4.0.1.1 = INTEGER: 1', '1.2.3.4.0.2.1 = invalid-type: 1'] + expect(STDERR).to receive(:puts).with('Unknown SNMP type (invalid-type) on line 2: 1.2.3.4.0.2.1 = invalid-type: 1').once + expect(described_class.from_array(data)).to eq [[1, '1']] + end + + end # describe #from_array + end From 4a839a021b267f343cf91f22562f27ce38c9d5de Mon Sep 17 00:00:00 2001 From: Robert Gauld Date: Sat, 3 Jun 2017 20:11:55 +0100 Subject: [PATCH 4/4] Prepare for release --- version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.rb b/version.rb index 7f638a6..feebe77 100644 --- a/version.rb +++ b/version.rb @@ -1,3 +1,3 @@ module SNMPTableViewer - VERSION = "0.0.5.dev" + VERSION = "0.0.5" end