studio_api 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README +93 -0
  2. data/Rakefile +64 -0
  3. data/VERSION +1 -0
  4. data/lib/studio_api/appliance.rb +365 -0
  5. data/lib/studio_api/build.rb +17 -0
  6. data/lib/studio_api/connection.rb +102 -0
  7. data/lib/studio_api/file.rb +70 -0
  8. data/lib/studio_api/generic_request.rb +160 -0
  9. data/lib/studio_api/package.rb +12 -0
  10. data/lib/studio_api/pattern.rb +12 -0
  11. data/lib/studio_api/repository.rb +35 -0
  12. data/lib/studio_api/rpm.rb +33 -0
  13. data/lib/studio_api/running_build.rb +35 -0
  14. data/lib/studio_api/studio_resource.rb +70 -0
  15. data/lib/studio_api/template_set.rb +12 -0
  16. data/lib/studio_api/util.rb +38 -0
  17. data/lib/studio_api.rb +31 -0
  18. data/test/appliance_test.rb +189 -0
  19. data/test/build_test.rb +45 -0
  20. data/test/connection_test.rb +21 -0
  21. data/test/file_test.rb +52 -0
  22. data/test/generic_request_test.rb +66 -0
  23. data/test/repository_test.rb +42 -0
  24. data/test/resource_test.rb +49 -0
  25. data/test/responses/appliance.xml +27 -0
  26. data/test/responses/appliances.xml +199 -0
  27. data/test/responses/build.xml +17 -0
  28. data/test/responses/builds.xml +19 -0
  29. data/test/responses/file.xml +12 -0
  30. data/test/responses/files.xml +14 -0
  31. data/test/responses/gpg_key.xml +25 -0
  32. data/test/responses/gpg_keys.xml +77 -0
  33. data/test/responses/repositories.xml +42 -0
  34. data/test/responses/repository.xml +8 -0
  35. data/test/responses/rpm.xml +10 -0
  36. data/test/responses/rpms.xml +404 -0
  37. data/test/responses/running_build.xml +7 -0
  38. data/test/responses/running_builds.xml +23 -0
  39. data/test/responses/software.xml +50 -0
  40. data/test/responses/software_installed.xml +729 -0
  41. data/test/responses/software_search.xml +64 -0
  42. data/test/responses/status-broken.xml +9 -0
  43. data/test/responses/status.xml +4 -0
  44. data/test/responses/template_sets.xml +380 -0
  45. data/test/rpm_test.rb +59 -0
  46. data/test/running_build_test.rb +50 -0
  47. data/test/template_set_test.rb +35 -0
  48. metadata +181 -0
data/README ADDED
@@ -0,0 +1,93 @@
1
+ Studio API: Wrapper to STUDIO API
2
+ ====================================
3
+
4
+ Synopsis
5
+ --------
6
+
7
+ Studio API library is intended to easier access to Studio API from ruby,
8
+ but keep it easily extensible and easy maintanable, so it not need rewritting
9
+ when new attributes introduced to Studio. It leads also that user need to know
10
+ API documentation, which specify what options you need to use (see http://susestudio.com/help/api/v1 ).
11
+ It has also feature which allows using in multiuser system (like server).
12
+
13
+ Example
14
+ -------
15
+
16
+ Example usage ( more in class documentation ). Example show how to clone appliance,
17
+ upload own rpm, select it, find new software and add it, run new build and then print information about appliance.
18
+ Note: All error handling is for simplicity removed. It is same as for ActiveResource so if you want see him, search
19
+ for ActiveResource error handling.
20
+
21
+ require 'rubygems'
22
+ # If you want use it from git adapt LOAD_PATH
23
+ # you can load just required parts if you need, but Util loaded all classes to properly set it
24
+ require 'studio_api'
25
+
26
+ # Fill up Studio credentials (user name, API key, API URL)
27
+ # See https://susestudio.com/user/show_api_key if you are using SUSE Studio online
28
+ connection = StudioApi::Connection.new('user', 'pwd', 'https://susestudio.com/api/v1/user')
29
+ # Setup the connection for all ActiveResource based class
30
+ StudioApi::Util.configure_studio_connection connection
31
+
32
+ # Find template with KDE4 for SLE11SP1
33
+ templates = StudioApi::TemplateSet.find(:all).find {|s| s.name == "default" }.template
34
+ template = templates.find { |t| t.name == "SLED 11 SP1, KDE 4 desktop" }
35
+ # clone template to new appliance
36
+ appliance = StudioApi::Appliance.clone template.appliance_id, :name => "New cool appliance", :arch => "i686"
37
+ puts "Created appliance #{appliance.inspect}"
38
+
39
+ #add own rpm built agains SLED11_SP1
40
+ StudioApi::Rpm.upload "/home/jreidinger/rpms/kezboard-1.0-1.60.noarch.rpm", "SLED11_SP1"
41
+ # and choose it in appliance ( and of course add repository with own rpms)
42
+ appliance.add_user_repository
43
+ appliance.add_package "kezboard", :version => "1.0-1.60"
44
+
45
+ # find samba package and if it is not found in repositories in appliance, try it in all repos
46
+ result = appliance.search_software("samba").find { |s| s.name == "samba" }
47
+ unless result #it is not found in available repos
48
+ result = appliance.search_software("samba", :all_repos => "true").find { |s| s.name == "samba" }
49
+ # add repo which contain samba
50
+ appliance.add_repository result.repository_id
51
+ end
52
+ appliance.add_package "samba"
53
+
54
+ #check if appliance is OK
55
+ if appliance.status.state != "ok"
56
+ raise "appliance is not OK - #{appliance.status.issues.inspect}"
57
+ end
58
+ debugger
59
+ build = StudioApi::RunningBuild.new(:appliance_id => appliance.id, :image_type => "xen")
60
+ build.save
61
+ build.reload
62
+ while build.state != "finished"
63
+ puts "building (#{build.state}) - #{build.percent}%"
64
+ sleep 5
65
+ build.reload
66
+ end
67
+
68
+ final_build = StudioApi::Build.find build.id
69
+ puts final_build.inspect
70
+
71
+ # to clear after playing with appliance if you keep same name, clean remove appliances with:
72
+ # appliances = StudioApi::Appliance.find :all
73
+ # appliances.select{ |a| a.name =~ /cool/i }.each{ |a| a.destroy }
74
+
75
+ Second example contain how to easy mock calling studio stuff without mock server. Using mocha
76
+
77
+ require 'mocha'
78
+ require 'studio_api'
79
+
80
+ APPLIANCE_UUID = "68c91080-ccca-4270-a1d3-10e714ddd1c6"
81
+ APPLIANCE_VERSION = "0.0.1"
82
+ APPLIANCE_STUDIO_ID = "97216"
83
+ BUILD_ID = "180420"
84
+ APPLIANCE_1 = StudioApi::Appliance.new :id => APPLIANCE_STUDIO_ID, :name => "Test", :arch => 'i386',
85
+ :last_edited => "2010-10-08 14:46:07 UTC", :estimated_raw_size => "390 MB", :estimated_compressed_size => "140 MB",
86
+ :edit_url => "http://susestudio.com/appliance/edit/266657", :icon_url => "http://susestudio.com/api/v1/user/appliance_icon/266657",
87
+ :basesystem => "SLES11_SP1", :uuid => APPLIANCE_UUID, :parent => {:id => "202443", :name => "SLES 11 SP1, Just enough OS (JeOS)"},
88
+ :builds => [{:id =>BUILD_ID,:version => APPLIANCE_VERSION, :image_type => "vmx", :image_size => "695",
89
+ :compressed_image_size => "121",
90
+ :download_url => "http://susestudio.com/download/a0f0217f0645099c9e41c42e9bf89976/josefs_SLES_11_SP1_git_test.i686-0.0.1.vmx.tar.gz"}]
91
+
92
+ #real mocking
93
+ StudioApi::Appliance.stubs(:find).with(APPLIANCE_STUDIO_ID).returns(APPLIANCE_1)
data/Rakefile ADDED
@@ -0,0 +1,64 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ require 'rake/testtask'
4
+
5
+ task :default => "test"
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << "test"
9
+ t.pattern = 'test/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ begin
14
+ require 'yard'
15
+ YARD::Rake::YardocTask.new do |t|
16
+ t.files = ['lib/**/*.rb'] # optional
17
+ t.options = [] # optional
18
+ end
19
+ rescue LoadError
20
+ puts "Yard not available. To generate documentation install it with: gem install yard"
21
+ end
22
+
23
+
24
+ begin
25
+ require 'jeweler'
26
+ Jeweler::Tasks.new do |s|
27
+ s.name = %q{studio_api}
28
+ s.summary = %q{Studio Api Interface.}
29
+ s.description = %q{Studio Api makes it easier to use Studio via API.
30
+ Instead of adapting each ActiveResource to its behavior and
31
+ manually adding multipart file upload it wrapp in in Active
32
+ Resource like interface. It is possible to define credentials
33
+ for whole api, or use it per partes, so it allow using it for
34
+ different studio users together.}
35
+
36
+ s.files = FileList['[A-Z]*', 'lib/studio_api/*.rb','lib/studio_api.rb', 'test/**/*.rb']
37
+ s.require_path = 'lib'
38
+ s.test_files = Dir[*['test/*_test.rb','test/responses/*.xml']]
39
+ s.has_rdoc = true
40
+ s.extra_rdoc_files = ["README.rdoc"]
41
+ s.rdoc_options = ['--line-numbers', "--main", "README.rdoc"]
42
+ s.authors = ["Josef Reidinger"]
43
+ s.email = %q{jreidinger@suse.cz}
44
+ s.homepage = "http://github.com/jreidinger/studio_api"
45
+ s.add_dependency "activeresource", ">= 1.3.8"
46
+ s.add_dependency "xml-simple", ">= 1.0.0"
47
+ s.platform = Gem::Platform::RUBY
48
+ end
49
+ Jeweler::GemcutterTasks.new
50
+
51
+ desc "Create package directory containing all things to build RPM"
52
+ task :package => [:build] do
53
+ pkg_name = "rubygem-studio_api"
54
+ include FileUtils::Verbose
55
+ rm_rf "package"
56
+ mkdir "package"
57
+ cp "#{pkg_name}.changes","package/"
58
+ cp "#{pkg_name}.spec.template","package/#{pkg_name}.spec"
59
+ sh 'cp pkg/*.gem package/'
60
+ sh "sed -i \"s:<VERSION>:`cat VERSION`:\" package/#{pkg_name}.spec"
61
+ end
62
+ rescue LoadError
63
+ puts "Jeweler not available. To generate gem install it with: gem install jeweler"
64
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.3.0
@@ -0,0 +1,365 @@
1
+ require "studio_api/studio_resource"
2
+ require "studio_api/generic_request"
3
+ require "studio_api/pattern"
4
+ require "studio_api/package"
5
+ require "xmlsimple"
6
+ require "fileutils"
7
+ require 'cgi'
8
+
9
+ module StudioApi
10
+ # Represents appliance in studio
11
+ # beside information about itself contains also information about its
12
+ # relative object like packages, signing keys etc
13
+ # Each method try to be ActiveResource compatible, so each can throw ConnectionError
14
+ class Appliance < ActiveResource::Base
15
+ extend StudioApi::StudioResource
16
+
17
+ self.element_name = "appliance"
18
+
19
+ # Represents status of appliance
20
+ # used as output for Appliance#status
21
+ # @see Appliance#status
22
+ class Status < ActiveResource::Base
23
+ extend StudioResource
24
+ self.element_name = "status"
25
+ end
26
+
27
+ # Represents repository assigned to appliance
28
+ # supports find :all and deleting from appliance
29
+ class Repository < ActiveResource::Base
30
+ extend StudioResource
31
+ self.prefix = "/appliances/:appliance_id/"
32
+ self.element_name = "repository"
33
+ mattr_accessor :appliance
34
+
35
+ #for delete repository doesn't work clasic method from ARes
36
+ # @see StudioApi::Appliance#remove_repository
37
+ def destroy
38
+ self.class.appliance.remove_repository id
39
+ end
40
+ end
41
+
42
+ # Represents GPGKey assigned to appliance
43
+ class GpgKey < ActiveResource::Base
44
+ extend StudioResource
45
+ self.prefix = "/appliances/:appliance_id/"
46
+ self.element_name = "gpg_key"
47
+ mattr_accessor :appliance
48
+
49
+ # upload new GPG key to appliance
50
+ # @param (#to_i) appliance_id id of appliance to which load gpg key
51
+ # @param (#to_s) name of gpg key
52
+ # @param (File, String) opened file containing key or key in string
53
+ # @param (Hash) options additional options keys as it allow studio API
54
+ # @example Load from file
55
+ # File.open ("/etc/my.cert") do |file|
56
+ # StudioApi::Appliance::GpgKey.create 1234, "my new cool key", file, :target => "rpm"
57
+ # end
58
+ def self.create (appliance_id, name, key, options={})
59
+ options[:target] ||= "rpm"
60
+ data = {}
61
+ if key.is_a?(IO) && key.respond_to?(:path) #if key is string, that pass it in request, if not pack it in body
62
+ data[:file] = key
63
+ else
64
+ options[:key] = key.to_s
65
+ end
66
+ request_str = "/appliances/#{appliance_id.to_i}/gpg_keys?name=#{name}"
67
+ options.each do |k,v|
68
+ request_str << "&#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
69
+ end
70
+ response = GenericRequest.new(studio_connection).post request_str, data
71
+ self.new Hash.from_xml(response)["gpg_key"]
72
+ end
73
+ end
74
+
75
+ # gets status of appliance
76
+ # @return [StudioApi::Appliance::Status] resource of status
77
+ def status
78
+ my_status = Status#.dup FIXME this doesn't work well with AciveResource :(
79
+ my_status.studio_connection = self.class.studio_connection
80
+ #rails is so smart, that it ignores prefix for calls. At least it is good that we don't want to do such things from library users
81
+ from = Util.join_relative_url( self.class.site.path,"appliances/#{id.to_i}/status")
82
+ my_status.find :one, :from => from
83
+ end
84
+
85
+ # Gets file content from finished build.
86
+ # @param [StudioApi::Build, StudioApi::Appliance::Build] build from which download file
87
+ # @param [#to_s] src_path path in appliance fs to required file
88
+ # @return [String] content of file
89
+ def file_content_from_build (build,src_path)
90
+ rq = GenericRequest.new self.class.studio_connection
91
+ rq.get "/appliances/#{id.to_i}/image_files?build_id=#{build.id.to_i}&path=#{CGI.escape src_path.to_s}"
92
+ end
93
+
94
+ # Gets all repositories assigned to appliance
95
+ # @return [StudioApi::Appliance::Repository] assigned repositories
96
+ def repositories
97
+ my_repo = Repository.dup
98
+ my_repo.studio_connection = self.class.studio_connection
99
+ my_repo.appliance = self
100
+ my_repo.find :all, :params => { :appliance_id => id }
101
+ end
102
+
103
+ # remove repositories from appliance
104
+ # @param (#to_s,Array<#to_s>)
105
+ # @return (Array<StudioApi::Repository>) list of remaining repositories
106
+ # @example various way to remove repo
107
+ # appl = Appliance.find 1234
108
+ # appl.remove_repository 5678
109
+ # appl.remove_repository [5678,34,56,78,90]
110
+ # appl.remove_repository 5678,34,56,78,90
111
+
112
+ def remove_repository (*repo_ids)
113
+ response = nil
114
+ repo_ids.flatten.each do |repo_id|
115
+ rq = GenericRequest.new self.class.studio_connection
116
+ response = rq.post "/appliances/#{id}/cmd/remove_repository?repo_id=#{repo_id.to_i}"
117
+ end
118
+ Hash.from_xml(response)["repositories"].collect{ |r| Repository.new r }
119
+ end
120
+
121
+ # adds repositories to appliance
122
+ # @param (#to_s,Array<#to_s>)
123
+ # @return (Array<StudioApi::Repository>) list of all repositories including new one
124
+ # @example various way to add repo
125
+ # appl = Appliance.find 1234
126
+ # appl.add_repository 5678
127
+ # appl.add_repository [5678,34,56,78,90]
128
+ # appl.add_repository 5678,34,56,78,90
129
+ def add_repository (*repo_ids)
130
+ response = nil
131
+ repo_ids.flatten.each do |repo_id|
132
+ rq = GenericRequest.new self.class.studio_connection
133
+ response = rq.post "/appliances/#{id}/cmd/add_repository?repo_id=#{repo_id.to_i}"
134
+ end
135
+ Hash.from_xml(response)["repositories"].collect{ |r| Repository.new r }
136
+ end
137
+
138
+ # adds repository for user rpms
139
+ def add_user_repository
140
+ rq = GenericRequest.new self.class.studio_connection
141
+ rq.post "/appliances/#{id}/cmd/add_user_repository"
142
+ end
143
+
144
+ # clones appliance or template
145
+ # @see (StudioApi::TemplateSet)
146
+ # @param (#to_i) source_id id of source appliance
147
+ # @param (Hash<String,String>) options optional parameters to clone command
148
+ # @return (StudioApi::Appliance) resulted appliance
149
+ def self.clone source_id,options={}
150
+ request_str = "/appliances?clone_from=#{source_id.to_i}"
151
+ options.each do |k,v|
152
+ request_str << "&#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
153
+ end
154
+ response = GenericRequest.new(studio_connection).post request_str, options
155
+ Appliance.new Hash.from_xml(response)["appliance"]
156
+ end
157
+
158
+ # Gets all GPG keys assigned to appliance
159
+ # @return [Array<StudioApi::Appliance::GpgKey>] included keys
160
+ def gpg_keys
161
+ my_key = GpgKey.dup
162
+ my_key.studio_connection = self.class.studio_connection
163
+ my_key.find :all, :params => { :appliance_id => id }
164
+ end
165
+
166
+ # Gets GPG key assigned to appliance with specified id
167
+ # @param (#to_s) key_id id of requested key
168
+ # @return [StudioApi::Appliance::GpgKey,nil] found key or nil if it is not found
169
+ def gpg_key( key_id )
170
+ my_key = GpgKey.dup
171
+ my_key.studio_connection = self.class.studio_connection
172
+ my_key.find key_id, :params => { :appliance_id => id }
173
+ end
174
+
175
+ # add GPG key to appliance
176
+ # @params (see GpgKey#create)
177
+ # @return [StudioApi::Appliance::GpgKey] created key
178
+ def add_gpg_key (name, key, options={})
179
+ my_key = GpgKey.dup
180
+ my_key.studio_connection = self.class.studio_connection
181
+ my_key.create id, name, key, options
182
+ end
183
+
184
+ # Gets list of all explicitelly selected software ( package and patterns)
185
+ # in appliance
186
+ # @return (Array<StudioApi::Package,StudioApi::Pattern>) list of selected packages and patterns
187
+ def selected_software
188
+ request_str = "/appliances/#{id.to_i}/software"
189
+ response = GenericRequest.new(self.class.studio_connection).get request_str
190
+ attrs = XmlSimple.xml_in response
191
+ convert_selectable attrs
192
+ end
193
+
194
+ # Gets list of all installed (include dependencies) software
195
+ # (package and patterns) in appliance
196
+ # @param (Hash) hash of options, see studio API
197
+ # @return (Array<StudioApi::Package,StudioApi::Pattern>) list of installed packages and patterns
198
+ def installed_software (options = {})
199
+ request_str = "/appliances/#{id.to_i}/software/installed"
200
+ unless options.empty?
201
+ first = true
202
+ options.each do |k,v|
203
+ separator = first ? "?" : "&"
204
+ first = false
205
+ request_str << "#{separator}#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
206
+ end
207
+ end
208
+ response = GenericRequest.new(self.class.studio_connection).get request_str
209
+ attrs = XmlSimple.xml_in response
210
+ res = []
211
+ attrs["repository"].each do |repo|
212
+ options = { "repository_id" => repo["id"].to_i }
213
+ res += convert_selectable repo["software"][0], options
214
+ end
215
+ res
216
+ end
217
+
218
+ # Search software (package and patterns) in appliance
219
+ # @param (#to_s) search_string string which is used for search
220
+ # @param (Hash<#to_s,#to_s>) options optional parameters for search, see api documentation
221
+ # @return (Array<StudioApi::Package,StudioApi::Pattern>) list of installed packages and patterns
222
+ def search_software (search_string,options={})
223
+ request_str = "/appliances/#{id.to_i}/software/search?q=#{CGI.escape search_string.to_s}"
224
+ options.each do |k,v|
225
+ request_str << "&#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
226
+ end
227
+ response = GenericRequest.new(self.class.studio_connection).get request_str
228
+ attrs = XmlSimple.xml_in response
229
+ res = []
230
+ attrs["repository"].each do |repo|
231
+ options = { "repository_id" => repo["id"].to_i }
232
+ res += convert_selectable repo["software"][0], options
233
+ end
234
+ res
235
+ end
236
+
237
+ # Returns rpm file as String
238
+ # @param (#to_s) name of rpm
239
+ # @param (Hash<#to_s,#to_s>) options additional options, see API documentation
240
+ def rpm_content(name, options={})
241
+ request_str = "/appliances/#{id.to_i}/cmd/download_package?name=#{CGI.escape name.to_s}"
242
+ options.each do |k,v|
243
+ request_str << "&#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
244
+ end
245
+ GenericRequest.new(self.class.studio_connection).get request_str
246
+ end
247
+
248
+ # Select new package to be installed in appliance.
249
+ #
250
+ # Dependencies is automatic resolved, but its repository have to be already
251
+ # included in appliance
252
+ # @param(#to_s) name of package
253
+ # @param (Hash<#to_s,#to_s>) options optional parameters for adding packages, see api documentation
254
+ # @return [Hash<String,String>] return status after software change. It contains
255
+ # three keys - state, packages_added and packages_removed
256
+ def add_package (name, options={})
257
+ software_command "add_package",{:name => name}.merge(options)
258
+ end
259
+
260
+ # Deselect package from appliance.
261
+ #
262
+ # Dependencies is automatic resolved (so unneeded dependencies not installed),
263
+ # but unused repositories is kept
264
+ # @param(#to_s) name of package
265
+ # @return [Hash<String,String>] return status after software change. It contains
266
+ # three keys - state, packages_added and packages_removed
267
+ def remove_package (name)
268
+ software_command "remove_package",:name => name
269
+ end
270
+
271
+ # Select new pattern to be installed in appliance.
272
+ #
273
+ # Dependencies is automatic resolved, but its repositories have to be already
274
+ # included in appliance
275
+ # @param(#to_s) name of pattern
276
+ # @param (Hash<#to_s,#to_s>) options optional parameters for adding patterns, see api documentation
277
+ # @return [Hash<String,String>] return status after software change. It contains
278
+ # three keys - state, packages_added and packages_removed
279
+ def add_pattern (name, options={})
280
+ software_command "add_pattern",{:name => name}.merge(options)
281
+ end
282
+
283
+ # Deselect pattern from appliance.
284
+ #
285
+ # Dependencies is automatic resolved (so unneeded dependencies not installed),
286
+ # but unused repositories is kept
287
+ # @param(#to_s) name of pattern
288
+ # @return [Hash<String,String>] return status after software change. It contains
289
+ # three keys - state, packages_added and packages_removed
290
+ def remove_pattern (name)
291
+ software_command "remove_pattern",:name => name
292
+ end
293
+
294
+ # Bans package ( so it cannot be installed even as dependency).
295
+ # @param(#to_s) name of package
296
+ # @return [Hash<String,String>] return status after software change. It contains
297
+ # three keys - state, packages_added and packages_removed
298
+ def ban_package(name)
299
+ software_command "ban_package",:name => name
300
+ end
301
+
302
+ # Unbans package ( so then it can be installed).
303
+ # @param(#to_s) name of package
304
+ # @return [Hash<String,String>] return status after software change. It contains
305
+ # three keys - state, packages_added and packages_removed
306
+ def unban_package(name)
307
+ software_command "unban_package",:name => name
308
+ end
309
+
310
+ private
311
+ #internal overwrite of ActiveResource::Base methods
312
+ def new?
313
+ false #Appliance has only POST method
314
+ end
315
+
316
+ #studio post method for clone is special, as it sometime doesn't have element inside
317
+ def custom_method_element_url(method_name,options = {})
318
+ prefix_options, query_options = split_options(options)
319
+ method_string = method_name.blank? ? "" : "/#{method_name}"
320
+ "#{self.class.prefix(prefix_options)}#{self.class.collection_name}#{method_string}#{self.class.send :query_string,query_options}"
321
+ end
322
+ def self.custom_method_collection_url(method_name,options = {})
323
+ prefix_options, query_options = split_options(options)
324
+ "#{prefix(prefix_options)}#{collection_name}#{query_string query_options}"
325
+ end
326
+
327
+ def convert_selectable attrs, preset_options = {}
328
+ res = []
329
+ (attrs["pattern"]||[]).each do |pattern|
330
+ res << create_model_based_on_attrs(Pattern, pattern, preset_options)
331
+ end
332
+ (attrs["package"]||[]).each do |package|
333
+ res << create_model_based_on_attrs( Package, package, preset_options)
334
+ end
335
+ res
336
+ end
337
+
338
+ #generic factory to create model based on attrs which can be string of hash of options + content which is same as string
339
+ def create_model_based_on_attrs model, attrs, preset_options
340
+ case attrs
341
+ when Hash
342
+ name = attrs.delete "content"
343
+ model.new(name, preset_options.merge(attrs))
344
+ when String
345
+ model.new(attrs)
346
+ else
347
+ raise "Unknown format of element #{model}"
348
+ end
349
+ end
350
+
351
+ def software_command type, options={}
352
+ request_str = "/appliances/#{id.to_i}/cmd/#{type}"
353
+ unless options.empty?
354
+ first = true
355
+ options.each do |k,v|
356
+ separator = first ? "?" : "&"
357
+ first = false
358
+ request_str << "#{separator}#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
359
+ end
360
+ end
361
+ response = GenericRequest.new(self.class.studio_connection).post request_str, options
362
+ Hash.from_xml(response)["success"]["details"]["status"]
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,17 @@
1
+ require "studio_api/studio_resource"
2
+
3
+ module StudioApi
4
+ # Represents created build in studio. It allows finding and deleting.
5
+ #
6
+ # @example Delete version 0.0.1 (all types)
7
+ # builds = Build.find(:all,:params=>{:appliance_id => 1234})
8
+ # versions1 = builds.select { |b| b.version == "0.0.1" }
9
+ # versions1.each {|v| v.destroy }
10
+
11
+ class Build < ActiveResource::Base
12
+ extend StudioResource
13
+
14
+ self.element_name = "build"
15
+ undef_method :save
16
+ end
17
+ end
@@ -0,0 +1,102 @@
1
+ #
2
+ # Copyright (c) 2010 Novell, Inc.
3
+ # All Rights Reserved.
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public License as
7
+ # published by the Free Software Foundation; version 2.1 of the license.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public License
15
+ # along with this library; if not, contact Novell, Inc.
16
+ #
17
+ # To contact Novell about this file by physical or electronic mail,
18
+ # you may find current contact information at www.novell.com
19
+
20
+ require 'uri'
21
+ require 'openssl'
22
+ require 'studio_api/generic_request'
23
+
24
+ module StudioApi
25
+ # Represents information needed for connection to studio.
26
+ # In common case it is just needed once initialize and then pass it to classes.
27
+ class Connection
28
+ # SSL attributes which can be set into ssl attributes. For more details see openssl library
29
+ SSL_ATTRIBUTES = [ :key, :cert, :ca_file, :ca_path, :verify_mode, :verify_callback, :verify_depth, :cert_store ]
30
+ # Represents login name for studio API
31
+ attr_reader :user
32
+ # Represents API key for studio API
33
+ attr_reader :password
34
+ # Represents URI pointing to studio site including path to API
35
+ # @example
36
+ # connection.uri == URI.parse "http://susestudio.com/api/v1/user/"
37
+ attr_reader :uri
38
+ # Represents proxy object needed for connection to studio API.
39
+ # nil represents that no proxy needed
40
+ attr_reader :proxy
41
+ # Represents timeout for connection in seconds.
42
+ attr_reader :timeout
43
+ # Represents settings for SSL verification in case of uri is https.
44
+ # It is Hash with keys from SSL_ATTRIBUTES
45
+ attr_reader :ssl
46
+
47
+ # Creates new object
48
+ # @example
49
+ # StudioApi::Connection.new "user","pwd","https://susestudio.com//api/v1/user/",
50
+ # :timeout => 120, :proxy => "http://user:pwd@proxy",
51
+ # :ssl => { :verify_mode => OpenSSL::SSL::VERIFY_PEER,
52
+ # :ca_path => "/etc/studio.cert"}
53
+ # @param [String] user login to studio API
54
+ # @param (String) password API key for studio
55
+ # @param (String,URI) uri pointing to studio site including path to api
56
+ # @param (Hash) options hash of additional options. Represents other attributes.
57
+ # @option options [URI,String] :proxy (nil) see proxy attribute
58
+ # @option options [String, Fixnum] :timeout (45) see timeout attribute. Specified in seconds
59
+ # @option options [Hash] :ssl ( {:verify_mode = OpenSSL::SSL::VERIFY_NONE}) see ssl attribute
60
+ #
61
+ def initialize(user, password, uri, options={})
62
+ @user = user
63
+ @password = password
64
+ self.uri = uri
65
+ self.proxy = options[:proxy] #nil as default is OK
66
+ @timeout = options[:timeout].to_i || 45
67
+ @ssl = options[:ssl] || { :verify_mode => OpenSSL::SSL::VERIFY_NONE } # don't verify as default
68
+ end
69
+
70
+ def api_version
71
+ @version ||= version_detect
72
+ end
73
+ protected
74
+
75
+ # Overwritte uri object.
76
+ # @param (String,URI) value new uri to site. If String is passed then it is parsed by URI.parse, which can throw exception
77
+ def uri=(value)
78
+ if value.is_a? String
79
+ @uri = URI.parse value
80
+ else
81
+ @uri = value
82
+ end
83
+ end
84
+
85
+ # Overwritte proxy object.
86
+ # @param (String,URI,nil) value new proxy to site. If String is passed then it is parsed by URI.parse, which can throw exception. If nil is passed then it means disable proxy.
87
+ def proxy=(value)
88
+ if value.is_a? String
89
+ @proxy = URI.parse value
90
+ else
91
+ @proxy = value
92
+ end
93
+ end
94
+
95
+ private
96
+ def version_detect
97
+ rq = GenericRequest.new self
98
+ response = rq.get "/api_version"
99
+ Hash.from_xml(response)["version"]
100
+ end
101
+ end
102
+ end