studio_api 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|