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
@@ -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'