vbox-ng 0.1.3 → 1.0.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.
- data/Gemfile +2 -2
- data/Gemfile.lock +7 -1
- data/README.md +7 -8
- data/Rakefile +15 -5
- data/VERSION +1 -1
- data/lib/vbox-ng.rb +14 -2
- data/lib/vbox.rb +14 -2
- data/lib/vbox/cli.rb +208 -75
- data/lib/vbox/cmdlineapi.rb +136 -90
- data/lib/vbox/vm.rb +111 -11
- data/spec/cmdlineapi_spec.rb +52 -0
- data/spec/spec_helper.rb +46 -1
- data/spec/support/vboxmanage_simulator.rb +136 -0
- data/spec/support/vboxmanage_simulator.yml +1171 -0
- data/spec/vm_spec.rb +101 -5
- data/vbox-ng.gemspec +11 -5
- metadata +24 -5
data/lib/vbox/cmdlineapi.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'awesome_print'
|
3
|
+
require 'open3'
|
3
4
|
|
4
5
|
module VBOX
|
5
6
|
|
@@ -12,28 +13,72 @@ module VBOX
|
|
12
13
|
|
13
14
|
def initialize options={}
|
14
15
|
@options = options
|
15
|
-
@options[:verbose]
|
16
|
-
@options[:verbose] ||=
|
16
|
+
@options[:verbose] = VBOX.verbosity
|
17
|
+
@options[:verbose] ||= 2 if @options[:dry_run] && @options[:verbose] < 2
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def quiet &block
|
23
|
+
prev_verbose = @options[:verbose]
|
24
|
+
@options[:verbose] = -100
|
25
|
+
r = yield
|
26
|
+
ensure
|
27
|
+
@options[:verbose] = prev_verbose
|
25
28
|
r
|
26
29
|
end
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
+
def vboxmanage *args
|
32
|
+
args = args.map do |arg|
|
33
|
+
case arg
|
34
|
+
when Hash
|
35
|
+
arg.map{ |k,v| ["--#{k}", v == '' ? nil : v.to_s] }
|
36
|
+
else
|
37
|
+
arg.to_s
|
38
|
+
end
|
39
|
+
end.flatten.compact
|
40
|
+
|
41
|
+
puts "[.] VBoxManage #{args.join(' ')}".gray if @options[:verbose] >= 2
|
31
42
|
exit if @options[:dry_run]
|
32
|
-
|
33
|
-
|
34
|
-
|
43
|
+
|
44
|
+
stdout = stderr = nil
|
45
|
+
Open3.popen3("VBoxManage", *args.map(&:to_s)) do |i,o,e,x|
|
46
|
+
i.close
|
47
|
+
stdout = o.read.to_s.strip
|
48
|
+
stderr = e.read.to_s.strip
|
49
|
+
@success = (x ? x.value : $?).success?
|
50
|
+
end
|
51
|
+
|
52
|
+
if @options[:verbose] >= 0
|
53
|
+
STDERR.puts( success? ? stderr : stderr.red ) unless stderr.empty?
|
54
|
+
puts(stdout) if @options[:verbose] >= 3 && !stdout.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
stdout
|
58
|
+
end
|
59
|
+
|
60
|
+
# internal method that indicates result of recent vboxmanage() cmd
|
61
|
+
def success?
|
62
|
+
@success
|
35
63
|
end
|
36
64
|
|
65
|
+
# # run as in backtick operator, and return result
|
66
|
+
# def ` cmd
|
67
|
+
# puts "[.] #{cmd}".gray if @options[:verbose] >= 2
|
68
|
+
# exit if @options[:dry_run]
|
69
|
+
# r = super
|
70
|
+
# #exit 1 unless success?# r
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# # run as in system() call
|
74
|
+
# def system *args
|
75
|
+
# puts "[.] #{args.inspect}".gray if @options[:verbose] >= 2
|
76
|
+
# exit if @options[:dry_run]
|
77
|
+
# r = super
|
78
|
+
# #exit 1 unless success?# r
|
79
|
+
# end
|
80
|
+
|
81
|
+
public
|
37
82
|
def get_vm_details vm_or_name_or_uuid
|
38
83
|
name_or_uuid = case vm_or_name_or_uuid
|
39
84
|
when String
|
@@ -41,32 +86,25 @@ module VBOX
|
|
41
86
|
vm_or_name_or_uuid
|
42
87
|
when VM
|
43
88
|
vm = vm_or_name_or_uuid
|
44
|
-
vm.uuid
|
89
|
+
vm.uuid || vm.name
|
45
90
|
end
|
46
91
|
|
47
|
-
|
92
|
+
h = {}
|
93
|
+
data = quiet{ vboxmanage :showvminfo, name_or_uuid, "--machinereadable" }
|
48
94
|
data.each_line do |line|
|
49
|
-
k,v = line.
|
95
|
+
k,v = line.split('=',2)
|
50
96
|
next unless v
|
51
|
-
|
52
|
-
case k
|
53
|
-
when 'name'
|
54
|
-
vm.name = v.strip.sub(/^"/,'').sub(/"$/,'')
|
55
|
-
when 'UUID'
|
56
|
-
vm.uuid = v.strip.sub(/^"/,'').sub(/"$/,'')
|
57
|
-
when 'memory'
|
58
|
-
vm.memory_size = v.to_i
|
59
|
-
when 'VMState'
|
60
|
-
vm.state = v.tr('"','').to_sym
|
61
|
-
# when 'CfgFile'
|
62
|
-
# dir = File.dirname(v.tr('"',''))
|
63
|
-
# s = `du -s -BM "#{dir}"`
|
64
|
-
# vm.dir_size = s.split("\t").first.tr("M","")
|
65
|
-
end
|
97
|
+
h[qstrip(k)] = qstrip(v)
|
66
98
|
end
|
67
|
-
|
99
|
+
h.empty? ? nil : h
|
68
100
|
end
|
69
101
|
|
102
|
+
def createvm vm
|
103
|
+
options = { :name => vm.name, :register => '' }
|
104
|
+
options[:uuid] = vm.uuid if vm.uuid
|
105
|
+
a = vboxmanage :createvm, options
|
106
|
+
success? end
|
107
|
+
|
70
108
|
# for natural string sort order
|
71
109
|
# http://stackoverflow.com/questions/4078906/is-there-a-natural-sort-by-method-for-ruby
|
72
110
|
def _naturalize s
|
@@ -74,56 +112,60 @@ module VBOX
|
|
74
112
|
end
|
75
113
|
|
76
114
|
def list_vms params = {}
|
77
|
-
|
78
|
-
|
79
|
-
else
|
80
|
-
data = `VBoxManage list vms`
|
81
|
-
end
|
82
|
-
r = []
|
83
|
-
data.strip.each_line do |line|
|
115
|
+
vms = []
|
116
|
+
vboxmanage(:list, :vms).each_line do |line|
|
84
117
|
if line[UUID_RE]
|
85
118
|
vm = VM.new
|
86
119
|
vm.uuid = $&
|
87
|
-
vm.name = line.gsub($&, '')
|
88
|
-
|
120
|
+
vm.name = qstrip(line.gsub($&, ''))
|
121
|
+
vms << vm
|
89
122
|
end
|
90
123
|
end
|
91
|
-
|
124
|
+
if params[:include_state]
|
125
|
+
# second pass
|
126
|
+
h = Hash[*vms.map{ |vm| [vm.uuid, vm] }.flatten]
|
127
|
+
uuid = nil # declare variable for inner loop
|
128
|
+
vboxmanage(:list, :runningvms).each_line do |line|
|
129
|
+
h[uuid].state = :running if (uuid=line[UUID_RE]) && h[uuid]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
vms.sort_by{ |vm| _naturalize(vm.name) }
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
# strip quotes
|
137
|
+
def qstrip s
|
138
|
+
s.strip.sub /\A"(.*)"\Z/, '\1'
|
92
139
|
end
|
93
140
|
|
141
|
+
public
|
94
142
|
def get_vm_info name
|
95
|
-
data = `VBoxManage showvminfo "#{name}" --machinereadable`
|
96
143
|
h = {}
|
97
|
-
|
98
|
-
line.strip!
|
144
|
+
vboxmanage(:showvminfo, name, "--machinereadable").each_line do |line|
|
99
145
|
k,v = line.split('=',2)
|
100
|
-
h[k] = v
|
146
|
+
h[qstrip(k)] = qstrip(v)
|
101
147
|
end
|
102
148
|
h
|
103
149
|
end
|
104
150
|
|
105
|
-
def show name
|
106
|
-
get_vm_info(name).each do |k,v|
|
107
|
-
next if ['"none"', '"off"', '""'].include?(v)
|
108
|
-
puts "#{k}=#{v}"
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
151
|
COMMANDS.each do |cmd|
|
113
|
-
|
114
|
-
|
115
|
-
|
152
|
+
unless method_defined?(cmd.to_sym)
|
153
|
+
define_method(cmd) do |name|
|
154
|
+
vboxmanage :controlvm, name, cmd
|
116
155
|
end
|
117
|
-
|
156
|
+
end
|
118
157
|
end
|
119
158
|
|
120
|
-
def start name
|
121
|
-
|
122
|
-
|
159
|
+
def start name, options = {}
|
160
|
+
headless = @options[:headless]
|
161
|
+
headless = options[:headless] if options.key?(:headless)
|
162
|
+
|
163
|
+
if ENV['DISPLAY'] && !headless
|
164
|
+
vboxmanage :startvm, name
|
123
165
|
else
|
124
|
-
puts "[.] $DISPLAY is not set, assuming --headless".gray unless
|
166
|
+
puts "[.] $DISPLAY is not set, assuming --headless".gray unless headless
|
125
167
|
@options[:headless] = true
|
126
|
-
|
168
|
+
vboxmanage :startvm, name, :type => :headless
|
127
169
|
end
|
128
170
|
end
|
129
171
|
|
@@ -131,10 +173,10 @@ module VBOX
|
|
131
173
|
def get_snapshots _name
|
132
174
|
r = []
|
133
175
|
name = uuid = nil
|
134
|
-
|
176
|
+
vboxmanage(:snapshot, _name, 'list', '--machinereadable').each_line do |line|
|
135
177
|
k,v = line.strip.split('=',2)
|
136
178
|
next unless v
|
137
|
-
v = v
|
179
|
+
v = qstrip(v)
|
138
180
|
case k
|
139
181
|
when /SnapshotName/
|
140
182
|
name = v
|
@@ -152,12 +194,13 @@ module VBOX
|
|
152
194
|
# d0 -> d1, d2, d3
|
153
195
|
# d1 -> d1.1, d1.2, d1.3
|
154
196
|
# d1.1 -> d1.1.1, d1.1.2, d1.1.3
|
197
|
+
# xx -> xx.1, xx.2, xx.3
|
155
198
|
def _gen_vm_name parent_name
|
156
199
|
# try to guess new name
|
200
|
+
names = list_vms.map(&:name)
|
157
201
|
numbers = parent_name.scan /\d+/
|
158
202
|
if numbers.any?
|
159
203
|
lastnum = numbers.last
|
160
|
-
names = list_vms.map(&:name)
|
161
204
|
if lastnum.to_i == 0
|
162
205
|
# d0 -> d1, d2, d3
|
163
206
|
newnum = lastnum.to_i + 1
|
@@ -177,13 +220,21 @@ module VBOX
|
|
177
220
|
newnum += 1
|
178
221
|
end
|
179
222
|
end
|
223
|
+
else
|
224
|
+
# xx -> xx.1, xx.2, xx.3
|
225
|
+
newnum = 1
|
226
|
+
while true
|
227
|
+
newname = "#{parent_name}.#{newnum}"
|
228
|
+
return newname unless names.include?(newname)
|
229
|
+
newnum += 1
|
230
|
+
end
|
180
231
|
end
|
181
232
|
nil
|
182
233
|
end
|
183
234
|
|
184
235
|
def take_snapshot vm_name, params = {}
|
185
|
-
|
186
|
-
exit 1 unless
|
236
|
+
vboxmanage "snapshot", vm_name, "take", params[:name] || "noname"
|
237
|
+
exit 1 unless success?
|
187
238
|
get_snapshots(vm_name).last
|
188
239
|
end
|
189
240
|
|
@@ -208,9 +259,9 @@ module VBOX
|
|
208
259
|
end
|
209
260
|
|
210
261
|
def _clone old_vm_name, params
|
211
|
-
args =
|
212
|
-
if new_vm_name = params[
|
213
|
-
args
|
262
|
+
args = {}
|
263
|
+
if new_vm_name = params[:name] || _gen_vm_name(old_vm_name)
|
264
|
+
args[:name] = new_vm_name
|
214
265
|
end
|
215
266
|
|
216
267
|
snapshot = @clone_use_snapshot ||= case params[:snapshot].to_s
|
@@ -219,47 +270,42 @@ module VBOX
|
|
219
270
|
when 'last'
|
220
271
|
get_snapshots(old_vm_name).last
|
221
272
|
else
|
222
|
-
|
223
|
-
exit 1
|
273
|
+
raise "no :snapshot param"
|
224
274
|
end
|
225
275
|
unless snapshot
|
226
|
-
|
227
|
-
exit 1
|
276
|
+
raise "failed to get snapshot"
|
228
277
|
end
|
229
278
|
|
230
|
-
args
|
231
|
-
args
|
232
|
-
args
|
233
|
-
|
234
|
-
system "VBoxManage", "clonevm", old_vm_name, *args
|
235
|
-
return false unless $?.success?
|
279
|
+
args[:options] = :link
|
280
|
+
args[:register] = ''
|
281
|
+
args[:snapshot] = snapshot.uuid
|
236
282
|
|
283
|
+
vboxmanage :clonevm, old_vm_name, args
|
284
|
+
return false unless success?
|
237
285
|
get_vm_info(old_vm_name).each do |k,v|
|
238
286
|
if k =~ /^macaddress/
|
239
|
-
old_mac = v.
|
287
|
+
old_mac = v.downcase
|
240
288
|
puts "[.] old #{k}=#{old_mac}"
|
241
289
|
old_automac = _name2macpart(old_vm_name)
|
242
290
|
if old_automac && old_mac[-old_automac.size..-1] == old_automac
|
243
291
|
new_automac = _name2macpart(new_vm_name)
|
244
292
|
new_mac = old_mac[0,old_mac.size-new_automac.size] + new_automac
|
245
293
|
puts "[.] new #{k}=#{new_mac}"
|
246
|
-
modify new_vm_name, k
|
294
|
+
modify new_vm_name, k => new_mac
|
247
295
|
end
|
248
296
|
end
|
249
297
|
end
|
250
298
|
new_vm_name
|
251
299
|
end
|
252
300
|
|
253
|
-
def modify
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
puts "#{k}=#{h[k]}"
|
258
|
-
end
|
301
|
+
def modify vm, vars
|
302
|
+
id = vm.is_a?(VM) ? (vm.uuid || vm.name) : vm.to_s
|
303
|
+
vboxmanage :modifyvm, id, vars
|
304
|
+
success?
|
259
305
|
end
|
260
306
|
|
261
307
|
def delete name
|
262
|
-
|
308
|
+
vboxmanage :unregistervm, name, "--delete"
|
263
309
|
end
|
264
310
|
alias :destroy :delete
|
265
311
|
end
|
data/lib/vbox/vm.rb
CHANGED
@@ -1,15 +1,86 @@
|
|
1
1
|
module VBOX
|
2
2
|
class VM
|
3
|
-
attr_accessor :name, :uuid, :
|
3
|
+
attr_accessor :name, :uuid, :state
|
4
4
|
|
5
|
-
def initialize
|
6
|
-
@
|
5
|
+
def initialize params = {}
|
6
|
+
@metadata = params[:metadata]
|
7
|
+
_parse_metadata
|
8
|
+
@name = params[:name] if params[:name]
|
9
|
+
@uuid = params[:uuid] if params[:uuid]
|
7
10
|
end
|
8
11
|
|
12
|
+
private
|
13
|
+
|
14
|
+
def _parse_metadata
|
15
|
+
return unless @metadata && @metadata.any?
|
16
|
+
@name = @metadata['name']
|
17
|
+
@uuid = @metadata['UUID']
|
18
|
+
@state = @metadata['VMState'].to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def deep_copy x
|
22
|
+
Marshal.load(Marshal.dump(x))
|
23
|
+
end
|
24
|
+
|
25
|
+
public
|
26
|
+
|
27
|
+
def metadata
|
28
|
+
if !@metadata || @metadata.empty?
|
29
|
+
reload_metadata
|
30
|
+
end
|
31
|
+
@metadata
|
32
|
+
end
|
33
|
+
alias :fetch_metadata :metadata
|
34
|
+
|
9
35
|
%w'start pause resume reset poweroff savestate acpipowerbutton acpisleepbutton destroy'.each do |action|
|
10
|
-
define_method "#{action}!" do
|
11
|
-
VBOX.api.send( action, uuid || name )
|
36
|
+
define_method "#{action}!" do |*args|
|
37
|
+
VBOX.api.send( action, uuid || name, *args )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create!
|
42
|
+
raise "cannot create VM w/o name" if self.name.to_s == ""
|
43
|
+
VBOX.api.createvm(self) || raise("failed to create VM")
|
44
|
+
reload_metadata
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# set some variable: change VM name, memory size, pae, etc
|
49
|
+
def set_var k, v = nil
|
50
|
+
reload_metadata unless @metadata
|
51
|
+
@metadata_orig ||= deep_copy(@metadata)
|
52
|
+
|
53
|
+
if k.is_a?(Hash) && v.nil?
|
54
|
+
k.each do |kk,vv|
|
55
|
+
@metadata[kk.to_s] = vv.to_s
|
56
|
+
end
|
57
|
+
elsif !k.is_a?(Hash) && !v.is_a?(Hash)
|
58
|
+
@metadata[k.to_s] = v.to_s
|
59
|
+
else
|
60
|
+
raise "invalid params combination"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
alias :set_vars :set_var
|
64
|
+
|
65
|
+
# save modified metadata, if any
|
66
|
+
def save
|
67
|
+
return nil if @metadata == @metadata_orig
|
68
|
+
vars = {}
|
69
|
+
@metadata.each do |k,v|
|
70
|
+
vars[k] = v if @metadata[k].to_s != @metadata_orig[k].to_s
|
12
71
|
end
|
72
|
+
VBOX.api.modify self, vars
|
73
|
+
end
|
74
|
+
|
75
|
+
# reload all VM metadata info from VirtualBox
|
76
|
+
def reload_metadata
|
77
|
+
raise "cannot reload metadata if name & uuid are NULL" unless name || uuid
|
78
|
+
@metadata = VBOX.api.get_vm_details(self)
|
79
|
+
|
80
|
+
# make a 'deep copy' of @metadata to detect changed vars
|
81
|
+
# dup() or clone() does not fit here b/c they leave hash values linked to each other
|
82
|
+
@metadata_orig = deep_copy(@metadata)
|
83
|
+
_parse_metadata
|
13
84
|
end
|
14
85
|
|
15
86
|
def clone! params
|
@@ -35,16 +106,23 @@ module VBOX
|
|
35
106
|
def dir_size
|
36
107
|
@dir_size ||=
|
37
108
|
begin
|
38
|
-
|
39
|
-
|
40
|
-
dir = File.dirname(v.tr('"',''))
|
109
|
+
return nil unless v=metadata['CfgFile']
|
110
|
+
dir = File.dirname(v)
|
41
111
|
`du -sm "#{dir}"`.split("\t").first.tr("M","").to_i
|
42
112
|
end
|
43
113
|
end
|
44
114
|
|
115
|
+
def memory_size
|
116
|
+
metadata['memory'].to_i
|
117
|
+
end
|
118
|
+
|
119
|
+
def snapshots
|
120
|
+
VBOX.api.get_snapshots(uuid||name)
|
121
|
+
end
|
122
|
+
|
45
123
|
class << self
|
46
124
|
def all
|
47
|
-
VBOX.api.list_vms
|
125
|
+
VBOX.api.list_vms :include_state => true
|
48
126
|
end
|
49
127
|
|
50
128
|
def first
|
@@ -52,10 +130,32 @@ module VBOX
|
|
52
130
|
end
|
53
131
|
|
54
132
|
def find name_or_uuid
|
55
|
-
VBOX.api.get_vm_details name_or_uuid
|
133
|
+
r = VBOX.api.get_vm_details name_or_uuid
|
134
|
+
r ? VM.new(:metadata => r) : nil
|
56
135
|
end
|
57
|
-
|
58
136
|
alias :[] :find
|
137
|
+
|
138
|
+
def find_all glob
|
139
|
+
all.keep_if do |vm|
|
140
|
+
expand_glob(glob){ |glob1| File.fnmatch(glob1, vm.name) }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# expand globs like "d{1-30}" to d1,d2,d3,d4,...,d29,d30
|
145
|
+
def expand_glob glob, &block
|
146
|
+
if glob[/\{(\d+)-(\d+)\}/]
|
147
|
+
$1.to_i.upto($2.to_i) do |i|
|
148
|
+
expand_glob glob.sub($&,i.to_s), &block
|
149
|
+
end
|
150
|
+
else
|
151
|
+
yield glob
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def create! *args
|
156
|
+
new(*args).create!
|
157
|
+
end
|
158
|
+
|
59
159
|
end
|
60
160
|
end
|
61
161
|
end
|