Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolr dangerous asset behavior and unit tests #359

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ matrix:
env: RUN_RUBY=true RUN_SCALA=false GEM_NAME=collins-client
- jdk: oraclejdk7
env: RUN_RUBY=true RUN_SCALA=false GEM_NAME=collins-state
- jdk: oraclejdk7
env: RUN_RUBY=true RUN_SCALA=false GEM_NAME=consolr
script:
- if $RUN_SCALA; then activator-1.3.4-minimal/activator test; fi
- if $RUN_RUBY; then cd support/ruby/$GEM_NAME; bundle install --path ./vendor/bundle; bundle exec rake; fi
Expand Down
1 change: 1 addition & 0 deletions support/ruby/consolr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage/
2 changes: 2 additions & 0 deletions support/ruby/consolr/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--color
--format documentation
4 changes: 4 additions & 0 deletions support/ruby/consolr/.versions.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ruby=ruby-2.2.1
ruby-gemset=consolr
#ruby-gem-install=bundler rake
#ruby-bundle-install=true
8 changes: 8 additions & 0 deletions support/ruby/consolr/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'rspec/core'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.fail_on_error = true
spec.pattern = FileList['spec/**/*_spec.rb']
end

task :default => :spec
27 changes: 26 additions & 1 deletion support/ruby/consolr/bin/consolr
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
#!/usr/bin/env ruby

require 'consolr'
require 'consolr/version'

options = {}
opt_parser = OptionParser.new do |opt|
opt.banner = 'Usage: consolr [OPTIONS]'
opt.separator ''
opt.separator 'Options'

opt.on('-c', '--console', 'console into node via SOL') { options[:console] = true }
opt.on('-d', '--dangerous', 'list dangerous stuff') { options[:dangerous] = true }
opt.on('-f', '--force', 'force run dangerous actions') { options[:force] = true }
opt.on('-H', '--hostname ASSET', 'asset hostname') { |hostname| options[:hostname] = hostname }
opt.on('-i', '--identify', 'turn on chassis UID') { options[:identify] = true }
opt.on('-k', '--kick', 'kick if someone is hogging the console') { options[:kick] = true }
opt.on('-l', '--log LOG', 'System Event Log (SEL) [list|clear]') { |log| options[:log] = log }
opt.on('-o', '--on', 'turn on node') { options[:on] = true }
opt.on('-r', '--reboot', 'restart node') { options[:reboot] = true }
opt.on('-s', '--sdr', 'Sensor Data Repository (SDR)') { options[:sdr] = true }
opt.on('-t', '--tag ASSET', 'asset tag') { |tag| options[:tag] = tag }
opt.on('-x', '--off', 'turn off node') { options[:off] = true }

opt.on_tail('-h', '--help', 'help') { puts opt; exit 0 }
opt.on_tail('-v', '--version', 'version') { puts Consolr::VERSION; exit 0 }
end
opt_parser.parse!

console = Consolr::Console.new
console.start
console.start options
6 changes: 6 additions & 0 deletions support/ruby/consolr/consolr.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_runtime_dependency "collins_auth", "~> 0.1.2"
spec.add_runtime_dependency "net-ping", "~> 1.7.7"

spec.add_development_dependency "rake", "~> 10.4.2"
spec.add_development_dependency "rspec", "~> 3.3.0"
spec.add_development_dependency "simplecov", "~> 0.10.0"
spec.add_development_dependency "webmock", "~> 1.21.0"
end
76 changes: 27 additions & 49 deletions support/ruby/consolr/lib/consolr.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
$LOAD_PATH.unshift('./lib/')

require 'consolr/version'
require 'collins_auth'
require 'net/ping'
require 'optparse'
require 'yaml'
require 'consolr/version'

module Consolr
class Console
Expand Down Expand Up @@ -35,7 +36,7 @@ def initialize
puts "------"
exit 1
end

begin
@ipmitool_exec = config_params['ipmitool'] # ipmitool absolute path
rescue Exception => e
Expand All @@ -45,7 +46,7 @@ def initialize
puts "-------"
exit 1
end

# Will be ignored for dangerous actions, no matter what, even with --force
begin
@dangerous_assets = config_params['dangerous_assets']
Expand All @@ -72,50 +73,22 @@ def initialize
end

@dangerous_actions = [:off, :reboot] # Must match the symbol in options{}

@options = {}
@opt_parser = OptionParser.new do |opt|
opt.banner = 'Usage: consolr [OPTIONS]'
opt.separator ''
opt.separator 'Options'

opt.on('-c', '--console', 'console into node via SOL') { options[:console] = true }
opt.on('-d', '--dangerous', 'list dangerous stuff') { options[:dangerous] = true }
opt.on('-f', '--force', 'force run dangerous actions') { options[:force] = true }
opt.on('-H', '--hostname ASSET', 'asset hostname') { |hostname| options[:hostname] = hostname }
opt.on('-i', '--identify', 'turn on chassis UID') { options[:identify] = true }
opt.on('-k', '--kick', 'kick if someone is hogging the console') { options[:kick] = true }
opt.on('-l', '--log LOG', 'System Event Log (SEL) [list|clear]') { |log| options[:log] = log }
opt.on('-o', '--on', 'turn on node') { options[:on] = true }
opt.on('-r', '--reboot', 'restart node') { options[:reboot] = true }
opt.on('-s', '--sdr', 'Sensor Data Repository (SDR)') { options[:sdr] = true }
opt.on('-t', '--tag ASSET', 'asset tag') { |tag| options[:tag] = tag }
opt.on('-x', '--off', 'turn off node') { options[:off] = true }
opt.on_tail('-h', '--help', 'help') { puts opt; exit 0 }
opt.on_tail('-v', '--version', 'version') { puts Consolr::VERSION; exit 0 }
end
end

def ipmitool_cmd action
system("#{@ipmitool_exec} -I lanplus -H #{@node.ipmi.address} -U #{@node.ipmi.username} -P #{@node.ipmi.password} #{action}")
return $?.exitstatus == 0 ? "SUCCESS" : "FAILED"
end

def start
@opt_parser.parse! # extract from ARGV[]
def start options
abort("Please pass either the asset tag or hostname") if options[:tag].nil? and options[:hostname].nil?

abort("Cannot find #{@ipmitool_exec}") unless File.exist?(@ipmitool_exec)

dangerous_body = "Dangerous actions: #{dangerous_actions.join(', ')}\n"\
"Dangerous status: #{dangerous_status.join(', ')} (override with --force)\n"\
"Dangerous assets: #{dangerous_assets.join(', ')} (ignored no matter what, even with --force)"

if options[:dangerous]
puts dangerous_body
exit 1
end

begin
collins = Collins::Authenticator.setup_client
rescue Exception => e
Expand All @@ -125,25 +98,24 @@ def start
puts "-------"
exit 1
end

if options[:tag] and options[:hostname]
abort("Please pass either the hostname OR the tag but not both.")
end

# match assets like vm-67f5eh, zt-*, etc.
nodes = options[:tag] ? (collins.find :tag => options[:tag]) : (collins.find :hostname => options[:hostname])
@node = nodes.length == 1 ? nodes.first : abort("Found #{nodes.length} assets, aborting.")
%x(/bin/ping -c 1 #{@node.ipmi.address})
abort("Cannot ping IP #{@node.ipmi.address} (#{@node.tag})") unless $?.exitstatus == 0

if dangerous_assets.include?(@node.tag) and dangerous_actions.any?
puts "Asset #{@node.tag} is a crucial asset. Can't execute dangerous actions on this asset."

abort("Cannot ping IP #{@node.ipmi.address} (#{@node.tag})") unless Net::Ping::External.new(@node.ipmi.address).ping?

selected_dangerous_actions = dangerous_actions.select { |o| options[o] }
if dangerous_assets.include?(@node.tag) and selected_dangerous_actions.any?
abort "Asset #{@node.tag} is a crucial asset. Can't ever execute dangerous actions on this asset.\n#{dangerous_body}"
end

if options[:force].nil? and dangerous_actions.any? and dangerous_status.include?(@node.status)
puts "Cannot run dangerous commands on #{@node.hostname} (#{@node.tag} - #{@node.status})"
abort dangerous_body

if options[:force].nil? and selected_dangerous_actions.any? and dangerous_status.include?(@node.status)
abort "Cannot run dangerous commands on #{@node.hostname} (#{@node.tag} - #{@node.status}) because it is in a protected status. This can be overridden with the --force flag\n#{dangerous_body}"
end

case
Expand Down Expand Up @@ -171,12 +143,18 @@ def start
raise OptionParser::MissingArgument, "specify an action"
rescue OptionParser::MissingArgument => e
puts e
puts @opt_parser
exit 0
exit 1
end
end
end

private

def ipmitool_cmd action
system("#{@ipmitool_exec} -I lanplus -H #{@node.ipmi.address} -U #{@node.ipmi.username} -P #{@node.ipmi.password} #{action}")
return $?.exitstatus == 0 ? "SUCCESS" : "FAILED"
end

end

end
2 changes: 1 addition & 1 deletion support/ruby/consolr/lib/consolr/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Consolr
VERSION = "1.0.1"
VERSION = "1.0.2"
end
4 changes: 4 additions & 0 deletions support/ruby/consolr/spec/configs/collins_rspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
host: https://collins.example.net
username: "collins_user"
password: "collins_pass"
timeout: 30
7 changes: 7 additions & 0 deletions support/ruby/consolr/spec/configs/consolr_rspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# this is a dummy config for the unit tests
ipmitool: spec/rspec_ipmi
dangerous_assets:
- "dangerous-allocated-tag"
- "dangerous-maintenance-tag"
dangerous_status:
- "Allocated"
133 changes: 133 additions & 0 deletions support/ruby/consolr/spec/consolr_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
require 'spec_helper'

describe Consolr::Console do

before (:each) do
@console = Consolr::Console.new
end

it 'fails if no hostname is provided' do
expect { @console.start( {:hostname => nil}) }.to raise_error(SystemExit, /asset tag or hostname/)
end

it 'fails if both tag and hostname are passed' do
expect { @console.start({:hostname => 'host.dc.net', :tag => '001234' }) }.to raise_error(SystemExit, /not both/)
end

it 'fails if tag cannot be found' do
expect { @console.start({:tag => 'bogus_tag'}) }.to raise_error(SystemExit, /Found 0 assets/)
end

it 'fails if hostname cannot be found' do
expect { @console.start({:hostname => 'bogus_hostname'}) }.to raise_error(SystemExit, /Found 0 assets/)
end

it 'fails if multiple hosts are found' do
expect { @console.start({:hostname => 'hostname-with-multiple-assets.dc.net'}) }.to raise_error(SystemExit, /Found \d+ assets/)
end

safe_boolean_actions = {:console => "--> Opening SOL session (type ~~. to quit)\nsol activate", :kick => 'sol deactivate', :identify => 'chassis identify', :sdr => 'sdr elist all', :on => 'power on'}
dangerous_boolean_actions = {:off => 'power off', :reboot => 'power cycle'}

describe 'safe allocated asset' do
safe_allocated_tag = 'safe-allocated-tag'
safe_boolean_actions.each do |action, response|
it "does #{action}" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps adding "safe" in the it description would be clearer and symmetric with how danger is done.

expect { @console.start({:tag => safe_allocated_tag, action => true}) }.to output("#{response}\nSUCCESS\n").to_stdout_from_any_process
end
end

dangerous_boolean_actions.each do |action, response|
it "refuses to do dangerous action #{action}" do
expect { @console.start({:tag => safe_allocated_tag, action => true}) }.to raise_error(SystemExit, /Cannot run dangerous command/)
end
it "can force dangerous action #{action}" do
expect { @console.start({:tag => safe_allocated_tag, action => true, :force => true}) }.to output("#{response}\nSUCCESS\n").to_stdout_from_any_process
end
end

it 'does log list' do
expect { @console.start({:tag => safe_allocated_tag, :log => 'list'}) }.to output("sel list\nSUCCESS\n").to_stdout_from_any_process
end

it 'does log clear' do
expect { @console.start({:tag => safe_allocated_tag, :log => 'clear'}) }.to output("sel clear\nSUCCESS\n").to_stdout_from_any_process
end
end

describe 'safe maintenance asset' do
safe_maintenance_tag= 'safe-maintenance-tag'
safe_boolean_actions.each do |action, response|
it "does #{action}" do
expect { @console.start({:tag => safe_maintenance_tag, action => true}) }.to output("#{response}\nSUCCESS\n").to_stdout_from_any_process
end
end

dangerous_boolean_actions.each do |action, response|
it "does dangerous action #{action}" do
expect { @console.start({:tag => safe_maintenance_tag, action => true, :force => true}) }.to output("#{response}\nSUCCESS\n").to_stdout_from_any_process
end
end

it 'does log list' do
expect { @console.start({:tag => safe_maintenance_tag, :log => 'list'}) }.to output("sel list\nSUCCESS\n").to_stdout_from_any_process
end

it 'does log clear' do
expect { @console.start({:tag => safe_maintenance_tag, :log => 'clear'}) }.to output("sel clear\nSUCCESS\n").to_stdout_from_any_process
end
end

describe 'dangerous allocated asset' do
dangerous_allocated_tag= 'dangerous-allocated-tag'
safe_boolean_actions.each do |action, response|
it "does #{action}" do
expect { @console.start({:tag => dangerous_allocated_tag, action => true}) }.to output("#{response}\nSUCCESS\n").to_stdout_from_any_process
end
end

dangerous_boolean_actions.each do |action, response|
it "refuses to do dangerous action #{action}" do
expect { @console.start({:tag => dangerous_allocated_tag, action => true}) }.to raise_error(SystemExit, /Asset .* is a crucial asset/)
end
it "refuses to do dangerous action #{action} with force" do
expect { @console.start({:tag => dangerous_allocated_tag, action => true, :force => true}) }.to raise_error(SystemExit, /Asset .* is a crucial asset/)
end
end

it 'does log list' do
expect { @console.start({:tag => dangerous_allocated_tag, :log => 'list'}) }.to output("sel list\nSUCCESS\n").to_stdout_from_any_process
end

it 'does log clear' do
expect { @console.start({:tag => dangerous_allocated_tag, :log => 'clear'}) }.to output("sel clear\nSUCCESS\n").to_stdout_from_any_process
end
end

describe 'dangerous maintenance asset' do
dangerous_maintenance_tag= 'dangerous-maintenance-tag'
safe_boolean_actions.each do |action, response|
it "does #{action}" do
expect { @console.start({:tag => dangerous_maintenance_tag, action => true}) }.to output("#{response}\nSUCCESS\n").to_stdout_from_any_process
end
end

dangerous_boolean_actions.each do |action, response|
it "refuses to do dangerous action #{action}" do
expect { @console.start({:tag => dangerous_maintenance_tag, action => true}) }.to raise_error(SystemExit, /Asset .* is a crucial asset/)
end
it "refuses to do dangerous action #{action} with force" do
expect { @console.start({:tag => dangerous_maintenance_tag, action => true, :force=> true}) }.to raise_error(SystemExit, /Asset .* is a crucial asset/)
end

end

it 'does log list' do
expect { @console.start({:tag => dangerous_maintenance_tag, :log => 'list'}) }.to output("sel list\nSUCCESS\n").to_stdout_from_any_process
end

it 'does log clear' do
expect { @console.start({:tag => dangerous_maintenance_tag, :log => 'clear'}) }.to output("sel clear\nSUCCESS\n").to_stdout_from_any_process
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come these log commands are considered safe and tested via that mechanism?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log takes an argument (list or clear). The others take booleans. I think we should change the way its structured, but this PR was already too big.

end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--- !ruby/object:Collins::Asset
extras:
ATTRIBS:
'0':
HOSTNAME: web-dc0ce734.ewr01.tumblr.net
id: 1
status: Allocated
tag: dangerous-allocated-tag
type: SERVER_NODE
state: !ruby/object:Collins::AssetState
description: A service in this state is operational.
id: 3
label: Running
name: RUNNING
status: !ruby/object:OpenStruct
table: {}
ipmi: !ruby/object:Collins::Ipmi
address: 1.2.3.4
asset_id: 3812
gateway: 1.2.3.1
id: 2
netmask: 255.255.240.0
password: ipmi-test-tag-password
username: ipmi-test-tag-user
Loading