spectre-core 1.8.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,29 @@
1
+ module Spectre
2
+ module Diagnostic
3
+ module Stopwatch
4
+ @@duration = 0.0
5
+
6
+ class << self
7
+ def start_watch
8
+ @@start_time = Time.now
9
+ end
10
+
11
+ def stop_watch
12
+ @@end_time = Time.now
13
+ end
14
+
15
+ def measure
16
+ start_watch
17
+ yield
18
+ stop_watch
19
+ end
20
+
21
+ def duration
22
+ @@end_time - @@start_time
23
+ end
24
+ end
25
+
26
+ Spectre.delegate :start_watch, :stop_watch, :duration, :measure, to: Stopwatch
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'ostruct'
2
+
3
+ def to_recursive_ostruct(hash)
4
+ OpenStruct.new(hash.each_with_object({}) do |(key, val), memo|
5
+ memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
6
+ end)
7
+ end
8
+
9
+ module Spectre
10
+ module Environment
11
+ class << self
12
+ @@environment = OpenStruct.new
13
+
14
+ def env
15
+ @@environment
16
+ end
17
+ end
18
+
19
+ Spectre.register do |config|
20
+ @@environment = to_recursive_ostruct(config)
21
+ @@environment.freeze
22
+ end
23
+
24
+ Spectre.delegate :env, to: Environment
25
+ end
26
+ end
@@ -0,0 +1,195 @@
1
+ require 'net/ftp'
2
+ require 'net/sftp'
3
+ require 'logger'
4
+ require 'json'
5
+
6
+
7
+ module Spectre
8
+ module FTP
9
+ @@cfg = {}
10
+
11
+ class FTPConnection < DslClass
12
+ def initialize host, username, password, opts, logger
13
+ @__logger = logger
14
+ @__session = nil
15
+
16
+ @__host = host
17
+ @__username = username
18
+ @__password = password
19
+ @__opts = opts
20
+ end
21
+
22
+ def connect!
23
+ return unless @__session == nil or @__session.closed?
24
+ @__logger.info "Connecting to '#{@__host}' with user '#{@__username}'"
25
+ @__session = Net::FTP.new(@__host, @__opts)
26
+ @__session.login @__username, @__password
27
+ end
28
+
29
+ def close
30
+ return unless @__session and not @__session.closed?
31
+ @__session.close
32
+ end
33
+
34
+ def can_connect?
35
+ begin
36
+ connect!
37
+ return true
38
+ rescue
39
+ return false
40
+ end
41
+ end
42
+
43
+ def download remotefile, to: File.basename(remotefile)
44
+ connect!
45
+ @__logger.info "Downloading '#{@__username}@#{@__host}:#{File.join @__session.pwd, remotefile}' to '#{File.expand_path to}'"
46
+ @__session.getbinaryfile(remotefile, to)
47
+ end
48
+
49
+ def upload localfile, to: File.basename(localfile)
50
+ connect!
51
+ @__logger.info "Uploading '#{File.expand_path localfile}' to '#{@__username}@#{@__host}:#{File.join @__session.pwd, to}'"
52
+ @__session.putbinaryfile(localfile, to)
53
+ end
54
+
55
+ def list
56
+ connect!
57
+ file_list = @__session.list
58
+ @__logger.info "Listing file in #{@__session.pwd}\n#{file_list}"
59
+ file_list
60
+ end
61
+ end
62
+
63
+
64
+ class SFTPConnection < DslClass
65
+ def initialize host, username, opts, logger
66
+ opts[:non_interactive] = true
67
+
68
+ @__logger = logger
69
+ @__session = nil
70
+ @__host = host
71
+ @__username = username
72
+ @__opts = opts
73
+ end
74
+
75
+ def connect!
76
+ return unless @__session == nil or @__session.closed?
77
+ @__logger.info "Connecting to '#{@__host}' with user '#{@__username}'"
78
+ @__session = Net::SFTP.start(@__host, @__username, @__opts)
79
+ @__session.connect!
80
+ end
81
+
82
+ def close
83
+ return unless @__session and not @__session.closed?
84
+ # @__session.close!
85
+ end
86
+
87
+ def can_connect?
88
+ begin
89
+ connect!
90
+ return true
91
+ rescue
92
+ return false
93
+ end
94
+ end
95
+
96
+ def download remotefile, to: File.basename(remotefile)
97
+ @__logger.info "Downloading '#{@__username}@#{@__host}:#{remotefile}' to '#{File.expand_path to}'"
98
+ connect!
99
+ @__session.download!(remotefile, to)
100
+ end
101
+
102
+ def upload localfile, to: File.basename(localfile)
103
+ @__logger.info "Uploading '#{File.expand_path localfile}' to '#{@__username}@#{@__host}:#{to}'"
104
+ connect!
105
+ @__session.upload!(localfile, to)
106
+ end
107
+
108
+ def stat path
109
+ connect!
110
+ file_info = @__session.stat! path
111
+ @__logger.info "Stat '#{path}'\n#{JSON.pretty_generate file_info.attributes}"
112
+ file_info.attributes
113
+ end
114
+
115
+ def exists path
116
+ begin
117
+ file_info = @__session.stat! path
118
+
119
+ rescue Net::SFTP::StatusException => e
120
+ return false if e.description == 'no such file'
121
+ raise e
122
+ end
123
+
124
+ return true
125
+ end
126
+ end
127
+
128
+
129
+ class << self
130
+ def ftp name, config={}, &block
131
+ raise "FTP connection '#{name}' not configured" unless @@cfg.has_key?(name) or config.count > 0
132
+ cfg = @@cfg[name] || {}
133
+
134
+ host = config[:host] || cfg['host'] || name
135
+ username = config[:username] || cfg['username']
136
+ password = config[:password] || cfg['password']
137
+
138
+ opts = {}
139
+ opts[:ssl] = config[:ssl]
140
+ opts[:port] = config[:port] || cfg['port'] || 21
141
+
142
+ @@logger.info "Connecting to #{host} with user #{username}"
143
+
144
+ ftp_conn = FTPConnection.new(host, username, password, opts, @@logger)
145
+
146
+ begin
147
+ ftp_conn.instance_eval &block
148
+ ensure
149
+ ftp_conn.close
150
+ end
151
+ end
152
+
153
+ def sftp name, config={}, &block
154
+ raise "FTP connection '#{name}' not configured" unless @@cfg.has_key?(name) or config.count > 0
155
+
156
+ cfg = @@cfg[name] || {}
157
+
158
+ host = config[:host] || cfg['host'] || name
159
+ username = config[:username] || cfg['username']
160
+ password = config[:password] || cfg['password']
161
+
162
+ opts = {}
163
+ opts[:password] = password
164
+ opts[:port] = config[:port] || cfg['port'] || 22
165
+ opts[:keys] = [cfg['key']] if cfg.has_key? 'key'
166
+ opts[:passphrase] = cfg['passphrase'] if cfg.has_key? 'passphrase'
167
+
168
+ opts[:auth_methods] = []
169
+ opts[:auth_methods].push 'publickey' if opts[:keys]
170
+ opts[:auth_methods].push 'password' if opts[:password]
171
+
172
+ sftp_con = SFTPConnection.new(host, username, opts, @@logger)
173
+
174
+ begin
175
+ sftp_con.instance_eval &block
176
+ ensure
177
+ sftp_con.close
178
+ end
179
+ end
180
+ end
181
+
182
+ Spectre.register do |config|
183
+ @@logger = ::Logger.new config['log_file'], progname: 'spectre/ftp'
184
+
185
+ if config.has_key? 'ftp'
186
+
187
+ config['ftp'].each do |name, cfg|
188
+ @@cfg[name] = cfg
189
+ end
190
+ end
191
+ end
192
+
193
+ Spectre.delegate :ftp, :sftp, to: self
194
+ end
195
+ end
@@ -0,0 +1,64 @@
1
+ require 'securerandom'
2
+ require 'json'
3
+ require 'date'
4
+ require 'ostruct'
5
+
6
+ class ::String
7
+ def as_json
8
+ JSON.parse(self, object_class: OpenStruct)
9
+ end
10
+
11
+ def as_date
12
+ DateTime.parse(self)
13
+ end
14
+
15
+ def content with: nil
16
+ fail "'#{self}' is not a file path, or the file does not exist." if !File.exists? self
17
+ file_content = File.read(self)
18
+
19
+ if with
20
+ with.each do |key, value|
21
+ file_content = file_content.gsub '#{' + key.to_s + '}', value.to_s
22
+ end
23
+ end
24
+
25
+ file_content
26
+ end
27
+
28
+ def exists?
29
+ File.exists? self
30
+ end
31
+
32
+ def remove!
33
+ fail "'#{self}' is not a file path, or the file does not exist." if !File.exists? self
34
+
35
+ File.delete self
36
+ end
37
+
38
+ def trim count=50
39
+ if (self.length + 3) > count
40
+ return self[0..count] + '...'
41
+ end
42
+
43
+ self
44
+ end
45
+ end
46
+
47
+
48
+ class ::OpenStruct
49
+ def to_json *args, **kwargs
50
+ self.to_h.inject({}) { |memo, (k,v)| memo[k] = v.is_a?(OpenStruct) ? v.to_h : v; memo }.to_json(*args, **kwargs)
51
+ end
52
+ end
53
+
54
+
55
+ class ::Hash
56
+ def symbolize_keys
57
+ self.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
58
+ end
59
+ end
60
+
61
+
62
+ def uuid length = 5
63
+ SecureRandom.uuid().gsub('-', '')[0..length]
64
+ end
@@ -0,0 +1,343 @@
1
+ require 'net/http'
2
+ require 'openssl'
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'logger'
6
+ require 'ostruct'
7
+
8
+
9
+ module Spectre
10
+ module Http
11
+ DEFAULT_HTTP_CONFIG = {
12
+ 'method' => 'GET',
13
+ 'path' => '',
14
+ 'host' => nil,
15
+ 'port' => 80,
16
+ 'scheme' => 'http',
17
+ 'use_ssl' => false,
18
+ 'cert' => nil,
19
+ 'headers' => nil,
20
+ 'query' => nil,
21
+ 'content_type' => '',
22
+ }
23
+
24
+ @@modules = []
25
+
26
+ class SpectreHttpRequest < Spectre::DslClass
27
+ def initialize request
28
+ @__req = request
29
+ end
30
+
31
+ def method method_name
32
+ @__req['method'] = method_name.upcase
33
+ end
34
+
35
+ def url base_url
36
+ @__req['base_url'] = base_url
37
+ end
38
+
39
+ def path url_path
40
+ @__req['path'] = url_path
41
+ end
42
+
43
+ def header name, value
44
+ @__req['headers'] = [] if not @__req['headers']
45
+ @__req['headers'].append [name, value.to_s.strip]
46
+ end
47
+
48
+ def param name, value
49
+ @__req['query'] = [] if not @__req['query']
50
+ @__req['query'].append [name, value.to_s.strip]
51
+ end
52
+
53
+ def content_type media_type
54
+ @__req['headers'] = [] if not @__req['headers']
55
+ @__req['headers'].append ['Content-Type', media_type]
56
+ end
57
+
58
+ def json data
59
+ data = data.to_h if data.is_a? OpenStruct
60
+ body JSON.pretty_generate(data)
61
+ content_type 'application/json'
62
+ end
63
+
64
+ def body body_content
65
+ raise 'Body value must be a string' if not body_content.is_a? String
66
+ @__req['body'] = body_content
67
+ end
68
+
69
+ def ensure_success!
70
+ @__req['ensure_success'] = true
71
+ end
72
+
73
+ def ensure_success?
74
+ @__req['ensure_success']
75
+ end
76
+
77
+ def authenticate method
78
+ @__req['auth'] = method
79
+ end
80
+
81
+ def certificate path
82
+ @__req['cert'] = path
83
+ use_ssl!
84
+ end
85
+
86
+ def use_ssl!
87
+ @__req['use_ssl'] = true
88
+ end
89
+
90
+ def to_s
91
+ @__req.to_s
92
+ end
93
+
94
+ alias_method :auth, :authenticate
95
+ alias_method :cert, :certificate
96
+ alias_method :media_type, :content_type
97
+ end
98
+
99
+ class SpectreHttpHeader
100
+ def initialize headers
101
+ @headers = headers || {}
102
+ end
103
+
104
+ def [] key
105
+ @headers[key.downcase].first
106
+ end
107
+
108
+ def to_s
109
+ @headers.to_s
110
+ end
111
+ end
112
+
113
+ class SpectreHttpResponse
114
+ def initialize res
115
+ @res = res
116
+ @data = nil
117
+ @headers = SpectreHttpHeader.new @res[:headers]
118
+ end
119
+
120
+ def code
121
+ @res[:code]
122
+ end
123
+
124
+ def message
125
+ @res[:message]
126
+ end
127
+
128
+ def headers
129
+ @headers
130
+ end
131
+
132
+ def body
133
+ @res[:body]
134
+ end
135
+
136
+ def json
137
+ return nil if not @res[:body]
138
+
139
+ if @data == nil
140
+ begin
141
+ @data = JSON.parse(@res[:body], object_class: OpenStruct)
142
+ rescue
143
+ raise "Body content is not a valid JSON:\n#{@res[:body]}"
144
+ end
145
+ end
146
+
147
+ @data
148
+ end
149
+
150
+ def success?
151
+ @res[:code] < 400
152
+ end
153
+
154
+ def to_s
155
+ @res.to_s
156
+ end
157
+
158
+ def pretty
159
+ @res.pretty
160
+ end
161
+ end
162
+
163
+
164
+ class << self
165
+ @@http_cfg = {}
166
+ @@response = nil
167
+ @@request = nil
168
+ @@modules = []
169
+
170
+ def https name, &block
171
+ http(name, secure: true, &block)
172
+ end
173
+
174
+ def http name, secure: nil, &block
175
+ req = {}
176
+
177
+ if @@http_cfg.has_key? name
178
+ req.merge! @@http_cfg[name]
179
+ raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." if !req['base_url']
180
+ else
181
+ req['base_url'] = name
182
+ end
183
+
184
+ req['user_ssl'] = secure if secure != nil
185
+
186
+ SpectreHttpRequest.new(req).instance_eval(&block) if block_given?
187
+
188
+ invoke(req)
189
+ end
190
+
191
+ def request
192
+ raise 'No request has been invoked yet' unless @@request
193
+ @@request
194
+ end
195
+
196
+ def response
197
+ raise 'There is no response. No request has been invoked yet.' unless @@response
198
+ @@response
199
+ end
200
+
201
+ def register mod
202
+ raise 'Module must not be nil' unless mod
203
+ @@modules << mod
204
+ end
205
+
206
+ private
207
+
208
+ def try_format_json str, pretty: false
209
+ return str unless str or str.empty?
210
+
211
+ begin
212
+ json = JSON.parse str
213
+
214
+ if pretty
215
+ str = JSON.pretty_generate(json)
216
+ else
217
+ str = JSON.dump(json)
218
+ end
219
+ rescue
220
+ # do nothing
221
+ end
222
+
223
+ str
224
+ end
225
+
226
+ def invoke req
227
+ @@request = nil
228
+
229
+ if req['cert'] or req['use_ssl']
230
+ scheme = 'https'
231
+ else
232
+ scheme = 'http'
233
+ end
234
+
235
+ base_url = req['base_url']
236
+
237
+ if not base_url.match /http(?:s)?:\/\//
238
+ base_url = scheme + '://' + base_url
239
+ end
240
+
241
+ if req['path']
242
+ base_url = base_url + '/' if not base_url.end_with? '/'
243
+ base_url += req['path']
244
+ end
245
+
246
+ uri = URI(base_url)
247
+
248
+ raise "'#{uri}' is not a valid uri" if not uri.host
249
+
250
+ uri.query = URI.encode_www_form(req['query']) unless not req['query'] or req['query'].empty?
251
+
252
+ net_http = Net::HTTP.new(uri.host, uri.port)
253
+
254
+ if uri.scheme == 'https'
255
+ net_http.use_ssl = true
256
+
257
+ if req.has_key? 'cert'
258
+ raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
259
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
260
+ net_http.ca_file = req['cert']
261
+ else
262
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_NONE
263
+ end
264
+ end
265
+
266
+ net_req = Net::HTTPGenericRequest.new(req['method'], true, true, uri)
267
+ net_req.body = req['body']
268
+ net_req.content_type = req['content_type'] if req['content_type'] and not req['content_type'].empty?
269
+
270
+ if req['headers']
271
+ req['headers'].each do |header|
272
+ net_req[header[0]] = header[1]
273
+ end
274
+ end
275
+
276
+ req_id = SecureRandom.uuid()[0..5]
277
+
278
+ # Log request
279
+
280
+ req_log = "[>] #{req_id} #{req['method']} #{uri}"
281
+ req['headers'].each do |header|
282
+ req_log += "\n#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}"
283
+ end if req['headers']
284
+ req_log += "\n" + try_format_json(req['body'], pretty: true) if req['body'] != nil and not req['body'].empty?
285
+
286
+ @@logger.info req_log
287
+
288
+ # Request
289
+
290
+ start_time = Time.now
291
+
292
+ @@modules.each do |mod|
293
+ mod.on_req(net_http, net_req, req) if mod.respond_to? :on_req
294
+ end
295
+
296
+ net_res = net_http.request(net_req)
297
+
298
+ end_time = Time.now
299
+
300
+ @@modules.each do |mod|
301
+ mod.on_res(net_http, net_res, req) if mod.respond_to? :on_res
302
+ end
303
+
304
+ # Log response
305
+
306
+ res_log = "[<] #{req_id} #{net_res.code} #{net_res.message} (#{end_time - start_time}s)\n"
307
+ net_res.each_header do |header, value|
308
+ res_log += "#{header.to_s.ljust(30, '.')}: #{value}\n"
309
+ end
310
+ res_log += try_format_json(net_res.body, pretty: true) if net_res.body != nil and !net_res.body.empty?
311
+
312
+ @@logger.info(res_log)
313
+
314
+ if req['ensure_success']
315
+ code = Integer(net_res.code)
316
+ fail "Response code of #{req_id} did not indicate success: #{net_res.code} #{net_res.message}" if code >= 400
317
+ end
318
+
319
+ @@request = OpenStruct.new(req)
320
+ @@response = SpectreHttpResponse.new({
321
+ code: net_res.code.to_i,
322
+ message: net_res.message,
323
+ headers: net_res.to_hash,
324
+ body: net_res.body
325
+ })
326
+ end
327
+ end
328
+
329
+ Spectre.register do |config|
330
+ @@logger = ::Logger.new config['log_file'], progname: 'spectre/http'
331
+
332
+ if config.has_key? 'http'
333
+ @@http_cfg = {}
334
+
335
+ config['http'].each do |name, cfg|
336
+ @@http_cfg[name] = cfg
337
+ end
338
+ end
339
+ end
340
+
341
+ Spectre.delegate :http, :https, :request, :response, to: self
342
+ end
343
+ end