ovh-provisioner 0.1.0

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.
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