ovh-provisioner 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.gitlab-ci.yml +23 -0
  4. data/.rspec +2 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG +7 -0
  7. data/CONTRIBUTING.md +62 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE +202 -0
  10. data/README.md +107 -0
  11. data/Rakefile +24 -0
  12. data/bin/console +32 -0
  13. data/bin/ovh_provisioner +27 -0
  14. data/bin/setup +23 -0
  15. data/lib/ovh/provisioner.rb +43 -0
  16. data/lib/ovh/provisioner/api_list.rb +158 -0
  17. data/lib/ovh/provisioner/api_object/api_object.rb +125 -0
  18. data/lib/ovh/provisioner/api_object/dedicated_server.rb +225 -0
  19. data/lib/ovh/provisioner/api_object/domain_zone.rb +115 -0
  20. data/lib/ovh/provisioner/api_object/ip.rb +83 -0
  21. data/lib/ovh/provisioner/api_object/record.rb +48 -0
  22. data/lib/ovh/provisioner/api_object/vrack.rb +92 -0
  23. data/lib/ovh/provisioner/cli.rb +173 -0
  24. data/lib/ovh/provisioner/cli_domain.rb +138 -0
  25. data/lib/ovh/provisioner/cli_ip.rb +64 -0
  26. data/lib/ovh/provisioner/cli_vrack.rb +71 -0
  27. data/lib/ovh/provisioner/init.rb +77 -0
  28. data/lib/ovh/provisioner/self_cli.rb +81 -0
  29. data/lib/ovh/provisioner/spawner.rb +63 -0
  30. data/lib/ovh/provisioner/version.rb +24 -0
  31. data/ovh-provisioner.gemspec +53 -0
  32. data/spec/config.yml +53 -0
  33. data/spec/helpers/highline_helper.rb +36 -0
  34. data/spec/ovh/provisioner/cli_domain_spec.rb +140 -0
  35. data/spec/ovh/provisioner/cli_ip_spec.rb +90 -0
  36. data/spec/ovh/provisioner/cli_spec.rb +186 -0
  37. data/spec/ovh/provisioner/cli_vrack_spec.rb +83 -0
  38. data/spec/ovh/provisioner/stubs/domain_stubs.rb +204 -0
  39. data/spec/ovh/provisioner/stubs/ip_stubs.rb +152 -0
  40. data/spec/ovh/provisioner/stubs/server_stubs.rb +146 -0
  41. data/spec/ovh/provisioner/stubs/vrack_stubs.rb +87 -0
  42. data/spec/ovh/provisioner_spec.rb +25 -0
  43. data/spec/spec_helper.rb +47 -0
  44. metadata +350 -0
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ #
6
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+
21
+ require 'bundler/setup'
22
+ require 'ovh/provisioner'
23
+
24
+ # You can add fixtures and/or initialization code here to make experimenting
25
+ # with your gem easier. You can also use a different console, if you like.
26
+
27
+ # (If you use this, don't forget to add pry to your Gemfile!)
28
+ # require "pry"
29
+ # Pry.start
30
+
31
+ require 'irb'
32
+ IRB.start
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ #
6
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+
21
+ # Trap interrupts to quit cleanly. See
22
+ # https://twitter.com/mitchellh/status/283014103189053442
23
+ Signal.trap('INT') { exit 1 }
24
+
25
+ require 'bundler/setup'
26
+ require 'ovh/provisioner'
27
+ OVH::Provisioner.start
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ #
3
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ set -euo pipefail
19
+ IFS=$'\n\t'
20
+
21
+ bundle install
22
+
23
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'logger'
20
+
21
+ module OVH
22
+ # Main module
23
+ module Provisioner
24
+ class << self
25
+ attr_accessor :logger
26
+
27
+ Dir[File.join(File.dirname(__FILE__), '*', '*.rb')].each do |file|
28
+ require file
29
+ end
30
+
31
+ def start
32
+ OVH::Provisioner::Cli.start(ARGV)
33
+ rescue StandardError => error
34
+ puts error.message
35
+ puts error.backtrace
36
+ exit 1
37
+ end
38
+ end
39
+
40
+ # Initialize the base logger
41
+ OVH::Provisioner.logger = Logger.new(STDOUT)
42
+ end
43
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'celluloid/current'
20
+ require 'yaml'
21
+
22
+ # rubocop:disable Metrics/ClassLength
23
+ module OVH
24
+ module Provisioner
25
+ # Represent an API list, that is a collection of a given kind of object
26
+ class APIList
27
+ include Celluloid
28
+ attr_reader :list
29
+
30
+ # targets is a list of regex on object id
31
+ # to match nothing, do not set any target
32
+ # to match everything, add a '' target
33
+ def initialize(api_object, parent, *targets)
34
+ @parent = parent
35
+ @url = api_object.entrypoint(parent).to_s
36
+ @name = api_object.name.split('::').last
37
+ @filter = generate_filter(targets)
38
+ @futures = {}
39
+ @list = []
40
+ end
41
+
42
+ def init_properties
43
+ write = !@completed
44
+ unless @completed
45
+ # In ATOM concurrency, this is enough to avoid a race condition.
46
+ @completed = true
47
+ spawner = Actor[Spawner::NAME]
48
+ get.keep_if { |s| @filter.call(s) }.each do |i|
49
+ @futures[i] = spawner.future.get(@name, parent: @parent, id: i)
50
+ end
51
+ end
52
+ wait(write)
53
+ end
54
+
55
+ def format(*fields)
56
+ items = classify(list, *fields)
57
+ items = deep_sort(items)
58
+ yamlize(items)
59
+ end
60
+
61
+ def parallel_map(method, args = [], subject = nil)
62
+ futures =
63
+ if subject.nil?
64
+ list.map { |item| item.future.send(method, *args) }
65
+ else
66
+ list.map { |item| subject.future.send(method, item, *args) }
67
+ end
68
+ futures.map(&:value)
69
+ end
70
+
71
+ def puts_each(method, args = [], subject = nil, remove_duplicate = true)
72
+ results = parallel_map(method, args, subject)
73
+ results = results.map(&:lines).flatten.compact.uniq if remove_duplicate
74
+ results.each { |i| puts i unless i.nil? }
75
+ end
76
+
77
+ private
78
+
79
+ def get(path = '')
80
+ OVH::Provisioner.client.get("#{@url}/#{path}")
81
+ end
82
+
83
+ def wait(write = false)
84
+ @futures.each_pair do |key, future|
85
+ v = future.value
86
+ @list << v if write
87
+ @futures.delete(key)
88
+ end
89
+ @list.sort! if write
90
+ end
91
+
92
+ def generate_filter(targets)
93
+ lambda do |id|
94
+ targets.reduce(false) do |acc, target|
95
+ acc | (id.to_s =~ /#{target.to_s}/)
96
+ end
97
+ end
98
+ end
99
+
100
+ def classify(items, *fields)
101
+ items.each_with_object({}) do |item, hash|
102
+ item = organize(item, *fields)
103
+ insert!(hash, @url[1..-1].tr('/', '_') => item)
104
+ hash
105
+ end
106
+ end
107
+
108
+ def organize(item, *fields)
109
+ head, *tail = *fields
110
+ return [item.to_s] if head.nil?
111
+
112
+ key = item.send(head.to_sym)
113
+ { key => organize(item, *tail) }
114
+ end
115
+
116
+ def insert!(hash, item)
117
+ if hash.is_a?(Array)
118
+ hash << item.first
119
+ else
120
+ key = item.keys.first
121
+ if hash[key].nil?
122
+ hash.merge!(item)
123
+ else
124
+ insert!(hash[key], item[key])
125
+ end
126
+ end
127
+ end
128
+
129
+ def deep_sort(hash)
130
+ return hash.sort if hash.is_a?(Array)
131
+ return hash unless hash.is_a?(Hash)
132
+
133
+ result = {}
134
+ hash.each { |k, v| result[k] = deep_sort(v) }
135
+ result.sort.to_h
136
+ end
137
+
138
+ def yamlize(items)
139
+ token = 'TO_REMOVE_AFTER_TOYAML'
140
+ yaml = prep_for_yaml(items, token).to_yaml
141
+ yaml.lines[1..-1].grep_v(/\- \|\-/).grep_v(/#{token}/).join('')
142
+ end
143
+
144
+ def prep_for_yaml(obj, token)
145
+ if obj.is_a?(Hash)
146
+ obj.map { |k, v| [k, prep_for_yaml(v, token)] }.to_h
147
+ elsif obj.is_a?(Array)
148
+ obj.map { |e| prep_for_yaml(e, token) }
149
+ elsif obj.is_a?(String)
150
+ "#{obj}\n#{token}"
151
+ else
152
+ obj
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'celluloid/current'
20
+
21
+ module OVH
22
+ module Provisioner
23
+ module APIObject
24
+ # Base API Object
25
+ class APIObject
26
+ include Celluloid
27
+ include Comparable
28
+
29
+ # Define @attributes to contain all attr_readers
30
+ def self.attr_reader(*vars)
31
+ @attributes ||= superclass.attributes.dup unless superclass == Object
32
+ @attributes ||= []
33
+ @attributes.concat vars
34
+ super
35
+ end
36
+
37
+ attr_reader :id
38
+
39
+ def self.attributes # rubocop:disable Style/TrivialAccessors
40
+ @attributes
41
+ end
42
+
43
+ def attributes
44
+ self.class.attributes
45
+ end
46
+
47
+ def initialize(id, parent = nil)
48
+ @id = id
49
+ @url = "#{self.class.entrypoint(parent)}/#{normalize(id)}"
50
+ @futures = {}
51
+ end
52
+
53
+ def init_properties
54
+ unless @completed
55
+ # In ATOM concurrency, this is enough to avoid a race condition.
56
+ @completed = true
57
+ infos = private_methods(false).grep(/^set_/)
58
+ infos.each { |i| @futures[i] = future.send(i) }
59
+ end
60
+ @futures.each_pair do |key, future|
61
+ future.value
62
+ @futures.delete(key)
63
+ end
64
+ end
65
+
66
+ def to_s
67
+ "#{self.class.entrypoint}: #{id}\n" \
68
+ "#{attributes.map { |s| "- #{send(s)}" }.join("\n")}"
69
+ end
70
+
71
+ def self.entrypoint(parent = nil)
72
+ classpath = underscore(classname).tr('_', '/')
73
+ parent.nil? ? "/#{classpath}" : "#{parent}/#{classpath}"
74
+ end
75
+
76
+ def self.classname
77
+ name.split('::').last
78
+ end
79
+
80
+ # inspired from Rails' ActiveSupport
81
+ def self.underscore(string)
82
+ res = string.gsub(/::/, '/')
83
+ res.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
84
+ res.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
85
+ res.tr('-', '_').downcase
86
+ end
87
+
88
+ def <=>(other)
89
+ id <=> other.id
90
+ end
91
+
92
+ def config
93
+ OVH::Provisioner.config
94
+ end
95
+
96
+ def get(path = '')
97
+ call(:get, ["#{@url}/#{path}"])
98
+ end
99
+
100
+ def post(path, body = nil)
101
+ call(:post, ["#{@url}/#{path}", body])
102
+ end
103
+
104
+ def delete(path = '')
105
+ call(:delete, ["#{@url}/#{path}"])
106
+ end
107
+
108
+ private
109
+
110
+ def call(method, args)
111
+ OVH::Provisioner.client.send(method, *args)
112
+ rescue OVH::RESTError => error
113
+ error.to_s.split(':')[-1].strip
114
+ end
115
+
116
+ def normalize(id)
117
+ id = id.gsub('/', '%2F') if id.is_a? String
118
+ id
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ Dir[File.dirname(__FILE__) + '/*.rb'].each { |file| require file }
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2015-2016 Sam4Mobile, 2017-2018 Make.org
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'hashdiff'
20
+
21
+ module OVH
22
+ module Provisioner
23
+ module APIObject
24
+ # Represent a dedicated server
25
+ class DedicatedServer < APIObject # rubocop:disable Metrics/ClassLength
26
+ # install_status const
27
+ IDLE = 'Server is not being installed or reinstalled at the moment'
28
+ NO_OS = 'none_64'
29
+
30
+ attr_reader :server_id, :reverse, :ip, :dc, :location, :os, :boot
31
+ attr_reader :state
32
+ attr_reader :flavor, :flavor_tag
33
+ attr_reader :vrack, :vrack_id
34
+ attr_reader :install_status
35
+
36
+ def install
37
+ raise 'Please provide a valid template' if config['template'].nil?
38
+
39
+ ok, reason = can_install
40
+ if ok
41
+ result = post('install/start', install_body)
42
+ if result.is_a?(String) # Meaning an error message
43
+ reason = result
44
+ ok = false
45
+ end
46
+ end
47
+ "#{id}: #{ok ? 'ok' : 'failed'}\n #{reason}"
48
+ end
49
+
50
+ def to_s
51
+ "#{reverse}[#{id}/#{server_id}]" \
52
+ "\n #{location} - #{flavor} - #{vrack}:#{ip} " \
53
+ "- #{boot}:#{os} - #{state}" \
54
+ "\n #{install_status}"
55
+ end
56
+
57
+ def newname
58
+ name_scheme = Provisioner.config['name_scheme']
59
+ return reverse if name_scheme.nil?
60
+
61
+ subst = attributes.map { |a| { a => send(a) } }.reduce(&:merge)
62
+ name_scheme % subst
63
+ end
64
+
65
+ def rename(domain, name = newname)
66
+ full_name = "#{name}.#{domain.id}"
67
+ return "#{reverse}: no change" if reverse == full_name
68
+
69
+ # check if name is already used
70
+ records = domain.filter_records('subdomain' => newname)
71
+ return "#{reverse}: name already used" unless records.list.empty?
72
+
73
+ # remove old name (only if it belongs to the same domain)
74
+ remove_old_names(domain)
75
+
76
+ domain.add_record(name, 'A', ip)
77
+ end
78
+
79
+ def define_reverse(domain, name = newname)
80
+ full_name = "#{name}.#{domain.id}"
81
+ return "#{reverse}: no change" if reverse == full_name
82
+
83
+ add_reverse(full_name)
84
+ end
85
+
86
+ def ipmi
87
+ request_ipmi
88
+
89
+ (1..10).each do |_|
90
+ result = get('features/ipmi/access?type=kvmipJnlp')
91
+ unless result.is_a?(String)
92
+ launch_ipmi(result['value'])
93
+ break
94
+ end
95
+ sleep(1)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def can_install
102
+ if install_status != IDLE
103
+ [false, "Installation ongoing: #{install_status}"]
104
+ elsif !config['force'] && os != NO_OS
105
+ [false,
106
+ "An OS is already installed (#{os}) but option force is false"]
107
+ else [true, "Installation of #{config['template']} launched"]
108
+ end
109
+ end
110
+
111
+ def install_body
112
+ details = { 'customHostname' => reverse.chomp('.') }
113
+ %w[customHostname useDistribKernel sshKeyName].each do |opt|
114
+ value = config[APIObject.underscore(opt)]
115
+ details.merge!(opt => value) unless value.nil?
116
+ end
117
+ {
118
+ 'templateName' => config['template'],
119
+ 'details' => details
120
+ }
121
+ end
122
+
123
+ def set_general
124
+ general = get
125
+ @reverse = get_reverse(general)
126
+ @server_id = general['serverId']
127
+ @ip = general['ip']
128
+ @dc = general['datacenter']
129
+ @location = dc.gsub(/[0-9]*$/, '')
130
+ @os = general['os']
131
+ @boot = get_boot(general)
132
+ @state = general['state']
133
+ end
134
+
135
+ def get_reverse(general)
136
+ (general['reverse'] || id).chomp('.')
137
+ end
138
+
139
+ def get_boot(general)
140
+ get("boot/#{general['bootId']}")['bootType']
141
+ end
142
+
143
+ def set_flavor
144
+ hardware = get('specifications/hardware')
145
+ flavors = get_flavors_from_config(hardware)
146
+ raise "Too many flavor possible: #{flavors}" if flavors.size > 1
147
+
148
+ @flavor, @flavor_tag =
149
+ if flavors.first.nil?
150
+ %w[undefined undef]
151
+ else
152
+ [flavors.first, config['flavors'][flavors.first]['tag']]
153
+ end
154
+ end
155
+
156
+ def get_flavors_from_config(hardware)
157
+ (config['flavors'].dup || {}).map do |flavor, desc|
158
+ diff = HashDiff.diff(hardware, desc['hardware'])
159
+ signs = diff.map(&:first).uniq - ['-']
160
+ signs.empty? ? flavor : nil
161
+ end.compact
162
+ end
163
+
164
+ def set_vrack
165
+ vracks = get('vrack')
166
+ raise "Too many vracks for #{server}: #{vracks}" if vracks.size > 1
167
+
168
+ if vracks.first.nil?
169
+ @vrack_id = '-1'
170
+ @vrack = 'none'
171
+ else
172
+ @vrack_id = vracks.first
173
+ @vrack = Actor[Spawner::NAME].get('Vrack', id: @vrack_id).name
174
+ end
175
+ end
176
+
177
+ def set_install_status
178
+ status = get('install/status')
179
+ if status.is_a?(String)
180
+ status = {
181
+ 'progress' => ['status' => 'doing', 'comment' => status]
182
+ }
183
+ end
184
+ @install_status = extract_status(status)
185
+ end
186
+
187
+ def extract_status(status)
188
+ cur = status['progress'].select { |step| step['status'] == 'doing' }
189
+ cur << { 'comment' => 'Preparing installation' } if cur.empty?
190
+ cur.first['comment']
191
+ end
192
+
193
+ def remove_old_names(domain)
194
+ old_name = reverse.match(/^(.*)\.#{domain.id}$/).to_a.last
195
+ return if old_name.nil?
196
+
197
+ records = domain.filter_records('subdomain' => old_name)
198
+ records.parallel_map(:rm_record, [], domain)
199
+ end
200
+
201
+ def add_reverse(full_name)
202
+ ip_obj = Actor[Spawner::NAME].get('IP', id: "#{ip}/32")
203
+ result = ip_obj.add_reverse(full_name)
204
+ cant = "Cannot check if #{full_name}. resolves to #{ip}"
205
+ if result == cant
206
+ result = "#{reverse}: DNS propagation is not finished, try later_"
207
+ end
208
+ result
209
+ end
210
+
211
+ def request_ipmi
212
+ body = { 'ttl' => '15', 'type' => 'kvmipJnlp' }
213
+ result = post('features/ipmi/access', body)
214
+ puts result if result.is_a?(String)
215
+ end
216
+
217
+ def launch_ipmi(jnlp)
218
+ filename = "/tmp/#{id}.jnlp"
219
+ File.open(filename, 'w') { |file| file.write(jnlp) }
220
+ system("javaws #{filename}")
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end