studio_api 2.3.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/README +93 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/lib/studio_api/appliance.rb +365 -0
- data/lib/studio_api/build.rb +17 -0
- data/lib/studio_api/connection.rb +102 -0
- data/lib/studio_api/file.rb +70 -0
- data/lib/studio_api/generic_request.rb +160 -0
- data/lib/studio_api/package.rb +12 -0
- data/lib/studio_api/pattern.rb +12 -0
- data/lib/studio_api/repository.rb +35 -0
- data/lib/studio_api/rpm.rb +33 -0
- data/lib/studio_api/running_build.rb +35 -0
- data/lib/studio_api/studio_resource.rb +70 -0
- data/lib/studio_api/template_set.rb +12 -0
- data/lib/studio_api/util.rb +38 -0
- data/lib/studio_api.rb +31 -0
- data/test/appliance_test.rb +189 -0
- data/test/build_test.rb +45 -0
- data/test/connection_test.rb +21 -0
- data/test/file_test.rb +52 -0
- data/test/generic_request_test.rb +66 -0
- data/test/repository_test.rb +42 -0
- data/test/resource_test.rb +49 -0
- data/test/responses/appliance.xml +27 -0
- data/test/responses/appliances.xml +199 -0
- data/test/responses/build.xml +17 -0
- data/test/responses/builds.xml +19 -0
- data/test/responses/file.xml +12 -0
- data/test/responses/files.xml +14 -0
- data/test/responses/gpg_key.xml +25 -0
- data/test/responses/gpg_keys.xml +77 -0
- data/test/responses/repositories.xml +42 -0
- data/test/responses/repository.xml +8 -0
- data/test/responses/rpm.xml +10 -0
- data/test/responses/rpms.xml +404 -0
- data/test/responses/running_build.xml +7 -0
- data/test/responses/running_builds.xml +23 -0
- data/test/responses/software.xml +50 -0
- data/test/responses/software_installed.xml +729 -0
- data/test/responses/software_search.xml +64 -0
- data/test/responses/status-broken.xml +9 -0
- data/test/responses/status.xml +4 -0
- data/test/responses/template_sets.xml +380 -0
- data/test/rpm_test.rb +59 -0
- data/test/running_build_test.rb +50 -0
- data/test/template_set_test.rb +35 -0
- 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
|