virtualbox 0.1.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.
Files changed (36) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +11 -0
  3. data/Rakefile +32 -0
  4. data/Readme.md +75 -0
  5. data/TODO +13 -0
  6. data/VERSION +1 -0
  7. data/lib/virtualbox.rb +11 -0
  8. data/lib/virtualbox/abstract_model.rb +95 -0
  9. data/lib/virtualbox/abstract_model/attributable.rb +193 -0
  10. data/lib/virtualbox/abstract_model/dirty.rb +164 -0
  11. data/lib/virtualbox/abstract_model/relatable.rb +163 -0
  12. data/lib/virtualbox/attached_device.rb +104 -0
  13. data/lib/virtualbox/command.rb +54 -0
  14. data/lib/virtualbox/dvd.rb +31 -0
  15. data/lib/virtualbox/errors.rb +7 -0
  16. data/lib/virtualbox/ext/subclass_listing.rb +24 -0
  17. data/lib/virtualbox/hard_drive.rb +169 -0
  18. data/lib/virtualbox/image.rb +94 -0
  19. data/lib/virtualbox/nic.rb +150 -0
  20. data/lib/virtualbox/storage_controller.rb +122 -0
  21. data/lib/virtualbox/vm.rb +287 -0
  22. data/test/test_helper.rb +25 -0
  23. data/test/virtualbox/abstract_model/attributable_test.rb +150 -0
  24. data/test/virtualbox/abstract_model/dirty_test.rb +66 -0
  25. data/test/virtualbox/abstract_model/relatable_test.rb +141 -0
  26. data/test/virtualbox/abstract_model_test.rb +146 -0
  27. data/test/virtualbox/attached_device_test.rb +92 -0
  28. data/test/virtualbox/command_test.rb +30 -0
  29. data/test/virtualbox/dvd_test.rb +58 -0
  30. data/test/virtualbox/ext/subclass_listing_test.rb +25 -0
  31. data/test/virtualbox/hard_drive_test.rb +161 -0
  32. data/test/virtualbox/image_test.rb +113 -0
  33. data/test/virtualbox/nic_test.rb +119 -0
  34. data/test/virtualbox/storage_controller_test.rb +79 -0
  35. data/test/virtualbox/vm_test.rb +313 -0
  36. metadata +103 -0
@@ -0,0 +1,122 @@
1
+ module VirtualBox
2
+ # Represents a single storage controller which can be attached to a
3
+ # virtual machine.
4
+ #
5
+ # **Currently, storage controllers can not be created from scratch.
6
+ # Therefore, the only way to use this model is through a relationship
7
+ # of a {VM} object.**
8
+ #
9
+ # # Attributes and Relationships
10
+ #
11
+ # Properties of the storage controller are exposed using standard ruby instance
12
+ # methods which are generated on the fly. Because of this, they are not listed
13
+ # below as available instance methods.
14
+ #
15
+ # These attributes can be accessed and modified via standard ruby-style
16
+ # `instance.attribute` and `instance.attribute=` methods. The attributes are
17
+ # listed below.
18
+ #
19
+ # Relationships are also accessed like attributes but can't be set. Instead,
20
+ # they are typically references to other objects such as an {AttachedDevice} which
21
+ # in turn have their own attributes which can be modified.
22
+ #
23
+ # ## Attributes
24
+ #
25
+ # This is copied directly from the class header, but lists all available
26
+ # attributes. If you don't understand what this means, read {Attributable}.
27
+ #
28
+ # attribute :parent, :readonly => true
29
+ # attribute :name
30
+ # attribute :type
31
+ # attribute :max_ports, :populate_key => :maxportcount
32
+ # attribute :ports, :populate_key => :portcount
33
+ #
34
+ # ## Relationships
35
+ #
36
+ # In addition to the basic attributes, a virtual machine is related
37
+ # to other things. The relationships are listed below. If you don't
38
+ # understand this, read {Relatable}.
39
+ #
40
+ # relationship :devices, AttachedDevice, :dependent => :destroy
41
+ #
42
+ class StorageController < AbstractModel
43
+ attribute :parent, :readonly => true
44
+ attribute :name
45
+ attribute :type
46
+ attribute :max_ports, :populate_key => :maxportcount
47
+ attribute :ports, :populate_key => :portcount
48
+ relationship :devices, AttachedDevice, :dependent => :destroy
49
+
50
+ class <<self
51
+ # Populates a relationship with another model.
52
+ #
53
+ # **This method typically won't be used except internally.**
54
+ #
55
+ # @return [Array<StorageController>]
56
+ def populate_relationship(caller, data)
57
+ relation = []
58
+
59
+ counter = 0
60
+ loop do
61
+ break unless data["storagecontrollername#{counter}".to_sym]
62
+ nic = new(counter, caller, data)
63
+ relation.push(nic)
64
+ counter += 1
65
+ end
66
+
67
+ relation
68
+ end
69
+
70
+ # Destroys a relationship with another model.
71
+ #
72
+ # **This method typically won't be used except internally.**
73
+ def destroy_relationship(caller, data, *args)
74
+ data.each { |v| v.destroy(*args) }
75
+ end
76
+ end
77
+
78
+ # Since storage controllers still can't be created from scratch,
79
+ # this method shouldn't be called. Instead, storage controllers
80
+ # can be retrieved through relationships of other models such
81
+ # as {VM}.
82
+ def initialize(index, caller, data)
83
+ super()
84
+
85
+ @index = index
86
+
87
+ # Setup the index specific attributes
88
+ populate_data = {}
89
+ self.class.attributes.each do |name, options|
90
+ key = options[:populate_key] || name
91
+ value = data["storagecontroller#{key}#{index}".to_sym]
92
+ populate_data[key] = value
93
+ end
94
+
95
+ # Make sure to merge in device data so those relationships will be
96
+ # setup properly
97
+ populate_data.merge!(extract_devices(index, data))
98
+
99
+ populate_attributes(populate_data.merge({
100
+ :parent => caller
101
+ }))
102
+ end
103
+
104
+ # Extracts related devices for a storage controller.
105
+ #
106
+ # **This method typically won't be used except internally.**
107
+ #
108
+ # @return [Hash]
109
+ def extract_devices(index, data)
110
+ name = data["storagecontrollername#{index}".downcase.to_sym].downcase
111
+
112
+ device_data = {}
113
+ data.each do |k,v|
114
+ next unless k.to_s =~ /^#{name}-/
115
+
116
+ device_data[k] = v
117
+ end
118
+
119
+ device_data
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,287 @@
1
+ module VirtualBox
2
+ # Represents a single VirtualBox virtual machine. All attributes which are
3
+ # not read-only can be modified and saved.
4
+ #
5
+ # # Finding Virtual Machines
6
+ #
7
+ # Two methods are used to find virtual machines: {VM.all} and {VM.find}. Each
8
+ # return a {VM} object. An example is shown below:
9
+ #
10
+ # vm = VirtualBox::VM.find("MyWindowsXP")
11
+ # puts vm.name # => "MyWindowsXP"
12
+ #
13
+ # # Modifying Virtual Machines
14
+ #
15
+ # Virtual machines can be modified a lot like [ActiveRecord](http://ar.rubyonrails.org/)
16
+ # objects. This is best shown through example:
17
+ #
18
+ # vm = VirtualBox::VM.find("MyWindowsXP")
19
+ # vm.memory = 256
20
+ # vm.name = "WindowsXP"
21
+ # vm.save
22
+ #
23
+ # # Attributes and Relationships
24
+ #
25
+ # Properties of the virtual machine are exposed using standard ruby instance
26
+ # methods which are generated on the fly. Because of this, they are not listed
27
+ # below as available instance methods.
28
+ #
29
+ # These attributes can be accessed and modified via standard ruby-style
30
+ # `instance.attribute` and `instance.attribute=` methods. The attributes are
31
+ # listed below.
32
+ #
33
+ # Relationships are also accessed like attributes but can't be set. Instead,
34
+ # they are typically references to other objects such as a {HardDrive} which
35
+ # in turn have their own attributes which can be modified.
36
+ #
37
+ # ## Attributes
38
+ #
39
+ # This is copied directly from the class header, but lists all available
40
+ # attributes. If you don't understand what this means, read {Attributable}.
41
+ #
42
+ # attribute :uuid, :readonly => true
43
+ # attribute :name
44
+ # attribute :ostype
45
+ # attribute :memory
46
+ # attribute :vram
47
+ # attribute :acpi
48
+ # attribute :ioapic
49
+ # attribute :cpus
50
+ # attribute :synthcpu
51
+ # attribute :pae
52
+ # attribute :hwvirtex
53
+ # attribute :hwvirtexexcl
54
+ # attribute :nestedpaging
55
+ # attribute :vtxvpid
56
+ # attribute :accelerate3d
57
+ # attribute :biosbootmenu, :populate_key => :bootmenu
58
+ # attribute :boot1
59
+ # attribute :boot2
60
+ # attribute :boot3
61
+ # attribute :boot4
62
+ # attribute :clipboard
63
+ # attribute :monitorcount
64
+ # attribute :usb
65
+ # attribute :audio
66
+ # attribute :vrdp
67
+ # attribute :state, :populate_key => :vmstate, :readonly => true
68
+ #
69
+ # ## Relationships
70
+ #
71
+ # In addition to the basic attributes, a virtual machine is related
72
+ # to other things. The relationships are listed below. If you don't
73
+ # understand this, read {Relatable}.
74
+ #
75
+ # relationship :nics, Nic
76
+ # relationship :storage_controllers, StorageController, :dependent => :destroy
77
+ #
78
+ class VM < AbstractModel
79
+ attribute :uuid, :readonly => true
80
+ attribute :name
81
+ attribute :ostype
82
+ attribute :memory
83
+ attribute :vram
84
+ attribute :acpi
85
+ attribute :ioapic
86
+ attribute :cpus
87
+ attribute :synthcpu
88
+ attribute :pae
89
+ attribute :hwvirtex
90
+ attribute :hwvirtexexcl
91
+ attribute :nestedpaging
92
+ attribute :vtxvpid
93
+ attribute :accelerate3d
94
+ attribute :biosbootmenu, :populate_key => :bootmenu
95
+ attribute :boot1
96
+ attribute :boot2
97
+ attribute :boot3
98
+ attribute :boot4
99
+ attribute :clipboard
100
+ attribute :monitorcount
101
+ attribute :usb
102
+ attribute :audio
103
+ attribute :vrdp
104
+ attribute :state, :populate_key => :vmstate, :readonly => true
105
+ relationship :nics, Nic
106
+ relationship :storage_controllers, StorageController, :dependent => :destroy
107
+
108
+ class <<self
109
+ # Returns an array of all available VMs.
110
+ #
111
+ # @return [Array<VM>]
112
+ def all
113
+ raw = Command.vboxmanage("list vms")
114
+ parse_vm_list(raw)
115
+ end
116
+
117
+ # Finds a VM by UUID or registered name and returns a
118
+ # new VM object. If the VM doesn't exist, will return `nil`.
119
+ #
120
+ # @return [VM]
121
+ def find(name)
122
+ new(raw_info(name))
123
+ end
124
+
125
+ # Imports a VM, blocking the entire thread during this time.
126
+ # When finished, on success, will return the VM object. This
127
+ # VM object can be used to make any modifications necessary
128
+ # (RAM, cpus, etc.).
129
+ #
130
+ # @return [VM] The newly imported virtual machine
131
+ def import(source_path)
132
+ raw = Command.vboxmanage("import #{Command.shell_escape(source_path)}")
133
+ return nil unless raw
134
+
135
+ find(parse_vm_name(raw))
136
+ end
137
+
138
+ # Gets the non-machine-readable info for a given VM and returns
139
+ # it as a raw string.
140
+ #
141
+ # **This method typically won't be used except internally.**
142
+ #
143
+ # @return [String]
144
+ def human_info(name)
145
+ Command.vboxmanage("showvminfo #{name}")
146
+ end
147
+
148
+ # Gets the VM info (machine readable) for a given VM and returns it
149
+ # as a hash.
150
+ #
151
+ # @return [Hash] Parsed VM info.
152
+ def raw_info(name)
153
+ raw = Command.vboxmanage("showvminfo #{name} --machinereadable")
154
+ parse_vm_info(raw)
155
+ end
156
+
157
+ # Parses the machine-readable format outputted by VBoxManage showvminfo
158
+ # into a hash. Ignores lines which don't match the format.
159
+ def parse_vm_info(raw)
160
+ parsed = {}
161
+ raw.lines.each do |line|
162
+ # Some lines aren't configuration, we just ignore them
163
+ next unless line =~ /^"?(.+?)"?="?(.+?)"?$/
164
+ parsed[$1.downcase.to_sym] = $2.strip
165
+ end
166
+
167
+ parsed
168
+ end
169
+
170
+ # Parses the list of VMs returned by the "list vms" command used
171
+ # in {VM.all}.
172
+ #
173
+ # **This method typically won't be used except internally.**
174
+ #
175
+ # @return [Array] Array of virtual machines.
176
+ def parse_vm_list(raw)
177
+ results = []
178
+ raw.lines.each do |line|
179
+ next unless line =~ /^"(.+?)"\s+\{(.+?)\}$/
180
+ results.push(find($1.to_s))
181
+ end
182
+
183
+ results
184
+ end
185
+
186
+ # Parses the vm name from the import results.
187
+ #
188
+ # **This method typically won't be used except internally.**
189
+ #
190
+ # @return [String] Parsed VM name
191
+ def parse_vm_name(raw)
192
+ return nil unless raw =~ /VM name "(.+?)"/
193
+ $1.to_s
194
+ end
195
+ end
196
+
197
+ # Creates a new instance of a virtual machine.
198
+ #
199
+ # **Currently can NOT be used to create a NEW virtual machine**.
200
+ # Support for creating new virtual machines will be added shortly.
201
+ # For now, this is only used by {VM.find} and {VM.all} to
202
+ # initialize the VMs.
203
+ def initialize(data)
204
+ super()
205
+
206
+ populate_attributes(data)
207
+ @original_name = data[:name]
208
+ end
209
+
210
+ # State of the virtual machine. Returns the state of the virtual
211
+ # machine. This state will represent the state that was assigned
212
+ # when the VM was found unless `reload` is set to `true`.
213
+ #
214
+ # @param [Boolean] reload If true, will reload the state to current
215
+ # value.
216
+ # @return [String] Virtual machine state.
217
+ def state(reload=false)
218
+ if reload
219
+ info = self.class.raw_info(@original_name)
220
+ write_attribute(:state, info[:vmstate])
221
+ end
222
+
223
+ read_attribute(:state)
224
+ end
225
+
226
+ # Saves the virtual machine if modified. This method saves any modified
227
+ # attributes of the virtual machine. If any related attributes were saved
228
+ # as well (such as storage controllers), those will be saved, too.
229
+ def save
230
+ # Make sure we save the new name first if that was changed, or
231
+ # we'll get some inconsistencies later
232
+ if name_changed?
233
+ save_attribute(:name, name)
234
+ @original_name = name
235
+ end
236
+
237
+ super
238
+ end
239
+
240
+ # Saves a single attribute of the virtual machine. This should **not**
241
+ # be called except interally. Instead, you're probably looking for {#save}.
242
+ #
243
+ # **This method typically won't be used except internally.**
244
+ def save_attribute(key, value)
245
+ Command.vboxmanage("modifyvm #{@original_name} --#{key} #{Command.shell_escape(value.to_s)}")
246
+ super
247
+ end
248
+
249
+ # Starts the virtual machine. The virtual machine can be started in a
250
+ # variety of modes:
251
+ #
252
+ # * **gui** -- The VirtualBox GUI will open with the screen of the VM.
253
+ # * **headless** -- The VM will run in the background. No GUI will be
254
+ # present at all.
255
+ #
256
+ # All modes will start their processes and return almost immediately.
257
+ # Both the GUI and headless mode will not block the ruby process.
258
+ def start(mode=:gui)
259
+ Command.vboxmanage("startvm #{@original_name} --type #{mode}")
260
+ end
261
+
262
+ # Stops the VM by directly calling "poweroff." Immediately halts the
263
+ # virtual machine without saving state. This could result in a loss
264
+ # of data.
265
+ def stop
266
+ Command.vboxmanage("controlvm #{@original_name} poweroff")
267
+ end
268
+
269
+ # Destroys the virtual machine. This method also removes all attached
270
+ # media (required by VirtualBox to destroy a VM). By default,
271
+ # this **will not** destroy attached hard drives, but will if given
272
+ # the `destroy_image` option.
273
+ #
274
+ # @overload destroy(opts = {})
275
+ # Passes options to the destroy method.
276
+ # @option opts [Boolean] :destroy_image (false) If true, will
277
+ # also destroy all attached images such as hard drives, disk
278
+ # images, etc.
279
+ def destroy(*args)
280
+ # Call super first to destroy relationships, necessary before
281
+ # unregistering a VM
282
+ super
283
+
284
+ Command.vboxmanage("unregistervm #{@original_name} --delete")
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,25 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), '..', 'vendor', 'gems', 'environment')
3
+ rescue LoadError
4
+ puts <<-ENVERR
5
+ ==================================================
6
+ ERROR: Gem environment file not found!
7
+
8
+ This gem uses bundler to handle gem dependencies. To setup the
9
+ test environment, please run `gem bundle test` If you don't
10
+ have bundler, you can install that with `gem install bundler`
11
+ ==================================================
12
+ ENVERR
13
+ exit
14
+ end
15
+
16
+ # ruby-debug, not necessary, but useful if we have it
17
+ begin
18
+ require 'ruby-debug'
19
+ rescue LoadError; end
20
+
21
+ require 'contest'
22
+ require 'mocha'
23
+
24
+ # The actual library
25
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'virtualbox')
@@ -0,0 +1,150 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'test_helper')
2
+
3
+ class AttributableTest < Test::Unit::TestCase
4
+ class EmptyAttributeModel
5
+ include VirtualBox::AbstractModel::Attributable
6
+ end
7
+
8
+ class AttributeModel < EmptyAttributeModel
9
+ attribute :foo
10
+ attribute :bar
11
+
12
+ def initialize
13
+ super
14
+
15
+ populate_attributes({
16
+ :foo => "foo",
17
+ :bar => "bar"
18
+ })
19
+ end
20
+ end
21
+
22
+ context "subclasses" do
23
+ class SubModel < AttributeModel
24
+ attribute :baz
25
+ end
26
+
27
+ should "have foo bar and baz" do
28
+ attributes = SubModel.attributes
29
+ assert attributes.has_key?(:foo)
30
+ assert attributes.has_key?(:bar)
31
+ assert attributes.has_key?(:baz)
32
+ end
33
+ end
34
+
35
+ context "attribute options" do
36
+ context "custom populate keys" do
37
+ class CustomPopulateModel < AttributeModel
38
+ attribute :foo, :populate_key => :foo_key
39
+ end
40
+
41
+ setup do
42
+ @model = CustomPopulateModel.new
43
+ end
44
+
45
+ should "use the populate key instead of the attribute name" do
46
+ @model.populate_attributes({
47
+ :foo => "not me!",
48
+ :foo_key => "bar"
49
+ })
50
+
51
+ assert_equal "bar", @model.foo
52
+ end
53
+ end
54
+
55
+ context "readonly attributes" do
56
+ class ReadonlyModel < AttributeModel
57
+ attribute :foo, :readonly => :readonly
58
+
59
+ def initialize
60
+ super
61
+ populate_attributes({ :foo => "foo" })
62
+ end
63
+ end
64
+
65
+ setup do
66
+ @model = ReadonlyModel.new
67
+ end
68
+
69
+ should "be readonly" do
70
+ assert @model.readonly_attribute?(:foo)
71
+ end
72
+
73
+ should "allow reading" do
74
+ assert_equal "foo", @model.foo
75
+ end
76
+
77
+ should "not allow writing" do
78
+ assert_raises(NoMethodError) { @model.foo = "YO" }
79
+ end
80
+ end
81
+
82
+ context "default values" do
83
+ class DefaultModel < EmptyAttributeModel
84
+ attribute :foo, :default => "FOO!"
85
+ attribute :bar
86
+ end
87
+
88
+ setup do
89
+ @model = DefaultModel.new
90
+ end
91
+
92
+ should "read default values" do
93
+ assert_equal "FOO!", @model.foo
94
+ assert_nil @model.bar
95
+ end
96
+ end
97
+ end
98
+
99
+ context "populating attributes" do
100
+ setup do
101
+ @model = AttributeModel.new
102
+ end
103
+
104
+ should "write all valid attributes" do
105
+ new_attributes = {
106
+ :foo => "zxcv",
107
+ :bar => "qwerty"
108
+ }
109
+
110
+ @model.populate_attributes(new_attributes)
111
+ new_attributes.each do |k,v|
112
+ assert_equal v, @model.send(k)
113
+ end
114
+ end
115
+ end
116
+
117
+ context "reading and writing attributes" do
118
+ setup do
119
+ @model = AttributeModel.new
120
+ @checkstring = "HEY"
121
+ end
122
+
123
+ should "be able to read an entire hash of attributes" do
124
+ atts = @model.attributes
125
+ assert atts.is_a?(Hash)
126
+ assert atts.has_key?(:foo)
127
+ assert atts.has_key?(:bar)
128
+ end
129
+
130
+ should "be able to write defined attributes" do
131
+ assert_nothing_raised {
132
+ @model.foo = @check_string
133
+ }
134
+ end
135
+
136
+ should "be able to read defined attributes" do
137
+ assert_nothing_raised {
138
+ assert_equal "foo", @model.foo
139
+ }
140
+ end
141
+
142
+ should "raise an error if attempting to write an undefined attribute" do
143
+ assert_raises(NoMethodError) { @model.baz = @check_string }
144
+ end
145
+
146
+ should "raise an error if attempting to read an undefined attribute" do
147
+ assert_raises(NoMethodError) { @model.baz }
148
+ end
149
+ end
150
+ end