onering-client 0.0.46 → 0.0.50

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.
data/lib/onering/api.rb CHANGED
@@ -1,221 +1,276 @@
1
- require 'rest_client'
2
- require 'uri'
3
- require 'multi_json'
4
1
  require 'yaml'
5
- require 'addressable/uri'
2
+ require 'hashlib'
6
3
  require 'deep_merge'
4
+ require 'addressable/uri'
5
+ require 'active_support/core_ext'
6
+ require 'httparty'
7
7
 
8
8
  module Onering
9
- module API
9
+ class API
10
+ module Actions
11
+ class Retry < ::Exception; end
12
+ end
13
+
10
14
  module Errors
15
+ class Exception < ::Exception; end
11
16
  class NotConnected < Exception; end
12
17
  class ClientError < Exception; end
13
18
  class ServerError < Exception; end
14
19
  class ConnectionTimeout < Exception; end
15
- class ClientPemNotFound < Exception; end
16
- end
17
-
18
- class Base
19
- include Onering::Util
20
-
21
- DEFAULT_BASE="https://onering"
22
- DEFAULT_PATH="/api"
23
- DEFAULT_OPTIONS_FILE=[
24
- "~/.onering/cli.yml",
25
- "/etc/onering/cli.yml"
26
- ]
27
-
28
- DEFAULT_CLIENT_PEM=[
29
- "~/.onering/client.pem",
30
- "/etc/onering/client.pem"
31
- ]
32
-
33
- DEFAULT_VALIDATION_PEM=[
34
- "/etc/onering/validation.pem"
35
- ]
36
-
37
- class<<self
38
- def connect(options={})
39
- # list all existing config files from least specific to most
40
- @_configfiles = ([options[:config]] + DEFAULT_OPTIONS_FILE).compact.select{|i|
41
- (File.exists?(File.expand_path(i)) rescue false)
42
- }.reverse
43
-
44
- # merge all config files with more-specific values overriding less-specific ones
45
- @_config = {}
46
- @_configfiles.each do |i|
47
- c = YAML.load(File.read(File.expand_path(i))) rescue {}
48
- @_config.deep_merge!(c)
49
- end
20
+ class AuthenticationMissing < Exception; end
21
+ end
50
22
 
51
- if options[:host].is_a?(URI)
52
- @_uri = options[:host]
53
- elsif options[:host].is_a?(String)
54
- @_uri = Addressable::URI.parse("#{options[:host]}/#{DEFAULT_PATH}")
55
- else
56
- @_uri = Addressable::URI.parse("#{@_config['url'] || DEFAULT_BASE}/#{@_config['apiroot'] || DEFAULT_PATH}")
57
- end
23
+ include Onering::Util
24
+ include ::HTTParty
58
25
 
59
- unless @_uri.nil?
60
- begin
61
- @_pemfile = ([options[:pemfile], @_config['pemfile']]+DEFAULT_CLIENT_PEM).compact.select{|i|
62
- (File.exists?((File.expand_path(i) rescue i)) rescue nil)
63
- }.compact.first
26
+ attr_accessor :url
27
+ format :json
64
28
 
65
- raise Errors::ClientPemNotFound if @_pemfile.nil?
29
+ DEFAULT_CONFIG={}
30
+ DEFAULT_BASE="https://onering"
31
+ DEFAULT_PATH="/api"
32
+ DEFAULT_OPTIONS_FILE=["~/.onering/cli.yml", "/etc/onering/cli.yml"]
33
+ DEFAULT_CLIENT_PEM=["~/.onering/client.pem", "/etc/onering/client.pem"]
34
+ DEFAULT_CLIENT_KEY=["~/.onering/client.key", "/etc/onering/client.key"]
35
+ DEFAULT_VALIDATION_PEM="/etc/onering/validation.pem"
66
36
 
67
- @_pem = File.read((File.expand_path(@_pemfile) rescue @_pemfile))
68
37
 
69
- @rest = RestClient::Resource.new("#{@_uri.scheme}://#{@_uri.host}:#{@_uri.port || 443}", {
70
- :timeout => 120,
71
- :open_timeout => 30,
72
- :ssl_client_cert => OpenSSL::X509::Certificate.new(@_pem),
73
- :ssl_client_key => OpenSSL::PKey::RSA.new(@_pem),
74
- :verify_peer => OpenSSL::SSL::VERIFY_PEER
75
- })
38
+ def initialize(options={})
39
+ @_config = {}
40
+ @_plugins = {}
41
+ @_connection_options = options
76
42
 
77
- rescue Errors::ClientPemNotFound
78
- # client PEM not present, attempt autoregistration
79
- STDERR.puts("Onering client.pem not found, attempting automatic registration...")
43
+ # load and merge all config file sources
44
+ _load_config(@_connection_options[:configfile], @_connection_options.get(:config, {}))
80
45
 
81
- begin
82
- @_validation = ([options[:validationfile], @_config['validationfile']]+DEFAULT_VALIDATION_PEM).compact.select{|i|
83
- (File.exists?((File.expand_path(i) rescue i)) rescue nil)
84
- }.compact.first
46
+ # set API connectivity details
47
+ Onering::API.base_uri @_config.get(:url, DEFAULT_BASE)
85
48
 
86
- if @_validation.nil?
87
- raise Errors::ClientError.new("Cannot automatically register client, cannot find validation.pem")
88
- end
49
+ Onering::Reporter.setup()
50
+ connect(options) if options.get(:autoconnect, true)
51
+ end
89
52
 
90
- @_validation = File.read(@_validation)
91
-
92
- @rest = RestClient::Resource.new("#{@_uri.scheme}://#{@_uri.host}:#{@_uri.port || 443}", {
93
- :timeout => 120,
94
- :open_timeout => 30,
95
- :ssl_client_cert => OpenSSL::X509::Certificate.new(@_validation),
96
- :ssl_client_key => OpenSSL::PKey::RSA.new(@_validation),
97
- :verify_peer => OpenSSL::SSL::VERIFY_PEER
98
- })
99
-
100
-
101
- clients = [{
102
- :path => "/etc/onering",
103
- :name => (@_config['id'] || File.read("/etc/hardware.id")).strip.chomp,
104
- :keyname => 'system',
105
- :autodelete => true
106
- },{
107
- :path => "~/.onering",
108
- :name => ENV['USER'],
109
- :keyname => 'cli',
110
- :autodelete => false
111
- }]
112
-
113
- # attempt to autoregister clients from least specific to most (machine account then user account)
114
- clients.each do |client|
115
- # determine if we can create this client
116
- client[:path] = (File.expand_path(client[:path]) rescue client[:path])
117
- next unless File.writable?(File.dirname(client[:path]))
118
- Dir.mkdir(client[:path]) unless File.directory?(client[:path])
119
- next unless File.writable?(client[:path])
120
-
121
- begin
122
- response = @rest["/api/users/#{client[:name]}/keys/#{client[:keyname]}"].get({
123
- :params => {
124
- :cert => 'pem',
125
- :autodelete => client[:autodelete]
126
- }
127
- })
128
-
129
- rescue RestClient::Forbidden
130
- STDERR.puts("Cannot re-download key '#{client[:keyname]}' for client #{client[:name]}. Please remove the client key from Onering and try again.")
131
- next
132
-
133
- rescue RestClient::Exception => e
134
- raise Errors::ClientError.new("HTTP #{e.http_code}: #{e.message}")
135
- end
136
-
137
-
138
- File.open("#{client[:path]}/client.pem", "w") do |file|
139
- file.puts(response.to_str)
140
- STDERR.puts("Successfully registered client key #{client[:name]}:#{client[:keyname]}, key is at #{file.path}")
141
- break
142
- end
143
- end
53
+ def connect(options={})
54
+ # setup authentication
55
+ _setup_auth()
144
56
 
145
- if clients.select{|i| p = "#{i[:path]}/client.pem"; File.exists?((File.expand_path(p) rescue p)) }.empty?
146
- raise Errors::ClientError.new("Unable to register a Onering client.")
147
- end
57
+ return self
58
+ end
148
59
 
149
- retry
150
60
 
151
- rescue Exception => e
152
- STDERR.puts("Error occurred during autoregistration: #{e.class.name} - #{e.message}")
153
- end
154
- end
61
+ def request(method, endpoint, options={})
62
+ endpoint = [@_config.get(:path, DEFAULT_PATH).strip, endpoint.sub(/^\//,'')].join('/')
63
+
64
+ case method
65
+ when :post
66
+ rv = Onering::API.post(endpoint, options)
67
+ when :put
68
+ rv = Onering::API.put(endpoint, options)
69
+ when :delete
70
+ rv = Onering::API.delete(endpoint, options)
71
+ when :head
72
+ rv = Onering::API.head(endpoint, options)
73
+ else
74
+ rv = Onering::API.get(endpoint, options)
75
+ end
155
76
 
156
- else
157
- raise Errors::ClientError.new("Could not parse API URL.")
158
- end
159
- end
77
+ if rv.code >= 500
78
+ raise Errors::ServerError.new("HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message','') rescue ''}")
79
+ elsif rv.code >= 400
80
+ raise Errors::ClientError.new("HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message', '') rescue ''}")
81
+ else
82
+ rv
83
+ end
84
+ end
160
85
 
161
- def request(endpoint, options={})
162
- options = @_config.merge(options)
163
- options[:method] = (options[:method].to_s.downcase.to_sym rescue nil)
164
- request = nil
165
86
 
166
- uri = Addressable::URI.parse("#{@_uri.to_s}/#{endpoint}")
167
- uri.query_values = options[:fields] if options[:fields]
87
+ def get(endpoint, options={})
88
+ request(:get, endpoint, options)
89
+ end
168
90
 
169
- raise Errors::NotConnected unless @rest
91
+ def post(endpoint, options={}, &block)
92
+ if block_given?
93
+ request(:post, endpoint, options.merge({
94
+ :body => yield
95
+ }))
96
+ else
97
+ request(:post, endpoint, options)
98
+ end
99
+ end
170
100
 
171
- begin
172
- case options[:method]
173
- when :post
174
- data = (options[:data].nil? ? nil : MultiJson.dump(options[:data]))
101
+ def put(endpoint, options={})
102
+ if block_given?
103
+ request(:put, endpoint, options.merge({
104
+ :body => yield
105
+ }))
106
+ else
107
+ request(:put, endpoint, options)
108
+ end
109
+ end
175
110
 
176
- response = @rest[uri.request_uri].post(data, {
177
- :content_type => 'application/json'
178
- })
111
+ def delete(endpoint, options={})
112
+ request(:delete, endpoint, options)
113
+ end
179
114
 
180
- when :delete
181
- response = @rest[uri.request_uri].delete()
182
115
 
183
- when :head
184
- response = @rest[uri.request_uri].head()
185
- else
186
- response = @rest[uri.request_uri].get()
187
- end
116
+ # I'm not a huge fan of what's happening here, but metaprogramming is hard...
117
+ #
118
+ # "Don't let the perfect be the enemy of the good."
119
+ #
120
+ def method_missing(method, *args, &block)
121
+ modname = method.to_s.split('_').map(&:capitalize).join
188
122
 
189
- rescue RestClient::Unauthorized => e
190
- raise Errors::ClientError.new("You are not authorized to perform this request")
123
+ if not (plugin = (Onering::API.const_get(modname) rescue nil)).nil?
124
+ @_plugins[method] ||= plugin.new.connect(@_connection_options)
125
+ return @_plugins[method]
126
+ else
127
+ super
128
+ end
129
+ end
191
130
 
192
- rescue RestClient::Exception => e
193
- raise Errors::ClientError.new("(HTTP #{e.http_code}) #{e.class.name}: #{e.message}")
194
- end
131
+ def opt(name, default=nil)
132
+ @_config.get(name, default)
133
+ end
195
134
 
196
- begin
197
- rv = (response.empty? ? nil : MultiJson.load(response))
198
- rescue Exception
199
- rv = response
200
- end
201
135
 
202
- rv
203
- end
136
+ def status()
137
+ Onering::API.get("/").parsed_response
138
+ end
204
139
 
205
- def make_filter(filter)
206
- filter = filter.collect{|k,v| "#{k}/#{v}" } if filter.is_a?(Hash)
207
- filter = filter.collect{|i| i.sub(':','/') }.join("/") if filter.is_a?(Array)
208
- filter
209
- end
210
140
 
211
- def echo(obj)
212
- if obj.is_a?(Array)
213
- obj.each do |i|
214
- puts i
141
+
142
+ private
143
+ # -----------------------------------------------------------------------------
144
+ def _load_config(configfile, config={})
145
+ if configfile.nil?
146
+ configfile = []
147
+ else
148
+ # recursively grab all .yml files if directory is specified
149
+ configfile = (File.directory?(configfile) ? Dir.glob(File.join(configfile, "**", "*.yml")).sort : [configfile])
150
+ end
151
+
152
+ # list all existing config files from least specific to most
153
+ @_configfiles = (configfile + DEFAULT_OPTIONS_FILE).compact.select{|i|
154
+ (File.exists?(File.expand_path(i)) rescue false)
155
+ }.reverse
156
+
157
+ # merge all config files with more-specific values overriding less-specific ones
158
+ @_config = DEFAULT_CONFIG
159
+ @_configfiles.each do |i|
160
+ c = YAML.load(File.read(File.expand_path(i))) rescue {}
161
+ @_config.deep_merge!(c)
162
+ end
163
+
164
+ # settings specified in the library override everything
165
+ @_config.deep_merge!(config.compact) unless config.empty?
166
+ end
167
+
168
+
169
+ # -----------------------------------------------------------------------------
170
+ def _setup_auth()
171
+ type = @_config.get('authentication.type', :auto)
172
+
173
+ case type.to_sym
174
+ when :token
175
+ _setup_auth_token()
176
+
177
+ else
178
+ _setup_auth_ssl()
179
+ end
180
+ end
181
+
182
+
183
+ # -----------------------------------------------------------------------------
184
+ def _setup_auth_ssl()
185
+ begin
186
+ # get first keyfile found
187
+ key = (([@_config.get('authentication.keyfile')] + DEFAULT_CLIENT_PEM).compact.select{|i|
188
+ (File.exists?(File.expand_path(i)) rescue false)
189
+ }).first
190
+
191
+ # SSL client key not found, attempt autoregistration...
192
+ if key.nil?
193
+ if @_config.get('authentication.autoregister', true)
194
+ validation_key = @_config.get('authentication.validation_keyfile', DEFAULT_VALIDATION_PEM)
195
+ validation_key = (File.expand_path(validation_key) rescue validation_key)
196
+
197
+ # if validation key exists, autoregister
198
+ if File.size?(validation_key)
199
+ # set the authentication PEM to validation.pem
200
+ Onering::API.pem(File.read(validation_key))
201
+
202
+ # attempt to create client.pem from least-specific to most, first writable path wins
203
+ clients = [{
204
+ :path => "/etc/onering",
205
+ :name => fact('hardwareid'),
206
+ :keyname => 'system',
207
+ :autodelete => true
208
+ },{
209
+ :path => "~/.onering",
210
+ :name => ENV['USER'],
211
+ :keyname => 'cli',
212
+ :autodelete => false
213
+ }]
214
+
215
+ # for each client attempt...
216
+ clients.each do |client|
217
+ # expand and assemble path
218
+ client[:path] = (File.expand_path(client[:path]) rescue client[:path])
219
+ keyfile = File.join(client[:path], 'client.pem')
220
+
221
+ # skip this if we can't write to the parent directory
222
+ next unless File.writable?(client[:path])
223
+ Dir.mkdir(client[:path]) unless File.directory?(client[:path])
224
+ next if File.exists?(keyfile)
225
+
226
+ # attempt to create/download the keyfile
227
+ response = self.class.get("/api/users/#{client[:name].strip}/keys/#{client[:keyname]}")
228
+
229
+ # if successful, write the file
230
+ if response.code < 400 and response.body
231
+ File.open(keyfile, 'w').puts(response.body)
232
+ raise Actions::Retry.new
233
+ else
234
+ # all errors are fatal at this stage
235
+ raise Errors::ClientError.new("Cannot autoregister client: HTTP #{response.code} - #{(response.parsed_response || {}).get('error.message', 'Unknown error')}")
236
+ end
237
+ end
238
+
239
+ # it is an error to not have created a client.pem by now
240
+ raise Errors::AuthenticationMissing.new("Cannot autoregister client: keyfile not created")
241
+
242
+ else
243
+ # cannot autoregister without a validation.pem
244
+ raise Errors::AuthenticationMissing.new("Cannot autoregister client: validation keyfile is missing")
215
245
  end
246
+ else
247
+ raise Errors::AuthenticationMissing.new("Cannot find SSL key and autoregistration is disabled")
216
248
  end
249
+ else
250
+ Onering::API.pem(File.read((File.expand_path(key) rescue key)))
217
251
  end
252
+
253
+ rescue Actions::Retry
254
+ retry
218
255
  end
219
256
  end
257
+
258
+
259
+ # -----------------------------------------------------------------------------
260
+ def _setup_auth_token()
261
+ # get first keyfile found
262
+ key = @_config.get('authentication.key')
263
+ raise Errors::AuthenticationMissing.new("Cannot find an API token") if key.nil?
264
+
265
+ # set auth mechanism
266
+ Onering::API.headers({
267
+ 'X-Auth-Mechanism' => 'token'
268
+ })
269
+
270
+ # set default parameters
271
+ Onering::API.default_params({
272
+ :token => key
273
+ })
274
+ end
220
275
  end
221
- end
276
+ end
@@ -0,0 +1,59 @@
1
+ module Onering
2
+ module CLI
3
+ module Automation
4
+ def self.configure(global={})
5
+ @requests = Onering::CLI.connect(global).automation_requests
6
+ @jobs = Onering::CLI.connect(global).automation_jobs
7
+ @tasks = Onering::CLI.connect(global).automation_tasks
8
+ end
9
+
10
+ def self.run(args)
11
+ sc = args.shift # subcommand
12
+ ssc = args.shift # sub-subcommand
13
+
14
+ case (sc.downcase.to_sym rescue nil)
15
+ # -----------------------------------------------------------------------------
16
+ when :requests
17
+ @opts = ::Trollop::options do
18
+ banner <<-EOS
19
+ Options:
20
+ EOS
21
+ stop_on %w{show status requeue flush}
22
+ end
23
+
24
+ case (ssc.downcase.to_sym rescue nil)
25
+ when :show
26
+ return @requests.show(args[0])
27
+
28
+ when :requeue
29
+ return @requests.requeue_all_failed() if args[0].nil?
30
+ return @requests.requeue(args[0])
31
+
32
+ when :status
33
+ fields = (args.empty? ? ["status"] : args)
34
+ rv = {}
35
+ out = @requests.summary(fields)
36
+
37
+ _rejigger_hash = Proc.new do |h|
38
+ [*h].collect{|i|
39
+ [i['id'], (i['children'].nil? ? i['count'] : Hash[_rejigger_hash.call(i['children'])])]
40
+ }
41
+ end
42
+
43
+ return Hash[_rejigger_hash.call(out)]
44
+
45
+ when :flush
46
+ return @requests.flush_queue()
47
+ end
48
+
49
+ # -----------------------------------------------------------------------------
50
+ when :jobs
51
+
52
+
53
+ # -----------------------------------------------------------------------------
54
+ when :tasks
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ module Onering
2
+ module CLI
3
+ module Call
4
+ def self.configure(global={})
5
+ @api = Onering::CLI.connect(global)
6
+
7
+ @opts = ::Trollop::options do
8
+ banner <<-EOS
9
+ Call an arbitrary Onering API endpoint and return the output
10
+
11
+ Usage:
12
+ onering call [options] [endpoint]
13
+
14
+ Examples:
15
+ # Returns the API status page at path /api/
16
+ $ onering call /
17
+
18
+ # Returns details about the authenticated user
19
+ $ onering call users/current
20
+
21
+ # Delete the device called '0bf29c'
22
+ $ onering call devices/0bf29c -m delete
23
+
24
+ Options:
25
+ EOS
26
+ opt :method, "The HTTP method to use when performing the request (default: GET)", :default => 'get', :short => "-m", :type => :string
27
+ end
28
+ end
29
+
30
+ def self.run(args)
31
+ rv = @api.request(@opts[:method], args.first)
32
+ return (rv.parsed_response || rv.response.body)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,93 @@
1
+ module Onering
2
+ module CLI
3
+ module Devices
4
+ def self.configure(global={})
5
+ @api = Onering::CLI.connect(global).devices
6
+
7
+ @opts = ::Trollop::options do
8
+ banner <<-EOS
9
+ Options:
10
+ EOS
11
+
12
+ opt :query, "The Onering urlquery to filter devices by", :short => '-f', :type => :string
13
+ opt :id, "The node ID of the device to operate on", :short => '-i', :type => :string
14
+
15
+ # subcommands
16
+ stop_on %w{show get set list find save}
17
+ end
18
+ end
19
+
20
+ def self.run(args)
21
+ sc = args.shift
22
+
23
+ case (sc.downcase.to_sym rescue nil)
24
+ # -----------------------------------------------------------------------------
25
+ when :show
26
+ return @api.devices.show(args[0])
27
+
28
+ # -----------------------------------------------------------------------------
29
+ when :get
30
+ raise "Expected 1 parameter, got #{args.length}" unless args.length == 1
31
+
32
+ if @opts[:query_given]
33
+ # doing this until a bulk field query endpoint is built
34
+ return @api.list('id', {
35
+ :filter => @opts[:query]
36
+ }).collect{|i| @api.get_field(i, args[0])}
37
+
38
+ elsif @opts[:id_given]
39
+ return @api.get_field(@opts[:id], args[0])
40
+
41
+ end
42
+
43
+ # -----------------------------------------------------------------------------
44
+ when :set
45
+ raise "Expected 2 parameters, got #{args.length}" unless args.length == 2
46
+
47
+ if @opts.get(:query)
48
+ # doing this until a bulk field set endpoint is built
49
+ return @api.list('id', {
50
+ :filter => @opts[:query]
51
+ }).collect{|i| @api.set_field(i, args[0])}
52
+
53
+ elsif @opts.get(:id)
54
+ return @api.set_field(@opts[:id], args[0], args[1])
55
+
56
+ end
57
+
58
+ # -----------------------------------------------------------------------------
59
+ when :list
60
+ raise "Expected 1 parameter, got #{args.length}" unless args.length >= 1
61
+ return @api.list(args[0], {
62
+ :filter => [@opts[:query], args[1]].compact.join('/')
63
+ }.compact)
64
+
65
+ # -----------------------------------------------------------------------------
66
+ when :find
67
+ raise "Expected 1 parameter, got #{args.length}" unless args.length == 1
68
+ return @api.find(args[0])
69
+
70
+ # -----------------------------------------------------------------------------
71
+ when :save
72
+ rv = @api.save(args[0] || @opts[:id]) do
73
+ # read from pipe
74
+ if not STDIN.tty?
75
+ STDIN.read
76
+
77
+ # read from specified file
78
+ elsif (File.readable?(args[1]) rescue false)
79
+ File.read(args[1])
80
+
81
+ else
82
+ raise "Cannot save data, no input data specified"
83
+ end
84
+ end
85
+
86
+ rv.parsed_response
87
+ else
88
+ raise "Unknown subcommand #{sc.inspect}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ module Onering
2
+ module CLI
3
+ module Fact
4
+ def self.configure(global={})
5
+ Onering::Reporter.setup()
6
+
7
+ @opts = ::Trollop::options do
8
+ banner <<-EOS
9
+ Return the value of an internal fact.
10
+
11
+ Usage:
12
+ onering [global] fact [name]
13
+
14
+ EOS
15
+ end
16
+ end
17
+
18
+ def self.run(args)
19
+ return Onering::Util.fact(args[0])
20
+ end
21
+ end
22
+ end
23
+ end