virtualbox 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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