virtualbox 0.1.0

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