bluebox-boxcutter 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +51 -0
- data/README.md +36 -0
- data/Rakefile +14 -0
- data/bin/boxcutter +4 -0
- data/bluebox-boxcutter.gemspec +33 -0
- data/doc/screenshot.png +0 -0
- data/lib/bluebox-boxcutter.rb +94 -0
- data/lib/bluebox-boxcutter/cli.rb +111 -0
- data/lib/bluebox-boxcutter/git.rb +32 -0
- data/lib/bluebox-boxcutter/kvm.rb +36 -0
- data/lib/bluebox-boxcutter/machine.rb +84 -0
- data/lib/bluebox-boxcutter/password.rb +37 -0
- data/lib/bluebox-boxcutter/razor.rb +59 -0
- data/lib/bluebox-boxcutter/ui.rb +53 -0
- data/lib/bluebox-boxcutter/version.rb +3 -0
- data/spec/boxcutter_spec.rb +11 -0
- data/spec/fixtures/private/hosts.json +4 -0
- data/spec/fixtures/private/hosts/live_search_machines +4 -0
- data/spec/fixtures/private/hosts/show/123.json +13 -0
- data/spec/fixtures/private/machines/123/edit +18 -0
- data/spec/fixtures/private/service_password +13 -0
- data/spec/fixtures/private/service_passwords/fetch_password/163 +14 -0
- data/spec/fixtures/private/service_passwords/fetch_password/164 +14 -0
- data/spec/fixtures/private/service_passwords/fetch_password/165 +14 -0
- data/spec/git_spec.rb +17 -0
- data/spec/machine_spec.rb +46 -0
- data/spec/razor_spec.rb +28 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/ui_spec.rb +82 -0
- metadata +223 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
module Boxcutter
|
2
|
+
class Machine
|
3
|
+
|
4
|
+
def self.search(search_string)
|
5
|
+
ids = name_to_id_hash(search_string)
|
6
|
+
[].tap do |res|
|
7
|
+
res << [:name, :id, :url]
|
8
|
+
ids.each_pair do |name, id|
|
9
|
+
res << [ name, id, Boxcutter::Boxpanel.machine_url(id) ]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# given hostname, return a mash of machine attributes
|
15
|
+
def self.get(name)
|
16
|
+
machine_id = id name
|
17
|
+
machine_info_url = "/private/hosts/show/#{machine_id}.json"
|
18
|
+
json = Boxcutter::Boxpanel.get(machine_info_url)
|
19
|
+
mash = Mash.new( { :key => "value" } )
|
20
|
+
mash.merge! Boxcutter::flatten_hash(JSON::parse(json))
|
21
|
+
end
|
22
|
+
|
23
|
+
# return a hash of {hostname => machine_id}
|
24
|
+
def self.name_to_id_hash(regex_string = '.')
|
25
|
+
Hash.new.tap do |this|
|
26
|
+
regex = Regexp.new regex_string
|
27
|
+
JSON::parse(Boxpanel.get('/private/hosts.json')).each do |i|
|
28
|
+
this[i["hostname"]] = i["id"].to_s if i['hostname'] =~ regex
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# given a hostname, return its id
|
34
|
+
def self.id(name)
|
35
|
+
ids = name_to_id_hash name
|
36
|
+
raise "found more than one host matching '#{name}'" if ids.size > 1
|
37
|
+
raise "unknown machine #{name}" unless ids[name]
|
38
|
+
ids[name]
|
39
|
+
end
|
40
|
+
|
41
|
+
# given a dom and an input field name, return its value, or nil
|
42
|
+
def self.get_input_value(dom, name)
|
43
|
+
elements = dom.css("input##{name}")
|
44
|
+
elements.empty? ? nil : elements.first['value']
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.host_id(machine)
|
48
|
+
machine.id
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.switchport_id(machine)
|
52
|
+
machine.network_interfaces_eth0_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.eth0_mac(machine)
|
56
|
+
machine.network_interfaces_eth0_mac_address
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.power_cycle!(machine)
|
60
|
+
body = Boxpanel.post '/private/hosts/do_power_cycle', :body => {:id => Boxcutter::Machine::host_id(machine), :delay => 0 }
|
61
|
+
raise "reboot failed" unless body =~ /job_queue/
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.switch_vlan!(machine, vlan_id)
|
65
|
+
body = Boxpanel.post(
|
66
|
+
"/private/machine_interfaces/submit_switchport_job/#{Boxcutter::Machine::switchport_id(machine)}",
|
67
|
+
:body => {:vlan => 255, :description => machine[:hostname], :enabled => 1, :commit => 'Submit'})
|
68
|
+
raise 'vlan switch failed' unless body =~ Regexp.new(machine[:hostname])
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.clear_hwtoken!(machine)
|
72
|
+
Boxpanel.post("/private/hosts/clear_hwagent_token/#{Boxcutter::Machine::host_id(machine)}", :body => {})
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.machine_summary(machine)
|
76
|
+
Array.new.tap do |a|
|
77
|
+
a << "Hostname: #{machine[:hostname]}"
|
78
|
+
a << "Customer: #{machine[:customer_business_name]}"
|
79
|
+
a << "Location: #{machine[:location_description]}"
|
80
|
+
a << "Purpose: #{machine[:purpose]}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
module Boxcutter
|
3
|
+
module Password
|
4
|
+
def self.search(regexp_string)
|
5
|
+
regexp = Regexp.new regexp_string
|
6
|
+
paths = name_to_path_hash
|
7
|
+
[[:name, :uri, :user, :pass]].tap do |res|
|
8
|
+
name_to_path_hash.each_pair do |name, path|
|
9
|
+
if name =~ regexp
|
10
|
+
pw = get_pw(path)
|
11
|
+
res << [name].concat(pw.values)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.name_to_path_hash()
|
18
|
+
dom = Boxpanel.get_html '/private/service_password'
|
19
|
+
pw_links = dom.css('a').select { |a| a['onclick'] and a['onclick'] =~ /fetch_password/ }
|
20
|
+
Hash[
|
21
|
+
pw_links.map do |l|
|
22
|
+
[ l.text, l['onclick'].split("'").select { |s| s =~ /service_password\/fetch_password/ }.first ]
|
23
|
+
end
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.get_pw(path)
|
28
|
+
dom = Boxpanel.get_html path
|
29
|
+
{
|
30
|
+
:uri => dom.css('a').first['href'],
|
31
|
+
:user => dom.css('input')[0]['value'],
|
32
|
+
:pass => dom.css('input')[1]['value']
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Boxcutter
|
5
|
+
module Razor
|
6
|
+
|
7
|
+
ENDPOINT='http://razor.sea03.blueboxgrid.com:80'
|
8
|
+
USER = 'boxpanel'
|
9
|
+
PASS = '4nlpb4IFQxjx'
|
10
|
+
|
11
|
+
class StubbleClient
|
12
|
+
include HTTParty
|
13
|
+
base_uri ENDPOINT
|
14
|
+
basic_auth USER, PASS
|
15
|
+
|
16
|
+
def self.tags()
|
17
|
+
resp = get '/tags'
|
18
|
+
raise 'get /tags failed' unless resp.code == 200
|
19
|
+
JSON.parse resp.body
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.delete_mac(mac)
|
23
|
+
resp = delete '/mac', :body => {:mac => mac }
|
24
|
+
raise 'delete /mac failed' unless resp.code == 200
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.add_mac_to_tag(mac, tag)
|
28
|
+
resp = post '/mac', :body => {:mac => mac, :tag => tag }
|
29
|
+
raise 'post /mac failed' unless resp.code == 200
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log(mac)
|
33
|
+
resp = get "/mac/#{mac}/log"
|
34
|
+
raise 'get /mac/X/log failed' unless resp.code == 200
|
35
|
+
JSON.parse resp.body
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.image!(machine, tag_name)
|
40
|
+
raise "unknown razor tag #{tag_name}" unless StubbleClient.tags.include?(tag_name)
|
41
|
+
mac = Boxcutter::Machine::eth0_mac(machine)
|
42
|
+
|
43
|
+
raise "unknown mac" unless mac and mac =~ /[a-f0-9:]+/
|
44
|
+
|
45
|
+
StubbleClient.delete_mac mac
|
46
|
+
StubbleClient.add_mac_to_tag mac, tag_name
|
47
|
+
|
48
|
+
Boxcutter.msg "switching machine into vlan 255."
|
49
|
+
Boxcutter::Machine.switch_vlan! machine, 255
|
50
|
+
|
51
|
+
Boxcutter.msg "initiating razor boot"
|
52
|
+
Boxcutter::Machine.power_cycle! machine
|
53
|
+
|
54
|
+
Boxcutter.msg "removing hardware token from Boxpanel machine record"
|
55
|
+
Boxcutter::Machine.clear_hwtoken! machine
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
require 'terminal-table'
|
3
|
+
|
4
|
+
module Boxcutter
|
5
|
+
|
6
|
+
@@colors = true
|
7
|
+
@@yes = false
|
8
|
+
@@quiet = false
|
9
|
+
|
10
|
+
def self.error!(msg)
|
11
|
+
STDERR.puts msg.red
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.no_colors!
|
16
|
+
@@colors = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.quiet!
|
20
|
+
@@quiet = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.msg(s)
|
24
|
+
puts (@@colors ? s.green : s) unless @@quiet
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.yes!
|
28
|
+
@@yes = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.confirm(prompt)
|
32
|
+
if @@yes
|
33
|
+
yield
|
34
|
+
else
|
35
|
+
puts "#{prompt} : are you sure? [y/n]".yellow
|
36
|
+
answer = STDIN.gets.chomp
|
37
|
+
answer == 'y' ? yield : error!('aborting at user request')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.table_of_hashes(rows)
|
42
|
+
keys = rows.empty? ? [] : rows.first.keys.map { |k| k.dup }
|
43
|
+
table([keys] + rows.map { |r| r.values })
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.table(rows, headers_present=true)
|
47
|
+
headers = nil
|
48
|
+
headers = rows.shift.map { |x| x.to_s } if headers_present
|
49
|
+
headers.map! { |h| h.dup.cyan } if @@colors
|
50
|
+
Terminal::Table.new(:headings => headers, :rows => rows).to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<a href="https://boxpanel.bluebox.net/private/machines/123/edit" class="asset">foo1</a>
|
2
|
+
<a href="https://boxpanel.bluebox.net/private/machines/124/edit" class="asset">foo2</a>
|
3
|
+
<a href="https://boxpanel.bluebox.net/private/machines/125/edit" class="asset">bar1</a>
|
4
|
+
<a href="https://boxpanel.bluebox.net/private/machines/126/edit" class="asset">bar2</a>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<input id="host_hostname" name="host[hostname]" size="30" type="text" value="foo1.blah.com" />
|
2
|
+
<input hint="e.g.: 2.0" id="host_cpu_cores" name="host[cpu_cores]" size="8" type="text" value="8.0000" />
|
3
|
+
<input id="host_memory_size" name="host[memory_size]" size="8" type="text" value="16 GB" />
|
4
|
+
<input id="host_disk_size" name="host[disk_size]" size="8" type="text" value="696.8 GB" />
|
5
|
+
<input id="asset_serial" name="asset[serial]" size="30" type="text" value="2t3fi51" />
|
6
|
+
|
7
|
+
<form action="/private/hosts/98765" class="form-horizontal" method="post" onsubmit="new Ajax.Request('/private/hosts/98765', {asynchronous:true, evalScripts:true, method:'put', parameters:Form.serialize(this)}); return false;">
|
8
|
+
|
9
|
+
<div id="hidden_content_confirm_power_cycle_foo1" style="display: none;"></div><a href="#" onclick="new Ajax.Updater('hidden_content_confirm_power_cycle_foo1', '/private/hosts/confirm_power_cycle/98765', {asynchronous:true, evalScripts:true, method:'get', onComplete:function(request){RedBox.addHiddenContent('hidden_content_confirm_power_cycle_foo1'); }, onLoading:function(request){RedBox.loading(); }}); return false;"><button class="btn btn-danger" name="button" type="submit">Power Cycle</button></a>
|
10
|
+
|
11
|
+
<a href="/private/machine_interfaces/switchport_switcher/68686">[Edit Switchport]</a>
|
12
|
+
<a href="/private/hosts/setup_dns/1205043">[Setup DNS Records]</a>
|
13
|
+
|
14
|
+
<td class="inactiveHeaderLeft" style="padding: 3px; border: 1px solid #EEE;">
|
15
|
+
<div style="float: left;"><span style=" font-weight: bold;">Interface eth0</span> (00:30:48:cc:d6:32)
|
16
|
+
connected to <a href="/private/machines/3750387/edit">dsw01a.sea03</a> interface Et102/1/7
|
17
|
+
</div>
|
18
|
+
</td>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<html>
|
2
|
+
<body>
|
3
|
+
|
4
|
+
...
|
5
|
+
|
6
|
+
<a href="#" onclick="new Ajax.Updater('hidden_content_fetch_password_163', '/private/service_password/fetch_password/163', {asynchronous:true, evalScripts:true, onComplete:function(request){RedBox.addHiddenContent('hidden_content_fetch_password_163'); }, onLoading:function(request){RedBox.loading(); }}); return false;">foo number 1</a>
|
7
|
+
<a href="#" onclick="new Ajax.Updater('hidden_content_fetch_password_164', '/private/service_password/fetch_password/164', {asynchronous:true, evalScripts:true, onComplete:function(request){RedBox.addHiddenContent('hidden_content_fetch_password_164'); }, onLoading:function(request){RedBox.loading(); }}); return false;">foo number 2</a>
|
8
|
+
<a href="#" onclick="new Ajax.Updater('hidden_content_fetch_password_165', '/private/service_password/fetch_password/165', {asynchronous:true, evalScripts:true, onComplete:function(request){RedBox.addHiddenContent('hidden_content_fetch_password_165'); }, onLoading:function(request){RedBox.loading(); }}); return false;">kvm9</a>
|
9
|
+
|
10
|
+
...
|
11
|
+
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
<div>
|
3
|
+
<a href="https://some-auth-url" title="some-auth-url" target="_blank">Click Here</a>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<div>
|
7
|
+
<strong>Username:</strong>
|
8
|
+
<input style="width: 100%;" type="text" value="some user" />
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div>
|
12
|
+
<strong>Password:</strong>
|
13
|
+
<input style="width: 100%;" type="text" value="XXXX" />
|
14
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
<div>
|
3
|
+
<a href="https://another-auth-url" title="another-auth-url" target="_blank">Click Here</a>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<div>
|
7
|
+
<strong>Username:</strong>
|
8
|
+
<input style="width: 100%;" type="text" value="another user" />
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div>
|
12
|
+
<strong>Password:</strong>
|
13
|
+
<input style="width: 100%;" type="text" value="YYYY" />
|
14
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
<div>
|
3
|
+
<a href="https://yet-another-auth-url" title="yet-another-auth-url" target="_blank">Click Here</a>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<div>
|
7
|
+
<strong>Username:</strong>
|
8
|
+
<input style="width: 100%;" type="text" value="yet-another user" />
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div>
|
12
|
+
<strong>Password:</strong>
|
13
|
+
<input style="width: 100%;" type="text" value="ZZZ" />
|
14
|
+
</div>
|
data/spec/git_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boxcutter::Git do
|
4
|
+
|
5
|
+
before do
|
6
|
+
Boxcutter::Git::clone
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
Boxcutter::Git::rm_clone
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should get the upstream version' do
|
14
|
+
Boxcutter::Git::upstream_version.should match /[0-9]+.[0-9]+.[0-9]+/
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boxcutter::Machine do
|
4
|
+
|
5
|
+
before do
|
6
|
+
stub_boxpanel_calls!
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should grab machine ids and names from machine list' do
|
10
|
+
Boxcutter::Machine.name_to_id_hash().should == {
|
11
|
+
"foo1" => "123",
|
12
|
+
"foo2" => "124",
|
13
|
+
"bar1" => "125",
|
14
|
+
"bar2" => "126"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should search machines by regexp' do
|
19
|
+
Boxcutter::Machine.search('bar*').should == [
|
20
|
+
[:name, :id, :url],
|
21
|
+
["bar1", "125", "https://boxpanel.bluebox.net/private/machines/125/edit"],
|
22
|
+
["bar2", "126", "https://boxpanel.bluebox.net/private/machines/126/edit"]
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should provide id given hostname' do
|
27
|
+
Boxcutter::Machine.id('foo1').should == '123'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should issue power cycles' do
|
31
|
+
Boxcutter::Boxpanel.should_receive(:post).
|
32
|
+
with('/private/hosts/do_power_cycle', :body => {:id => '98765', :delay => 0 }).
|
33
|
+
and_return('window.location.href = "/private/job_queue/queue";')
|
34
|
+
Boxcutter::Machine.power_cycle!(Boxcutter::Machine.get 'foo1')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should switch vlans' do
|
38
|
+
Boxcutter::Boxpanel.should_receive(:post).
|
39
|
+
with('/private/machine_interfaces/submit_switchport_job/68686',
|
40
|
+
:body=>{:vlan=>255, :description=>"foo1.blah.com", :enabled=>1, :commit=>"Submit"}
|
41
|
+
).
|
42
|
+
and_return('foo1.blah.com')
|
43
|
+
Boxcutter::Machine.switch_vlan!(Boxcutter::Machine.get('foo1'), 255)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/spec/razor_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boxcutter::Razor do
|
4
|
+
|
5
|
+
before do
|
6
|
+
stub_boxpanel_calls!
|
7
|
+
Boxcutter::Razor::StubbleClient.stub(:tags).and_return [ 'scientific-6', 'ubuntu-precise' ]
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should raise on unknown tag name' do
|
11
|
+
expect { Boxcutter::Razor.image!({}, 'bad-tag') }.to raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should raise if no mac is provided' do
|
15
|
+
expect { Boxcutter::Razor.image!({}, 'ubuntu-precise') }.to raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should prepare razor and vlan, and reboot the machine' do
|
19
|
+
mac = 'aa:bb:cc:dd:ee:ff'
|
20
|
+
machine = Mash.new( { :network_interfaces_eth0_mac_address => mac } )
|
21
|
+
Boxcutter::Razor::StubbleClient.should_receive(:delete_mac).with(mac)
|
22
|
+
Boxcutter::Razor::StubbleClient.should_receive(:add_mac_to_tag).with(mac, 'ubuntu-precise')
|
23
|
+
Boxcutter::Machine.should_receive(:switch_vlan!).with(machine, 255)
|
24
|
+
Boxcutter::Machine.should_receive(:power_cycle!).with(machine)
|
25
|
+
Boxcutter::Machine.should_receive(:clear_hwtoken!).with(machine)
|
26
|
+
Boxcutter::Razor.image! machine, 'ubuntu-precise'
|
27
|
+
end
|
28
|
+
end
|