virtualbox 0.4.3 → 0.5.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 (44) hide show
  1. data/.gitignore +3 -3
  2. data/Gemfile +8 -8
  3. data/Rakefile +2 -0
  4. data/Readme.md +11 -2
  5. data/VERSION +1 -1
  6. data/docs/WhatsNew.md +40 -24
  7. data/lib/virtualbox.rb +9 -0
  8. data/lib/virtualbox/abstract_model.rb +80 -10
  9. data/lib/virtualbox/abstract_model/attributable.rb +61 -1
  10. data/lib/virtualbox/abstract_model/relatable.rb +88 -6
  11. data/lib/virtualbox/attached_device.rb +18 -10
  12. data/lib/virtualbox/command.rb +13 -0
  13. data/lib/virtualbox/dvd.rb +35 -0
  14. data/lib/virtualbox/exceptions.rb +1 -0
  15. data/lib/virtualbox/extra_data.rb +10 -21
  16. data/lib/virtualbox/global.rb +126 -0
  17. data/lib/virtualbox/hard_drive.rb +33 -9
  18. data/lib/virtualbox/image.rb +2 -3
  19. data/lib/virtualbox/media.rb +19 -0
  20. data/lib/virtualbox/nic.rb +28 -67
  21. data/lib/virtualbox/shared_folder.rb +7 -12
  22. data/lib/virtualbox/storage_controller.rb +8 -36
  23. data/lib/virtualbox/system_property.rb +55 -0
  24. data/lib/virtualbox/usb.rb +72 -0
  25. data/lib/virtualbox/vm.rb +126 -25
  26. data/test/test_helper.rb +118 -12
  27. data/test/virtualbox/abstract_model/attributable_test.rb +55 -5
  28. data/test/virtualbox/abstract_model/relatable_test.rb +66 -4
  29. data/test/virtualbox/abstract_model_test.rb +140 -8
  30. data/test/virtualbox/attached_device_test.rb +10 -7
  31. data/test/virtualbox/command_test.rb +13 -0
  32. data/test/virtualbox/dvd_test.rb +50 -28
  33. data/test/virtualbox/extra_data_test.rb +11 -51
  34. data/test/virtualbox/global_test.rb +78 -0
  35. data/test/virtualbox/hard_drive_test.rb +34 -57
  36. data/test/virtualbox/image_test.rb +0 -5
  37. data/test/virtualbox/nic_test.rb +11 -64
  38. data/test/virtualbox/shared_folder_test.rb +5 -5
  39. data/test/virtualbox/storage_controller_test.rb +15 -30
  40. data/test/virtualbox/system_property_test.rb +71 -0
  41. data/test/virtualbox/usb_test.rb +35 -0
  42. data/test/virtualbox/vm_test.rb +62 -121
  43. data/virtualbox.gemspec +15 -2
  44. metadata +23 -4
@@ -72,6 +72,7 @@ module VirtualBox
72
72
  attribute :parent, :readonly => true
73
73
  attribute :uuid, :readonly => true
74
74
  attribute :port
75
+ attribute :type, :readonly => true
75
76
  relationship :image, Image
76
77
 
77
78
  class <<self
@@ -84,10 +85,8 @@ module VirtualBox
84
85
  relation = Proxies::Collection.new(caller)
85
86
 
86
87
  counter = 0
87
- loop do
88
- break unless data["#{caller.name}-#{counter}-0".downcase.to_sym]
89
- nic = new(counter, caller, data)
90
- relation.push(nic)
88
+ data.css("AttachedDevice").each do |ad|
89
+ relation << new(counter, caller, ad)
91
90
  counter += 1
92
91
  end
93
92
 
@@ -230,12 +229,21 @@ module VirtualBox
230
229
  #
231
230
  # **This method should never be called except internally.**
232
231
  def populate_from_data(index, caller, data)
233
- populate_attributes({
234
- :parent => caller,
235
- :port => index,
236
- :medium => data["#{caller.name}-#{index}-0".downcase.to_sym],
237
- :uuid => data["#{caller.name}-ImageUUID-#{index}-0".downcase.to_sym]
238
- })
232
+ # Get the regular attributes
233
+ attrs = {}
234
+ data.attributes.each do |key, value|
235
+ attrs[key.downcase.to_sym] = value.to_s
236
+ end
237
+
238
+ # Get the Image UUID
239
+ image = data.css("Image")
240
+ if image.empty?
241
+ attrs[:uuid] = nil
242
+ else
243
+ attrs[:uuid] = image[0]["uuid"][1..-2]
244
+ end
245
+
246
+ populate_attributes(attrs.merge({ :parent => caller }))
239
247
  end
240
248
  end
241
249
  end
@@ -14,6 +14,19 @@ module VirtualBox
14
14
  @@vboxmanage = "VBoxManage"
15
15
 
16
16
  class <<self
17
+ # Reads the XML file and returns a Nokogiri document. Reads the XML data
18
+ # from the specified file and returns a Nokogiri document.
19
+ #
20
+ # @param [String] File name.
21
+ # @return [Nokogiri::XML::Document]
22
+ def parse_xml(filename)
23
+ f = File.open(filename, "r")
24
+ result = Nokogiri::XML(f)
25
+ f.close
26
+
27
+ result
28
+ end
29
+
17
30
  # Returns true if the last run command was a success. Obviously this
18
31
  # will introduce all sorts of thread-safe problems. Those will have to
19
32
  # be addressed another time.
@@ -22,6 +22,12 @@ module VirtualBox
22
22
  class <<self
23
23
  # Returns an array of all available DVDs as DVD objects
24
24
  def all
25
+ Global.global.media.dvds
26
+ end
27
+
28
+ # Returns an array of all available DVDs by parsing the VBoxManage
29
+ # output
30
+ def all_from_command
25
31
  raw = Command.vboxmanage("list", "dvds")
26
32
  parse_raw(raw)
27
33
  end
@@ -34,6 +40,26 @@ module VirtualBox
34
40
  def empty_drive
35
41
  new(:empty_drive)
36
42
  end
43
+
44
+ def populate_relationship(caller, doc)
45
+ result = Proxies::Collection.new(caller)
46
+
47
+ # TODO: Location in this case is relative the vboxconfig path.
48
+ # We need to expand it. Also, size/accessible is not available.
49
+ doc.css("MediaRegistry DVDImages Image").each do |hd_node|
50
+ data = {}
51
+ hd_node.attributes.each do |key, value|
52
+ data[key.downcase.to_sym] = value.to_s
53
+ end
54
+
55
+ # Massage UUID to proper format
56
+ data[:uuid] = data[:uuid][1..-2]
57
+
58
+ result << new(data)
59
+ end
60
+
61
+ result
62
+ end
37
63
  end
38
64
 
39
65
  def initialize(*args)
@@ -70,10 +96,19 @@ module VirtualBox
70
96
  return false if empty_drive?
71
97
 
72
98
  Command.vboxmanage("closemedium", "dvd", uuid, "--delete")
99
+ Global.reload!
73
100
  true
74
101
  rescue Exceptions::CommandFailedException
75
102
  raise if raise_errors
76
103
  false
77
104
  end
105
+
106
+ # Lazy load the lazy attributes for this model.
107
+ def load_attribute(name)
108
+ # Since the lazy attributes are related, we just load them all at once
109
+ loaded_image = self.class.all_from_command.detect { |o| o.uuid == self.uuid }
110
+
111
+ write_attribute(:accessible, loaded_image.accessible)
112
+ end
78
113
  end
79
114
  end
@@ -5,6 +5,7 @@ module VirtualBox
5
5
  class Exception < ::Exception; end
6
6
 
7
7
  class CommandFailedException < Exception; end
8
+ class ConfigurationException < Exception; end
8
9
  class InvalidRelationshipObjectException < Exception; end
9
10
  class NonSettableRelationshipException < Exception; end
10
11
  class ValidationFailedException < Exception; end
@@ -46,37 +46,26 @@ module VirtualBox
46
46
  # @return [Array<ExtraData>]
47
47
  def global(reload=false)
48
48
  if !@@global_data || reload
49
- raw = Command.vboxmanage("getextradata", "global", "enumerate")
50
- @@global_data = parse_kv_pairs(raw)
49
+ @@global_data = Global.global.extra_data
51
50
  end
52
51
 
53
52
  @@global_data
54
53
  end
55
54
 
56
- # Parses the key-value pairs from the extra data enumerated
57
- # output.
58
- #
59
- # @param [String] raw The raw output from enumerating extra data.
60
- # @return [Hash]
61
- def parse_kv_pairs(raw, parent=nil)
62
- data = new(parent)
63
- raw.split("\n").each do |line|
64
- next unless line =~ /^Key: (.+?), Value: (.+?)$/i
65
- data[$1.to_s] = $2.strip.to_s
66
- end
67
-
68
- data.clear_dirty!
69
- data
70
- end
71
-
72
55
  # Populates a relationship with another model.
73
56
  #
74
57
  # **This method typically won't be used except internally.**
75
58
  #
76
59
  # @return [Array<ExtraData>]
77
- def populate_relationship(caller, data)
78
- raw = Command.vboxmanage("getextradata", caller.name, "enumerate")
79
- parse_kv_pairs(raw, caller)
60
+ def populate_relationship(caller, doc)
61
+ data = new(caller)
62
+
63
+ doc.css("ExtraData ExtraDataItem").each do |extradata|
64
+ data[extradata["name"].to_s] = extradata["value"].to_s
65
+ end
66
+
67
+ data.clear_dirty!
68
+ data
80
69
  end
81
70
 
82
71
  # Saves the relationship. This simply calls {#save} on every
@@ -0,0 +1,126 @@
1
+ module VirtualBox
2
+ # Represents the VirtualBox main configuration file (VirtualBox.xml)
3
+ # which VirtualBox uses to keep track of all known virtual machines
4
+ # and images. This "global" configuration has many relationships which
5
+ # allow the user to retrieve a list of all VMs, media, global extra data,
6
+ # etc. Indeed, even methods like {VM.all} are implemented using this class.
7
+ #
8
+ # # Setting the Path to VirtualBox.xml
9
+ #
10
+ # **This is extremely important.**
11
+ #
12
+ # Much of the virtualbox gem requires a proper path to the global XML configuration
13
+ # file for VirtualBox. This path is system and installation dependent. {Global}
14
+ # does its best to guess the path by trying the default paths based on the
15
+ # platform ruby is running on, but this is hardly foolproof. If you receive an
16
+ # {Exceptions::ConfigurationException} at some point while running virtualbox,
17
+ # you should use {Global.vboxconfig=} to set the path. An example is below:
18
+ #
19
+ # # Most installations won't need to do this, since the gem "guesses"
20
+ # # the path based on OS, but if you need to set vboxconfig path
21
+ # # explicitly:
22
+ # VirtualBox::Global.vboxconfig = "~/.MyCustom/VirtualBox.xml"
23
+ #
24
+ # # Getting Global Data
25
+ #
26
+ # To retrieve the global data, use `Global.global`. This value is _cached_
27
+ # between calls, so subsequent calls will not go through the entire parsing
28
+ # process. To force a reload, set the `reload` parameter to true. Besides
29
+ # setting the parameter explicitly, some actions will implicitly force the
30
+ # global data to reload on the next call, such as saving a VM or destroying
31
+ # an image, for example.
32
+ #
33
+ # # Retrieve global data for the first time. This will parse all the
34
+ # # data.
35
+ # global_object = VirtualBox::Global.global
36
+ #
37
+ # # Subsequent calls are near-instant:
38
+ # VirtualBox::Global.global
39
+ #
40
+ # # Or we can choose to reload the data...
41
+ # reloaded_object = VirtualBox::Global.global(true)
42
+ #
43
+ # # Relationships
44
+ #
45
+ # While a global object doesn't have attributes, it does have many
46
+ # relationships. The relationships are listed below. If you don't
47
+ # understand this, read {Relatable}.
48
+ #
49
+ # relationship :vms, VM, :lazy => true
50
+ # relationship :media, Media
51
+ # relationship :extra_data, ExtraData
52
+ #
53
+ class Global < AbstractModel
54
+ # The path to the global VirtualBox XML configuration file. This is
55
+ # entirely system dependent and can be set with {vboxconfig=}. The default
56
+ # is guessed based on the platform.
57
+ #
58
+ # TODO: Windows
59
+ @@vboxconfig = if RUBY_PLATFORM.downcase.include?("darwin")
60
+ "~/Library/VirtualBox/VirtualBox.xml"
61
+ elsif RUBY_PLATFORM.downcase.include?("linux")
62
+ "~/.VirtualBox/VirtualBox.xml"
63
+ else
64
+ "Unknown"
65
+ end
66
+
67
+ relationship :vms, VM, :lazy => true
68
+ relationship :media, Media
69
+ relationship :extra_data, ExtraData
70
+
71
+ @@global_data = nil
72
+
73
+ class <<self
74
+ # Retrieves the global data. The return value of this call is cached,
75
+ # and can be reloaded by setting the `reload` parameter to true. Besides
76
+ # explicitly setting the parameter, some actions within the library
77
+ # force global to reload itself on the next call, such as saving a VM,
78
+ # or destroying an image.
79
+ #
80
+ # @param [Boolean] reload True if you want to force a reload of the data.
81
+ # @return [Global]
82
+ def global(reload = false)
83
+ if !@@global_data || reload || reload?
84
+ @@global_data = new(config)
85
+ reloaded!
86
+ end
87
+
88
+ @@global_data
89
+ end
90
+
91
+ # Sets the path to the VirtualBox.xml file. This file should already
92
+ # exist. VirtualBox itself manages this file, not this library.
93
+ #
94
+ # @param [String] Full path to the VirtualBox.xml file
95
+ def vboxconfig=(value)
96
+ @@vboxconfig = value
97
+ end
98
+
99
+ # Returns the XML document of the configuration. This will raise an
100
+ # {Exceptions::ConfigurationException} if the vboxconfig file doesn't
101
+ # exist.
102
+ #
103
+ # @return [Nokogiri::XML::Document]
104
+ def config
105
+ raise Exceptions::ConfigurationException.new("The path to the global VirtualBox config must be set. See Global.vboxconfig=") unless File.exist?(File.expand_path(@@vboxconfig))
106
+ Command.parse_xml(File.expand_path(@@vboxconfig))
107
+ end
108
+
109
+ # Expands path relative to the configuration file.
110
+ #
111
+ # @return [String]
112
+ def expand_path(path)
113
+ File.expand_path(path, File.dirname(@@vboxconfig))
114
+ end
115
+ end
116
+
117
+ def initialize(document)
118
+ @document = document
119
+ populate_attributes(@document)
120
+ end
121
+
122
+ def load_relationship(name)
123
+ populate_relationship(:vms, @document)
124
+ end
125
+ end
126
+ end
@@ -78,21 +78,15 @@ module VirtualBox
78
78
  #
79
79
  class HardDrive < Image
80
80
  attribute :format, :default => "VDI"
81
- attribute :size
81
+ attribute :size, :lazy => true
82
82
 
83
83
  class <<self
84
84
  # Returns an array of all available hard drives as HardDrive
85
85
  # objects.
86
86
  #
87
- # @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
88
- # will be raised if the command failed.
89
87
  # @return [Array<HardDrive>]
90
- def all(raise_errors=false)
91
- raw = Command.vboxmanage("list", "hdds")
92
- parse_blocks(raw).collect { |v| find(v[:uuid], raise_errors) }
93
- rescue Exceptions::CommandFailedException
94
- raise if raise_errors
95
- false
88
+ def all
89
+ Global.global.media.hard_drives
96
90
  end
97
91
 
98
92
  # Finds one specific hard drive by UUID or file name. If the
@@ -117,6 +111,27 @@ module VirtualBox
117
111
  raise if raise_errors
118
112
  nil
119
113
  end
114
+
115
+ def populate_relationship(caller, doc)
116
+ result = Proxies::Collection.new(caller)
117
+
118
+ doc.css("MediaRegistry HardDisks HardDisk").each do |hd_node|
119
+ data = {}
120
+ hd_node.attributes.each do |key, value|
121
+ data[key.downcase.to_sym] = value.to_s
122
+ end
123
+
124
+ # Strip the brackets off of UUID
125
+ data[:uuid] = data[:uuid][1..-2]
126
+
127
+ # Expand location relative to config location
128
+ data[:location] = Global.expand_path(data[:location]) if data[:location]
129
+
130
+ result << new(data)
131
+ end
132
+
133
+ result
134
+ end
120
135
  end
121
136
 
122
137
  # Clone hard drive, possibly also converting formats. All formats
@@ -215,5 +230,14 @@ module VirtualBox
215
230
  raise if raise_errors
216
231
  false
217
232
  end
233
+
234
+ # Lazy load the lazy attributes for this model.
235
+ def load_attribute(name)
236
+ # Since the lazy attributes are related, we just load them all at once
237
+ loaded_hd = self.class.find(uuid, true)
238
+
239
+ write_attribute(:size, loaded_hd.size)
240
+ write_attribute(:accessible, loaded_hd.accessible)
241
+ end
218
242
  end
219
243
  end
@@ -19,7 +19,7 @@ module VirtualBox
19
19
 
20
20
  attribute :uuid, :readonly => true
21
21
  attribute :location
22
- attribute :accessible, :readonly => true
22
+ attribute :accessible, :readonly => true, :lazy => true
23
23
 
24
24
  class <<self
25
25
  # Parses the raw output of virtualbox into image objects. Used by
@@ -72,8 +72,7 @@ module VirtualBox
72
72
  #
73
73
  # @return [Array<Image>]
74
74
  def populate_relationship(caller, data)
75
- return DVD.empty_drive if data[:medium] == "emptydrive"
76
- return nil if data[:uuid].nil?
75
+ return DVD.empty_drive if data[:uuid].nil?
77
76
 
78
77
  subclasses.each do |subclass|
79
78
  next unless subclass.respond_to?(:all)
@@ -0,0 +1,19 @@
1
+ module VirtualBox
2
+ # Represents the media registry within the global VirtualBox configuration.
3
+ class Media < AbstractModel
4
+ attribute :parent, :readonly => true
5
+ relationship :hard_drives, HardDrive
6
+ relationship :dvds, DVD
7
+
8
+ class <<self
9
+ def populate_relationship(caller, data)
10
+ new(caller, data)
11
+ end
12
+ end
13
+
14
+ def initialize(parent, document)
15
+ populate_attributes({ :parent => parent }, :ignore_relationships => true)
16
+ populate_relationships(document)
17
+ end
18
+ end
19
+ end
@@ -35,72 +35,22 @@ module VirtualBox
35
35
  class Nic < AbstractModel
36
36
  attribute :parent, :readonly => :readonly
37
37
  attribute :nic
38
- attribute :nictype
39
- attribute :macaddress
40
- attribute :cableconnected
38
+ attribute :nictype, :populate_key => "type"
39
+ attribute :macaddress, :populate_key => "MACAddress"
40
+ attribute :cableconnected, :populate_key => "cable"
41
41
  attribute :bridgeadapter
42
42
 
43
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.split("\n").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
44
  # Populates the nic relationship for anything which is related to it.
84
45
  #
85
46
  # **This method typically won't be used except internally.**
86
47
  #
87
48
  # @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]
49
+ def populate_relationship(caller, doc)
50
+ relation = Proxies::Collection.new(caller)
96
51
 
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
52
+ doc.css("Hardware Network Adapter").each do |adapter|
53
+ relation << new(caller, adapter)
104
54
  end
105
55
 
106
56
  relation
@@ -121,21 +71,32 @@ module VirtualBox
121
71
  # Since there is currently no way to create a _new_ nic, this is
122
72
  # only used internally. Developers should NOT try to initialize their
123
73
  # own nic objects.
124
- def initialize(index, caller, data)
74
+ def initialize(caller, data)
125
75
  super()
126
76
 
127
- @index = index
77
+ @index = data["slot"].to_i + 1
78
+
79
+ # Set the parent
80
+ write_attribute(:parent, caller)
81
+
82
+ # Convert each attribute value to a string
83
+ attrs = {}
84
+ data.attributes.each do |key, value|
85
+ attrs[key] = value.to_s
86
+ end
87
+
88
+ populate_attributes(attrs)
128
89
 
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
90
+ # The `nic` attribute is a bit more complicated, but not by
91
+ # much
92
+ if data["enabled"] == "true"
93
+ write_attribute(:nic, data.children[1].name.downcase)
94
+ else
95
+ write_attribute(:nic, "none")
134
96
  end
135
97
 
136
- populate_attributes(populate_data.merge({
137
- :parent => caller
138
- }))
98
+ # Clear dirtiness
99
+ clear_dirty!
139
100
  end
140
101
 
141
102
  # Saves a single attribute of the nic. This method is automatically