virtualbox 0.2.0 → 0.3.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/.yardopts CHANGED
@@ -1,3 +1,4 @@
1
1
  -
2
2
  docs/GettingStarted.md
3
+ docs/WhatsNew.md
3
4
  TODO
data/TODO CHANGED
@@ -5,6 +5,5 @@ But they will definitely all make it for version 1.0.
5
5
 
6
6
  * Creating a VM from scratch (non-import)
7
7
  * Parsing bridged IFs
8
- * Shared folders
9
8
  * Snapshots
10
9
  * Getting/Setting guest properties
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
data/docs/WhatsNew.md ADDED
@@ -0,0 +1,41 @@
1
+ # What's New in 0.3.x?
2
+
3
+ ## Shared Folders
4
+
5
+ Shared folders are a great feature of VirtualBox which allows the host system
6
+ to share data with guest systems easily using the native filesystem. Attaching,
7
+ modifying, and removing these shared folders are now supported. A quick example
8
+ below:
9
+
10
+ vm = VirtualBox::VM.find("FooVM")
11
+ folder = VirtualBox::SharedFolder.new
12
+ folder.name = "hosthome"
13
+ folder.hostpath = "/home/username"
14
+ vm.shared_folders << folder
15
+ vm.save
16
+
17
+ For full documentation on this new feature, read about them at
18
+ {VirtualBox::SharedFolder}.
19
+
20
+ ## Validations
21
+
22
+ Many of the models for the virtualbox library now come complete with data
23
+ validations. These validations are performed within the library itself prior to
24
+ calling the virtualbox commands. They work very much the same was as ActiveRecord
25
+ validations:
26
+
27
+ sf = VirtualBox::SharedFolder.new(hash_of_values)
28
+ if !sf.valid?
29
+ puts "#{sf.errors.length} errors with the folder"
30
+ else
31
+ sf.save
32
+ end
33
+
34
+ In addition to `valid?` there is `errors` which returns a hash of all the errors,
35
+ including errors on relationships. There is also the `validate` method which
36
+ runs the validations, but you really shouldn't have the need to call that directly.
37
+
38
+ All validations are run automatically on `save`, which will return `false` if
39
+ they fail. If you choose to raise errors on the save, a `ValidationFailedException`
40
+ will be raised (in contrast to a `CommandFailedException`, which serves its own
41
+ role).
data/lib/virtualbox.rb CHANGED
@@ -8,5 +8,6 @@ require 'virtualbox/attached_device'
8
8
  require 'virtualbox/dvd'
9
9
  require 'virtualbox/hard_drive'
10
10
  require 'virtualbox/nic'
11
+ require 'virtualbox/shared_folder'
11
12
  require 'virtualbox/storage_controller'
12
13
  require 'virtualbox/vm'
@@ -1,6 +1,7 @@
1
1
  require 'virtualbox/abstract_model/attributable'
2
2
  require 'virtualbox/abstract_model/dirty'
3
3
  require 'virtualbox/abstract_model/relatable'
4
+ require 'virtualbox/abstract_model/validatable'
4
5
 
5
6
  module VirtualBox
6
7
  # AbstractModel is the base class used for most of virtualbox's classes.
@@ -11,6 +12,7 @@ module VirtualBox
11
12
  include Attributable
12
13
  include Dirty
13
14
  include Relatable
15
+ include Validatable
14
16
 
15
17
  # Returns a boolean denoting if the record is new or existing. This
16
18
  # method is provided for subclasses to use to differentiate between
@@ -36,6 +38,35 @@ module VirtualBox
36
38
  @new_record = false
37
39
  end
38
40
 
41
+ # Returns the errors for a model.
42
+ def errors
43
+ error_hash = super
44
+
45
+ self.class.relationships.each do |name, options|
46
+ next unless options && options[:klass].respond_to?(:errors_for_relationship)
47
+ relationship_errors = options[:klass].errors_for_relationship(self, relationship_data[name])
48
+
49
+ error_hash.merge!({ :foos => relationship_errors }) if relationship_errors.length > 0
50
+ end
51
+
52
+ error_hash
53
+ end
54
+
55
+ # Validates the model and relationships.
56
+ def validate(*args)
57
+ # First clear all previous errors
58
+ clear_errors
59
+
60
+ # Then do the validations
61
+ failed = false
62
+ self.class.relationships.each do |name, options|
63
+ next unless options && options[:klass].respond_to?(:validate_relationship)
64
+ failed = true if !options[:klass].validate_relationship(self, relationship_data[name], *args)
65
+ end
66
+
67
+ return !failed
68
+ end
69
+
39
70
  # Saves the model attributes and relationships.
40
71
  #
41
72
  # The method can be passed any arbitrary arguments, which are
@@ -0,0 +1,44 @@
1
+ module VirtualBox
2
+ class AbstractModel
3
+ # Provides validation methods for a class. Unlike ActiveRecord,
4
+ # validations are instance-level rather than class-level.
5
+ module Validatable
6
+ def errors
7
+ @errors ||= {}
8
+ end
9
+
10
+ def add_error(field, error)
11
+ errors[field] ||= []
12
+ errors[field].push(error)
13
+ end
14
+
15
+ def clear_errors
16
+ @errors = {}
17
+ end
18
+
19
+ def valid?
20
+ validate
21
+ errors.empty?
22
+ end
23
+
24
+ # Subclasses should override this method.
25
+ def validate
26
+ true
27
+ end
28
+
29
+ def validates_presence_of(field)
30
+ if field.is_a?(Array)
31
+ field.map { |v| validates_presence_of(v) }.all? { |v| v == true }
32
+ else
33
+ value = send(field)
34
+ if value.nil? || value == ""
35
+ add_error(field, "must not be blank.")
36
+ return false
37
+ else
38
+ return true
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -140,16 +140,28 @@ module VirtualBox
140
140
  end
141
141
  end
142
142
 
143
+ # Validates an attached device.
144
+ def validate
145
+ super
146
+
147
+ validates_presence_of :parent
148
+ validates_presence_of :image
149
+ validates_presence_of :port
150
+ end
151
+
143
152
  # Saves or creates an attached device.
144
153
  #
145
154
  # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
146
155
  # will be raised if the command failed.
147
156
  # @return [Boolean] True if command was successful, false otherwise.
148
- def save(raise_errors=false)
149
- raise Exceptions::NoParentException.new if parent.nil?
150
- raise Exceptions::InvalidObjectException.new("Image must be set") if image.nil?
157
+ def save(raise_errors=false)
151
158
  return true unless changed?
152
159
 
160
+ if !valid?
161
+ raise Exceptions::ValidationFailedException.new(errors) if raise_errors
162
+ return false
163
+ end
164
+
153
165
  # If the port changed, we have to destroy the old one, then create
154
166
  # a new one
155
167
  destroy({:port => port_was}, raise_errors) if port_changed? && !port_was.nil?
@@ -5,9 +5,8 @@ module VirtualBox
5
5
  class Exception < ::Exception; end
6
6
 
7
7
  class CommandFailedException < Exception; end
8
- class InvalidObjectException < Exception; end
9
8
  class InvalidRelationshipObjectException < Exception; end
10
9
  class NonSettableRelationshipException < Exception; end
11
- class NoParentException < Exception; end
10
+ class ValidationFailedException < Exception; end
12
11
  end
13
12
  end
@@ -138,6 +138,14 @@ module VirtualBox
138
138
  "hdd"
139
139
  end
140
140
 
141
+ # Validates a hard drive.
142
+ def validate
143
+ super
144
+
145
+ validates_presence_of :format
146
+ validates_presence_of :size
147
+ end
148
+
141
149
  # Creates a new hard drive.
142
150
  #
143
151
  # **This method should NEVER be called. Call {#save} instead.**
@@ -146,6 +154,11 @@ module VirtualBox
146
154
  # will be raised if the command failed.
147
155
  # @return [Boolean] True if command was successful, false otherwise.
148
156
  def create(raise_errors=false)
157
+ if !valid?
158
+ raise Exceptions::ValidationFailedException.new(errors) if raise_errors
159
+ return false
160
+ end
161
+
149
162
  raw = Command.vboxmanage("createhd --filename #{location} --size #{size} --format #{read_attribute(:format)} --remember")
150
163
  return nil unless raw =~ /UUID: (.+?)$/
151
164
 
@@ -0,0 +1,243 @@
1
+ module VirtualBox
2
+ # Represents a shared folder in VirtualBox. In VirtualBox, shared folders are a
3
+ # method for basically "symlinking" a folder on the guest system to a folder which
4
+ # exists on the host system. This allows for sharing of files across the virtual
5
+ # machine.
6
+ #
7
+ # **Note:** Whenever modifying shared folders on a VM, the changes won't take
8
+ # effect until a _cold reboot_ occurs. This means actually closing the virtual
9
+ # machine _completely_, then restarting it. You can't just hit "Start > Restart"
10
+ # or do a `sudo reboot`. It doesn't work that way!
11
+ #
12
+ # # Getting Shared Folders
13
+ #
14
+ # All shared folders are attached to a {VM} object, by definition. Therefore, to
15
+ # get a list of the shared folders, first {VM.find find} the VM you need, then
16
+ # use the `shared_folders` relationship to access an array of the shared folders.
17
+ # With this array, you can create, modify, update, and delete the shared folders
18
+ # for that virtual machine.
19
+ #
20
+ # # Creating a Shared Folder
21
+ #
22
+ # **This whole section will assume you already looked up a {VM} and assigned it to
23
+ # a local variable named `vm`.**
24
+ #
25
+ # With a VM found, creating a shared folder is just a few lines of code:
26
+ #
27
+ # folder = VirtualBox::SharedFolder.new
28
+ # folder.name = "desktop-images"
29
+ # folder.hostpath = File.expand_path("~/Desktop/images")
30
+ # vm.shared_folders << folder
31
+ # folder.save # Or you can call vm.save, which works too!
32
+ #
33
+ # # Modifying an Existing Shared Folder
34
+ #
35
+ # **This whole section will assume you already looked up a {VM} and assigned it to
36
+ # a local variable named `vm`.**
37
+ #
38
+ # Nothing tricky here: You treat existing shared folder objects just as if they
39
+ # were new ones. Assign a new name and/or a new path, then save.
40
+ #
41
+ # folder = vm.shared_folders.first
42
+ # folder.name = "rufus"
43
+ # folder.save # Or vm.save
44
+ #
45
+ # **Note**: The VirtualBox-saavy will know that VirtualBox doesn't actually
46
+ # expose a way to edit shared folders. Under the hood, the virtualbox ruby
47
+ # library is actually deleting the old shared folder, then creating a new
48
+ # one with the new details. This shouldn't affect the way anything works for
49
+ # the VM itself.
50
+ #
51
+ # # Deleting a Shared Folder
52
+ #
53
+ # **This whole section will assume you already looked up a {VM} and assigned it to
54
+ # a local variable named `vm`.**
55
+ #
56
+ # folder = vm.shared_folder.first
57
+ # folder.destroy
58
+ #
59
+ # Poof! It'll be gone. This is usually the place where I warn you about this
60
+ # being non-reversable, but since no _data_ was actually destroyed, this is
61
+ # not too risky. You could always just recreate the shared folder with the
62
+ # same name and path and it'll be like nothing happened.
63
+ #
64
+ # # Attributes and Relationships
65
+ #
66
+ # Properties of the model are exposed using standard ruby instance
67
+ # methods which are generated on the fly. Because of this, they are not listed
68
+ # below as available instance methods.
69
+ #
70
+ # These attributes can be accessed and modified via standard ruby-style
71
+ # `instance.attribute` and `instance.attribute=` methods. The attributes are
72
+ # listed below.
73
+ #
74
+ # Relationships are also accessed like attributes but can't be set. Instead,
75
+ # they are typically references to other objects such as an {AttachedDevice} which
76
+ # in turn have their own attributes which can be modified.
77
+ #
78
+ # ## Attributes
79
+ #
80
+ # This is copied directly from the class header, but lists all available
81
+ # attributes. If you don't understand what this means, read {Attributable}.
82
+ #
83
+ # attribute :parent, :readonly => :readonly
84
+ # attribute :name
85
+ # attribute :hostpath
86
+ #
87
+ class SharedFolder < AbstractModel
88
+ attribute :parent, :readonly => :readonly
89
+ attribute :name, :populate_key => "SharedFolderNameMachineMapping"
90
+ attribute :hostpath, :populate_key => "SharedFolderPathMachineMapping"
91
+
92
+ class <<self
93
+ # Populates the shared folder relationship for anything which is related to it.
94
+ #
95
+ # **This method typically won't be used except internally.**
96
+ #
97
+ # @return [Array<SharedFolder>]
98
+ def populate_relationship(caller, data)
99
+ relation = []
100
+
101
+ counter = 1
102
+ loop do
103
+ break unless data["SharedFolderNameMachineMapping#{counter}".downcase.to_sym]
104
+
105
+ folder = new(counter, caller, data)
106
+ relation.push(folder)
107
+ counter += 1
108
+ end
109
+
110
+ relation
111
+ end
112
+
113
+ # Saves the relationship. This simply calls {#save} on every
114
+ # member of the relationship.
115
+ #
116
+ # **This method typically won't be used except internally.**
117
+ def save_relationship(caller, data)
118
+ # Just call save on each folder with the VM
119
+ data.each do |sf|
120
+ sf.save
121
+ end
122
+ end
123
+ end
124
+
125
+ # @overload initialize(data={})
126
+ # Creates a new SharedFolder which is a new record. This
127
+ # should be attached to a VM and saved.
128
+ # @param [Hash] data (optional) A hash which contains initial attribute
129
+ # values for the SharedFolder.
130
+ # @overload initialize(index, caller, data)
131
+ # Creates an SharedFolder for a relationship. **This should
132
+ # never be called except internally.**
133
+ # @param [Integer] index Index of the shared folder
134
+ # @param [Object] caller The parent
135
+ # @param [Hash] data A hash of data which must be used
136
+ # to extract the relationship data.
137
+ def initialize(*args)
138
+ super()
139
+
140
+ if args.length == 3
141
+ initialize_for_relationship(*args)
142
+ elsif args.length == 1
143
+ initialize_for_data(*args)
144
+ elsif args.length == 0
145
+ return
146
+ else
147
+ raise NoMethodError.new
148
+ end
149
+ end
150
+
151
+ # Initializes the record for use in a relationship. This
152
+ # is automatically called by {#initialize} if it has three
153
+ # parameters.
154
+ #
155
+ # **This method typically won't be used except internally.**
156
+ def initialize_for_relationship(index, caller, data)
157
+ # Setup the index specific attributes
158
+ populate_data = {}
159
+ self.class.attributes.each do |name, options|
160
+ key = options[:populate_key] || name
161
+ value = data["#{key}#{index}".downcase.to_sym]
162
+ populate_data[key] = value
163
+ end
164
+
165
+ populate_attributes(populate_data.merge({
166
+ :parent => caller
167
+ }))
168
+ end
169
+
170
+ # Initializes a record with initial data but keeping it a "new
171
+ # record." This is called automatically if {#initialize} is given
172
+ # only a single parameter. View {#initialize} for documentation.
173
+ def initialize_for_data(data)
174
+ self.class.attributes.each do |name, options|
175
+ data[options[:populate_key]] = data[name]
176
+ end
177
+
178
+ populate_attributes(data)
179
+ new_record!
180
+ end
181
+
182
+ # Validates a shared folder.
183
+ def validate
184
+ super
185
+
186
+ validates_presence_of :parent
187
+ validates_presence_of :name
188
+ validates_presence_of :hostpath
189
+ end
190
+
191
+ # Saves or creates a shared folder.
192
+ #
193
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
194
+ # will be raised if the command failed.
195
+ # @return [Boolean] True if command was successful, false otherwise.
196
+ def save(raise_errors=false)
197
+ return true unless changed?
198
+
199
+ if !valid?
200
+ raise Exceptions::ValidationFailedException.new(errors) if raise_errors
201
+ return false
202
+ end
203
+
204
+ # If this isn't a new record, we destroy it first
205
+ destroy(raise_errors) if !new_record?
206
+
207
+ Command.vboxmanage("sharedfolder add #{Command.shell_escape(parent.name)} --name #{Command.shell_escape(name)} --hostpath #{Command.shell_escape(hostpath)}")
208
+ existing_record!
209
+ clear_dirty!
210
+
211
+ true
212
+ rescue Exceptions::CommandFailedException
213
+ raise if raise_errors
214
+ false
215
+ end
216
+
217
+ # Relationship callback when added to a collection. This is automatically
218
+ # called by any relationship collection when this object is added.
219
+ def added_to_relationship(parent)
220
+ write_attribute(:parent, parent)
221
+ end
222
+
223
+ # Destroys the shared folder. This doesn't actually delete the folder
224
+ # from the host system. Instead, it simply removes the mapping to the
225
+ # virtual machine, meaning it will no longer be possible to mount it
226
+ # from within the virtual machine.
227
+ #
228
+ # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
229
+ # will be raised if the command failed.
230
+ # @return [Boolean] True if command was successful, false otherwise.
231
+ def destroy(raise_errors=false)
232
+ # If the name changed, we have to be sure to use the previous
233
+ # one.
234
+ name_value = name_changed? ? name_was : name
235
+
236
+ Command.vboxmanage("sharedfolder remove #{Command.shell_escape(parent.name)} --name #{Command.shell_escape(name_value)}")
237
+ true
238
+ rescue Exceptions::CommandFailedException
239
+ raise if raise_errors
240
+ false
241
+ end
242
+ end
243
+ end
data/lib/virtualbox/vm.rb CHANGED
@@ -74,6 +74,7 @@ module VirtualBox
74
74
  #
75
75
  # relationship :nics, Nic
76
76
  # relationship :storage_controllers, StorageController, :dependent => :destroy
77
+ # relationship :shared_folders, SharedFolder
77
78
  #
78
79
  class VM < AbstractModel
79
80
  attribute :uuid, :readonly => true
@@ -104,6 +105,7 @@ module VirtualBox
104
105
  attribute :state, :populate_key => :vmstate, :readonly => true
105
106
  relationship :nics, Nic
106
107
  relationship :storage_controllers, StorageController, :dependent => :destroy
108
+ relationship :shared_folders, SharedFolder
107
109
 
108
110
  class <<self
109
111
  # Returns an array of all available VMs.
@@ -0,0 +1,125 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
2
+
3
+ class ValidatableTest < Test::Unit::TestCase
4
+ class ValidatableModel
5
+ include VirtualBox::AbstractModel::Validatable
6
+
7
+ attr_accessor :foos
8
+ attr_accessor :bars
9
+ end
10
+
11
+ context "errors" do
12
+ setup do
13
+ @model = ValidatableModel.new
14
+ end
15
+
16
+ should "have no errors by default" do
17
+ assert @model.errors.empty?
18
+ end
19
+
20
+ should "be able to add errors" do
21
+ @model.add_error(:foo, "is blank")
22
+ assert !@model.errors.empty?
23
+ assert !@model.errors[:foo].nil?
24
+ assert_equal "is blank", @model.errors[:foo].first
25
+ end
26
+
27
+ should "be able to add multiple errors" do
28
+ @model.add_error(:foo, "foo")
29
+ @model.add_error(:foo, "bar")
30
+ assert !@model.errors.empty?
31
+ assert !@model.errors[:foo].nil?
32
+ assert_equal 2, @model.errors[:foo].length
33
+ end
34
+
35
+ should "be able to clear errors" do
36
+ @model.add_error(:foo, "foo")
37
+ assert !@model.errors.empty?
38
+ @model.clear_errors
39
+ assert @model.errors.empty?
40
+ end
41
+ end
42
+
43
+ context "validity" do
44
+ setup do
45
+ @model = ValidatableModel.new
46
+ end
47
+
48
+ should "call validate on valid?" do
49
+ @model.expects(:validate)
50
+ assert @model.valid?
51
+ end
52
+
53
+ should "be valid if there are no errors" do
54
+ assert @model.valid?
55
+ end
56
+
57
+ should "be invalid if there are any errors" do
58
+ @model.add_error(:foo, "foo")
59
+ assert !@model.valid?
60
+ end
61
+
62
+ should "have a validate method by default which returns true" do
63
+ assert @model.validate
64
+ end
65
+ end
66
+
67
+ context "specific validations" do
68
+ setup do
69
+ @model = ValidatableModel.new
70
+ end
71
+
72
+ context "validates_presence_of" do
73
+ setup do
74
+ @model.foos = "foo"
75
+ @model.bars = "bar"
76
+ end
77
+
78
+ should "not add an error if not blank" do
79
+ @model.validates_presence_of(:foos)
80
+ assert @model.valid?
81
+ end
82
+
83
+ should "add an error if blank field" do
84
+ @model.foos = ""
85
+ @model.validates_presence_of(:foos)
86
+ assert !@model.valid?
87
+ end
88
+
89
+ should "add an error for a nil field" do
90
+ @model.foos = nil
91
+ @model.validates_presence_of(:foos)
92
+ assert !@model.valid?
93
+ end
94
+
95
+ should "validate multiple fields" do
96
+ @model.bars = nil
97
+ @model.validates_presence_of([:foos, :bars])
98
+
99
+ assert !@model.valid?
100
+ assert @model.errors[:bars]
101
+ end
102
+
103
+ should "return false on invalid" do
104
+ @model.bars = nil
105
+ assert !@model.validates_presence_of(:bars)
106
+ end
107
+
108
+ should "return true on valid" do
109
+ @model.bars = "foo"
110
+ assert @model.validates_presence_of(:bars)
111
+ end
112
+
113
+ should "return false if any are invalid on multiple fields" do
114
+ @model.bars = nil
115
+ assert !@model.validates_presence_of([:foos, :bars])
116
+ end
117
+
118
+ should "return true if all fields are valid" do
119
+ @model.foos = "foo"
120
+ @model.bars = "bar"
121
+ assert @model.validates_presence_of([:foos, :bars])
122
+ end
123
+ end
124
+ end
125
+ end
@@ -5,6 +5,9 @@ class AbstractModelTest < Test::Unit::TestCase
5
5
  def self.set_relationship(caller, old_value, new_value)
6
6
  new_value
7
7
  end
8
+
9
+ def self.validate_relationship(caller, data)
10
+ end
8
11
  end
9
12
 
10
13
  class Bar; end
@@ -16,6 +19,52 @@ class AbstractModelTest < Test::Unit::TestCase
16
19
  relationship :bars, Bar, :dependent => :destroy
17
20
  end
18
21
 
22
+ context "validation" do
23
+ setup do
24
+ @model = FakeModel.new
25
+ end
26
+
27
+ should "clear all previous errors" do
28
+ @model.expects(:clear_errors).once
29
+ @model.validate
30
+ end
31
+
32
+ should "call validate_relationship on each relationship class" do
33
+ Foo.expects(:validate_relationship).once.with(@model, nil)
34
+ @model.validate
35
+ end
36
+
37
+ should "forward arguments to validate_relationship" do
38
+ Foo.expects(:validate_relationship).once.with(@model, nil, "HELLO")
39
+ @model.validate("HELLO")
40
+ end
41
+
42
+ should "succeed if all relationships succeeded" do
43
+ Foo.expects(:validate_relationship).returns(true)
44
+ assert @model.validate
45
+ end
46
+
47
+ should "fail if one relationship failed to validate" do
48
+ Foo.expects(:validate_relationship).returns(true)
49
+ Bar.expects(:validate_relationship).returns(false)
50
+ assert !@model.validate
51
+ end
52
+
53
+ context "errors" do
54
+ should "return the errors of the relationships, as well as the model itself" do
55
+ @model.foo = nil
56
+ assert !@model.validate
57
+
58
+ @model.validates_presence_of(:foo)
59
+ Foo.expects(:errors_for_relationship).with(@model, nil).returns("BAD")
60
+ errors = @model.errors
61
+ assert errors.has_key?(:foos)
62
+ assert_equal "BAD", errors[:foos]
63
+ assert errors.has_key?(:foo)
64
+ end
65
+ end
66
+ end
67
+
19
68
  context "new/existing records" do
20
69
  setup do
21
70
  @model = FakeModel.new
@@ -19,6 +19,34 @@ class AttachedDeviceTest < Test::Unit::TestCase
19
19
  VirtualBox::Command.stubs(:execute).returns('')
20
20
  end
21
21
 
22
+ context "validations" do
23
+ setup do
24
+ @ad = VirtualBox::AttachedDevice.new
25
+ @ad.image = VirtualBox::DVD.empty_drive
26
+ @ad.port = 7
27
+ @ad.added_to_relationship(@caller)
28
+ end
29
+
30
+ should "be valid with all fields" do
31
+ assert @ad.valid?
32
+ end
33
+
34
+ should "be invalid with no image" do
35
+ @ad.image = nil
36
+ assert !@ad.valid?
37
+ end
38
+
39
+ should "be invalid with no port" do
40
+ @ad.port = nil
41
+ assert !@ad.valid?
42
+ end
43
+
44
+ should "be invalid if not in a relationship" do
45
+ @ad.write_attribute(:parent, nil)
46
+ assert !@ad.valid?
47
+ end
48
+ end
49
+
22
50
  context "medium" do
23
51
  setup do
24
52
  @ad = VirtualBox::AttachedDevice.new
@@ -62,6 +90,12 @@ class AttachedDeviceTest < Test::Unit::TestCase
62
90
  @value.save
63
91
  end
64
92
 
93
+ should "return false and not call vboxmanage if invalid" do
94
+ VirtualBox::Command.expects(:vboxmanage).never
95
+ @value.expects(:valid?).returns(false)
96
+ assert !@value.save
97
+ end
98
+
65
99
  should "not call destroy if the port didn't change" do
66
100
  @value.expects(:destroy).never
67
101
  assert !@value.port_changed?
@@ -90,9 +124,16 @@ class AttachedDeviceTest < Test::Unit::TestCase
90
124
  @ad.port = 3
91
125
  end
92
126
 
93
- should "raise a NoParentException if it wasn't added to a relationship" do
94
- assert_raises(VirtualBox::Exceptions::NoParentException) {
95
- @ad.save
127
+ should "return false and not call vboxmanage if invalid" do
128
+ VirtualBox::Command.expects(:vboxmanage).never
129
+ @ad.expects(:valid?).returns(false)
130
+ assert !@ad.save
131
+ end
132
+
133
+ should "raise a ValidationFailedException if invalid and raise_errors is true" do
134
+ @ad.expects(:valid?).returns(false)
135
+ assert_raises(VirtualBox::Exceptions::ValidationFailedException) {
136
+ @ad.save(true)
96
137
  }
97
138
  end
98
139
 
@@ -106,13 +147,6 @@ class AttachedDeviceTest < Test::Unit::TestCase
106
147
  @ad.expects(:destroy).never
107
148
  assert @ad.save
108
149
  end
109
-
110
- should "not raise an InvalidObjectException if no image is set" do
111
- @ad.image = nil
112
- assert_raises(VirtualBox::Exceptions::InvalidObjectException) {
113
- @ad.save
114
- }
115
- end
116
150
 
117
151
  should "call the proper vboxcommand" do
118
152
  VirtualBox::Command.expects(:vboxmanage).with("storageattach #{@vm.name} --storagectl #{VirtualBox::Command.shell_escape(@caller.name)} --port #{@ad.port} --device 0 --type #{@image.image_type} --medium #{@ad.medium}")
@@ -23,6 +23,29 @@ raw
23
23
  VirtualBox::Command.stubs(:vboxmanage).with("showhdinfo #{@name}").returns(@find_raw)
24
24
  end
25
25
 
26
+ context "validations" do
27
+ setup do
28
+ @hd = VirtualBox::HardDrive.new
29
+ @hd.size = 2000
30
+ end
31
+
32
+ should "be valid with a size and format" do
33
+ assert @hd.valid?
34
+ end
35
+
36
+ should "be invalid if size is nil" do
37
+ @hd.size = nil
38
+ assert !@hd.valid?
39
+ end
40
+
41
+ should "clear validations when rechecking" do
42
+ @hd.size = nil
43
+ assert !@hd.valid?
44
+ @hd.size = 700
45
+ assert @hd.valid?
46
+ end
47
+ end
48
+
26
49
  context "destroying a hard drive" do
27
50
  setup do
28
51
  @hd = VirtualBox::HardDrive.find(@name)
@@ -136,6 +159,19 @@ raw
136
159
  @hd.save(true)
137
160
  }
138
161
  end
162
+
163
+ should "not run if invalid" do
164
+ @hd.expects(:valid?).returns(false)
165
+ VirtualBox::Command.expects(:vboxmanage).never
166
+ assert !@hd.save
167
+ end
168
+
169
+ should "raise a ValidationFailedException if invalid and raise_errors is true" do
170
+ @hd.expects(:valid?).returns(false)
171
+ assert_raises(VirtualBox::Exceptions::ValidationFailedException) {
172
+ @hd.save(true)
173
+ }
174
+ end
139
175
  end
140
176
 
141
177
  context "finding a single hard drive" do
@@ -0,0 +1,231 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'test_helper')
2
+
3
+ class SharedFolderTest < Test::Unit::TestCase
4
+ setup do
5
+ @data = {
6
+ :sharedfoldernamemachinemapping1 => "foofolder",
7
+ :sharedfolderpathmachinemapping1 => "/foo",
8
+ :sharedfoldernamemachinemapping2 => "barfolder",
9
+ :sharedfolderpathmachinemapping2 => "/bar"
10
+ }
11
+
12
+ @caller = mock("caller")
13
+ @caller.stubs(:name).returns("foo")
14
+
15
+ VirtualBox::Command.stubs(:execute)
16
+ end
17
+
18
+ context "validations" do
19
+ setup do
20
+ @sf = VirtualBox::SharedFolder.new
21
+ @sf.name = "foo"
22
+ @sf.hostpath = "bar"
23
+ @sf.added_to_relationship(@caller)
24
+ end
25
+
26
+ should "be valid with all fields" do
27
+ assert @sf.valid?
28
+ end
29
+
30
+ should "be invalid with no name" do
31
+ @sf.name = nil
32
+ assert !@sf.valid?
33
+ end
34
+
35
+ should "be invalid with no hostpath" do
36
+ @sf.hostpath = nil
37
+ assert !@sf.valid?
38
+ end
39
+
40
+ should "be invalid if not in a relationship" do
41
+ @sf.write_attribute(:parent, nil)
42
+ assert !@sf.valid?
43
+ end
44
+ end
45
+
46
+ context "saving an existing shared folder" do
47
+ setup do
48
+ @value = VirtualBox::SharedFolder.populate_relationship(@caller, @data)
49
+ @value = @value[0]
50
+ @value.name = "different"
51
+ assert @value.changed?
52
+ end
53
+
54
+ should "first destroy the shared folder then recreate it" do
55
+ seq = sequence("create_seq")
56
+ @value.expects(:destroy).in_sequence(seq)
57
+ VirtualBox::Command.expects(:vboxmanage).in_sequence(seq)
58
+ assert @value.save
59
+ end
60
+
61
+ should "call destroy with raise errors if set" do
62
+ @value.expects(:destroy).with(true).once
63
+ assert @value.save(true)
64
+ end
65
+ end
66
+
67
+ context "creating a new shared folder" do
68
+ setup do
69
+ @sf = VirtualBox::SharedFolder.new
70
+ @sf.name = "foo"
71
+ @sf.hostpath = "bar"
72
+ end
73
+
74
+ should "return false and not call vboxmanage if invalid" do
75
+ VirtualBox::Command.expects(:vboxmanage).never
76
+ @sf.expects(:valid?).returns(false)
77
+ assert !@sf.save
78
+ end
79
+
80
+ should "raise a ValidationFailedException if invalid and raise_errors is true" do
81
+ @sf.expects(:valid?).returns(false)
82
+ assert_raises(VirtualBox::Exceptions::ValidationFailedException) {
83
+ @sf.save(true)
84
+ }
85
+ end
86
+
87
+ context "has a parent" do
88
+ setup do
89
+ @sf.added_to_relationship(@caller)
90
+ VirtualBox::Command.stubs(:vboxmanage)
91
+ end
92
+
93
+ should "not call destroy since its a new record" do
94
+ @sf.expects(:destroy).never
95
+ assert @sf.save
96
+ end
97
+
98
+ should "call the proper vboxcommand" do
99
+ VirtualBox::Command.expects(:vboxmanage).with("sharedfolder add #{@caller.name} --name #{@sf.name} --hostpath #{@sf.hostpath}")
100
+ assert @sf.save
101
+ end
102
+
103
+ should "return false if the command failed" do
104
+ VirtualBox::Command.stubs(:vboxmanage).raises(VirtualBox::Exceptions::CommandFailedException)
105
+ assert !@sf.save
106
+ end
107
+
108
+ should "return true if the command was a success" do
109
+ assert @sf.save
110
+ end
111
+
112
+ should "raise an exception if true sent to save and error occured" do
113
+ VirtualBox::Command.stubs(:vboxmanage).raises(VirtualBox::Exceptions::CommandFailedException)
114
+ assert_raises(VirtualBox::Exceptions::CommandFailedException) {
115
+ @sf.save(true)
116
+ }
117
+ end
118
+
119
+ should "not be a new record after saving" do
120
+ assert @sf.new_record?
121
+ assert @sf.save
122
+ assert !@sf.new_record?
123
+ end
124
+
125
+ should "not be changed after saving" do
126
+ assert @sf.changed?
127
+ assert @sf.save
128
+ assert !@sf.changed?
129
+ end
130
+ end
131
+ end
132
+
133
+ context "constructor" do
134
+ should "call initialize_for_relationship if 3 args are given" do
135
+ VirtualBox::SharedFolder.any_instance.expects(:initialize_for_relationship).with(1,2,3).once
136
+ VirtualBox::SharedFolder.new(1,2,3)
137
+ end
138
+
139
+ should "raise a NoMethodError if anything other than 0,1,or 3 arguments" do
140
+ 2.upto(9) do |i|
141
+ next if i == 3
142
+ args = Array.new(i, "A")
143
+
144
+ assert_raises(NoMethodError) {
145
+ VirtualBox::SharedFolder.new(*args)
146
+ }
147
+ end
148
+ end
149
+
150
+ should "populate from a hash if one argument is given" do
151
+ VirtualBox::SharedFolder.any_instance.expects(:initialize_for_data).with("HI").once
152
+ VirtualBox::SharedFolder.new("HI")
153
+ end
154
+
155
+ context "initializing from data" do
156
+ setup do
157
+ @sf = VirtualBox::SharedFolder.new({:name => "foo", :hostpath => "bar"})
158
+ end
159
+
160
+ should "allow the use of :name and :hostpath in the hash" do
161
+ assert_equal "foo", @sf.name
162
+ assert_equal "bar", @sf.hostpath
163
+ end
164
+
165
+ should "keep the record new" do
166
+ assert @sf.new_record?
167
+ end
168
+ end
169
+ end
170
+
171
+ context "destroying" do
172
+ setup do
173
+ @value = VirtualBox::SharedFolder.populate_relationship(@caller, @data)
174
+ @value = @value[0]
175
+ end
176
+
177
+ should "call the proper command" do
178
+ VirtualBox::Command.expects(:vboxmanage).with("sharedfolder remove #{@caller.name} --name #{@value.name}").once
179
+ assert @value.destroy
180
+ end
181
+
182
+ should "shell escape VM name and storage controller name" do
183
+ shell_seq = sequence("shell_seq")
184
+ VirtualBox::Command.expects(:shell_escape).with(@caller.name).in_sequence(shell_seq)
185
+ VirtualBox::Command.expects(:shell_escape).with(@value.name).in_sequence(shell_seq)
186
+ VirtualBox::Command.expects(:vboxmanage).in_sequence(shell_seq)
187
+ assert @value.destroy
188
+ end
189
+
190
+ should "return false if destroy failed" do
191
+ VirtualBox::Command.stubs(:vboxmanage).raises(VirtualBox::Exceptions::CommandFailedException)
192
+ assert !@value.destroy
193
+ end
194
+
195
+ should "raise an exception if destroy failed and an error occured" do
196
+ VirtualBox::Command.stubs(:vboxmanage).raises(VirtualBox::Exceptions::CommandFailedException)
197
+ assert_raises(VirtualBox::Exceptions::CommandFailedException) {
198
+ @value.destroy(true)
199
+ }
200
+ end
201
+
202
+ should "use the old name if it was changed" do
203
+ @value.name = "DIFFERENT"
204
+ shell_seq = sequence("shell_seq")
205
+ VirtualBox::Command.expects(:shell_escape).with(@caller.name).in_sequence(shell_seq)
206
+ VirtualBox::Command.expects(:shell_escape).with(@value.name_was).in_sequence(shell_seq)
207
+ VirtualBox::Command.expects(:vboxmanage).in_sequence(shell_seq)
208
+ assert @value.destroy
209
+ end
210
+ end
211
+
212
+ context "populating relationships" do
213
+ setup do
214
+ @value = VirtualBox::SharedFolder.populate_relationship(@caller, @data)
215
+ end
216
+
217
+ should "create the correct amount of objects" do
218
+ assert_equal 2, @value.length
219
+ end
220
+
221
+ should "parse the proper data" do
222
+ value = @value[0]
223
+ assert_equal "foofolder", value.name
224
+ assert_equal "/foo", value.hostpath
225
+
226
+ value = @value[1]
227
+ assert_equal "barfolder", value.name
228
+ assert_equal "/bar", value.hostpath
229
+ end
230
+ end
231
+ end
@@ -71,7 +71,11 @@ uart2="off"
71
71
  audio="none"
72
72
  clipboard="bidirectional"
73
73
  vrdp="off"
74
- usb="off"
74
+ usb="off"
75
+ SharedFolderNameMachineMapping1="mysharedfolder"
76
+ SharedFolderPathMachineMapping1="/virtualbox"
77
+ SharedFolderNameMachineMapping2="otherfolder"
78
+ SharedFolderPathMachineMapping2="/virtualbox/lib"
75
79
  showvminfo
76
80
 
77
81
  @name = "foo"
@@ -354,6 +358,7 @@ raw
354
358
  should "save the relationships as well" do
355
359
  VirtualBox::Nic.expects(:save_relationship).once
356
360
  VirtualBox::StorageController.expects(:save_relationship).once
361
+ VirtualBox::SharedFolder.expects(:save_relationship).once
357
362
  assert @vm.save
358
363
  end
359
364
  end
@@ -391,6 +396,12 @@ raw
391
396
  assert @vm.storage_controllers.is_a?(Array)
392
397
  assert_equal 2, @vm.storage_controllers.length
393
398
  end
399
+
400
+ should "properly load shared folder relationship" do
401
+ assert @vm.shared_folders
402
+ assert @vm.shared_folders.is_a?(Array)
403
+ assert_equal 2, @vm.shared_folders.length
404
+ end
394
405
  end
395
406
 
396
407
  context "parsing the showvminfo output" do
data/virtualbox.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{virtualbox}
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Mitchell Hashimoto"]
12
- s.date = %q{2010-01-26}
12
+ s.date = %q{2010-01-28}
13
13
  s.description = %q{Create and modify virtual machines in VirtualBox using pure ruby.}
14
14
  s.email = %q{mitchell.hashimoto@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -24,11 +24,13 @@ Gem::Specification.new do |s|
24
24
  "TODO",
25
25
  "VERSION",
26
26
  "docs/GettingStarted.md",
27
+ "docs/WhatsNew.md",
27
28
  "lib/virtualbox.rb",
28
29
  "lib/virtualbox/abstract_model.rb",
29
30
  "lib/virtualbox/abstract_model/attributable.rb",
30
31
  "lib/virtualbox/abstract_model/dirty.rb",
31
32
  "lib/virtualbox/abstract_model/relatable.rb",
33
+ "lib/virtualbox/abstract_model/validatable.rb",
32
34
  "lib/virtualbox/attached_device.rb",
33
35
  "lib/virtualbox/command.rb",
34
36
  "lib/virtualbox/dvd.rb",
@@ -38,12 +40,14 @@ Gem::Specification.new do |s|
38
40
  "lib/virtualbox/image.rb",
39
41
  "lib/virtualbox/nic.rb",
40
42
  "lib/virtualbox/proxies/collection.rb",
43
+ "lib/virtualbox/shared_folder.rb",
41
44
  "lib/virtualbox/storage_controller.rb",
42
45
  "lib/virtualbox/vm.rb",
43
46
  "test/test_helper.rb",
44
47
  "test/virtualbox/abstract_model/attributable_test.rb",
45
48
  "test/virtualbox/abstract_model/dirty_test.rb",
46
49
  "test/virtualbox/abstract_model/relatable_test.rb",
50
+ "test/virtualbox/abstract_model/validatable_test.rb",
47
51
  "test/virtualbox/abstract_model_test.rb",
48
52
  "test/virtualbox/attached_device_test.rb",
49
53
  "test/virtualbox/command_test.rb",
@@ -53,6 +57,7 @@ Gem::Specification.new do |s|
53
57
  "test/virtualbox/image_test.rb",
54
58
  "test/virtualbox/nic_test.rb",
55
59
  "test/virtualbox/proxies/collection_test.rb",
60
+ "test/virtualbox/shared_folder_test.rb",
56
61
  "test/virtualbox/storage_controller_test.rb",
57
62
  "test/virtualbox/vm_test.rb",
58
63
  "virtualbox.gemspec"
@@ -67,6 +72,7 @@ Gem::Specification.new do |s|
67
72
  "test/virtualbox/abstract_model/attributable_test.rb",
68
73
  "test/virtualbox/abstract_model/dirty_test.rb",
69
74
  "test/virtualbox/abstract_model/relatable_test.rb",
75
+ "test/virtualbox/abstract_model/validatable_test.rb",
70
76
  "test/virtualbox/abstract_model_test.rb",
71
77
  "test/virtualbox/attached_device_test.rb",
72
78
  "test/virtualbox/command_test.rb",
@@ -76,6 +82,7 @@ Gem::Specification.new do |s|
76
82
  "test/virtualbox/image_test.rb",
77
83
  "test/virtualbox/nic_test.rb",
78
84
  "test/virtualbox/proxies/collection_test.rb",
85
+ "test/virtualbox/shared_folder_test.rb",
79
86
  "test/virtualbox/storage_controller_test.rb",
80
87
  "test/virtualbox/vm_test.rb"
81
88
  ]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: virtualbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mitchell Hashimoto
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-26 00:00:00 -08:00
12
+ date: 2010-01-28 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -30,11 +30,13 @@ files:
30
30
  - TODO
31
31
  - VERSION
32
32
  - docs/GettingStarted.md
33
+ - docs/WhatsNew.md
33
34
  - lib/virtualbox.rb
34
35
  - lib/virtualbox/abstract_model.rb
35
36
  - lib/virtualbox/abstract_model/attributable.rb
36
37
  - lib/virtualbox/abstract_model/dirty.rb
37
38
  - lib/virtualbox/abstract_model/relatable.rb
39
+ - lib/virtualbox/abstract_model/validatable.rb
38
40
  - lib/virtualbox/attached_device.rb
39
41
  - lib/virtualbox/command.rb
40
42
  - lib/virtualbox/dvd.rb
@@ -44,12 +46,14 @@ files:
44
46
  - lib/virtualbox/image.rb
45
47
  - lib/virtualbox/nic.rb
46
48
  - lib/virtualbox/proxies/collection.rb
49
+ - lib/virtualbox/shared_folder.rb
47
50
  - lib/virtualbox/storage_controller.rb
48
51
  - lib/virtualbox/vm.rb
49
52
  - test/test_helper.rb
50
53
  - test/virtualbox/abstract_model/attributable_test.rb
51
54
  - test/virtualbox/abstract_model/dirty_test.rb
52
55
  - test/virtualbox/abstract_model/relatable_test.rb
56
+ - test/virtualbox/abstract_model/validatable_test.rb
53
57
  - test/virtualbox/abstract_model_test.rb
54
58
  - test/virtualbox/attached_device_test.rb
55
59
  - test/virtualbox/command_test.rb
@@ -59,6 +63,7 @@ files:
59
63
  - test/virtualbox/image_test.rb
60
64
  - test/virtualbox/nic_test.rb
61
65
  - test/virtualbox/proxies/collection_test.rb
66
+ - test/virtualbox/shared_folder_test.rb
62
67
  - test/virtualbox/storage_controller_test.rb
63
68
  - test/virtualbox/vm_test.rb
64
69
  - virtualbox.gemspec
@@ -95,6 +100,7 @@ test_files:
95
100
  - test/virtualbox/abstract_model/attributable_test.rb
96
101
  - test/virtualbox/abstract_model/dirty_test.rb
97
102
  - test/virtualbox/abstract_model/relatable_test.rb
103
+ - test/virtualbox/abstract_model/validatable_test.rb
98
104
  - test/virtualbox/abstract_model_test.rb
99
105
  - test/virtualbox/attached_device_test.rb
100
106
  - test/virtualbox/command_test.rb
@@ -104,5 +110,6 @@ test_files:
104
110
  - test/virtualbox/image_test.rb
105
111
  - test/virtualbox/nic_test.rb
106
112
  - test/virtualbox/proxies/collection_test.rb
113
+ - test/virtualbox/shared_folder_test.rb
107
114
  - test/virtualbox/storage_controller_test.rb
108
115
  - test/virtualbox/vm_test.rb