Skip to content

Commit

Permalink
Merge pull request #359 from tumblr/will-consolr-allow-safe-actions-o…
Browse files Browse the repository at this point in the history
…n-dangerous-assets

Consolr dangerous asset behavior and unit tests
  • Loading branch information
William Richard committed Aug 17, 2015
2 parents aa00592 + 502de88 commit 1440505
Show file tree
Hide file tree
Showing 21 changed files with 419 additions and 51 deletions.
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
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
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

0 comments on commit 1440505

Please sign in to comment.