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.
@@ -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] ||= 2 if @options[:dry_run]
16
- @options[:verbose] ||= 0
16
+ @options[:verbose] = VBOX.verbosity
17
+ @options[:verbose] ||= 2 if @options[:dry_run] && @options[:verbose] < 2
17
18
  end
18
19
 
19
- # run as in backtick operator, and return result
20
- def ` cmd
21
- puts "[.] #{cmd}".gray if @options[:verbose] >= 2
22
- exit if @options[:dry_run]
23
- r = super
24
- #exit 1 unless $?.success?
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
- # run as in system() call
29
- def system *args
30
- puts "[.] #{args.inspect}".gray if @options[:verbose] >= 2
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
- r = super
33
- #exit 1 unless $?.success?
34
- r
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
- data = `VBoxManage showvminfo "#{name_or_uuid}" --machinereadable`
92
+ h = {}
93
+ data = quiet{ vboxmanage :showvminfo, name_or_uuid, "--machinereadable" }
48
94
  data.each_line do |line|
49
- k,v = line.strip.split('=',2)
95
+ k,v = line.split('=',2)
50
96
  next unless v
51
- vm.all_vars[k] = v # not stripping quotes here to save some CPU time
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
- (vm.name && vm.uuid) ? vm : nil
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
- if params[:running]
78
- data = `VBoxManage list runningvms`
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($&, '').strip.sub(/^"/,'').sub(/"$/,'')
88
- r << vm
120
+ vm.name = qstrip(line.gsub($&, ''))
121
+ vms << vm
89
122
  end
90
123
  end
91
- r.sort_by{ |vm| _naturalize(vm.name) }
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
- data.each_line do |line|
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
- class_eval <<-EOF unless method_defined?(cmd.to_sym)
114
- def #{cmd} name
115
- system "VBoxManage", "controlvm", name, "#{cmd}"
152
+ unless method_defined?(cmd.to_sym)
153
+ define_method(cmd) do |name|
154
+ vboxmanage :controlvm, name, cmd
116
155
  end
117
- EOF
156
+ end
118
157
  end
119
158
 
120
- def start name
121
- if ENV['DISPLAY'] && !@options[:headless]
122
- system "VBoxManage", "startvm", name
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 @options[:headless]
166
+ puts "[.] $DISPLAY is not set, assuming --headless".gray unless headless
125
167
  @options[:headless] = true
126
- system "VBoxManage", "startvm", name, "--type", "headless"
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
- `VBoxManage snapshot "#{_name}" list --machinereadable`.strip.each_line do |line|
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.strip.sub(/^"/,'').sub(/"$/,'')
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
- system "VBoxManage", "snapshot", vm_name, "take", params[:name] || "noname"
186
- exit 1 unless $?.success?
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['name'] || _gen_vm_name(old_vm_name)
213
- args += ["--name", new_vm_name]
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
- puts "[!] please gimme --snapshot=LAST OR --snapshot=NEW option"
223
- exit 1
273
+ raise "no :snapshot param"
224
274
  end
225
275
  unless snapshot
226
- puts "[!] failed to get snapshot, cannot continue".red
227
- exit 1
276
+ raise "failed to get snapshot"
228
277
  end
229
278
 
230
- args += ["--options","link"]
231
- args += ["--register"]
232
- args += ["--snapshot", snapshot.uuid]
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.tr('"','').downcase
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, new_mac, :quiet => true
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 name, k, v, params = {}
254
- system "VBoxManage", "modifyvm", name, "--#{k}", v
255
- if $?.success? && !params[:quiet]
256
- h = get_vm_info(k == 'name' ? v : name)
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
- system "VBoxManage", "unregistervm", name, "--delete"
308
+ vboxmanage :unregistervm, name, "--delete"
263
309
  end
264
310
  alias :destroy :delete
265
311
  end
@@ -1,15 +1,86 @@
1
1
  module VBOX
2
2
  class VM
3
- attr_accessor :name, :uuid, :memory_size, :dir_size, :state, :all_vars
3
+ attr_accessor :name, :uuid, :state
4
4
 
5
- def initialize
6
- @all_vars = {}
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
- VBOX.api.get_vm_details(self) unless @all_vars['CfgFile']
39
- return nil unless v=@all_vars['CfgFile']
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