ovirt 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.
data/lib/ovirt/vm.rb ADDED
@@ -0,0 +1,327 @@
1
+ module Ovirt
2
+ class Vm < Template
3
+
4
+ self.top_level_strings = [:name, :origin, :type, :description]
5
+ self.top_level_booleans = [:stateless]
6
+ self.top_level_integers = [:memory]
7
+ self.top_level_timestamps = [:creation_time, :start_time]
8
+ self.top_level_objects = [:cluster, :template, :host]
9
+
10
+ attr_accessor :creation_status_link
11
+
12
+ def initialize(*args)
13
+ super
14
+
15
+ @creation_status_link = @relationships.delete(:creation_status)
16
+ end
17
+
18
+ def creation_status
19
+ return nil if @creation_status_link.blank?
20
+ @service.status(@creation_status_link)
21
+ end
22
+
23
+ def start
24
+ if block_given?
25
+ operation(:start) { |xml| yield xml }
26
+ else
27
+ operation(:start)
28
+ end
29
+ rescue Ovirt::Error => err
30
+ raise VmAlreadyRunning, err.message if err.message.include?("VM is running.")
31
+ raise
32
+ end
33
+
34
+ def stop
35
+ operation(:stop)
36
+ rescue Ovirt::Error => err
37
+ raise VmIsNotRunning, err.message if err.message.include?("VM is not running")
38
+ raise
39
+ end
40
+
41
+ def shutdown
42
+ operation(:shutdown)
43
+ rescue Ovirt::Error => err
44
+ raise VmIsNotRunning, err.message if err.message.include?("VM is not running")
45
+ raise
46
+ end
47
+
48
+ def destroy
49
+ # TODO:
50
+ # 1. If VM was running, wait for it to stop
51
+ begin
52
+ stop
53
+ rescue VmIsNotRunning
54
+ end
55
+
56
+ super
57
+ end
58
+
59
+ def move(storage_domain)
60
+ response = operation(:move) do |xml|
61
+ xml.storage_domain(:id => self.class.object_to_id(storage_domain))
62
+ end
63
+
64
+ # Sample Response
65
+ # <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
66
+ # <action id="13be9571-61a0-40ef-be6c-50696f61cab1" href="/api/vms/5819ddca-47c0-47d8-8b75-c5b8d1f2b354/move/13be9571-61a0-40ef-be6c-50696f61cab1">
67
+ # <link rel="parent" href="/api/vms/5819ddca-47c0-47d8-8b75-c5b8d1f2b354"/>
68
+ # <link rel="replay" href="/api/vms/5819ddca-47c0-47d8-8b75-c5b8d1f2b354/move"/>
69
+ # <async>true</async>
70
+ # <storage_domain id="08d61895-b465-406f-955c-72fd9ddbbe05"/>
71
+ # <status>
72
+ # <state>pending</state>
73
+ # </status>
74
+ # </action>
75
+ doc = Nokogiri::XML(response)
76
+ action = doc.xpath("//action").first
77
+ raise Ovirt::Error, "No Action in Response: #{response.inspect}" if action.nil?
78
+ action['href']
79
+ end
80
+
81
+ # cpu_hash needs to look like { :cores => 1, :sockets => 1 }
82
+ def cpu_topology=(cpu_hash)
83
+ update! do |xml|
84
+ xml.cpu do
85
+ xml.topology(cpu_hash)
86
+ end
87
+ xml.memory self[:memory] # HACK: RHEVM BUG: RHEVM resets it to 10GB, unless we set it
88
+ end
89
+ end
90
+
91
+ def description=(value)
92
+ update! do |xml|
93
+ xml.description value.to_s
94
+ xml.memory self[:memory] # HACK: RHEVM BUG: RHEVM resets it to 10GB, unless we set it
95
+ end
96
+ end
97
+
98
+ def host_affinity=(host, affinity = :migratable)
99
+ update! do |xml|
100
+ xml.memory self[:memory] # HACK: RHEVM BUG: RHEVM resets it to 10GB, unless we set it
101
+ xml.placement_policy do
102
+ if host.nil?
103
+ xml.host
104
+ else
105
+ xml.host(:id => self.class.object_to_id(host))
106
+ end
107
+ xml.affinity affinity.to_s
108
+ end
109
+ end
110
+ end
111
+
112
+ def memory=(value)
113
+ update! do |xml|
114
+ xml.memory value
115
+ end
116
+ end
117
+
118
+ def memory_reserve=(value)
119
+ update! do |xml|
120
+ xml.memory_policy do
121
+ xml.guaranteed(value)
122
+ end
123
+ end
124
+ end
125
+
126
+ # Attaches a payload.
127
+ #
128
+ # payloads:: Hash of payload_type => {file_name => content} that will be
129
+ # attached. Acceptable payload_types are floppy or cdrom.
130
+ def attach_payload(payloads)
131
+ send("attach_payload_#{payload_version}".to_sym, payloads)
132
+ end
133
+
134
+ def payload_version
135
+ version = service.version
136
+
137
+ if version[:major].to_i >= 3
138
+ return "3_0" if version[:minor].to_i < 3
139
+ return "3_3"
140
+ end
141
+ end
142
+
143
+ def attach_payload_3_0(payloads)
144
+ update! do |xml|
145
+ xml.payloads do
146
+ payloads.each do |type, files|
147
+ xml.payload(:type => type) do
148
+ files.each do |file_name, content|
149
+ xml.file(:name => file_name) do
150
+ xml.content content
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ def attach_payload_3_3(payloads)
160
+ update! do |xml|
161
+ xml.payloads do
162
+ payloads.each do |type, files|
163
+ xml.payload(:type => type) do
164
+ xml.files do
165
+ files.each do |file_name, content|
166
+ xml.file do
167
+ xml.name file_name
168
+ xml.content content
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ # Detaches a payload
179
+ #
180
+ # types:: A payload type or Array of payload types to detach.
181
+ # Acceptable types are floppy or cdrom.
182
+ def detach_payload(types)
183
+ # HACK: The removal of payloads is not supported until possibly RHEVM 3.1.1
184
+ # https://bugzilla.redhat.com/show_bug.cgi?id=882649
185
+ # For now, just set the payload to blank content.
186
+ payload = Array(types).each_with_object({}) { |t, h| h[t] = {} }
187
+ attach_payload(payload)
188
+ end
189
+
190
+ # Attaches the +files+ as a floppy drive payload.
191
+ #
192
+ # files:: Hash of file_name => content that will be attached as a floppy
193
+ def attach_floppy(files)
194
+ attach_payload("floppy" => files || {})
195
+ end
196
+
197
+ # Detaches the floppy drive payload.
198
+ def detach_floppy
199
+ detach_payload("floppy")
200
+ end
201
+
202
+ def boot_from_network
203
+ start do |xml|
204
+ xml.vm do
205
+ xml.os do
206
+ xml.boot(:dev => 'network')
207
+ end
208
+ end
209
+ end
210
+ rescue Ovirt::Error => err
211
+ raise unless err.message =~ /disks .+ are locked/
212
+ raise VmNotReadyToBoot.new [err.message, err]
213
+ end
214
+
215
+ def boot_from_cdrom(iso_file_name)
216
+ start do |xml|
217
+ xml.vm do
218
+ xml.os do
219
+ xml.boot(:dev => 'cdrom')
220
+ end
221
+ xml.cdroms do
222
+ xml.cdrom do
223
+ xml.file(:id => iso_file_name)
224
+ end
225
+ end
226
+ end
227
+ end
228
+ rescue Ovirt::Error => err
229
+ raise unless err.message =~ /disks .+ are locked/
230
+ raise VmNotReadyToBoot.new [err.message, err]
231
+ end
232
+
233
+ def self.parse_xml(xml)
234
+ hash = super
235
+ node = xml_to_nokogiri(xml)
236
+
237
+ parse_first_node(node, :placement_policy, hash,
238
+ :node => [:affinity])
239
+
240
+ parse_first_node_with_hash(node, 'placement_policy/host', hash[:placement_policy][:host] = {},
241
+ :attribute => [:id])
242
+
243
+ parse_first_node(node, :memory_policy, hash,
244
+ :node_to_i => [:guaranteed])
245
+
246
+ hash[:guest_info] = {}
247
+ node.xpath('guest_info').each do |gi|
248
+ ips = hash[:guest_info][:ips] = []
249
+ gi.xpath('ips/ip').each do |ip|
250
+ ips << {:address => ip[:address]}
251
+ end
252
+ end
253
+
254
+ hash
255
+ end
256
+
257
+ def create_device(device_type)
258
+ builder = Nokogiri::XML::Builder.new do |xml|
259
+ xml.send(device_type) { yield xml if block_given? }
260
+ end
261
+ data = builder.doc.root.to_xml
262
+ path = "#{api_endpoint}/#{device_type.pluralize}"
263
+
264
+ @service.resource_post(path, data)
265
+ end
266
+
267
+ def create_nic(options = {})
268
+ create_device("nic") do |xml|
269
+ xml.name options[:name]
270
+ xml.interface options[:interface] unless options[:interface].blank?
271
+ xml.network(:id => options[:network_id]) unless options[:network_id].blank?
272
+ xml.mac(:address => options[:mac_address]) unless options[:mac_address].blank?
273
+ end
274
+ end
275
+
276
+ def create_disk(options = {})
277
+ create_device("disk") do |xml|
278
+ [:name, :interface, :format, :size, :type].each do |key|
279
+ next if options[key].blank?
280
+ xml.send("#{key}_", options[key])
281
+ end
282
+
283
+ [:sparse, :bootable, :wipe_after_delete, :propagate_errors].each do |key|
284
+ xml.send("#{key}_", options[key]) unless options[key].nil?
285
+ end
286
+
287
+ xml.storage_domains { xml.storage_domain(:id => options[:storage]) } if options[:storage]
288
+ end
289
+ end
290
+
291
+ def create_snapshot(desc)
292
+ builder = Nokogiri::XML::Builder.new do |xml|
293
+ xml.snapshot do
294
+ xml.description desc
295
+ end
296
+ end
297
+ data = builder.doc.root.to_xml
298
+ path = "#{api_endpoint}/snapshots"
299
+
300
+ response = @service.resource_post(path, data)
301
+
302
+ snap = Snapshot.create_from_xml(@service, response)
303
+
304
+ while snap[:snapshot_status] == "locked"
305
+ sleep 2
306
+ snap.reload
307
+ end
308
+ snap
309
+ end
310
+
311
+ def create_template(options={})
312
+ builder = Nokogiri::XML::Builder.new do |xml|
313
+ xml.template do
314
+ xml.name options[:name]
315
+ xml.vm(:id => self[:id])
316
+ end
317
+ end
318
+ data = builder.doc.root.to_xml
319
+
320
+ response = @service.resource_post(:templates, data)
321
+ Template.create_from_xml(@service, response)
322
+ rescue Ovirt::Error => err
323
+ raise TemplateAlreadyExists, err.message if err.message.include?("Template name already exists")
324
+ raise
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,14 @@
1
+ module Ovirt
2
+ class Vmpool < Object
3
+
4
+ self.top_level_strings = [:name, :description]
5
+ self.top_level_integers = [:size]
6
+ self.top_level_objects = [:cluster, :template]
7
+
8
+ def self.parse_xml(xml)
9
+ node, hash = xml_to_hash(xml)
10
+
11
+ hash
12
+ end
13
+ end
14
+ end
data/lib/ovirt.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'active_support/all'
2
+ require 'more_core_extensions/all'
3
+
4
+ require 'ovirt/exception'
5
+ require 'ovirt/object'
6
+ require 'ovirt/version'
7
+
8
+ require 'ovirt/api'
9
+ require 'ovirt/cdrom'
10
+ require 'ovirt/cluster'
11
+ require 'ovirt/data_center'
12
+ require 'ovirt/disk'
13
+ require 'ovirt/domain'
14
+ require 'ovirt/event'
15
+ require 'ovirt/event_monitor'
16
+ require 'ovirt/file'
17
+ require 'ovirt/group'
18
+ require 'ovirt/host'
19
+ require 'ovirt/host_nic'
20
+ require 'ovirt/inventory'
21
+ require 'ovirt/network'
22
+ require 'ovirt/nic'
23
+ require 'ovirt/permission'
24
+ require 'ovirt/permit'
25
+ require 'ovirt/role'
26
+ require 'ovirt/service'
27
+ require 'ovirt/snapshot'
28
+ require 'ovirt/statistic'
29
+ require 'ovirt/storage'
30
+ require 'ovirt/storage_domain'
31
+ require 'ovirt/tag'
32
+ require 'ovirt/template'
33
+ require 'ovirt/user'
34
+ require 'ovirt/vm'
35
+ require 'ovirt/vmpool'
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ovirt::Event do
4
+ context ".set_event_name" do
5
+ before :each do
6
+ @orig_log, $rhevm_log = $rhevm_log, double("logger")
7
+ end
8
+
9
+ after :each do
10
+ $rhevm_log = @orig_log
11
+ end
12
+
13
+ it "sets the name corresponding to a valid code" do
14
+ hash = {:code => 1}
15
+ described_class.send(:set_event_name, hash)
16
+ hash[:name].should eq Ovirt::Event::EVENT_CODES[1]
17
+ end
18
+
19
+ it "sets 'UNKNOWN' as the name with an invalid code" do
20
+ $rhevm_log.should_receive :warn
21
+ hash = {:code => -1, :description => "Invalid Code"}
22
+ described_class.send(:set_event_name, hash)
23
+ hash[:name].should eq "UNKNOWN"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ovirt::Object do
4
+ it ".api_endpoint" do
5
+ Ovirt::Object.api_endpoint.should == "objects"
6
+ Ovirt::Template.api_endpoint.should == "templates"
7
+ Ovirt::Cluster.api_endpoint.should == "clusters"
8
+ Ovirt::Vm.api_endpoint.should == "vms"
9
+ Ovirt::StorageDomain.api_endpoint.should == "storagedomains"
10
+ Ovirt::DataCenter.api_endpoint.should == "datacenters"
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'rest-client'
3
+
4
+ describe Ovirt::Service do
5
+ before do
6
+ @service = described_class.new(:server => "", :username => "", :password => "")
7
+ end
8
+
9
+ context "#resource_post" do
10
+ it "raises Ovirt::Error if HTTP 409 response code received" do
11
+ error_detail = "API error"
12
+ return_data = <<-EOX.chomp
13
+ <action>
14
+ <fault>
15
+ <detail>#{error_detail}</detail>
16
+ </fault>
17
+ </action>
18
+ EOX
19
+
20
+ rest_client = double('rest_client').as_null_object
21
+ rest_client.should_receive(:post) do |&block|
22
+ return_data.stub(:code).and_return(409)
23
+ block.call(return_data)
24
+ end
25
+
26
+ @service.stub(:create_resource).and_return(rest_client)
27
+ expect { @service.resource_post('uri', 'data') }.to raise_error(Ovirt::Error, error_detail)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+
11
+ # Run specs in random order to surface order dependencies. If you find an
12
+ # order dependency and want to debug it, you can fix the order by providing
13
+ # the seed, which is printed after each run.
14
+ # --seed 1234
15
+ config.order = 'random'
16
+ end
17
+
18
+ begin
19
+ require 'coveralls'
20
+ Coveralls.wear!
21
+ rescue LoadError
22
+ end
23
+
24
+ require 'ovirt'
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ovirt::Template do
4
+ before do
5
+ @service = double('service')
6
+ @template = Ovirt::Template.new(@service, {
7
+ :id => "128f9ffd-b82c-41e4-8c00-9742ed173bac",
8
+ :href => "/api/vms/128f9ffd-b82c-41e4-8c00-9742ed173bac",
9
+ :cluster => {
10
+ :id => "5be5d08a-a60b-11e2-bee6-005056a217db",
11
+ :href => "/api/clusters/5be5d08a-a60b-11e2-bee6-005056a217db"},
12
+ :template => {
13
+ :id => "00000000-0000-0000-0000-000000000000",
14
+ :href => "/api/templates/00000000-0000-0000-0000-000000000000"},
15
+ :name => "bd-skeletal-clone-from-template",
16
+ :origin => "rhev",
17
+ :type => "server",
18
+ :memory => 536870912,
19
+ :stateless => false,
20
+ :creation_time => "2013-09-04 16:24:20 -0400",
21
+ :status => {:state => "down"},
22
+ :display => {:type => "spice", :monitors => 1},
23
+ :usb => {:enabled => false},
24
+ :cpu => {:topology => {:sockets => 1, :cores => 1}},
25
+ :high_availability => {:priority => 1, :enabled => false},
26
+ :os => {:type => "rhel5_64", :boot_order => [{:dev => "hd"}]},
27
+ :custom_attributes => [],
28
+ :placement_policy => {:affinity => "migratable", :host => {}},
29
+ :memory_policy => {:guaranteed => 536870912},
30
+ :guest_info => {}
31
+ })
32
+ end
33
+
34
+ context "#create_vm" do
35
+ it "clones properties for skeletal clones" do
36
+ options = {:clone_type => :skeletal}
37
+ expected_data = {
38
+ :clone_type => :linked,
39
+ :memory => 536870912,
40
+ :stateless => false,
41
+ :type => "server",
42
+ :display => {:type => "spice", :monitors => 1},
43
+ :usb => {:enabled => false},
44
+ :cpu => {:topology => {:sockets => 1, :cores => 1}},
45
+ :high_availability => {:priority => 1, :enabled => false},
46
+ :os_type => "rhel5_64"}
47
+ @template.stub(:nics).and_return([])
48
+ @template.stub(:disks).and_return([])
49
+ @service.stub(:blank_template).and_return(double('blank template'))
50
+ @service.blank_template.should_receive(:create_vm).once.with(expected_data)
51
+ @template.create_vm(options)
52
+ end
53
+
54
+ it "overrides properties for linked clones" do
55
+ expected_data = <<-EOX.chomp
56
+ <vm>
57
+ <name>new name</name>
58
+ <cluster id=\"fb27f9a0-cb75-4e0f-8c07-8dec0c5ab483\"/>
59
+ <template id=\"128f9ffd-b82c-41e4-8c00-9742ed173bac\"/>
60
+ <memory>536870912</memory>
61
+ <stateless>false</stateless>
62
+ <type>server</type>
63
+ <display>
64
+ <type>spice</type>
65
+ <monitors>1</monitors>
66
+ </display>
67
+ <usb>
68
+ <enabled>false</enabled>
69
+ </usb>
70
+ <cpu>
71
+ <topology sockets="1" cores="1"/>
72
+ </cpu>
73
+ <high_availability>
74
+ <priority>1</priority>
75
+ <enabled>false</enabled>
76
+ </high_availability>
77
+ <os type=\"test\">
78
+ <boot dev=\"hd\"/>
79
+ </os>
80
+ </vm>
81
+ EOX
82
+ response_xml = <<-EOX.chomp
83
+ <vm>
84
+ <os type='foo'/>
85
+ <placement_policy><affinity>foo</affinity></placement_policy>
86
+ </vm>
87
+ EOX
88
+ options = {
89
+ :clone_type => :linked,
90
+ :name => 'new name',
91
+ :cluster => 'fb27f9a0-cb75-4e0f-8c07-8dec0c5ab483',
92
+ :os_type => 'test'}
93
+ @service.should_receive(:resource_post).once.with(:vms, expected_data).and_return(response_xml)
94
+ @template.create_vm(options)
95
+ end
96
+
97
+ context "#create_new_disks_from_template" do
98
+ before do
99
+ @disk = Ovirt::Disk.new(@service, {
100
+ :id=>"01eae62b-90df-424d-978c-beaa7eb2f7f6",
101
+ :href=>"/api/templates/54f1b9f4-0e89-4c72-9a26-f94dcb857264/disks/01eae62b-90df-424d-978c-beaa7eb2f7f6",
102
+ :name=>"clone_Disk1",
103
+ :storage_domains=>[{:id=>"aa7e70e5-40d0-43e2-a605-92ce6ba652a8"}]
104
+ })
105
+ @template.stub(:disks).and_return([@disk])
106
+
107
+ @vm = double('rhevm_vm')
108
+ end
109
+
110
+ it "without a storage override" do
111
+ expected_data = @disk.attributes.dup
112
+ expected_data[:storage] = expected_data[:storage_domains].first[:id]
113
+
114
+ @vm.should_receive(:create_disk).once.with(expected_data)
115
+ @template.send(:create_new_disks_from_template, @vm, {})
116
+ end
117
+
118
+ it "with a storage override" do
119
+ expected_data = @disk.attributes.dup
120
+ options = {:storage => "xxxxxxxx-40d0-43e2-a605-92ce6ba652a8"}
121
+ expected_data.merge!(options)
122
+
123
+ @vm.should_receive(:create_disk).once.with(expected_data)
124
+ @template.send(:create_new_disks_from_template, @vm, options)
125
+ end
126
+ end
127
+
128
+ context "build_clone_xml" do
129
+ it "Properly sets vm/cpu/topology attributes" do
130
+ Object.stub(:object_to_id)
131
+ xml = @template.send(:build_clone_xml, :name => "Blank", :cluster => "6b8f1c1e-3eb0-11e4-8420-56847afe9799")
132
+ nodeset = Nokogiri::XML.parse(xml).xpath("//vm/cpu/topology")
133
+ node = nodeset.first
134
+
135
+ expect(nodeset.length).to eq(1)
136
+ expect(node["cores"].to_i).to eq(1)
137
+ expect(node["sockets"].to_i).to eq(1)
138
+ end
139
+ end
140
+ end
141
+ end