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,31 @@
1
+ module VirtualBox
2
+ # Represents a DVD image stored by VirtualBox. These DVD images can be
3
+ # mounted onto virtual machines.
4
+ #
5
+ # # Finding all DVDs
6
+ #
7
+ # The only method at the moment of finding DVDs is to use {DVD.all}, which
8
+ # returns an array of {DVD}s.
9
+ #
10
+ # DVD.all
11
+ #
12
+ class DVD < Image
13
+ class <<self
14
+ # Returns an array of all available DVDs as DVD objects
15
+ def all
16
+ raw = Command.vboxmanage("list dvds")
17
+ parse_raw(raw)
18
+ end
19
+ end
20
+
21
+ # Deletes the DVD from VBox managed list and also from disk.
22
+ # This method will fail if the disk is currently mounted to any
23
+ # virtual machine.
24
+ #
25
+ # @return [Boolean]
26
+ def destroy
27
+ Command.vboxmanage("closemedium dvd #{uuid} --delete")
28
+ return $?.to_i == 0
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ module VirtualBox
2
+ # Gem specific exceptions will reside under this namespace for easy
3
+ # documentation and searching.
4
+ module Errors
5
+ class Exception < ::Exception; end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ # From: http://snippets.dzone.com/posts/show/2992
2
+ module VirtualBox::SubclassListing
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def subclasses(direct = false)
9
+ classes = []
10
+ if direct
11
+ ObjectSpace.each_object(Class) do |c|
12
+ next unless c.superclass == self
13
+ classes << c
14
+ end
15
+ else
16
+ ObjectSpace.each_object(Class) do |c|
17
+ next unless c.ancestors.include?(self) and (c != self)
18
+ classes << c
19
+ end
20
+ end
21
+ classes
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,169 @@
1
+ module VirtualBox
2
+ # Represents a hard disk which is registered with VirtualBox.
3
+ #
4
+ # # Finding a Hard Drive
5
+ #
6
+ # Hard drives can be found use {HardDrive.all} and {HardDrive.find}, which
7
+ # find all or a specific hard drive, respectively. Example below:
8
+ #
9
+ # VirtualBox::HardDrive.all
10
+ #
11
+ # Or:
12
+ #
13
+ # VirtualBox::HardDrive.find("Foo.vdi")
14
+ #
15
+ # # Creating a Hard Drive
16
+ #
17
+ # The hard drive is one of the few data items at the moment which supports
18
+ # new creation. Below is a simple example of how this works:
19
+ #
20
+ # hd = VirtualBox::HardDrive.new
21
+ # hd.location = "foo.vdi"
22
+ # hd.size = 2400 # in megabytes
23
+ # hd.save
24
+ #
25
+ # # You can now access other attributes, since its saved:
26
+ # hd.uuid
27
+ # hd.location # will return a full path now
28
+ #
29
+ # Although `VDI` is the default format for newly created hard drives, other
30
+ # formats are supported:
31
+ #
32
+ # hd = VirtualBox::HardDrive.new
33
+ # hd.format = "VMDK"
34
+ # hd.location = "bar.vmdk"
35
+ # hd.size = 9001 # Its over 9000! (If you don't understand the reference, just ignore this comment)
36
+ # hd.save
37
+ #
38
+ # Any formats listed by your VirtualBox installation's `VBoxManage list hddbackends` command
39
+ # can be used with the virtualbox gem.
40
+ #
41
+ # # Destroying Hard Drives
42
+ #
43
+ # Hard drives can also be deleted, which will completely remove them from
44
+ # disk. **This operation is not reversable**.
45
+ #
46
+ # hd = VirtualBox::HardDrive.find("foo")
47
+ # hd.destroy
48
+ #
49
+ # # Cloning Hard Drives
50
+ #
51
+ # Hard drives can just as easily be cloned as they can be created or destroyed.
52
+ #
53
+ # hd = VirtualBox::HardDrive.find("foo")
54
+ # cloned_hd = hd.clone("bar")
55
+ #
56
+ # In addition to simply cloning hard drives, this command can be used to
57
+ # clone to a different format:
58
+ #
59
+ # hd = VirtualBox::HardDrive.find("foo")
60
+ # hd.clone("bar", "VMDK") # Will clone and convert to VMDK format
61
+ #
62
+ # # Attributes
63
+ #
64
+ # Properties of the model are exposed using standard ruby instance
65
+ # methods which are generated on the fly. Because of this, they are not listed
66
+ # below as available instance methods.
67
+ #
68
+ # These attributes can be accessed and modified via standard ruby-style
69
+ # `instance.attribute` and `instance.attribute=` methods. The attributes are
70
+ # listed below. If you aren't sure what this means or you can't understand
71
+ # why the below is listed, please read {Attributable}.
72
+ #
73
+ # attribute :uuid, :readonly => true
74
+ # attribute :location
75
+ # attribute :accessible, :readonly => true
76
+ # attribute :format, :default => "VDI"
77
+ # attribute :size
78
+ #
79
+ class HardDrive < Image
80
+ attribute :format, :default => "VDI"
81
+ attribute :size
82
+
83
+ class <<self
84
+ # Returns an array of all available hard drives as HardDrive
85
+ # objects.
86
+ #
87
+ # @return [Array<HardDrive>]
88
+ def all
89
+ raw = Command.vboxmanage("list hdds")
90
+ parse_blocks(raw).collect { |v| find(v[:uuid]) }
91
+ end
92
+
93
+ # Finds one specific hard drive by UUID or file name. If the
94
+ # hard drive can not be found, will return `nil`.
95
+ #
96
+ # @return [HardDrive]
97
+ def find(id)
98
+ raw = Command.vboxmanage("showhdinfo #{id}")
99
+
100
+ # Return nil if the hard drive doesn't exist
101
+ return nil if raw =~ /VERR_FILE_NOT_FOUND/
102
+
103
+ data = raw.split(/\n\n/).collect { |v| parse_block(v) }.find { |v| !v.nil? }
104
+
105
+ # Set equivalent fields
106
+ data[:format] = data[:"storage format"]
107
+ data[:size] = data[:"logical size"].split(/\s+/)[0] if data.has_key?(:"logical size")
108
+
109
+ # Return new object
110
+ new(data)
111
+ end
112
+ end
113
+
114
+ # Clone hard drive, possibly also converting formats. All formats
115
+ # supported by your local VirtualBox installation are supported
116
+ # here. If no format is specified, the format of the source drive
117
+ # will be used.
118
+ #
119
+ # @param [String] outputfile The output file. This should be just a
120
+ # single filename, since VirtualBox will place it in the hard
121
+ # drives folder.
122
+ # @param [String] format The format to convert to.
123
+ # @return [HardDrive] The new, cloned hard drive.
124
+ def clone(outputfile, format="VDI")
125
+ raw = Command.vboxmanage("clonehd #{uuid} #{Command.shell_escape(outputfile)} --format #{format} --remember")
126
+ return nil unless raw =~ /UUID: (.+?)$/
127
+
128
+ self.class.find($1.to_s)
129
+ end
130
+
131
+ # Creates a new hard drive.
132
+ #
133
+ # **This method should NEVER be called. Call {#save} instead.**
134
+ def create
135
+ raw = Command.vboxmanage("createhd --filename #{location} --size #{size} --format #{read_attribute(:format)} --remember")
136
+ return nil unless raw =~ /UUID: (.+?)$/
137
+
138
+ # Just replace our attributes with the newly created ones. This also
139
+ # will set new_record to false.
140
+ populate_attributes(self.class.find($1.to_s).attributes)
141
+ end
142
+
143
+ # Saves the hard drive object. If the hard drive is new,
144
+ # this will create a new hard drive. Otherwise, it will
145
+ # save any other details about the existing hard drive.
146
+ #
147
+ # Currently, **saving existing hard drives does nothing**.
148
+ # This is a limitation of VirtualBox, rather than the library itself.
149
+ def save
150
+ if new_record?
151
+ # Create a new hard drive
152
+ create
153
+ else
154
+ super
155
+ end
156
+ end
157
+
158
+ # Destroys the hard drive. This deletes the hard drive off
159
+ # of disk.
160
+ #
161
+ # **This operation is not reversable.**
162
+ #
163
+ # @return [Boolean]
164
+ def destroy
165
+ Command.vboxmanage("closemedium disk #{uuid} --delete")
166
+ return $?.to_i == 0
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,94 @@
1
+ require 'virtualbox/ext/subclass_listing'
2
+
3
+ module VirtualBox
4
+ # An abstract class which encapsulates the shared behaviour of
5
+ # images such as {HardDrive} and {DVD}.
6
+ #
7
+ # ## Attributes
8
+ #
9
+ # All images expose the following attributes. If you don't know how to read
10
+ # this than read {Attributable}.
11
+ #
12
+ # attribute :uuid, :readonly => true
13
+ # attribute :location
14
+ # attribute :accessible, :readonly => true
15
+ #
16
+ # @abstract
17
+ class Image < AbstractModel
18
+ include SubclassListing
19
+
20
+ attribute :uuid, :readonly => true
21
+ attribute :location
22
+ attribute :accessible, :readonly => true
23
+
24
+ class <<self
25
+ # Parses the raw output of virtualbox into image objects. Used by
26
+ # subclasses to parse the output of their respective listing functions.
27
+ #
28
+ # **This method typically won't be used except internally.**
29
+ #
30
+ # @return [Array<Image>]
31
+ def parse_raw(raw)
32
+ parse_blocks(raw).collect { |v| new(v) }
33
+ end
34
+
35
+ # Parses the blocks of the output from virtualbox. VirtualBox outputs
36
+ # image listing in "blocks" which are then parsed down to their attributes.
37
+ #
38
+ # **This method typically won't be used except internally.**
39
+ #
40
+ # @return [Array<Hash>]
41
+ def parse_blocks(raw)
42
+ raw.split(/\n\n/).collect { |v| parse_block(v.chomp) }.compact
43
+ end
44
+
45
+ # Parses a single block from VirtualBox output.
46
+ #
47
+ # **This method typically won't be used except internally.**
48
+ #
49
+ # @return [Hash]
50
+ def parse_block(block)
51
+ return nil unless block =~ /^UUID:/i
52
+ hd = {}
53
+
54
+ # Parses each line which should be in the format:
55
+ # KEY: VALUE
56
+ block.lines.each do |line|
57
+ next unless line =~ /^(.+?):\s+(.+?)$/
58
+ hd[$1.downcase.to_sym] = $2.to_s
59
+ end
60
+
61
+ # If we don't have a location but have a path, use that, as they
62
+ # are equivalent but not consistent.
63
+ hd[:location] = hd[:path] if hd.has_key?(:path)
64
+
65
+ hd
66
+ end
67
+
68
+ # Searches the subclasses which implement all method, searching for
69
+ # a matching UUID and returning that as the relationship.
70
+ #
71
+ # **This method typically won't be used except internally.**
72
+ #
73
+ # @return [Array<Image>]
74
+ def populate_relationship(caller, data)
75
+ return nil if data[:uuid].nil?
76
+
77
+ subclasses.each do |subclass|
78
+ next unless subclass.respond_to?(:all)
79
+
80
+ matching = subclass.all.find { |obj| obj.uuid == data[:uuid] }
81
+ return matching unless matching.nil?
82
+ end
83
+ end
84
+ end
85
+
86
+ # **This should never be called directly on {Image}.** Instead, initialize
87
+ # one of the subclasses.
88
+ def initialize(info=nil)
89
+ super()
90
+
91
+ populate_attributes(info) if info
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,150 @@
1
+ module VirtualBox
2
+ # Represents a single NIC (Network Interface Card) of a virtual machine.
3
+ #
4
+ # **Currently, new NICs can't be created, so the only way to get this
5
+ # object is through a {VM}'s `nics` relationship.**
6
+ #
7
+ # # Editing a NIC
8
+ #
9
+ # Nics can be modified directly in their relationship to other
10
+ # virtual machines. When {VM#save} is called, it will also save any
11
+ # changes to its relationships.
12
+ #
13
+ # vm = VirtualBox::VM.find("foo")
14
+ # vm.nics[0].macaddress = @new_mac_address
15
+ # vm.save
16
+ #
17
+ # # Attributes
18
+ #
19
+ # Properties of the model are exposed using standard ruby instance
20
+ # methods which are generated on the fly. Because of this, they are not listed
21
+ # below as available instance methods.
22
+ #
23
+ # These attributes can be accessed and modified via standard ruby-style
24
+ # `instance.attribute` and `instance.attribute=` methods. The attributes are
25
+ # listed below. If you aren't sure what this means or you can't understand
26
+ # why the below is listed, please read {Attributable}.
27
+ #
28
+ # attribute :parent, :readonly => :readonly
29
+ # attribute :nic
30
+ # attribute :nictype
31
+ # attribute :macaddress
32
+ # attribute :cableconnected
33
+ # attribute :bridgeadapter
34
+ #
35
+ class Nic < AbstractModel
36
+ attribute :parent, :readonly => :readonly
37
+ attribute :nic
38
+ attribute :nictype
39
+ attribute :macaddress
40
+ attribute :cableconnected
41
+ attribute :bridgeadapter
42
+
43
+ class <<self
44
+ # Retrives the Nic data from human-readable vminfo. Since some data about
45
+ # nics is not exposed in the machine-readable virtual machine info, some
46
+ # extra parsing must be done to get these attributes. This method parses
47
+ # the nic-specific data from this human readable information.
48
+ #
49
+ # **This method typically won't be used except internally.**
50
+ #
51
+ # @return [Hash]
52
+ def nic_data(vmname)
53
+ raw = VM.human_info(vmname)
54
+
55
+ # Complicated chain of methods just maps parse_nic over each line,
56
+ # removing invalid ones, and then converting it into a single hash.
57
+ raw.lines.collect { |v| parse_nic(v) }.compact.inject({}) do |acc, obj|
58
+ acc.merge({ obj[0] => obj[1] })
59
+ end
60
+ end
61
+
62
+ # Parses nic data out of a single line of the human readable output
63
+ # of vm info.
64
+ #
65
+ # **This method typically won't be used except internally.**
66
+ #
67
+ # @return [Array] First element is nic name, second is data.
68
+ def parse_nic(raw)
69
+ return unless raw =~ /^NIC\s(\d):\s+(.+?)$/
70
+ return if $2.to_s.strip == "disabled"
71
+
72
+ data = {}
73
+ nicname = "nic#{$1}"
74
+ $2.to_s.split(/,\s+/).each do |raw_property|
75
+ next unless raw_property =~ /^(.+?):\s+(.+?)$/
76
+
77
+ data[$1.downcase.to_sym] = $2.to_s
78
+ end
79
+
80
+ return nicname.to_sym, data
81
+ end
82
+
83
+ # Populates the nic relationship for anything which is related to it.
84
+ #
85
+ # **This method typically won't be used except internally.**
86
+ #
87
+ # @return [Array<Nic>]
88
+ def populate_relationship(caller, data)
89
+ nic_data = nic_data(caller.name)
90
+
91
+ relation = []
92
+
93
+ counter = 1
94
+ loop do
95
+ break unless data["nic#{counter}".to_sym]
96
+
97
+ nictype = nic_data["nic#{counter}".to_sym][:type] rescue nil
98
+
99
+ nic = new(counter, caller, data.merge({
100
+ "nictype#{counter}".to_sym => nictype
101
+ }))
102
+ relation.push(nic)
103
+ counter += 1
104
+ end
105
+
106
+ relation
107
+ end
108
+
109
+ # Saves the relationship. This simply calls {#save} on every
110
+ # member of the relationship.
111
+ #
112
+ # **This method typically won't be used except internally.**
113
+ def save_relationship(caller, data)
114
+ # Just call save on each nic with the VM
115
+ data.each do |nic|
116
+ nic.save(caller.name)
117
+ end
118
+ end
119
+ end
120
+
121
+ # Since there is currently no way to create a _new_ nic, this is
122
+ # only used internally. Developers should NOT try to initialize their
123
+ # own nic objects.
124
+ def initialize(index, caller, data)
125
+ super()
126
+
127
+ @index = index
128
+
129
+ # Setup the index specific attributes
130
+ populate_data = {}
131
+ self.class.attributes.each do |name, options|
132
+ value = data["#{name}#{index}".to_sym]
133
+ populate_data[name] = value
134
+ end
135
+
136
+ populate_attributes(populate_data.merge({
137
+ :parent => caller
138
+ }))
139
+ end
140
+
141
+ # Saves a single attribute of the nic. This method is automatically
142
+ # called on {#save}.
143
+ #
144
+ # **This method typically won't be used except internally.**
145
+ def save_attribute(key, value, vmname)
146
+ Command.vboxmanage("modifyvm #{vmname} --#{key}#{@index} #{Command.shell_escape(value)}")
147
+ super
148
+ end
149
+ end
150
+ end