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.
@@ -0,0 +1,332 @@
1
+ require 'nokogiri'
2
+
3
+ module Ovirt
4
+ class Service
5
+ DEFAULT_OPTIONS = {}
6
+ REQUIRED_OPTIONS = [:server, :username, :password]
7
+ DEFAULT_PORT_3_0 = 8443
8
+ DEFAULT_PORT_3_1 = 443
9
+ DEFAULT_PORT = DEFAULT_PORT_3_1
10
+ DEFAULT_SCHEME = 'https'.freeze
11
+ SESSION_ID_KEY = 'JSESSIONID'.freeze
12
+
13
+ attr_accessor :session_id
14
+
15
+ def self.name_to_class(name)
16
+ Ovirt.const_get(name.camelize)
17
+ end
18
+
19
+ def xml_to_object(klass, xml)
20
+ klass.create_from_xml(self, xml)
21
+ end
22
+
23
+ def initialize(options={})
24
+ @options = DEFAULT_OPTIONS.merge(options)
25
+ parse_domain_name
26
+ REQUIRED_OPTIONS.each { |key| raise "No #{key.to_s} specified" unless @options.has_key?(key) }
27
+ @password = @options.delete(:password)
28
+ @session_id = @options[:session_id]
29
+ end
30
+
31
+ def inspect # just like the default inspect, but WITHOUT @password
32
+ "#<#{self.class.name}:0x#{(self.object_id << 1).to_s(16).rjust(14,'0')} @options=#{@options.inspect}>"
33
+ end
34
+
35
+ def api(reload = false)
36
+ @api = nil if reload
37
+ @api ||= xml_to_object(Api, resource_get)
38
+ end
39
+
40
+ def product_info
41
+ @product_info ||= api[:product_info]
42
+ end
43
+
44
+ def name
45
+ @name ||= product_info[:name]
46
+ end
47
+
48
+ def vendor
49
+ @vendor ||= product_info[:vendor]
50
+ end
51
+
52
+ def version
53
+ @version ||= product_info[:version]
54
+ end
55
+
56
+ def version_string
57
+ @version_string ||= "#{version[:major]}.#{version[:minor]}.#{version[:revision]}.#{version[:build]}"
58
+ end
59
+
60
+ def version_3_0?
61
+ version_string.starts_with?("3.0")
62
+ end
63
+
64
+ def summary
65
+ api(true)[:summary] # This is volatile information
66
+ end
67
+
68
+ def special_objects
69
+ @special_objects ||= api[:special_objects]
70
+ end
71
+
72
+ def blank_template
73
+ @blank_template ||= begin
74
+ href = special_objects[:"templates/blank"]
75
+ href.blank? ? nil : Template.find_by_href(self, href)
76
+ end
77
+ end
78
+
79
+ def root_tag
80
+ @root_tag ||= begin
81
+ href = special_objects[:"tags/root"]
82
+ href.blank? ? nil : Tag.find_by_href(self, href)
83
+ end
84
+ end
85
+
86
+ def iso_storage_domain
87
+ @iso_storage_domain ||= StorageDomain.iso_storage_domain(self)
88
+ end
89
+
90
+ def iso_images
91
+ iso_storage_domain.nil? ? [] : iso_storage_domain.iso_images
92
+ end
93
+
94
+ def disconnect
95
+ end
96
+
97
+ def get_resource_by_ems_ref(uri_suffix, element_name = nil)
98
+ xml = resource_get(uri_suffix)
99
+ doc = Nokogiri::XML(xml)
100
+ element_name ||= doc.root.name
101
+ klass = self.class.name_to_class(element_name)
102
+ xml_to_object(klass, doc.root)
103
+ end
104
+
105
+ def standard_collection(uri_suffix, element_name = nil, paginate=false, sort_by=:name)
106
+ if paginate
107
+ doc = paginate_resource_get(uri_suffix, sort_by)
108
+ else
109
+ xml = resource_get(uri_suffix)
110
+ doc = Nokogiri::XML(xml)
111
+ end
112
+ element_name ||= uri_suffix.singularize
113
+ klass = self.class.name_to_class(element_name)
114
+
115
+ xml_path = uri_suffix == 'api' ? element_name : "#{element_name.pluralize}/#{element_name}"
116
+ objects = doc.xpath("//#{xml_path}")
117
+ objects.collect { |obj| xml_to_object(klass, obj) }
118
+ end
119
+
120
+ def status(link)
121
+ response = resource_get(link)
122
+
123
+ node = Object.xml_to_nokogiri(response)
124
+ node.xpath('status/state').text
125
+ end
126
+
127
+ def api_uri(path = nil)
128
+ uri = "#{base_uri}/api"
129
+ unless path.nil?
130
+ parts = path.to_s.split('/')
131
+ parts.shift if parts.first == '' # Remove leading slash
132
+ parts.shift if parts.first == 'api' # We already have /api in our URI
133
+ uri += "/#{parts.join('/')}" unless parts.empty?
134
+ end
135
+ uri
136
+ end
137
+
138
+ def paginate_resource_get(path = nil, sort_by=:name, direction=:asc)
139
+ log_header = "#{self.class.name}#paginate_resource_get"
140
+ page = 1
141
+ full_xml = nil
142
+ loop do
143
+ uri = "#{path}?search=sortby%20#{sort_by}%20#{direction}%20page%20#{page}"
144
+ partial_xml_str = self.resource_get(uri)
145
+ if full_xml.nil?
146
+ full_xml = Nokogiri::XML(partial_xml_str)
147
+ else
148
+ partial_xml = Nokogiri::XML(partial_xml_str)
149
+ break if partial_xml.root.children.count == 0
150
+ $rhevm_log.debug "#{log_header}: Combining resource elements for <#{path}> from page:<#{page}>" if $rhevm_log && $rhevm_log.debug?
151
+ full_xml.root << partial_xml.root.children
152
+ end
153
+ page += 1
154
+ end
155
+ $rhevm_log.debug "#{log_header}: Combined elements for <#{path}>. Total elements:<#{full_xml.root.children.count}>" if $rhevm_log && $rhevm_log.debug?
156
+ return full_xml
157
+ end
158
+
159
+ def resource_get(path = nil)
160
+ resource_verb(path, :get)
161
+ end
162
+
163
+ def resource_put(path, payload, additional_headers={:content_type => :xml, :accept => :xml})
164
+ resource_verb(path, :put, payload, additional_headers)
165
+ end
166
+
167
+ def resource_post(path, payload, additional_headers={:content_type => :xml, :accept => :xml})
168
+ resource_verb(path, :post, payload, additional_headers)
169
+ end
170
+
171
+ def resource_delete(path)
172
+ resource_verb(path, :delete)
173
+ end
174
+
175
+ def create_resource(path = nil)
176
+ require "rest-client"
177
+ RestClient::Resource.new(api_uri(path), resource_options)
178
+ end
179
+
180
+ private
181
+
182
+ def resource_verb(path, verb, *args)
183
+ log_header = "#{self.class.name}#resource_#{verb}"
184
+
185
+ resource = create_resource(path)
186
+ $rhevm_log.info "#{log_header}: Sending URL: <#{resource.url}>" if $rhevm_log
187
+ $rhevm_log.debug "#{log_header}: With args: <#{args.inspect}>" if $rhevm_log.try(:debug?)
188
+ resource.send(verb, *args) do |response, request, result, &block|
189
+ case response.code
190
+ when 200
191
+ parse_normal_response(response, resource)
192
+ when 400, 409
193
+ parse_error_response(response)
194
+ else
195
+ response.return!(request, result, &block)
196
+ end
197
+ end
198
+ rescue RestClient::Unauthorized
199
+ if self.session_id
200
+ self.session_id = nil
201
+ retry
202
+ else
203
+ raise
204
+ end
205
+ rescue RestClient::ResourceNotFound, Ovirt::Error
206
+ raise
207
+ rescue Exception => e
208
+ msg = "#{log_header}: class = #{e.class.name}, message=#{e.message}, URI=#{resource.url}"
209
+ if $rhevm_log.nil?
210
+ puts msg
211
+ else
212
+ $rhevm_log.error msg
213
+ end
214
+ raise
215
+ end
216
+
217
+ def parse_normal_response(response, resource)
218
+ parse_set_cookie_header(response.headers[:set_cookie])
219
+ if $rhevm_log
220
+ log_header = "#{self.class.name}#parse_normal_response"
221
+ $rhevm_log.info "#{log_header}: Return from URL: <#{resource.url}> Data length:#{response.length}"
222
+ $rhevm_log.debug "#{log_header}: Return from URL: <#{resource.url}> Data:#{response}" if $rhevm_log.debug?
223
+ end
224
+ response
225
+ end
226
+
227
+ def parse_error_response(response)
228
+ doc = Nokogiri::XML(response)
229
+ action = doc.xpath("action").first
230
+ node = action || doc
231
+ reason = node.xpath("fault/detail").text
232
+ raise Ovirt::Error, reason
233
+ end
234
+
235
+ def parse_set_cookie_header(set_cookie_header)
236
+ set_cookie_header = set_cookie_header.first if set_cookie_header.kind_of?(Array)
237
+ set_cookie_header.to_s.split(";").each do |kv|
238
+ k, v = kv.strip.split("=")
239
+ self.session_id = v if k == SESSION_ID_KEY
240
+ end
241
+ end
242
+
243
+ def base_uri
244
+ if port.blank?
245
+ "#{scheme}://#{server}"
246
+ else
247
+ "#{scheme}://#{server}:#{port}"
248
+ end
249
+ end
250
+
251
+ def resource_options
252
+ headers = merge_headers({ 'Prefer' => 'persistent-auth' })
253
+ options = { :ssl_version => :SSLv3 }
254
+
255
+ if self.session_id
256
+ headers[:cookie] = "#{SESSION_ID_KEY}=#{self.session_id}"
257
+ else
258
+ options[:user] = fully_qualified_username
259
+ options[:password] = password
260
+ end
261
+
262
+ options[:headers] = headers
263
+ options[:timeout] = timeout if timeout
264
+ options[:open_timeout] = open_timeout if open_timeout
265
+ options
266
+ end
267
+
268
+ def merge_headers(hash)
269
+ h = @options[:headers] || {}
270
+ h.merge(hash)
271
+ end
272
+
273
+ def authorization_header
274
+ @authorization_header ||= { :authorization => "Basic #{authorization_value}" }
275
+ end
276
+
277
+ def authorization_value
278
+ @authorization_value ||= begin
279
+ require "base64"
280
+ Base64.encode64 "#{fully_qualified_username}:#{password}"
281
+ end
282
+ end
283
+
284
+ def scheme
285
+ @options[:scheme] || DEFAULT_SCHEME
286
+ end
287
+
288
+ def server
289
+ @options[:server]
290
+ end
291
+
292
+ def port
293
+ @options[:port] || DEFAULT_PORT
294
+ end
295
+
296
+ def fully_qualified_username
297
+ domain.blank? ? username : "#{username}@#{domain}"
298
+ end
299
+
300
+ def username
301
+ @options[:username]
302
+ end
303
+
304
+ def password
305
+ @password
306
+ end
307
+
308
+ def domain
309
+ @options[:domain]
310
+ end
311
+
312
+ def timeout
313
+ @options[:timeout] # NetHTTPSession's read_timeout
314
+ end
315
+
316
+ def open_timeout
317
+ @options[:open_timeout] # NetHTTPSessions's open_timeout
318
+ end
319
+
320
+ # Parse domain out of the username string
321
+ def parse_domain_name
322
+ if @options[:domain].blank? && !@options[:username].blank?
323
+ if @options[:username].include?('\\')
324
+ @options[:domain], @options[:username] = username.split('\\')
325
+ elsif @options[:username].include?('/')
326
+ @options[:domain], @options[:username] = username.split('/')
327
+ end
328
+ end
329
+ end
330
+
331
+ end
332
+ end
@@ -0,0 +1,28 @@
1
+ module Ovirt
2
+ class Snapshot < Object
3
+
4
+ self.top_level_strings = [:description, :snapshot_status, :type]
5
+ self.top_level_timestamps = [:date]
6
+ self.top_level_objects = [:vm]
7
+
8
+ def self.parse_xml(xml)
9
+ node, hash = xml_to_hash(xml)
10
+
11
+ hash
12
+ end
13
+
14
+ def initialize(service, options = {})
15
+ super
16
+ @relationships[:disks] = self[:href] + "/disks"
17
+ end
18
+
19
+ def delete
20
+ response = destroy
21
+ while self[:snapshot_status] == "locked" || self[:snapshot_status] == "ok"
22
+ sleep 2
23
+ break if (obj = self.class.find_by_href(@service, self[:href])).nil?
24
+ self.replace(obj)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ module Ovirt
2
+ class Statistic < Object
3
+
4
+ self.top_level_strings = [:name, :description, :type, :unit]
5
+
6
+ def self.parse_xml(xml)
7
+ node, hash = xml_to_hash(xml)
8
+
9
+ values = []
10
+ values_node = node.xpath('values').first
11
+ values_type = values_node['type']
12
+ values = values_node.xpath('value').collect do |v|
13
+ datum = v.xpath('datum').text
14
+ case values_type
15
+ when 'INTEGER'
16
+ datum = datum.to_i
17
+ when 'DECIMAL'
18
+ datum = datum.to_f
19
+ else
20
+ raise "unknown Values TYPE of <#{values_type}>"
21
+ end
22
+ datum
23
+ end
24
+ hash[:values] = values
25
+
26
+ [:vm, :nic, :disk].each do |type|
27
+ parent_node = node.xpath(type.to_s).first
28
+ next if parent_node.nil?
29
+ parent = hash_from_id_and_href(parent_node)
30
+ parent[:type] = type
31
+ hash[:parent] = parent
32
+ end
33
+
34
+ hash
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module Ovirt
2
+ class Storage < Object
3
+
4
+ self.top_level_objects = [:host]
5
+
6
+ def self.parse_xml(xml)
7
+ node, hash = xml_to_hash(xml)
8
+
9
+ parse_first_node(node, :volume_group, hash, :attribute => [:id])
10
+
11
+ hash
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ module Ovirt
2
+ class StorageDomain < Object
3
+
4
+ self.top_level_strings = [:name, :type, :storage_format]
5
+ self.top_level_booleans = [:master]
6
+ self.top_level_integers = [:available, :used, :committed]
7
+ self.top_level_objects = [:data_center]
8
+
9
+ def self.element_name
10
+ "storage_domain"
11
+ end
12
+
13
+ def self.parse_xml(xml)
14
+ node, hash = xml_to_hash(xml)
15
+
16
+ parse_first_node(node, :status, hash, :node => [:state])
17
+ parse_first_node(node, :storage, hash, :node => [:type, :address, :path])
18
+ parse_first_node(node, :storage, hash, :attribute => [:id])
19
+
20
+ node.xpath('storage/volume_group').each do |vg|
21
+ node.xpath('storage').each do |storage_node|
22
+ parse_first_node(storage_node, :volume_group, hash[:storage], :attribute => [:id])
23
+ end
24
+
25
+ vg_hash = hash[:storage][:volume_group]
26
+ unless vg_hash.blank?
27
+ parse_first_node(vg, :logical_unit, vg_hash, :attribute => [:id])
28
+
29
+ unless vg_hash.blank?
30
+ parse_first_node(vg, :logical_unit, vg_hash,
31
+ :node => [:address, :port, :target, :username, :serial, :vendor_id, :product_id, :lun_mapping, :portal, :size, :paths])
32
+ end
33
+ end
34
+ end
35
+
36
+ hash
37
+ end
38
+
39
+ def self.iso_storage_domain(service)
40
+ all(service).detect { |s| s[:type] == "iso" }
41
+ end
42
+
43
+ def iso_images
44
+ return [] if self[:type] != "iso"
45
+ @service.standard_collection(relationships[:files], 'file')
46
+ end
47
+ end
48
+ end
data/lib/ovirt/tag.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Ovirt
2
+ class Tag < Object
3
+
4
+ self.top_level_strings = [:name, :description]
5
+ self.top_level_objects = [:host, :user, :vm]
6
+
7
+ def self.parse_xml(xml)
8
+ node, hash = xml_to_hash(xml)
9
+
10
+ parent_node = node.xpath('parent').first
11
+ unless parent_node.nil?
12
+ tag_node = parent_node.xpath('tag').first
13
+ unless tag_node.nil?
14
+ parent = hash_from_id_and_href(tag_node)
15
+ parent[:type] = 'tag'
16
+ hash[:parent] = parent
17
+ end
18
+ end
19
+
20
+ hash
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,185 @@
1
+ module Ovirt
2
+ class Template < Object
3
+
4
+ self.top_level_strings = [:name, :description, :type]
5
+ self.top_level_booleans = [:stateless]
6
+ self.top_level_integers = [:memory]
7
+ self.top_level_timestamps = [:creation_time]
8
+ self.top_level_objects = [:cluster]
9
+
10
+ def self.parse_xml(xml)
11
+ node, hash = xml_to_hash(xml)
12
+
13
+ parse_first_node(node, :status, hash, :node => [:state])
14
+
15
+ parse_first_node(node, :display, hash,
16
+ :node => [:type, :address],
17
+ :node_to_i => [:port, :monitors])
18
+
19
+ parse_first_node(node, :usb, hash,
20
+ :node_to_bool => [:enabled])
21
+
22
+ parse_first_node_with_hash(node, 'cpu/topology', hash.store_path(:cpu, :topology, {}),
23
+ :attribute_to_i => [:sockets, :cores])
24
+
25
+ parse_first_node(node, :high_availability, hash,
26
+ :node_to_bool => [:enabled],
27
+ :node_to_i => [:priority])
28
+
29
+ parse_first_node(node, :os, hash,
30
+ :attribute => [:type],
31
+ :node => [:kernel, :initrd, :cmdline])
32
+
33
+ hash[:os][:boot_order] = boot_order = []
34
+ #Collect boot order
35
+ node.xpath('os/boot').each do |boot|
36
+ dev = boot['dev']
37
+ boot_order << {:dev => dev} unless dev.blank?
38
+ end
39
+
40
+ hash[:custom_attributes] = []
41
+ node.xpath('custom_properties/custom_property').each do |ca|
42
+ hash[:custom_attributes] << {:name => ca[:name], :value => ca[:value]}
43
+ end
44
+
45
+
46
+ hash
47
+ end
48
+
49
+ def os_type
50
+ self.attributes.fetch_path(:os, :type) || 'unassigned'
51
+ end
52
+
53
+ def getCfg(snap=nil)
54
+ #mor = snap ? getSnapMor(snap) : @vmMor
55
+ cfgProps = self.attributes
56
+
57
+ raise MiqException::MiqVimError, "Failed to retrieve configuration information for VM" if cfgProps.nil?
58
+
59
+ cfgHash = {}
60
+ cfgHash['displayname'] = cfgProps[:name]
61
+ cfgHash['guestos'] = cfgProps.fetch_path(:os, :type)
62
+ cfgHash['memsize'] = cfgProps[:memory] / 1048576 # in MB
63
+ cfgHash['numvcpu'] = cfgProps.fetch_path(:cpu, :sockets)
64
+
65
+ # Collect disk information
66
+ self.attributes[:disks] = self.send(:disks, :disk) if self[:disks].nil?
67
+ self.disks.each_with_index do |disk, idx|
68
+ storage_domain = disk[:storage_domains].first
69
+ storage_id = storage_domain && storage_domain[:id]
70
+ disk_key = disk[:image_id].blank? ? :id : :image_id
71
+ file_path = storage_id && File.join('/dev', storage_id, disk[disk_key])
72
+
73
+ tag = "scsi0:#{idx}"
74
+ cfgHash["#{tag}.present"] = "true"
75
+ cfgHash["#{tag}.devicetype"] = "disk"
76
+ cfgHash["#{tag}.filename"] = file_path.to_s
77
+ cfgHash["#{tag}.format"] = disk[:format]
78
+ #cfgHash["#{tag}.mode"] = dev['backing']['diskMode']
79
+ end
80
+ return cfgHash
81
+ end
82
+
83
+ REQUIRED_CLONE_PARAMETERS = [:name, :cluster]
84
+ CLONE_ATTRIBUTES_WITH_SCALARS = [:memory, :stateless, :type]
85
+ CLONE_ATTRIBUTES_WITH_HASHES = [:display, :usb, :cpu, :high_availability]
86
+ ALLOWED_CLONE_TYPES = [:full, :linked, :skeletal]
87
+
88
+ def create_vm(options = {})
89
+ options = options.dup
90
+ determine_clone_type(options)
91
+ options[:storage] = Object.object_to_id(options[:storage]) if options[:storage]
92
+
93
+ case options[:clone_type]
94
+ when :full; clone_to_vm(options)
95
+ when :linked; clone_to_vm(options)
96
+ when :skeletal; clone_to_vm_via_blank_template(options)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def determine_clone_type(options)
103
+ # Return the clone_type from the options if it matches one of the types in the allowed array
104
+ # otherwise return the first type from the allowed array as a default
105
+ options[:clone_type] = ALLOWED_CLONE_TYPES.include?(options[:clone_type]) ? options[:clone_type] : ALLOWED_CLONE_TYPES.first
106
+ end
107
+
108
+ def clone_to_vm_via_blank_template(options)
109
+ # Create a VM based the VM on the blank template using parameters from this template
110
+ # Disks are created from scratch, not copied
111
+ (CLONE_ATTRIBUTES_WITH_SCALARS + CLONE_ATTRIBUTES_WITH_HASHES).each do |key|
112
+ options[key] ||= self[key]
113
+ end
114
+ options[:os_type] ||= self.os_type
115
+
116
+ skeleton_options = options.dup
117
+ skeleton_options[:clone_type] = :linked
118
+ vm = @service.blank_template.create_vm(skeleton_options)
119
+
120
+ create_new_disks_from_template(vm, options)
121
+ vm
122
+ end
123
+
124
+ def create_new_disks_from_template(vm, options)
125
+ self.disks.each do |disk_object|
126
+ disk_options = disk_object.attributes_for_new_disk
127
+ disk_options[:sparse] = options[:sparse] unless options[:sparse].nil?
128
+ disk_options[:storage] = options[:storage] unless options[:storage].blank?
129
+ vm.create_disk(disk_options)
130
+ end
131
+ end
132
+
133
+ def clone_to_vm(options)
134
+ # Create a VM based on this template
135
+ REQUIRED_CLONE_PARAMETERS.each do |key|
136
+ raise ArgumentError, "#{key.inspect} cannot be blank" if options[key].blank?
137
+ end
138
+
139
+ response = @service.resource_post(:vms, build_clone_xml(options))
140
+ Vm.create_from_xml(@service, response)
141
+ rescue Ovirt::Error => err
142
+ raise VmAlreadyExists, err.message if err.message.include?("VM with the same name already exists")
143
+ raise
144
+ end
145
+
146
+ def build_clone_xml(options)
147
+ builder = Nokogiri::XML::Builder.new do |xml|
148
+ xml.vm do
149
+ xml.name options[:name]
150
+ xml.cluster(:id => Object.object_to_id(options[:cluster]))
151
+ xml.template(:id => self[:id])
152
+
153
+ CLONE_ATTRIBUTES_WITH_SCALARS.each do |key|
154
+ xml.send("#{key}_", options[key] || self[key])
155
+ end
156
+
157
+ CLONE_ATTRIBUTES_WITH_HASHES.each do |key|
158
+ xml.send("#{key}_") do
159
+ hash = options[key] || self[key]
160
+ hash.each { |k, v| xml.send("#{k}_", v) } unless hash.nil?
161
+ end
162
+ end
163
+
164
+ xml.os(:type => options[:os_type] || self.os_type) do
165
+ xml.boot(:dev => 'hd')
166
+ end
167
+
168
+ if options[:clone_type] == :full
169
+ xml.disks do
170
+ xml.clone_ true
171
+ self.disks.each do |disk_object|
172
+ xml.disk(:id => disk_object.attributes[:id]) do
173
+ xml.sparse options[:sparse] unless options[:sparse].nil?
174
+ xml.storage_domains { xml.storage_domain(:id => options[:storage]) } if options[:storage]
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ builder.doc.root.to_xml
183
+ end
184
+ end
185
+ end
data/lib/ovirt/user.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Ovirt
2
+ class User < Object
3
+
4
+ self.top_level_strings = [:name, :description, :domain, :user_name]
5
+ self.top_level_booleans = [:logged_in]
6
+
7
+ def self.parse_xml(xml)
8
+ node, hash = xml_to_hash(xml)
9
+ groups_node = node.xpath('groups').first
10
+ hash[:groups] = groups_node.xpath('group').collect { |group_node| group_node.text } unless groups_node.nil?
11
+
12
+ hash
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Ovirt
2
+ VERSION = "0.1.0"
3
+ end