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.
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
@@ -0,0 +1,70 @@
1
+ require "studio_api/studio_resource"
2
+ require "cgi"
3
+ module StudioApi
4
+ # Represents overlay files which can be loaded to appliance.
5
+ #
6
+ # Supports finding files for appliance, updating metadata, deleting, uploading and downloading.
7
+ #
8
+ # @example Find files for appliance
9
+ # StudioApi::File.find :all, :params => { :appliance_id => 1234 }
10
+ #
11
+ # @example Upload file Xorg.conf
12
+ # File.open ("/tmp/xorg.conf) { |file|
13
+ # StudioApi::File.upload file, 1234, :path => "/etc/X11",
14
+ # :filename => "Xorg.conf", :permissions => "0755",
15
+ # :owner => "root"
16
+ # }
17
+ #
18
+ # @example Update metadata
19
+ # file = StudioApi::File.find 1234
20
+ # file.owner = "root"
21
+ # file.path = "/etc"
22
+ # file.filename = "pg.conf"
23
+ # file.save
24
+
25
+ class File < ActiveResource::Base
26
+ extend StudioResource
27
+ self.element_name = "file"
28
+
29
+ # Downloads file to output. Allow downloading to stream or to path.
30
+ # @return [String] content of file
31
+ def content
32
+ rq = GenericRequest.new self.class.studio_connection
33
+ rq.get "/files/#{id.to_i}/data"
34
+ end
35
+
36
+ # Overwritte file content and keep metadata ( of course without such things like size )
37
+ # Immediatelly store new content
38
+ # @param (File,#to_s) input new content for file as String or open file
39
+ # @return [StudioApi::File] self with updated metadata
40
+ def overwrite ( content )
41
+ request_str = "/files/#{id.to_i}/data"
42
+ rq = GenericRequest.new self.class.studio_connection
43
+ response = rq.put request_str, :file => content
44
+ load Hash.from_xml(response)["file"]
45
+ end
46
+
47
+ # Uploads file to appliance
48
+ # @param (String,File) content as String or as opened File
49
+ # ( in this case its name is used as default for uploaded file name)
50
+ # @param (#to_i) appliance_id id of appliance where to upload
51
+ # @param (Hash<#to_s,#to_s>) options optional parameters, see API documentation
52
+ # @return [StudioApi::File] metadata of uploaded file
53
+ def self.upload ( content, appliance_id, options = {})
54
+ request_str = "files?appliance_id=#{appliance_id.to_i}"
55
+ options.each do |k,v|
56
+ request_str << "&#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
57
+ end
58
+ rq = GenericRequest.new studio_connection
59
+ response = rq.post request_str, :file => content
60
+ File.new Hash.from_xml(response)["file"]
61
+ end
62
+
63
+ private
64
+ # file uses for update parameter put
65
+ # @private
66
+ def new?
67
+ false
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,160 @@
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 'xmlsimple'
21
+ require 'uri'
22
+ require 'cgi'
23
+ require 'net/http'
24
+ require 'net/https'
25
+ require 'active_support'
26
+ require 'active_resource/formats'
27
+ require 'active_resource/connection'
28
+
29
+ require 'studio_api/util'
30
+
31
+ module StudioApi
32
+ # Class which use itself direct connection to studio for tasks where
33
+ # ActiveResource is not enough. For consistent api is all network exceptions
34
+ # mapped to ones used in ActiveResource.
35
+ #
36
+ # @example
37
+ # rq = StudioApi::GenericRequest.new @connection
38
+ # rq.get "/appliances"
39
+ # rq.post "/file", :file => "/etc/config"
40
+ class GenericRequest
41
+ # Creates new instance of request for given connection
42
+ # @param (StudioApi::Connection) connection information about connection
43
+ def initialize(connection)
44
+ @connection = connection
45
+ if connection.proxy
46
+ proxy = connection.proxy
47
+ @http = Net::HTTP.new(connection.uri.host, connection.uri.port,
48
+ proxy.host, proxy.port, proxy.user, proxy.password)
49
+ else
50
+ @http = Net::HTTP.new(connection.uri.host, connection.uri.port)
51
+ end
52
+ @http.read_timeout = connection.timeout
53
+ if connection.uri.scheme == "https"
54
+ @http.use_ssl = true
55
+ Connection::SSL_ATTRIBUTES.each do |attr|
56
+ @http.send :"#{attr}=", connection.ssl[attr.to_sym] if connection.ssl[attr.to_sym]
57
+ end
58
+ end
59
+ end
60
+
61
+ # sends get request
62
+ # @param (String) path relative path from api root
63
+ # @return (String) response body from studio
64
+ # @raise [ActiveResource::ConnectionError] when problem occur during connection
65
+ def get(path)
66
+ do_request Net::HTTP::Get.new Util.join_relative_url @connection.uri.request_uri,path
67
+ end
68
+
69
+ # sends delete request
70
+ # @param (String) path relative path from api root
71
+ # @return (String) response body from studio
72
+ # @raise [ActiveResource::ConnectionError] when problem occur during connection
73
+ def delete(path)
74
+ #Even it is not dry I want to avoid meta programming with dynamic code evaluation so code is clear
75
+ do_request Net::HTTP::Delete.new Util.join_relative_url @connection.uri.request_uri,path
76
+ end
77
+
78
+ # sends post request
79
+ # @param (String) path relative path from api root
80
+ # @param (Hash<#to_s,#to_s>,Hash<#to_s,#path>) data hash containing data to attach to body
81
+ # @return (String) response body from studio
82
+ # @raise [ActiveResource::ConnectionError] when problem occur during connection
83
+ def post(path,data={})
84
+ request = Net::HTTP::Post.new Util.join_relative_url @connection.uri.request_uri,path
85
+ set_data(request,data) unless data.empty?
86
+ do_request request
87
+ end
88
+
89
+ # sends post request
90
+ # @param (String) path relative path from api root
91
+ # @param (Hash<#to_s,#to_s>,Hash<#to_s,#path>) data hash containing data to attach to body
92
+ # @return (String) response body from studio
93
+ # @raise [ActiveResource::ConnectionError] when problem occur during connection
94
+ def put(path,data={})
95
+ request = Net::HTTP::Put.new Util.join_relative_url @connection.uri.request_uri,path
96
+ set_data(request,data) unless data.empty?
97
+ do_request request
98
+ end
99
+
100
+ private
101
+ def do_request(request)
102
+ request.basic_auth @connection.user, @connection.password
103
+ @http.start() do
104
+ response = @http.request request
105
+ unless response.kind_of? Net::HTTPSuccess
106
+ msg = error_message response
107
+ create_active_resource_exception response,msg
108
+ end
109
+ response.body
110
+ end
111
+ end
112
+
113
+ #XXX not so nice to use internal method, but better to be DRY and proper test if it works with supported rails
114
+ def create_active_resource_exception response,msg
115
+ response.instance_variable_set "@message",msg
116
+ ActiveResource::Connection.new('').send :handle_response, response
117
+ end
118
+
119
+ def error_message response
120
+ xml_parsed = XmlSimple.xml_in(response.body, {'KeepRoot' => true})
121
+ raise "Unknown error response from Studio: #{response.body}" unless xml_parsed['error']
122
+ msg = ""
123
+ xml_parsed['error'].each() {|error| msg << error['message'][0]+"\n" }
124
+ return msg
125
+ rescue RuntimeError
126
+ return response.message+"\n"+response.body
127
+ end
128
+
129
+ def set_data(request,data)
130
+ boundary = Time.now.to_i.to_s(16)
131
+ request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
132
+ body = ""
133
+ data.each do |key,value|
134
+ esc_key = CGI.escape(key.to_s)
135
+ body << "--#{boundary}\r\n"
136
+ if value.respond_to?(:read) && value.respond_to?(:path)
137
+ # ::File is needed to use "Ruby" file instead this one
138
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{::File.basename(value.path)}\"\r\n"
139
+ body << "Content-Type: #{mime_type(value.path)}\r\n\r\n"
140
+ body << value.read
141
+ else
142
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"\r\n\r\n#{value}"
143
+ end
144
+ body << "\r\n"
145
+ end
146
+ body << "--#{boundary}--\r\n\r\n"
147
+ request.body = body
148
+ request["Content-Length"] = request.body.size
149
+ end
150
+
151
+ def mime_type(file)
152
+ case
153
+ when file =~ /\.jpe?g\z/i then 'image/jpg'
154
+ when file =~ /\.gif\z/i then 'image/gif'
155
+ when file =~ /\.png\z/i then 'image/png'
156
+ else 'application/octet-stream'
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,12 @@
1
+ module StudioApi
2
+ # Represents package in appliance. Used mainly as data storage.
3
+ class Package
4
+ attr_accessor :name, :version, :repository_id, :arch, :checksum, :checksum_type
5
+ def initialize name, attributes = {}
6
+ @name = name
7
+ attributes.each do |k,v|
8
+ instance_variable_set "@#{k}", v
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module StudioApi
2
+ # Represents pattern in appliance. Used mainly as data storage.
3
+ class Pattern
4
+ attr_accessor :name, :version, :repository_id, :arch
5
+ def initialize name, attributes = {}
6
+ @name = name
7
+ attributes.each do |k,v|
8
+ instance_variable_set "@#{k}", v
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ require "studio_api/studio_resource"
2
+ module StudioApi
3
+ # Represents available repositories for appliance.
4
+ #
5
+ # Allows finding and importing repositories.
6
+ # When using find :all then there is optional parameters for base_system and filter
7
+ #
8
+ # @example Find repository with kde for SLE11
9
+ # StudioApi::Repository.find :all, :params => { :base_system => "sle11", :filter => "kde" }
10
+
11
+ class Repository < ActiveResource::Base
12
+ extend StudioResource
13
+
14
+ undef_method :save #save is useless there
15
+ undef_method :destroy #not allowed
16
+
17
+ # Import new repository to Studio
18
+ #
19
+ # note: Repository will be available to everyone
20
+ # @param (#to_s) url to repository
21
+ # @param (#to_s) name of created repository
22
+ # @return [StudioApi::Repository] imported repository
23
+ def self.import (url, name)
24
+ response = post '',:url => url, :name => name
25
+ attrs = Hash.from_xml response.body
26
+ Repository.new attrs["repository"]
27
+ end
28
+ private
29
+ #handle special studio collection method for import
30
+ def self.custom_method_collection_url(method_name, options = {})
31
+ prefix_options, query_options = split_options(options)
32
+ "#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ require "studio_api/studio_resource"
2
+ require 'cgi'
3
+
4
+ module StudioApi
5
+ # Represents Additional rpms which can user upload to studio.
6
+ #
7
+ # Allows uploading, downloading, listing (via find) and deleting
8
+ #
9
+ # @example Delete own rpm
10
+ # rpms = StudioApi::Rpm.find :all, :params => { :base_system => "SLE11" }
11
+ # my_pac = rpms.find {|r| r.filename =~ /my_pac/ }
12
+ # my_pac.delete
13
+ class Rpm < ActiveResource::Base
14
+ extend StudioResource
15
+ undef_method :save
16
+
17
+ self.element_name = "rpm"
18
+ # Upload file to studio account (user repository)
19
+ # @param (String,File) content of rpm as String or as opened file, in which case name is used as name
20
+ # @param (#to_s) base_system for which is rpm compiled
21
+ # @return [StudioApi::Rpm] uploaded RPM
22
+ def self.upload content, base_system
23
+ response = GenericRequest.new(studio_connection).post "/rpms?base_system=#{CGI.escape base_system.to_s}", :file => content
24
+ self.new Hash.from_xml(response)["rpm"]
25
+ end
26
+
27
+ # Downloads file to specified path.
28
+ # @return [String] content of rpm
29
+ def content
30
+ GenericRequest.new(self.class.studio_connection).get "/rpms/#{id.to_i}/data"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ require "studio_api/studio_resource"
2
+
3
+ require "cgi"
4
+
5
+ module StudioApi
6
+ # Represents running build in studio.
7
+ #
8
+ # Provide finding builds, canceling build process or running new build
9
+ # For parameters see API documentation
10
+ # @example Run new build and then cancel it
11
+ # rb = StudioApi::RunningBuild.new(:appliance_id => 1234, :force => "true", :multi => "true")
12
+ # rb.save!
13
+ # sleep 5
14
+ # rb.cancel
15
+ class RunningBuild < ActiveResource::Base
16
+ extend StudioResource
17
+
18
+ self.element_name = "running_build"
19
+
20
+ alias_method :cancel, :destroy
21
+
22
+ private
23
+ #overwrite create as studio doesn't interact well with enclosed parameters
24
+ def create
25
+ request_str = collection_path
26
+ request_str << "?appliance_id=#{attributes.delete("appliance_id").to_i}"
27
+ attributes.each do |k,v|
28
+ request_str << "&#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
29
+ end
30
+ connection.post(request_str,"",self.class.headers).tap do |response|
31
+ load_attributes_from_response response
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,70 @@
1
+ require "rubygems"
2
+ require 'active_resource'
3
+ require "studio_api/util"
4
+ require "studio_api/studio_resource"
5
+
6
+ module StudioApi
7
+ # Adds ability to ActiveResource::Base (short as ARes) to easy set connection to studio in
8
+ # dynamic way, which is not so easy as ARes is designed for static values.
9
+ # Also modify a few expectation of ActiveResource to fit studio API ( like
10
+ # missing xml suffix in calls ).
11
+ #
12
+ # @example Add new Studio Resource
13
+ # # enclose it in module allows to automatic settings with Util
14
+ # module StudioApi
15
+ # class NewCoolResource < ActiveResource::Base
16
+ # extend StudioResource
17
+ # end
18
+ # end
19
+
20
+ module StudioResource
21
+ # Gets studio connection. Mostly useful internally.
22
+ # @return (StudioApi::Connection,nil) object of studio connection or nil if not
23
+ # yet set
24
+ def studio_connection
25
+ @studio_connection
26
+ end
27
+
28
+ # Takes information from connection and sets it to ActiveResource::Base.
29
+ # Also take care properly of prefix as it need to join path from site with
30
+ # api prefix like appliance/:appliance_id .
31
+ # @param (StudioApi::Connection) connection source for connection in
32
+ # activeResource
33
+ # @return (StudioApi::Connection) unmodified parameter
34
+ def studio_connection= connection
35
+ self.site = connection.uri.to_s
36
+ # there is general problem, that when specified prefix in model, it doesn't
37
+ # contain uri.path as it is not know and uri is set during runtime, so we
38
+ # must add here manually adapt prefix otherwise site.path is ommitted in
39
+ # models which has own prefix in API
40
+ unless @original_prefix
41
+ if self.prefix_source == Util.join_relative_url(connection.uri.path,'/')
42
+ @original_prefix = "/"
43
+ else
44
+ @original_prefix = self.prefix_source
45
+ end
46
+ end
47
+ self.prefix = Util.join_relative_url connection.uri.path, @original_prefix
48
+ self.user = connection.user
49
+ self.password = connection.password
50
+ self.timeout = connection.timeout
51
+ self.proxy = connection.proxy.to_s if connection.proxy
52
+ self.ssl_options = connection.ssl
53
+ @studio_connection = connection
54
+ end
55
+
56
+ # We need to overwrite the paths methods because susestudio doesn't use the
57
+ # standard .xml filename extension which is expected by ActiveResource.
58
+ def element_path(id, prefix_options = {}, query_options = nil)
59
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
60
+ "#{prefix(prefix_options)}#{collection_name}/#{id}#{query_string(query_options)}"
61
+ end
62
+
63
+ # We need to overwrite the paths methods because susestudio doesn't use the
64
+ # standard .xml filename extension which is expected by ActiveResource.
65
+ def collection_path(prefix_options = {}, query_options = nil)
66
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
67
+ "#{prefix(prefix_options)}#{collection_name}#{query_string(query_options)}"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ require "studio_api/studio_resource"
2
+
3
+ module StudioApi
4
+ # Represents template sets. It is usefull when clone appliance.
5
+ # allows only reading
6
+ class TemplateSet < ActiveResource::Base
7
+ extend StudioResource
8
+ undef_method :save
9
+ undef_method :destroy
10
+ element_name = "template_set"
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module StudioApi
2
+ # Utility class for handling whole stack of Studio Api
3
+ class Util
4
+ # Set connection for all StudioApi class, so then you can use it without explicit settings
5
+ # It is useful when program use only one studio credentials
6
+ # @example
7
+ # connection = StudioApi::Connection.new ( "user", "password", "http://localhost/api")
8
+ # StudioApi::Util.configure_studio_connection connection
9
+ # appliances = StudioApi::Appliance.find :all
10
+ # @param [StudioApi::Connection] connection which is used for communication with studio
11
+ # @return [Array<Class>] return set of classes which is set
12
+
13
+ def self.configure_studio_connection connection
14
+ classes = get_all_usable_class StudioApi
15
+ classes.each {|c| c.studio_connection = connection}
16
+ end
17
+
18
+ # joins relative url for unix servers as URI.join require at least one
19
+ # absolute adress. Especially take care about only one slash otherwise studio
20
+ # returns 404.
21
+ # @param (Array<String>) args list of Strings to join
22
+ # @return (String) joined String
23
+ def self.join_relative_url(*args)
24
+ args.reduce do |base, append|
25
+ base= base[0..-2] if base.end_with? "/" #remove ending slash in base
26
+ append = append[1..-1] if append.start_with? "/" #remove leading slash in append
27
+ "#{base}/#{append}"
28
+ end
29
+ end
30
+ private
31
+ def self.get_all_usable_class (modul)
32
+ classes = modul.constants.collect{ |c| modul.const_get(c) }
33
+ classes = classes.select { |c| c.class == Class && c.respond_to?(:studio_connection=) }
34
+ inner_classes = classes.collect { |c| get_all_usable_class(c) }.flatten
35
+ classes + inner_classes
36
+ end
37
+ end
38
+ end
data/lib/studio_api.rb ADDED
@@ -0,0 +1,31 @@
1
+ #
2
+ # Copyright (c) 2009 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 'studio_api/appliance'
21
+ require 'studio_api/build'
22
+ require 'studio_api/connection'
23
+ require 'studio_api/file'
24
+ require 'studio_api/generic_request'
25
+ require 'studio_api/package'
26
+ require 'studio_api/pattern'
27
+ require 'studio_api/repository'
28
+ require 'studio_api/rpm'
29
+ require 'studio_api/running_build'
30
+ require 'studio_api/studio_resource'
31
+ require 'studio_api/template_set'