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
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## v0.0.13 (August 22, 2013)
|
2
|
+
|
3
|
+
A rework of updating, you can now just update boxutter via 'boxcutter update' and it will
|
4
|
+
clone down the latest and greatest and update your local gem
|
5
|
+
|
6
|
+
### Improvements
|
7
|
+
* Pull request [#32](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/32): [update] just update - Sam
|
8
|
+
|
9
|
+
## v0.0.12 (August 21, 2013)
|
10
|
+
|
11
|
+
You can now see if you have the latest version with: boxcutter check-for-update
|
12
|
+
|
13
|
+
### Improvements
|
14
|
+
* Pull request [#31](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/31): [update_avail] version check against upstream - Sam
|
15
|
+
|
16
|
+
## v0.0.11 (August 21, 2013)
|
17
|
+
|
18
|
+
### Improvements
|
19
|
+
* Pull request [#30](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/30): [more_info] show host info when rebooting or razoring - Sam
|
20
|
+
* Pull request [#29](https://github.blueboxgrid.com/bluebox-tech/boxcutter/pull/29): rm hwagent token - Sam
|
21
|
+
|
22
|
+
|
23
|
+
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
bluebox-boxcutter (0.0.8)
|
5
|
+
colorize
|
6
|
+
commander
|
7
|
+
httparty
|
8
|
+
mash
|
9
|
+
nokogiri
|
10
|
+
spunkmeyer (>= 0.0.5)
|
11
|
+
terminal-table
|
12
|
+
|
13
|
+
GEM
|
14
|
+
remote: https://rubygems.org/
|
15
|
+
specs:
|
16
|
+
colorize (0.5.8)
|
17
|
+
commander (4.1.4)
|
18
|
+
highline (~> 1.6.11)
|
19
|
+
diff-lcs (1.2.4)
|
20
|
+
highline (1.6.19)
|
21
|
+
httparty (0.11.0)
|
22
|
+
multi_json (~> 1.0)
|
23
|
+
multi_xml (>= 0.5.2)
|
24
|
+
mash (0.1.1)
|
25
|
+
mini_portile (0.5.1)
|
26
|
+
multi_json (1.7.7)
|
27
|
+
multi_xml (0.5.4)
|
28
|
+
nokogiri (1.6.0)
|
29
|
+
mini_portile (~> 0.5.0)
|
30
|
+
rake (10.1.0)
|
31
|
+
rspec (2.14.1)
|
32
|
+
rspec-core (~> 2.14.0)
|
33
|
+
rspec-expectations (~> 2.14.0)
|
34
|
+
rspec-mocks (~> 2.14.0)
|
35
|
+
rspec-core (2.14.4)
|
36
|
+
rspec-expectations (2.14.0)
|
37
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
38
|
+
rspec-mocks (2.14.1)
|
39
|
+
spunkmeyer (0.0.5)
|
40
|
+
colorize
|
41
|
+
sqlite3
|
42
|
+
sqlite3 (1.3.7)
|
43
|
+
terminal-table (1.4.5)
|
44
|
+
|
45
|
+
PLATFORMS
|
46
|
+
ruby
|
47
|
+
|
48
|
+
DEPENDENCIES
|
49
|
+
bluebox-boxcutter!
|
50
|
+
rake
|
51
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# boxcutter
|
2
|
+
|
3
|
+
A command-line interface to BoxPanel.
|
4
|
+
|
5
|
+
# prerequisites
|
6
|
+
|
7
|
+
This tool requres `ruby --version` >= `1.9.3`.
|
8
|
+
If you are not already running a suitable version, [rbenv](https://github.com/sstephenson/rbenv) is recommended.
|
9
|
+
|
10
|
+
# installation
|
11
|
+
|
12
|
+
git clone git@github.blueboxgrid.com:bluebox-tech/boxcutter.git
|
13
|
+
cd boxcutter
|
14
|
+
gem install rspec
|
15
|
+
rake install
|
16
|
+
|
17
|
+
# usage
|
18
|
+
|
19
|
+
*You must be logged into BoxPanel in the Chrome browser - boxcutter auths with your Chrome cookies*
|
20
|
+
|
21
|
+
```bash
|
22
|
+
boxcutter help # usage
|
23
|
+
boxcutter machine-search app # search for machine names given a substring
|
24
|
+
boxcutter machine-show ds590 # show machine details given its name
|
25
|
+
boxcutter password "kvm[2-5]" # show passwords given a password name regex
|
26
|
+
boxcutter kvm kvm9.sea03 # open a kvm console given the password name
|
27
|
+
boxcutter reboot ds590 # reboot a machine
|
28
|
+
boxcutter razor-tags # show available razor tags
|
29
|
+
boxcutter razor ds590 ubuntu-precise # razor a machine
|
30
|
+
boxcutter razor-log ds590 # show razor log messages
|
31
|
+
```
|
32
|
+
|
33
|
+
# example
|
34
|
+
|
35
|
+
![boxcutter screenshot](https://github.blueboxgrid.com/bluebox-tech/boxcutter/raw/master/doc/screenshot.png)
|
36
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
$: << "#{File.dirname __FILE__}/lib"
|
3
|
+
require 'bluebox-boxcutter/version'
|
4
|
+
|
5
|
+
task :install do
|
6
|
+
sh %{gem build bluebox-boxcutter.gemspec}
|
7
|
+
sh %{gem uninstall -x bluebox-boxcutter} if system 'gem which bluebox-boxcutter'
|
8
|
+
sh %{gem install bluebox-boxcutter-#{Boxcutter::VERSION}.gem --no-ri --no-rdoc}
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
12
|
+
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
13
|
+
t.rspec_opts = '--color'
|
14
|
+
end
|
data/bin/boxcutter
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#
|
2
|
+
# -*- encoding: utf-8 -*-
|
3
|
+
$: << "#{File.dirname __FILE__}/lib"
|
4
|
+
require 'bluebox-boxcutter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'bluebox-boxcutter'
|
8
|
+
s.version = Boxcutter::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = [ 'Sam Cooper', 'Tim Miller' ]
|
11
|
+
s.email = [ '' ]
|
12
|
+
s.homepage = 'https://github.blueboxgrid.com/bluebox-tech/boxcutter'
|
13
|
+
s.summary = %q{cli for boxpanel}
|
14
|
+
s.description = %q{cli for boxpanel.}
|
15
|
+
|
16
|
+
s.required_ruby_version = ">= 1.9.1"
|
17
|
+
s.required_rubygems_version = ">= 1.3.7"
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ['lib']
|
23
|
+
|
24
|
+
s.add_runtime_dependency 'colorize', '>= 0'
|
25
|
+
s.add_runtime_dependency 'commander', '>= 0'
|
26
|
+
s.add_runtime_dependency 'httparty', '>= 0'
|
27
|
+
s.add_runtime_dependency 'mash', '>= 0'
|
28
|
+
s.add_runtime_dependency 'nokogiri', '>= 0'
|
29
|
+
s.add_runtime_dependency 'spunkmeyer', '>= 0.0.5'
|
30
|
+
s.add_runtime_dependency 'terminal-table', '>= 0'
|
31
|
+
s.add_development_dependency 'rake', '>= 0'
|
32
|
+
s.add_development_dependency 'rspec', '>= 0'
|
33
|
+
end
|
data/doc/screenshot.png
ADDED
Binary file
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'bluebox-boxcutter/kvm'
|
3
|
+
require 'bluebox-boxcutter/machine'
|
4
|
+
require 'bluebox-boxcutter/password'
|
5
|
+
require 'bluebox-boxcutter/razor'
|
6
|
+
require 'bluebox-boxcutter/ui'
|
7
|
+
require 'bluebox-boxcutter/version'
|
8
|
+
require 'bluebox-boxcutter/git'
|
9
|
+
require 'nokogiri'
|
10
|
+
require 'spunkmeyer'
|
11
|
+
require 'mash'
|
12
|
+
|
13
|
+
module Boxcutter
|
14
|
+
|
15
|
+
def self.version
|
16
|
+
eval(File.read("#{File.dirname __FILE__}/../bluebox-boxcutter.gemspec")).version.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.install(git_repo)
|
20
|
+
|
21
|
+
# if we're passed a file path
|
22
|
+
if git_repo.match /^(\/.*)+\/?/
|
23
|
+
Dir.chdir git_repo
|
24
|
+
`rake install`
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.update
|
29
|
+
Boxcutter::Git::clone
|
30
|
+
upstream_version = Boxcutter::Git::upstream_version
|
31
|
+
our_version = Boxcutter::VERSION
|
32
|
+
if Gem::Version.new(upstream_version) > Gem::Version.new(our_version)
|
33
|
+
Boxcutter::install(Boxcutter::Git::LOCAL_CLONE)
|
34
|
+
end
|
35
|
+
Boxcutter::Git::rm_clone
|
36
|
+
end
|
37
|
+
|
38
|
+
# flatten any child hashes into top level hash
|
39
|
+
# e.g. {"x" => { "a" => "b" }
|
40
|
+
# will become {"x_a" => "b" }
|
41
|
+
def self.flatten_hash(hash, parent_key = "", new_hash = { })
|
42
|
+
hash.each do |key,value|
|
43
|
+
key_string = parent_key.empty? ? key : "#{parent_key}_#{key}"
|
44
|
+
if value.class == Hash
|
45
|
+
flatten_hash(value, key_string, new_hash)
|
46
|
+
else
|
47
|
+
new_hash.merge!({ key_string => value })
|
48
|
+
end
|
49
|
+
end
|
50
|
+
new_hash
|
51
|
+
end
|
52
|
+
|
53
|
+
# a boxpanel HTTP client, which auths with cookies from one's browser.
|
54
|
+
class Boxpanel
|
55
|
+
include HTTParty
|
56
|
+
|
57
|
+
BASE_URI = 'https://boxpanel.bluebox.net/'
|
58
|
+
base_uri BASE_URI
|
59
|
+
|
60
|
+
# get cookies from browser.
|
61
|
+
def self.cookie_header
|
62
|
+
cookies = Spunkmeyer.cookies BASE_URI
|
63
|
+
cookies.to_a.map { |c| "#{c.first}=#{c.last[:value]}" }.join '; '
|
64
|
+
end
|
65
|
+
|
66
|
+
# wrap the default get method with auth and error handling.
|
67
|
+
def self.get(path, options={}, &block)
|
68
|
+
options[:headers] ||= {}
|
69
|
+
options[:headers]['Cookie'] = cookie_header
|
70
|
+
resp = super path, options, &block
|
71
|
+
raise "request failed with #{resp.code}: #{path}" unless resp.code == 200
|
72
|
+
resp.body
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.get_html(path, options={}, &block)
|
76
|
+
Nokogiri::HTML Boxcutter::Boxpanel::get(path, options, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
# wrap the default post method with auth and error handling.
|
80
|
+
def self.post(path, options={}, &block)
|
81
|
+
options[:headers] ||= {}
|
82
|
+
options[:headers]['Cookie'] = cookie_header
|
83
|
+
resp = super path, options, &block
|
84
|
+
raise "post request failed with #{resp.code}: #{path}" unless resp.code == 200
|
85
|
+
resp.body
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.machine_url(machine_id)
|
89
|
+
BASE_URI + "private/machines/#{machine_id}/edit"
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'commander/import'
|
3
|
+
|
4
|
+
program :name, 'boxcutter'
|
5
|
+
program :version, Boxcutter::VERSION
|
6
|
+
program :description, 'CLI to boxpanel'
|
7
|
+
|
8
|
+
global_option('-y', '--yes', 'assume yes to all prompts') { Boxcutter.yes! }
|
9
|
+
global_option('-c', '--no-colors', 'no colors in stdout') { Boxcutter.no_colors! }
|
10
|
+
|
11
|
+
command 'machine-search'.to_sym do |c|
|
12
|
+
c.syntax ='boxcutter machine-search HOSTNAME_REGEX'
|
13
|
+
c.description = 'search for hostnames matching a regex'
|
14
|
+
c.action do |args, options|
|
15
|
+
Boxcutter.error! "missing regex: #{c.syntax}" unless args.length == 1
|
16
|
+
puts Boxcutter::table Boxcutter::Machine.search args.first
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias_command :'msearch', :'machine-search'
|
20
|
+
|
21
|
+
command 'machine-show'.to_sym do |c|
|
22
|
+
c.syntax ='boxcutter machine-show HOSTNAME'
|
23
|
+
c.description = "show a host's attributes"
|
24
|
+
c.action do |args, options|
|
25
|
+
Boxcutter.error! "missing hostname: #{c.syntax}" unless args.length == 1
|
26
|
+
puts Boxcutter::table Boxcutter::Machine.get args.first
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias_command :'mshow', :'machine-show'
|
30
|
+
|
31
|
+
command 'password'.to_sym do |c|
|
32
|
+
c.syntax ='boxcutter password NAME_REGEX'
|
33
|
+
c.description = 'search for passowords matching a given name regexp'
|
34
|
+
c.action do |args, options|
|
35
|
+
Boxcutter.error! "missing password regexp: #{c.syntax}" unless args.length == 1
|
36
|
+
puts Boxcutter::table Boxcutter::Password.search args.first
|
37
|
+
end
|
38
|
+
end
|
39
|
+
alias_command :'pw', :'password'
|
40
|
+
|
41
|
+
command 'kvm'.to_sym do |c|
|
42
|
+
c.syntax = 'boxcutter kvm PASSWORD_NAME_REGEX'
|
43
|
+
c.description = 'start a kvm console by password name'
|
44
|
+
c.action do |args, options|
|
45
|
+
Boxcutter.error! "missing kvm name: #{c.syntax}" unless args.length == 1
|
46
|
+
creds = Boxcutter::Password.search args.first
|
47
|
+
puts Boxcutter::table creds
|
48
|
+
error! "password regex is not unique: #{args.first}" if creds.length > 2
|
49
|
+
Boxcutter::KVM.start creds.last[1], creds.last[2], creds.last[3]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
command 'reboot'.to_sym do |c|
|
54
|
+
c.syntax = 'boxcutter reboot HOSTNAME'
|
55
|
+
c.description = 'reboot a host'
|
56
|
+
c.action do |args, options|
|
57
|
+
Boxcutter.error! "missing hostname: #{c.syntax}" unless args.length == 1
|
58
|
+
machine = Boxcutter::Machine.get args.first
|
59
|
+
Boxcutter::Machine::machine_summary(machine).each { |msg| Boxcutter.msg msg }
|
60
|
+
Boxcutter.confirm "this will reboot #{machine[:hostname]}!" do
|
61
|
+
Boxcutter::Machine.power_cycle! machine
|
62
|
+
Boxcutter::msg "reboot task issued for #{machine[:hostname]}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
command 'razor'.to_sym do |c|
|
68
|
+
c.syntax = 'boxcutter razor HOSTNAME RAZOR_TAG_NAME'
|
69
|
+
c.description = 'reimage a host with razor'
|
70
|
+
c.action do |args, options|
|
71
|
+
machine = Boxcutter::Machine.get(args.first)
|
72
|
+
Boxcutter.error! "must provide hostname and razor tag name" unless args.length == 2
|
73
|
+
Boxcutter::Machine::machine_summary(machine).each { |msg| Boxcutter.msg msg }
|
74
|
+
Boxcutter.confirm "this will re-image #{args.first}!" do
|
75
|
+
Boxcutter::Razor.image! machine, args.last
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
command 'razor-tags'.to_sym do |c|
|
81
|
+
c.syntax = 'boxcutter razor-tags'
|
82
|
+
c.description = 'show available razor tags'
|
83
|
+
c.action do
|
84
|
+
tags = Boxcutter::Razor::StubbleClient.tags
|
85
|
+
puts Boxcutter::table([[:name]] + tags.map { |t| [ t ] })
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
command 'razor-log'.to_sym do |c|
|
90
|
+
c.syntax = 'boxcutter razor-log HOSTNAME'
|
91
|
+
c.description = 'show current razor log for a host'
|
92
|
+
c.action do |args, options|
|
93
|
+
Boxcutter::error! "must provide hostname" unless args.length == 1
|
94
|
+
logs = Boxcutter::Razor::StubbleClient.log(Boxcutter::Machine::eth0_mac(Boxcutter::Machine.get(args.first)))
|
95
|
+
if logs.any?
|
96
|
+
logs.each { |l| l['timestamp'] = DateTime.strptime(l['timestamp'].to_s,'%s').to_s if l['timestamp'] }
|
97
|
+
logs.each { |l| l.delete 'node_uuid' if l['node_uuid'] }
|
98
|
+
puts Boxcutter::table_of_hashes(logs)
|
99
|
+
else
|
100
|
+
Boxcutter::error! "no logs for #{args.first}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
command 'update'.to_sym do |c|
|
106
|
+
c.syntax = 'boxcutter update'
|
107
|
+
c.description = 'update to the latest version'
|
108
|
+
c.action do |args, options|
|
109
|
+
Boxcutter::update
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Boxcutter
|
2
|
+
module Git
|
3
|
+
|
4
|
+
REPO = "git@github.blueboxgrid.com:bluebox-tech/boxcutter.git"
|
5
|
+
LOCAL_CLONE = "/tmp/boxcutter_gem"
|
6
|
+
|
7
|
+
def self.upstream_version
|
8
|
+
File.read("#{LOCAL_CLONE}/lib/bluebox-boxcutter/version.rb").match(/VERSION\s?=\s?['"](.*)['"]/)[1]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.clone
|
12
|
+
if git?
|
13
|
+
`git clone #{REPO} #{LOCAL_CLONE} 2>&1 > /dev/null`
|
14
|
+
if $?.to_i != 0
|
15
|
+
Boxcutter.error! "Problem cloning git repo: #{REPO} to #{LOCAL_CLONE}"
|
16
|
+
end
|
17
|
+
else
|
18
|
+
Boxcutter.error! "Could not find git"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.rm_clone
|
23
|
+
FileUtils.rm_rf LOCAL_CLONE
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.git?
|
27
|
+
`git --version`
|
28
|
+
$?.to_i == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Boxcutter
|
5
|
+
module KVM
|
6
|
+
|
7
|
+
# given a kvm url and creds, start the java kvm console thingy.
|
8
|
+
def self.start(url, user, password)
|
9
|
+
u = URI url
|
10
|
+
base, path = [ "#{u.scheme}://#{u.host}", u.path ]
|
11
|
+
path = '/auth.asp' # path is missing from some of the password entries.
|
12
|
+
|
13
|
+
client = Class.new do
|
14
|
+
include HTTParty
|
15
|
+
base_uri base
|
16
|
+
end
|
17
|
+
|
18
|
+
response = client.post(path, :body => {:login => user, :password => password, :action_login => 'Login'})
|
19
|
+
cookie = response.request.options[:headers]['Cookie']
|
20
|
+
jnlp_contents = client.get(extract_jnlp_path(response.body), :headers => {'Cookie' => cookie}).body
|
21
|
+
run_jnlp(jnlp_contents)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.extract_jnlp_path(html)
|
25
|
+
Nokogiri::HTML(html).css('a').map { |a| a['href'] }.select { |a| a =~ /spider.jnlp/ }.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.run_jnlp(contents)
|
29
|
+
Tempfile.new('kvm-webstart').tap do |f|
|
30
|
+
f.write contents
|
31
|
+
f.close
|
32
|
+
`javaws #{f.path} &`
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|