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.
@@ -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,3 @@
1
+ module Boxcutter
2
+ VERSION = '0.0.14'
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Boxcutter do
4
+
5
+ it 'should update' do
6
+ Boxcutter::Git.stub(:upstream_version).and_return "1000.1000.1000"
7
+ Boxcutter.should_receive(:install)
8
+ Boxcutter::update
9
+ end
10
+
11
+ end
@@ -0,0 +1,4 @@
1
+ [{"hostname":"foo1","id":123},
2
+ {"hostname":"foo2","id":124},
3
+ {"hostname":"bar1","id":125},
4
+ {"hostname":"bar2","id":126}]
@@ -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,13 @@
1
+ {
2
+ "id": "98765",
3
+ "network_interfaces": {
4
+ "eth0": {
5
+ "mac_address": "11:11:11:11:11",
6
+ "id": "68686"
7
+ }
8
+ },
9
+ "hostname": "foo1.blah.com",
10
+ "asset": {
11
+ "serial": "FFFFFF"
12
+ }
13
+ }
@@ -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>
@@ -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
@@ -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