bluebox-boxcutter 0.0.14
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/.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
|