bluebox-boxcutter 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -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