hyperkit 1.0.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.
@@ -0,0 +1,140 @@
1
+ ################################################################################
2
+ # #
3
+ # Modeled on Octokit::Default #
4
+ # #
5
+ # Original Octokit license #
6
+ # ---------------------------------------------------------------------------- #
7
+ # Copyright (c) 2009-2016 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober #
8
+ # #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a #
10
+ # copy of this software and associated documentation files (the "Software"), #
11
+ # to deal in the Software without restriction, including without limitation #
12
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense, #
13
+ # and/or sell copies of the Software, and to permit persons to whom the #
14
+ # Software is furnished to do so, subject to the following conditions: #
15
+ # #
16
+ # The above copyright notice and this permission notice shall be included #
17
+ # in all copies or substantial portions of the Software. #
18
+ # ---------------------------------------------------------------------------- #
19
+ # #
20
+ ################################################################################
21
+
22
+ require 'openssl'
23
+ require 'hyperkit/middleware/follow_redirects'
24
+ require 'hyperkit/response/raise_error'
25
+
26
+ module Hyperkit
27
+
28
+ # Default configuration options for {Client}
29
+ module Default
30
+
31
+ # Default API endpoint
32
+ API_ENDPOINT = "https://localhost:8443".freeze
33
+
34
+ # Default auto-sync value
35
+ AUTO_SYNC = true
36
+
37
+ # Default client certificate file for authentication
38
+ CLIENT_CERT = File.join(ENV['HOME'], '.config', 'lxc', 'client.crt').freeze
39
+
40
+ # Default client key file for authentication
41
+ CLIENT_KEY = File.join(ENV['HOME'], '.config', 'lxc', 'client.key').freeze
42
+
43
+ # Default media type
44
+ MEDIA_TYPE = 'application/json'
45
+
46
+ # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
47
+ RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
48
+
49
+ # Default Faraday middleware stack
50
+ MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
51
+ builder.use Hyperkit::Middleware::FollowRedirects
52
+ builder.use Hyperkit::Response::RaiseError
53
+ builder.adapter Faraday.default_adapter
54
+ end
55
+
56
+ # Default User Agent header string
57
+ USER_AGENT = "Hyperkit Ruby Gem #{Hyperkit::VERSION}".freeze
58
+
59
+ # Default to verifying SSL certificates
60
+ VERIFY_SSL = true
61
+
62
+ class << self
63
+
64
+ # Default options for Faraday::Connection
65
+ # @return [Hash]
66
+ def connection_options
67
+ {
68
+ :headers => {
69
+ :accept => default_media_type,
70
+ :user_agent => user_agent,
71
+ },
72
+ :ssl => {}
73
+ }
74
+ end
75
+
76
+ # Default media type from ENV or {MEDIA_TYPE}
77
+ # @return [String]
78
+ def default_media_type
79
+ ENV['HYPERKIT_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
80
+ end
81
+
82
+ # Configuration options
83
+ # @return [Hash]
84
+ def options
85
+ Hash[Hyperkit::Configurable.keys.map{|key| [key, send(key)]}]
86
+ end
87
+
88
+ # Default API endpoint from ENV or {API_ENDPOINT}
89
+ # @return [String]
90
+ def api_endpoint
91
+ ENV['HYPERKIT_API_ENDPOINT'] || API_ENDPOINT
92
+ end
93
+
94
+ # Default auto-sync value from ENV or {AUTO_SYNC}
95
+ def auto_sync
96
+ ENV['HYPERKIT_AUTO_SYNC'] || AUTO_SYNC
97
+ end
98
+
99
+ # Default client certificate file from ENV or {CLIENT_CERT}
100
+ # @return [String]
101
+ def client_cert
102
+ ENV['HYPERKIT_CLIENT_CERT'] || CLIENT_CERT
103
+ end
104
+
105
+ # Default client key file from ENV or {CLIENT_KEY}
106
+ # @return [String]
107
+ def client_key
108
+ ENV['HYPERKIT_KEY'] || CLIENT_KEY
109
+ end
110
+
111
+ # Default middleware stack for Faraday::Connection
112
+ # from {MIDDLEWARE}
113
+ # @return [String]
114
+ def middleware
115
+ MIDDLEWARE
116
+ end
117
+
118
+ # Default proxy server URI for Faraday connection from ENV
119
+ # @return [String]
120
+ def proxy
121
+ ENV['HYPERKIT_PROXY']
122
+ end
123
+
124
+ # Default User-Agent header string from ENV or {USER_AGENT}
125
+ # @return [String]
126
+ def user_agent
127
+ ENV['HYPERKIT_USER_AGENT'] || USER_AGENT
128
+ end
129
+
130
+ # Default to verifying peer SSL certificate
131
+ # @return [Boolean]
132
+ def verify_ssl
133
+ true
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+
140
+ end
@@ -0,0 +1,267 @@
1
+ ################################################################################
2
+ # #
3
+ # Modeled on Octokit::Error #
4
+ # #
5
+ # Original Octokit license #
6
+ # ---------------------------------------------------------------------------- #
7
+ # Copyright (c) 2009-2016 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober #
8
+ # #
9
+ # Permission is hereby granted, free of charge, to any person obtaining a #
10
+ # copy of this software and associated documentation files (the "Software"), #
11
+ # to deal in the Software without restriction, including without limitation #
12
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense, #
13
+ # and/or sell copies of the Software, and to permit persons to whom the #
14
+ # Software is furnished to do so, subject to the following conditions: #
15
+ # #
16
+ # The above copyright notice and this permission notice shall be included #
17
+ # in all copies or substantial portions of the Software. #
18
+ # ---------------------------------------------------------------------------- #
19
+ # #
20
+ ################################################################################
21
+
22
+
23
+ module Hyperkit
24
+
25
+ # Custom error class for rescuing from all LXD errors
26
+ class Error < StandardError
27
+
28
+ # Returns the appropriate Hyperkit::Error subclass based
29
+ # on status and response message
30
+ #
31
+ # @param [Hash] response HTTP response
32
+ # @return [Hyperkit::Error]
33
+ def self.from_response(response)
34
+
35
+ status = response[:status].to_i
36
+
37
+ err = from_status(response, status)
38
+
39
+ if err.nil?
40
+ err = from_async_operation(response)
41
+ end
42
+
43
+ err
44
+
45
+ end
46
+
47
+ def self.from_async_operation(response)
48
+
49
+ return nil if response.nil? || response[:body].empty?
50
+
51
+ begin
52
+ body = JSON.parse(response[:body])
53
+ rescue
54
+ return nil
55
+ end
56
+
57
+ if body.has_key?("metadata") && body["metadata"].is_a?(Hash)
58
+ status = body["metadata"]["status_code"].to_i
59
+ from_status(response, status)
60
+ end
61
+
62
+ end
63
+
64
+ def self.from_status(response, status)
65
+ if klass = case status
66
+ when 400 then Hyperkit::BadRequest
67
+ when 401 then Hyperkit::Unauthorized
68
+ when 403 then Hyperkit::Forbidden
69
+ when 404 then Hyperkit::NotFound
70
+ when 405 then Hyperkit::MethodNotAllowed
71
+ when 406 then Hyperkit::NotAcceptable
72
+ when 409 then Hyperkit::Conflict
73
+ when 415 then Hyperkit::UnsupportedMediaType
74
+ when 422 then Hyperkit::UnprocessableEntity
75
+ when 400..499 then Hyperkit::ClientError
76
+ when 500 then error_for_500(response)
77
+ when 501 then Hyperkit::NotImplemented
78
+ when 502 then Hyperkit::BadGateway
79
+ when 503 then Hyperkit::ServiceUnavailable
80
+ when 500..599 then Hyperkit::ServerError
81
+ end
82
+ klass.new(response)
83
+ end
84
+ end
85
+
86
+ def initialize(response=nil)
87
+ @response = response
88
+ super(build_error_message)
89
+ end
90
+
91
+ # Documentation URL returned by the API for some errors
92
+ #
93
+ # @return [String]
94
+ def documentation_url
95
+ data[:documentation_url] if data.is_a? Hash
96
+ end
97
+
98
+ # Array of validation errors
99
+ # @return [Array<Hash>] Error info
100
+ def errors
101
+ if data && data.is_a?(Hash)
102
+ data[:errors] || []
103
+ else
104
+ []
105
+ end
106
+ end
107
+
108
+ def self.error_for_500(response)
109
+
110
+ if response.body =~ /open: no such file or directory/i
111
+ Hyperkit::NotFound
112
+ elsif response.body =~ /open: is a directory/i
113
+ Hyperkit::BadRequest
114
+ else
115
+ Hyperkit::InternalServerError
116
+ end
117
+
118
+ end
119
+
120
+ private
121
+
122
+ def data
123
+ @data ||=
124
+ if (body = @response[:body]) && !body.empty?
125
+ if body.is_a?(String) &&
126
+ @response[:response_headers] &&
127
+ @response[:response_headers][:content_type] =~ /json/
128
+
129
+ Sawyer::Agent.serializer.decode(body)
130
+ else
131
+ body
132
+ end
133
+ else
134
+ nil
135
+ end
136
+ end
137
+
138
+ def response_message
139
+ case data
140
+ when Hash
141
+ data[:message]
142
+ when String
143
+ data
144
+ end
145
+ end
146
+
147
+ def response_error
148
+ err = nil
149
+
150
+ if data.is_a?(Hash) && data[:error]
151
+ err = data[:error]
152
+ elsif data.is_a?(Hash) && data[:metadata]
153
+ err = data[:metadata][:err]
154
+ end
155
+
156
+ "Error: #{err}" if err
157
+ end
158
+
159
+ def response_error_summary
160
+ return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
161
+
162
+ summary = "\nError summary:\n"
163
+ summary << data[:errors].map do |hash|
164
+ hash.map { |k,v| " #{k}: #{v}" }
165
+ end.join("\n")
166
+
167
+ summary
168
+ end
169
+
170
+ def build_error_message
171
+ return nil if @response.nil?
172
+
173
+ message = ""
174
+
175
+ if ! data.is_a?(Hash) || ! data[:metadata] || ! data[:metadata][:err]
176
+ message = "#{@response[:method].to_s.upcase} "
177
+ message << redact_url(@response[:url].to_s) + ": "
178
+ end
179
+
180
+ if data.is_a?(Hash) && data[:metadata] && data[:metadata][:status_code]
181
+ message << "#{data[:metadata][:status_code]} - "
182
+ else
183
+ message << "#{@response[:status]} - "
184
+ end
185
+
186
+ message << "#{response_message}" unless response_message.nil?
187
+ message << "#{response_error}" unless response_error.nil?
188
+ message << "#{response_error_summary}" unless response_error_summary.nil?
189
+ message << " // See: #{documentation_url}" unless documentation_url.nil?
190
+ message
191
+ end
192
+
193
+ def redact_url(url_string)
194
+ %w[secret].each do |token|
195
+ url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
196
+ end
197
+ url_string
198
+ end
199
+ end
200
+
201
+ # Raised on errors in the 400-499 range
202
+ class ClientError < Error; end
203
+
204
+ # Raised when LXD returns a 400 HTTP status code
205
+ class BadRequest < ClientError; end
206
+
207
+ # Raised when LXD returns a 401 HTTP status code
208
+ class Unauthorized < ClientError; end
209
+
210
+ # Raised when LXD returns a 403 HTTP status code
211
+ class Forbidden < ClientError; end
212
+
213
+ # Raised when LXD returns a 404 HTTP status code
214
+ class NotFound < ClientError; end
215
+
216
+ # Raised when LXD returns a 405 HTTP status code
217
+ class MethodNotAllowed < ClientError; end
218
+
219
+ # Raised when LXD returns a 406 HTTP status code
220
+ class NotAcceptable < ClientError; end
221
+
222
+ # Raised when LXD returns a 409 HTTP status code
223
+ class Conflict < ClientError; end
224
+
225
+ # Raised when LXD returns a 414 HTTP status code
226
+ class UnsupportedMediaType < ClientError; end
227
+
228
+ # Raised when LXD returns a 422 HTTP status code
229
+ class UnprocessableEntity < ClientError; end
230
+
231
+ # Raised on errors in the 500-599 range
232
+ class ServerError < Error; end
233
+
234
+ # Raised when LXD returns a 500 HTTP status code
235
+ class InternalServerError < ServerError; end
236
+
237
+ # Raised when LXD returns a 501 HTTP status code
238
+ class NotImplemented < ServerError; end
239
+
240
+ # Raised when LXD returns a 502 HTTP status code
241
+ class BadGateway < ServerError; end
242
+
243
+ # Raised when LXD returns a 503 HTTP status code
244
+ class ServiceUnavailable < ServerError; end
245
+
246
+ # Raised when a method requires an alias or a fingerprint, but
247
+ # none is provided
248
+ class ImageIdentifierRequired < StandardError; end
249
+
250
+ # Raised when a method requires attributes of an alias to be
251
+ # passed (e.g. description, traget), but none is provided
252
+ class AliasAttributesRequired < StandardError; end
253
+
254
+ # Raised when a method requires a protocol to be specified
255
+ # (e.g. "lxd" or "simplestreams"), but an invalid protocol is
256
+ # provided
257
+ class InvalidProtocol < StandardError; end
258
+
259
+ # Raise when a method is passed image attributes that are illegal
260
+ # (e.g. creating a container from a local image, but passing a protocol)
261
+ class InvalidImageAttributes < StandardError; end
262
+
263
+ # Raise when profiles are specified that do not exist on the server
264
+ class MissingProfiles < StandardError; end
265
+
266
+ end
267
+
@@ -0,0 +1,131 @@
1
+ require 'faraday'
2
+ require 'set'
3
+
4
+ module Hyperkit
5
+
6
+ module Middleware
7
+
8
+ # Public: Exception thrown when the maximum amount of requests is exceeded.
9
+ #
10
+ # Taken from Octokit, which was originally adapted from
11
+ # https://github.com/lostisland/faraday_middleware/blob/138766e/lib/faraday_middleware/response/follow_redirects.rb
12
+
13
+ class RedirectLimitReached < Faraday::Error::ClientError
14
+ attr_reader :response
15
+
16
+ def initialize(response)
17
+ super "too many redirects; last one to: #{response['location']}"
18
+ @response = response
19
+ end
20
+ end
21
+
22
+ # Public: Follow HTTP 301, 302, 303, and 307 redirects.
23
+ #
24
+ # For HTTP 303, the original GET, POST, PUT, DELETE, or PATCH request gets
25
+ # converted into a GET. For HTTP 301, 302, and 307, the HTTP method remains
26
+ # unchanged.
27
+ #
28
+ # This middleware currently only works with synchronous requests; i.e. it
29
+ # doesn't support parallelism.
30
+ class FollowRedirects < Faraday::Middleware
31
+ # HTTP methods for which 30x redirects can be followed
32
+ ALLOWED_METHODS = Set.new [:head, :options, :get, :post, :put, :patch, :delete]
33
+
34
+ # HTTP redirect status codes that this middleware implements
35
+ REDIRECT_CODES = Set.new [301, 302, 303, 307]
36
+
37
+ # Keys in env hash which will get cleared between requests
38
+ ENV_TO_CLEAR = Set.new [:status, :response, :response_headers]
39
+
40
+ # Default value for max redirects followed
41
+ FOLLOW_LIMIT = 3
42
+
43
+ # Regex that matches characters that need to be escaped in URLs, sans
44
+ # the "%" character which we assume already represents an escaped
45
+ # sequence.
46
+ URI_UNSAFE = /[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]%]/
47
+
48
+ # Public: Initialize the middleware.
49
+ #
50
+ # options - An options Hash (default: {}):
51
+ # :limit - A Fixnum redirect limit (default: 3).
52
+ def initialize(app, options = {})
53
+ super(app)
54
+ @options = options
55
+
56
+ @convert_to_get = Set.new [303]
57
+ end
58
+
59
+ def call(env)
60
+ perform_with_redirection(env, follow_limit)
61
+ end
62
+
63
+ private
64
+
65
+ def convert_to_get?(response)
66
+ ![:head, :options].include?(response.env[:method]) &&
67
+ @convert_to_get.include?(response.status)
68
+ end
69
+
70
+ def perform_with_redirection(env, follows)
71
+ request_body = env[:body]
72
+ response = @app.call(env)
73
+
74
+ response.on_complete do |response_env|
75
+ if follow_redirect?(response_env, response)
76
+ raise(RedirectLimitReached, response) if follows.zero?
77
+ new_request_env = update_env(response_env, request_body, response)
78
+ response = perform_with_redirection(new_request_env, follows - 1)
79
+ end
80
+ end
81
+ response
82
+ end
83
+
84
+ def update_env(env, request_body, response)
85
+ original_url = env[:url]
86
+ env[:url] += safe_escape(response["location"])
87
+ unless same_host?(original_url, env[:url])
88
+ env[:request_headers].delete("Authorization")
89
+ end
90
+
91
+ if convert_to_get?(response)
92
+ env[:method] = :get
93
+ env[:body] = nil
94
+ else
95
+ env[:body] = request_body
96
+ end
97
+
98
+ ENV_TO_CLEAR.each { |key| env.delete(key) }
99
+
100
+ env
101
+ end
102
+
103
+ def follow_redirect?(env, response)
104
+ ALLOWED_METHODS.include?(env[:method]) &&
105
+ REDIRECT_CODES.include?(response.status)
106
+ end
107
+
108
+ def follow_limit
109
+ @options.fetch(:limit, FOLLOW_LIMIT)
110
+ end
111
+
112
+ def same_host?(original_url, redirect_url)
113
+ original_uri = Addressable::URI.parse(original_url)
114
+ redirect_uri = Addressable::URI.parse(redirect_url)
115
+
116
+ redirect_uri.host.nil? || original_uri.host == redirect_uri.host
117
+ end
118
+
119
+ # Internal: Escapes unsafe characters from a URL which might be a path
120
+ # component only or a fully-qualified URI so that it can be joined onto a
121
+ # URI:HTTP using the `+` operator. Doesn't escape "%" characters so to not
122
+ # risk double-escaping.
123
+ def safe_escape(uri)
124
+ uri.to_s.gsub(URI_UNSAFE) { |match|
125
+ "%" + match.unpack("H2" * match.bytesize).join("%").upcase
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+