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,164 @@
1
+ module VirtualBox
2
+ class AbstractModel
3
+ # Tracks "dirtiness" of values for a class. Its not tied to AbstractModel
4
+ # in any way other than the namespace.
5
+ #
6
+ # # Checking if a Value was Changed
7
+ #
8
+ # Dynamic methods allow functionality for checking if values changed:
9
+ #
10
+ # obj.foo_changed?
11
+ #
12
+ # # Previous Value
13
+ #
14
+ # Can also view the previous value of an attribute:
15
+ #
16
+ # obj.foo # => "foo" initially
17
+ # obj.foo = "bar"
18
+ # obj.foo_was # => "foo"
19
+ #
20
+ # # Previous and Current Value
21
+ #
22
+ # Using the `_change` dynamic method, can view the changes of a field.
23
+ #
24
+ # obj.foo # => "foo" initially
25
+ # obj.foo = "bar"
26
+ # obj.foo_change # => ["foo", "bar"]
27
+ #
28
+ # # All Changes
29
+ #
30
+ # Can also view all changes for a class with the `changes` method.
31
+ #
32
+ # obj.foo # => "foo" initially
33
+ # obj.bar # => "bar" initially
34
+ # obj.foo = "far"
35
+ # obj.bar = "baz"
36
+ # obj.changes # => { :foo => ["foo", "far"], :bar => ["bar", "baz"]}
37
+ #
38
+ # # Setting Dirty
39
+ #
40
+ # Dirtiness tracking only occurs for values which the implementor
41
+ # explicitly sets as dirty. This is done with the {#set_dirty!}
42
+ # method. Example implementation below:
43
+ #
44
+ # class Person
45
+ # include VirtualBox::AbstractModel::Dirty
46
+ #
47
+ # attr_reader :name
48
+ #
49
+ # def name=(value)
50
+ # set_dirty!(:name, @name, value)
51
+ # @name = value
52
+ # end
53
+ # end
54
+ #
55
+ # The above example has all the changes necessary to track changes
56
+ # on an attribute.
57
+ #
58
+ # # Ignoring Dirtiness Tracking
59
+ #
60
+ # Sometimes, for features such as mass assignment, dirtiness tracking
61
+ # should be disabled. This can be done with the `ignore_dirty` method.
62
+ #
63
+ # ignore_dirty do |obj|
64
+ # obj.name = "Foo"
65
+ # end
66
+ #
67
+ # obj.changed? # => false
68
+ #
69
+ # # Clearing Dirty State
70
+ #
71
+ # Sometimes, such as after saving a model, dirty states should be cleared.
72
+ # This can be done with the `clear_dirty!` method.
73
+ #
74
+ # obj.clear_dirty!(:name)
75
+ # obj.name_changed? # => false
76
+ #
77
+ module Dirty
78
+ # Manages dirty state for an attribute. This method will handle
79
+ # setting the dirty state of an attribute (or even clearing it
80
+ # if the old value is reset). Any implementors of this mixin should
81
+ # call this for any fields they want tracked.
82
+ #
83
+ # @param [Symbol] name Name of field
84
+ # @param [Object] current Current value (not necessarilly the
85
+ # original value, but the **current** value)
86
+ # @param [Object] value The new value being set
87
+ def set_dirty!(name, current, value)
88
+ if current != value
89
+ # If its the first time this attribute has changed, store the
90
+ # original value in the first field
91
+ changes[name] ||= [current, nil]
92
+
93
+ # Then store the changed value
94
+ changes[name][1] = value
95
+
96
+ # If the value changed back to the original value, remove from the
97
+ # dirty hash
98
+ if changes[name][0] == changes[name][1]
99
+ changes.delete(name)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Clears dirty state for a field.
105
+ #
106
+ # @param [Symbol] key The field to clear dirty state.
107
+ def clear_dirty!(key)
108
+ changes.delete(key)
109
+ end
110
+
111
+ # Ignores any dirty changes during the duration of the block.
112
+ # Guarantees the dirty state will be the same before and after
113
+ # the method call, but not within the block itself.
114
+ def ignore_dirty(&block)
115
+ current_changes = @changed_attributes.dup rescue nil
116
+ yield self
117
+ @changed_attributes = current_changes
118
+ end
119
+
120
+ # Returns boolean denoting if field changed or not. If no attribute
121
+ # is specified, returns true of false showing whether the model
122
+ # changed at all.
123
+ #
124
+ # @param [Symbol] attribute The attribute to check, or if nil,
125
+ # all fields checked.
126
+ def changed?(attribute = nil)
127
+ if attribute.nil?
128
+ !changes.empty?
129
+ else
130
+ changes.has_key?(attribute)
131
+ end
132
+ end
133
+
134
+ # Returns hash of changes. Keys are fields, values are an
135
+ # array of the original value and the current value.
136
+ #
137
+ # @return [Hash]
138
+ def changes
139
+ @changed_attributes ||= {}
140
+ end
141
+
142
+ # Method missing is used to implement the "magic" methods of
143
+ # `field_changed`, `field_change`, and `field_was`.
144
+ def method_missing(meth, *args)
145
+ meth_string = meth.to_s
146
+
147
+ if meth_string =~ /^(.+?)_changed\?$/
148
+ changed?($1.to_sym)
149
+ elsif meth_string =~ /^(.+?)_change$/
150
+ changes[$1.to_sym]
151
+ elsif meth_string =~ /^(.+?)_was$/
152
+ change = changes[$1.to_sym]
153
+ if change.nil?
154
+ nil
155
+ else
156
+ change[0]
157
+ end
158
+ else
159
+ super
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,163 @@
1
+ module VirtualBox
2
+ class AbstractModel
3
+ # Provides simple relationship features to any class. These relationships
4
+ # can be anything, since this module makes no assumptions and doesn't
5
+ # differentiate between "has many" or "belongs to" or any of that.
6
+ #
7
+ # The way it works is simple:
8
+ #
9
+ # 1. Relationships are defined with a relationship name and a
10
+ # class of the relationship objects.
11
+ # 2. When {#populate_relationships} is called, `populate_relationship` is
12
+ # called on each relationship class (example: {StorageController.populate_relationship}).
13
+ # This is expected to return the relationship, which can be any object.
14
+ # 3. When {#save_relationships} is called, `save_relationship` is
15
+ # called on each relationship class, which manages saving its own
16
+ # relationship.
17
+ # 4. When {#destroy_relationships} is called, `destroy_relationship` is
18
+ # called on each relationship class, which manages destroying
19
+ # its own relationship.
20
+ #
21
+ # Be sure to read {ClassMethods} for complete documentation of methods.
22
+ #
23
+ # # Defining Relationships
24
+ #
25
+ # Every relationship has two mandatory parameters: the name and the class.
26
+ #
27
+ # relationship :bacons, Bacon
28
+ #
29
+ # In this case, there is a relationship `bacons` which refers to the `Bacon`
30
+ # class.
31
+ #
32
+ # # Accessing Relationships
33
+ #
34
+ # Relatable offers up dynamically generated accessors for every relationship
35
+ # which simply returns the relationship data.
36
+ #
37
+ # relationship :bacons, Bacon
38
+ #
39
+ # # Accessing through an instance "instance"
40
+ # instance.bacons # => whatever Bacon.populate_relationship created
41
+ #
42
+ # # Dependent Relationships
43
+ #
44
+ # By setting `:dependent => :destroy` on relationships, {AbstractModel}
45
+ # will automatically call {#destroy_relationships} when {AbstractModel#destroy}
46
+ # is called.
47
+ #
48
+ # This is not a feature built-in to Relatable but figured it should be
49
+ # mentioned here.
50
+ module Relatable
51
+ def self.included(base)
52
+ base.extend ClassMethods
53
+ end
54
+
55
+ module ClassMethods
56
+ # Define a relationship. The name and class must be specified. This
57
+ # class will be used to call the `populate_relationship,
58
+ # `save_relationship`, etc. methods.
59
+ #
60
+ # @param [Symbol] name Relationship name. This will also be used for
61
+ # the dynamically generated accessor.
62
+ # @param [Class] klass Class of the relationship.
63
+ # @option options [Symbol] :dependent (nil) - If set to `:destroy`
64
+ # {AbstractModel#destroy} will propagate through to relationships.
65
+ def relationship(name, klass, options = {})
66
+ @relationships ||= {}
67
+ @relationships[name] = { :klass => klass }.merge(options)
68
+ end
69
+
70
+ # Returns a hash of all the relationships.
71
+ #
72
+ # @return [Hash]
73
+ def relationships
74
+ @relationships ||= {}
75
+ end
76
+
77
+ # Used to propagate relationships to subclasses. This method makes sure that
78
+ # subclasses of a class with {Relatable} included will inherit the
79
+ # relationships as well, which would be the expected behaviour.
80
+ def inherited(subclass)
81
+ super rescue NoMethodError
82
+
83
+ relationships.each do |name, options|
84
+ subclass.relationship(name, nil, options)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Saves the model, calls save_relationship on all relations. It is up to
90
+ # the relation to determine whether anything changed, etc. Simply
91
+ # calls `save_relationship` on each relationshp class passing in the
92
+ # following parameters:
93
+ #
94
+ # * **caller** - The class which is calling save
95
+ # * **data** - The data associated with the relationship
96
+ #
97
+ # In addition to those two args, any arbitrary args may be tacked on to the
98
+ # end and they'll be pushed through to the `save_relationship` method.
99
+ def save_relationships(*args)
100
+ self.class.relationships.each do |name, options|
101
+ next unless options[:klass].respond_to?(:save_relationship)
102
+ options[:klass].save_relationship(self, relationship_data[name], *args)
103
+ end
104
+ end
105
+
106
+ # The equivalent to {Attributable#populate_attributes}, but with
107
+ # relationships.
108
+ def populate_relationships(data)
109
+ self.class.relationships.each do |name, options|
110
+ next unless options[:klass].respond_to?(:populate_relationship)
111
+ relationship_data[name] = options[:klass].populate_relationship(self, data)
112
+ end
113
+ end
114
+
115
+ # Calls `destroy_relationship` on each of the relationships. Any
116
+ # arbitrary args may be added and they will be forarded to the
117
+ # relationship's `destroy_relationship` method.
118
+ def destroy_relationships(*args)
119
+ self.class.relationships.each do |name, options|
120
+ destroy_relationship(name, *args)
121
+ end
122
+ end
123
+
124
+ # Destroys only a single relationship. Any arbitrary args
125
+ # may be added to the end and they will be pushed through to
126
+ # the class's `destroy_relationship` method.
127
+ #
128
+ # @param [Symbol] name The name of the relationship
129
+ def destroy_relationship(name, *args)
130
+ options = self.class.relationships[name]
131
+ return unless options && options[:klass].respond_to?(:destroy_relationship)
132
+ options[:klass].destroy_relationship(self, relationship_data[name], *args)
133
+ end
134
+
135
+ # Hash to data associated with relationships. You should instead
136
+ # use the accessors created by Relatable.
137
+ #
138
+ # @return [Hash]
139
+ def relationship_data
140
+ @relationship_data ||= {}
141
+ end
142
+
143
+ # Returns boolean denoting if a relationship exists.
144
+ #
145
+ # @return [Boolean]
146
+ def has_relationship?(key)
147
+ self.class.relationships.has_key?(key.to_sym)
148
+ end
149
+
150
+ # Method missing is used to add dynamic handlers for relationship
151
+ # accessors.
152
+ def method_missing(meth, *args)
153
+ meth_string = meth.to_s
154
+
155
+ if has_relationship?(meth)
156
+ relationship_data[meth.to_sym]
157
+ else
158
+ super
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,104 @@
1
+ module VirtualBox
2
+ # Represents an device which is attached to a storage controller. An example
3
+ # of such a device would be a CD or hard drive attached to an IDE controller.
4
+ #
5
+ # **Currently, attached devices can not be created from scratch. The only way
6
+ # to access them is through relationships with other models such as
7
+ # {StorageController}.**
8
+ #
9
+ # # Attributes and Relationships
10
+ #
11
+ # Properties of the model 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 :uuid
30
+ # attribute :medium
31
+ # attribute :port
32
+ #
33
+ # ## Relationships
34
+ #
35
+ # In addition to the basic attributes, a virtual machine is related
36
+ # to other things. The relationships are listed below. If you don't
37
+ # understand this, read {Relatable}.
38
+ #
39
+ # relationship :image, Image
40
+ #
41
+ class AttachedDevice < AbstractModel
42
+ attribute :parent, :readonly => true
43
+ attribute :uuid
44
+ attribute :medium
45
+ attribute :port
46
+ relationship :image, Image
47
+
48
+ class <<self
49
+ # Populate relationship with another model.
50
+ #
51
+ # **This method typically won't be used except internally.**
52
+ #
53
+ # @return [Array<AttachedDevice>]
54
+ def populate_relationship(caller, data)
55
+ relation = []
56
+
57
+ counter = 0
58
+ loop do
59
+ break unless data["#{caller.name}-#{counter}-0".downcase.to_sym]
60
+ nic = new(counter, caller, data)
61
+ relation.push(nic)
62
+ counter += 1
63
+ end
64
+
65
+ relation
66
+ end
67
+
68
+ # Destroy attached devices associated with another model.
69
+ #
70
+ # **This method typically won't be used except internally.**
71
+ def destroy_relationship(caller, data, *args)
72
+ data.each { |v| v.destroy(*args) }
73
+ end
74
+ end
75
+
76
+ # Since attached devices can not be created from scratch yet, this
77
+ # method should never be called. Instead access attached devices
78
+ # through relationships from other models such as {StorageController}.
79
+ def initialize(index, caller, data)
80
+ super()
81
+
82
+ populate_attributes({
83
+ :parent => caller,
84
+ :port => index,
85
+ :medium => data["#{caller.name}-#{index}-0".downcase.to_sym],
86
+ :uuid => data["#{caller.name}-ImageUUID-#{index}-0".downcase.to_sym]
87
+ })
88
+ end
89
+
90
+ # Destroys the attached device. By default, this only removes any
91
+ # media inserted within the device, but does not destroy it. This
92
+ # option can be specified, however, through the `destroy_image`
93
+ # option.
94
+ #
95
+ # @option options [Boolean] :destroy_image (false) If true, will also
96
+ # destroy the image associated with device.
97
+ def destroy(options={})
98
+ # parent = storagecontroller
99
+ # parent.parent = vm
100
+ Command.vboxmanage("storageattach #{Command.shell_escape(parent.parent.name)} --storagectl #{Command.shell_escape(parent.name)} --port #{port} --device 0 --medium none")
101
+ image.destroy if options[:destroy_image] && image
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,54 @@
1
+ module VirtualBox
2
+ # Used by the rest of the virtualbox library to call shell commands.
3
+ # It also can be used to change the path for your VBoxManage program.
4
+ #
5
+ # # Changing VBoxManage Path
6
+ #
7
+ # The rest of the library won't work without a proper path to VBoxManage,
8
+ # so it is crucial to set this properly right away. By default its set
9
+ # to `VBoxManage` which assumes that it is in your `PATH`.
10
+ #
11
+ # VirtualBox::Command.vboxmanage = "/opt/local/bin/VBoxManage"
12
+ #
13
+ class Command
14
+ @@vboxmanage = "VBoxManage"
15
+
16
+ class <<self
17
+ # Sets the path to VBoxManage, which is required for this gem to
18
+ # work.
19
+ def vboxmanage=(path)
20
+ @@vboxmanage = path
21
+ end
22
+
23
+ # Runs a VBoxManage command and returns the output.
24
+ def vboxmanage(command)
25
+ execute("#{@@vboxmanage} #{command}")
26
+ end
27
+
28
+ # Runs a command and returns a boolean result showing
29
+ # if the command ran successfully or not based on the
30
+ # exit code.
31
+ def test(command)
32
+ execute(command)
33
+ $?.to_i == 0
34
+ end
35
+
36
+ # Runs a command and returns the STDOUT result. The reason this is
37
+ # a method at the moment is because in the future we may want to
38
+ # change the way commands are run (replace the backticks), plus it
39
+ # makes testing easier.
40
+ def execute(command)
41
+ `#{command}`
42
+ end
43
+
44
+ # Shell escapes a string. This is almost a direct copy/paste from
45
+ # the ruby mailing list. I'm not sure how well it works but so far
46
+ # it hasn't failed!
47
+ def shell_escape(str)
48
+ str.to_s.gsub(/(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/n, '\\').
49
+ gsub(/\n/, "'\n'").
50
+ sub(/^$/, "''")
51
+ end
52
+ end
53
+ end
54
+ end