virtualbox 0.1.1 → 0.2.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.
@@ -14,6 +14,13 @@ module VirtualBox
14
14
  @@vboxmanage = "VBoxManage"
15
15
 
16
16
  class <<self
17
+ # Returns true if the last run command was a success. Obviously this
18
+ # will introduce all sorts of thread-safe problems. Those will have to
19
+ # be addressed another time.
20
+ def success?
21
+ $?.to_i == 0
22
+ end
23
+
17
24
  # Sets the path to VBoxManage, which is required for this gem to
18
25
  # work.
19
26
  def vboxmanage=(path)
@@ -22,7 +29,9 @@ module VirtualBox
22
29
 
23
30
  # Runs a VBoxManage command and returns the output.
24
31
  def vboxmanage(command)
25
- execute("#{@@vboxmanage} #{command}")
32
+ result = execute("#{@@vboxmanage} #{command}")
33
+ raise Exceptions::CommandFailedException.new(result) if !Command.success?
34
+ result
26
35
  end
27
36
 
28
37
  # Runs a command and returns a boolean result showing
@@ -30,7 +39,7 @@ module VirtualBox
30
39
  # exit code.
31
40
  def test(command)
32
41
  execute(command)
33
- $?.to_i == 0
42
+ success?
34
43
  end
35
44
 
36
45
  # Runs a command and returns the STDOUT result. The reason this is
@@ -9,6 +9,15 @@ module VirtualBox
9
9
  #
10
10
  # DVD.all
11
11
  #
12
+ # # Empty Drives
13
+ #
14
+ # Sometimes it is useful to have an empty drive. This is the case where you
15
+ # may have a DVD drive but it has no disk in it. To create an {AttachedDevice},
16
+ # an image _must_ be specified, and an empty drive is a simple option. Creating
17
+ # an empty drive is simple:
18
+ #
19
+ # DVD.empty_drive
20
+ #
12
21
  class DVD < Image
13
22
  class <<self
14
23
  # Returns an array of all available DVDs as DVD objects
@@ -16,16 +25,55 @@ module VirtualBox
16
25
  raw = Command.vboxmanage("list dvds")
17
26
  parse_raw(raw)
18
27
  end
28
+
29
+ # Returns an empty drive. This is useful for creating new
30
+ # or modifyingn existing {AttachedDevice} objects and
31
+ # attaching an empty drive to them.
32
+ #
33
+ # @return [DVD]
34
+ def empty_drive
35
+ new(:empty_drive)
36
+ end
37
+ end
38
+
39
+ def initialize(*args)
40
+ if args.length == 1 && args[0] == :empty_drive
41
+ @empty_drive = true
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ # Override of {Image#empty_drive?}. This will only be true if
48
+ # the DVD was created with {DVD.empty_drive}.
49
+ #
50
+ # @return [Boolean]
51
+ def empty_drive?
52
+ @empty_drive || false
53
+ end
54
+
55
+ # Override of {Image#image_type}.
56
+ def image_type
57
+ "dvddrive"
19
58
  end
20
59
 
21
60
  # Deletes the DVD from VBox managed list and also from disk.
22
61
  # This method will fail if the disk is currently mounted to any
23
- # virtual machine.
62
+ # virtual machine. This method also does nothing for empty drives
63
+ # (see {DVD.empty_drive}) and will return false automatically in
64
+ # that case.
24
65
  #
25
- # @return [Boolean]
26
- def destroy
66
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
67
+ # will be raised if the command failed.
68
+ # @return [Boolean] True if command was successful, false otherwise.
69
+ def destroy(raise_errors=false)
70
+ return false if empty_drive?
71
+
27
72
  Command.vboxmanage("closemedium dvd #{uuid} --delete")
28
- return $?.to_i == 0
73
+ true
74
+ rescue Exceptions::CommandFailedException
75
+ raise if raise_errors
76
+ false
29
77
  end
30
78
  end
31
79
  end
@@ -0,0 +1,13 @@
1
+ module VirtualBox
2
+ # Gem specific exceptions will reside under this namespace for easy
3
+ # documentation and searching.
4
+ module Exceptions
5
+ class Exception < ::Exception; end
6
+
7
+ class CommandFailedException < Exception; end
8
+ class InvalidObjectException < Exception; end
9
+ class InvalidRelationshipObjectException < Exception; end
10
+ class NonSettableRelationshipException < Exception; end
11
+ class NoParentException < Exception; end
12
+ end
13
+ end
@@ -120,24 +120,44 @@ module VirtualBox
120
120
  # single filename, since VirtualBox will place it in the hard
121
121
  # drives folder.
122
122
  # @param [String] format The format to convert to.
123
- # @return [HardDrive] The new, cloned hard drive.
124
- def clone(outputfile, format="VDI")
123
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
124
+ # will be raised if the command failed.
125
+ # @return [HardDrive] The new, cloned hard drive, or nil on failure.
126
+ def clone(outputfile, format="VDI", raise_errors=false)
125
127
  raw = Command.vboxmanage("clonehd #{uuid} #{Command.shell_escape(outputfile)} --format #{format} --remember")
126
128
  return nil unless raw =~ /UUID: (.+?)$/
127
129
 
128
130
  self.class.find($1.to_s)
131
+ rescue Exceptions::CommandFailedException
132
+ raise if raise_errors
133
+ nil
134
+ end
135
+
136
+ # Override of {Image#image_type}.
137
+ def image_type
138
+ "hdd"
129
139
  end
130
140
 
131
141
  # Creates a new hard drive.
132
142
  #
133
143
  # **This method should NEVER be called. Call {#save} instead.**
134
- def create
144
+ #
145
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
146
+ # will be raised if the command failed.
147
+ # @return [Boolean] True if command was successful, false otherwise.
148
+ def create(raise_errors=false)
135
149
  raw = Command.vboxmanage("createhd --filename #{location} --size #{size} --format #{read_attribute(:format)} --remember")
136
150
  return nil unless raw =~ /UUID: (.+?)$/
137
151
 
138
152
  # Just replace our attributes with the newly created ones. This also
139
153
  # will set new_record to false.
140
154
  populate_attributes(self.class.find($1.to_s).attributes)
155
+
156
+ # Return the success of the command
157
+ true
158
+ rescue Exceptions::CommandFailedException
159
+ raise if raise_errors
160
+ false
141
161
  end
142
162
 
143
163
  # Saves the hard drive object. If the hard drive is new,
@@ -146,10 +166,14 @@ module VirtualBox
146
166
  #
147
167
  # Currently, **saving existing hard drives does nothing**.
148
168
  # This is a limitation of VirtualBox, rather than the library itself.
149
- def save
169
+ #
170
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
171
+ # will be raised if the command failed.
172
+ # @return [Boolean] True if command was successful, false otherwise.
173
+ def save(raise_errors=false)
150
174
  if new_record?
151
175
  # Create a new hard drive
152
- create
176
+ create(raise_errors)
153
177
  else
154
178
  super
155
179
  end
@@ -160,10 +184,15 @@ module VirtualBox
160
184
  #
161
185
  # **This operation is not reversable.**
162
186
  #
163
- # @return [Boolean]
164
- def destroy
187
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
188
+ # will be raised if the command failed.
189
+ # @return [Boolean] True if command was successful, false otherwise.
190
+ def destroy(raise_errors=false)
165
191
  Command.vboxmanage("closemedium disk #{uuid} --delete")
166
- return $?.to_i == 0
192
+ true
193
+ rescue Exceptions::CommandFailedException
194
+ raise if raise_errors
195
+ false
167
196
  end
168
197
  end
169
198
  end
@@ -72,6 +72,7 @@ module VirtualBox
72
72
  #
73
73
  # @return [Array<Image>]
74
74
  def populate_relationship(caller, data)
75
+ return DVD.empty_drive if data[:medium] == "emptydrive"
75
76
  return nil if data[:uuid].nil?
76
77
 
77
78
  subclasses.each do |subclass|
@@ -80,6 +81,22 @@ module VirtualBox
80
81
  matching = subclass.all.find { |obj| obj.uuid == data[:uuid] }
81
82
  return matching unless matching.nil?
82
83
  end
84
+
85
+ nil
86
+ end
87
+
88
+ # Sets an image onto a relationship and/or removes it from a
89
+ # relationship. This method is automatically called by {Relatable}.
90
+ #
91
+ # **This method typically won't be used except internally.**
92
+ #
93
+ # @return [Image]
94
+ def set_relationship(caller, old_value, new_value)
95
+ # We don't actually destroy any images using this method,
96
+ # so just return the new value as long as its a valid object
97
+ raise Exceptions::InvalidRelationshipObjectException.new if new_value && !new_value.is_a?(Image)
98
+
99
+ return new_value
83
100
  end
84
101
  end
85
102
 
@@ -90,5 +107,25 @@ module VirtualBox
90
107
 
91
108
  populate_attributes(info) if info
92
109
  end
110
+
111
+ # The image type as a string for the virtualbox command line. This
112
+ # method should be overridden by any subclass and is expected to
113
+ # return the type which is used in command line parameters for
114
+ # attaching to storage controllers.
115
+ #
116
+ # @return [String]
117
+ def image_type
118
+ raise "This must be implemented by any subclasses"
119
+ end
120
+
121
+ # Returns boolean showing if empty drive or not. This method should be
122
+ # overriden by any subclass and is expected to return true of false
123
+ # showing if this image represents an empty drive of whatever type
124
+ # the subclass is.
125
+ #
126
+ # @return [Boolean]
127
+ def empty_drive?
128
+ false
129
+ end
93
130
  end
94
131
  end
@@ -142,7 +142,7 @@ module VirtualBox
142
142
  # called on {#save}.
143
143
  #
144
144
  # **This method typically won't be used except internally.**
145
- def save_attribute(key, value, vmname)
145
+ def save_attribute(key, value, vmname)
146
146
  Command.vboxmanage("modifyvm #{vmname} --#{key}#{@index} #{Command.shell_escape(value)}")
147
147
  super
148
148
  end
@@ -0,0 +1,29 @@
1
+ module VirtualBox
2
+ module Proxies
3
+ # A relationship which can be described as a collection, which
4
+ # is a set of items.
5
+ class Collection < Array
6
+ def initialize(parent)
7
+ super()
8
+
9
+ @parent = parent
10
+ end
11
+
12
+ def <<(item)
13
+ item.added_to_relationship(@parent) if item.respond_to?(:added_to_relationship)
14
+ push(item)
15
+ end
16
+
17
+ def clear
18
+ each do |item|
19
+ delete(item)
20
+ end
21
+ end
22
+
23
+ def delete(item)
24
+ return unless super
25
+ item.removed_from_relationship(@parent) if item.respond_to?(:removed_from_relationship)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -73,6 +73,17 @@ module VirtualBox
73
73
  def destroy_relationship(caller, data, *args)
74
74
  data.each { |v| v.destroy(*args) }
75
75
  end
76
+
77
+ # Saves the relationship. This simply calls {#save} on every
78
+ # member of the relationship.
79
+ #
80
+ # **This method typically won't be used except internally.**
81
+ def save_relationship(caller, data)
82
+ # Just call save on each nic with the VM
83
+ data.each do |sc|
84
+ sc.save
85
+ end
86
+ end
76
87
  end
77
88
 
78
89
  # Since storage controllers still can't be created from scratch,
data/lib/virtualbox/vm.rb CHANGED
@@ -226,7 +226,7 @@ module VirtualBox
226
226
  # Saves the virtual machine if modified. This method saves any modified
227
227
  # attributes of the virtual machine. If any related attributes were saved
228
228
  # as well (such as storage controllers), those will be saved, too.
229
- def save
229
+ def save(raise_errors=false)
230
230
  # Make sure we save the new name first if that was changed, or
231
231
  # we'll get some inconsistencies later
232
232
  if name_changed?
@@ -234,7 +234,12 @@ module VirtualBox
234
234
  @original_name = name
235
235
  end
236
236
 
237
- super
237
+ super()
238
+
239
+ true
240
+ rescue Exceptions::CommandFailedException
241
+ raise if raise_errors
242
+ return false
238
243
  end
239
244
 
240
245
  # Saves a single attribute of the virtual machine. This should **not**
@@ -246,6 +251,42 @@ module VirtualBox
246
251
  super
247
252
  end
248
253
 
254
+ # Exports a virtual machine. The virtual machine will be exported
255
+ # to the specified OVF file name. This directory will also have the
256
+ # `mf` file which contains the file checksums and also the virtual
257
+ # drives of the machine.
258
+ #
259
+ # Export also supports an additional options hash which can contain
260
+ # information that will be embedded with the virtual machine. View
261
+ # below for more information on the available options.
262
+ #
263
+ # This method will block until the export is complete, which takes about
264
+ # 60 to 90 seconds on my 2.2 GHz 2009 model MacBook Pro.
265
+ #
266
+ # @param [String] filename The file (not directory) to save the exported
267
+ # OVF file. This directory will also receive the checksum file and
268
+ # virtual disks.
269
+ # @option options [String] :product (nil) The name of the product
270
+ # @option options [String] :producturl (nil) The URL of the product
271
+ # @option options [String] :vendor (nil) The name of the vendor
272
+ # @option options [String] :vendorurl (nil) The URL for the vendor
273
+ # @option options [String] :version (nil) The version information
274
+ # @option options [String] :eula (nil) License text
275
+ # @option options [String] :eulafile (nil) License file
276
+ def export(filename, options={}, raise_error=false)
277
+ options = options.inject([]) do |acc, kv|
278
+ acc.push("--#{kv[0]} #{Command.shell_escape(kv[1])}")
279
+ end
280
+
281
+ options.unshift("--vsys 0") unless options.empty?
282
+
283
+ raw = Command.vboxmanage("export #{@original_name} -o #{Command.shell_escape(filename)} #{options.join(" ")}".strip)
284
+ true
285
+ rescue Exceptions::CommandFailedException
286
+ raise if raise_error
287
+ false
288
+ end
289
+
249
290
  # Starts the virtual machine. The virtual machine can be started in a
250
291
  # variety of modes:
251
292
  #
@@ -255,15 +296,74 @@ module VirtualBox
255
296
  #
256
297
  # All modes will start their processes and return almost immediately.
257
298
  # Both the GUI and headless mode will not block the ruby process.
258
- def start(mode=:gui)
299
+ #
300
+ # @param [Symbol] mode Described above.
301
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
302
+ # will be raised if the command failed.
303
+ # @return [Boolean] True if command was successful, false otherwise.
304
+ def start(mode=:gui, raise_errors=false)
259
305
  Command.vboxmanage("startvm #{@original_name} --type #{mode}")
306
+ true
307
+ rescue Exceptions::CommandFailedException
308
+ raise if raise_errors
309
+ false
260
310
  end
261
311
 
262
312
  # Stops the VM by directly calling "poweroff." Immediately halts the
263
313
  # virtual machine without saving state. This could result in a loss
264
314
  # of data.
265
- def stop
266
- Command.vboxmanage("controlvm #{@original_name} poweroff")
315
+ #
316
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
317
+ # will be raised if the command failed.
318
+ # @return [Boolean] True if command was successful, false otherwise.
319
+ def stop(raise_errors=false)
320
+ control(:poweroff, raise_errors)
321
+ end
322
+
323
+ # Pauses the VM, putting it on hold temporarily. The VM can be resumed
324
+ # again by calling {#resume}
325
+ #
326
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
327
+ # will be raised if the command failed.
328
+ # @return [Boolean] True if command was successful, false otherwise.
329
+ def pause(raise_errors=false)
330
+ control(:pause, raise_errors)
331
+ end
332
+
333
+ # Resume a paused VM.
334
+ #
335
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
336
+ # will be raised if the command failed.
337
+ # @return [Boolean] True if command was successful, false otherwise.
338
+ def resume(raise_errors=false)
339
+ control(:resume, raise_errors)
340
+ end
341
+
342
+ # Saves the state of a VM and stops it. The VM can be resumed
343
+ # again by calling "start" again.
344
+ #
345
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
346
+ # will be raised if the command failed.
347
+ # @return [Boolean] True if command was successful, false otherwise.
348
+ def save_state(raise_errors=false)
349
+ control(:savestate, raise_errors)
350
+ end
351
+
352
+ # Controls the virtual machine. This method is used by {#stop},
353
+ # {#pause}, {#resume}, and {#save_state} to control the virtual machine.
354
+ # Typically, you won't ever have to call this method and should
355
+ # instead call those.
356
+ #
357
+ # @param [String] command The command to run on controlvm
358
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
359
+ # will be raised if the command failed.
360
+ # @return [Boolean] True if command was successful, false otherwise.
361
+ def control(command, raise_errors=false)
362
+ Command.vboxmanage("controlvm #{@original_name} #{command}")
363
+ true
364
+ rescue Exceptions::CommandFailedException
365
+ raise if raise_errors
366
+ false
267
367
  end
268
368
 
269
369
  # Destroys the virtual machine. This method also removes all attached
data/lib/virtualbox.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  $:.unshift(File.expand_path(File.dirname(__FILE__)))
2
- require 'virtualbox/errors'
2
+ require 'virtualbox/exceptions'
3
3
  require 'virtualbox/command'
4
4
  require 'virtualbox/abstract_model'
5
+ require 'virtualbox/proxies/collection'
5
6
  require 'virtualbox/image'
6
7
  require 'virtualbox/attached_device'
7
8
  require 'virtualbox/dvd'
@@ -43,6 +43,14 @@ class DirtyTest < Test::Unit::TestCase
43
43
  assert !@model.changed?
44
44
  end
45
45
 
46
+ should "be able to clear dirty state on entire model" do
47
+ @model.foo = "changed"
48
+ @model.bar = "changed"
49
+ assert @model.changed?
50
+ @model.clear_dirty!
51
+ assert !@model.changed?
52
+ end
53
+
46
54
  should "show changes on specific field" do
47
55
  assert !@model.changed?
48
56
  @model.foo = "my value"
@@ -51,6 +59,11 @@ class DirtyTest < Test::Unit::TestCase
51
59
  assert_equal "foo", @model.foo_was
52
60
  end
53
61
 
62
+ should "return nil for field_was if its not changed" do
63
+ assert !@model.foo_changed?
64
+ assert_nil @model.foo_was
65
+ end
66
+
54
67
  should "show changes for the whole model" do
55
68
  assert !@model.changed?
56
69
  @model.foo = "foo2"
@@ -62,5 +75,9 @@ class DirtyTest < Test::Unit::TestCase
62
75
  assert_equal ["foo", "foo2"], changes[:foo]
63
76
  assert_equal ["bar", "bar2"], changes[:bar]
64
77
  end
78
+
79
+ should "still forward non-dirty magic methods up method_missing" do
80
+ assert_raises(NoMethodError) { @model.foobarbaz }
81
+ end
65
82
  end
66
83
  end
@@ -6,7 +6,10 @@ class RelatableTest < Test::Unit::TestCase
6
6
  "FOO"
7
7
  end
8
8
  end
9
- class BarRelatee; end
9
+ class BarRelatee
10
+ def self.set_relationship(caller, old_value, new_value)
11
+ end
12
+ end
10
13
 
11
14
  class RelatableModel
12
15
  include VirtualBox::AbstractModel::Relatable
@@ -19,6 +22,37 @@ class RelatableTest < Test::Unit::TestCase
19
22
  @data = {}
20
23
  end
21
24
 
25
+ context "setting a relationship" do
26
+ setup do
27
+ @model = RelatableModel.new
28
+ end
29
+
30
+ should "have a magic method relationship= which calls set_relationship" do
31
+ @model.expects(:set_relationship).with(:foos, "FOOS!")
32
+ @model.foos = "FOOS!"
33
+ end
34
+
35
+ should "raise a NonSettableRelationshipException if relationship can't be set" do
36
+ assert_raises(VirtualBox::Exceptions::NonSettableRelationshipException) {
37
+ @model.foos = "FOOS!"
38
+ }
39
+ end
40
+
41
+ should "call set_relationship on the relationship class" do
42
+ BarRelatee.expects(:populate_relationship).returns("foo")
43
+ @model.populate_relationships({})
44
+
45
+ BarRelatee.expects(:set_relationship).with(@model, "foo", "bars")
46
+ assert_nothing_raised { @model.bars = "bars" }
47
+ end
48
+
49
+ should "set the result of set_relationship as the new relationship data" do
50
+ BarRelatee.stubs(:set_relationship).returns("hello")
51
+ @model.bars = "zoo"
52
+ assert_equal "hello", @model.bars
53
+ end
54
+ end
55
+
22
56
  context "subclasses" do
23
57
  class SubRelatableModel < RelatableModel
24
58
  relationship :bars, Relatee
@@ -122,6 +156,20 @@ class RelatableTest < Test::Unit::TestCase
122
156
  end
123
157
  end
124
158
 
159
+ context "checking for relationships" do
160
+ setup do
161
+ @model = RelatableModel.new
162
+ end
163
+
164
+ should "return true for existing relationships" do
165
+ assert @model.has_relationship?(:foos)
166
+ end
167
+
168
+ should "return false for nonexistent relationships" do
169
+ assert !@model.has_relationship?(:bazs)
170
+ end
171
+ end
172
+
125
173
  context "populating relationships" do
126
174
  setup do
127
175
  @model = RelatableModel.new
@@ -1,7 +1,12 @@
1
1
  require File.join(File.dirname(__FILE__), '..', 'test_helper')
2
2
 
3
3
  class AbstractModelTest < Test::Unit::TestCase
4
- class Foo; end
4
+ class Foo
5
+ def self.set_relationship(caller, old_value, new_value)
6
+ new_value
7
+ end
8
+ end
9
+
5
10
  class Bar; end
6
11
 
7
12
  class FakeModel < VirtualBox::AbstractModel
@@ -30,6 +35,20 @@ class AbstractModelTest < Test::Unit::TestCase
30
35
  @model.save
31
36
  assert !@model.new_record?
32
37
  end
38
+
39
+ should "become a new record again if new_record! is called" do
40
+ assert @model.new_record?
41
+ @model.save
42
+ assert !@model.new_record?
43
+ @model.new_record!
44
+ assert @model.new_record?
45
+ end
46
+
47
+ should "become an existing record if existing_record! is called" do
48
+ assert @model.new_record?
49
+ @model.existing_record!
50
+ assert !@model.new_record?
51
+ end
33
52
  end
34
53
 
35
54
  context "subclasses" do
@@ -120,6 +139,18 @@ class AbstractModelTest < Test::Unit::TestCase
120
139
  end
121
140
  end
122
141
 
142
+ context "integrating relatable" do
143
+ setup do
144
+ @model = FakeModel.new
145
+ end
146
+
147
+ should "set dirty state when a relationship is set" do
148
+ assert !@model.changed?
149
+ @model.foos = "foo"
150
+ assert @model.changed?
151
+ end
152
+ end
153
+
123
154
  context "integrating attributable and dirty" do
124
155
  setup do
125
156
  @model = FakeModel.new