onering-agent 0.4.3

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTliNjI0MzU5MTllMGFkNDg1OGQxMjBhNzBmZmFhOTIyMWY2ZTBkYg==
5
+ data.tar.gz: !binary |-
6
+ YjQ5Y2NiOTlkZjQ5YjE3OTIyZmZiNjZhNjdhOWI3MGJhNjhhMzgxZQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YzFiYjNlMWYyMmY0MzZkM2NhMjBlZGVlYzE4Y2UyZjRjNDlhOGNmNzk1ZDdk
10
+ M2E2Njg2MDM2ZDg2YzcwYzk3ZjY0YmRlYWMyNzgzMmMyZDNiZGJhMWJjNWZj
11
+ MGNkNGU1NjNhYmRhMDY4MDA2NWVhYTNhMzk5N2Y4NThiMTI3MjA=
12
+ data.tar.gz: !binary |-
13
+ MjE5MzQ5ZTlhYzc1ZjIxNDkxMmE5NDM5OWRlYjgzNWVlYzQyNmVhN2U4ZTQ4
14
+ YjM2ZWM2MTZjODEwNWI2ZDM3Mjc5NTNkMjlkMTQzZDk1NWJlOTVmZDIyYmRm
15
+ OWIzOGU4MTZlZGNiNTQ5MzRhMmRiZGFmNDdkNTc2YTg5ZGUwZGU=
data/bin/onering ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ $:.push 'lib'
3
+ require 'trollop'
4
+ require 'onering'
5
+ require 'hashlib'
6
+ require 'rainbow'
7
+ require 'pp'
8
+
9
+ plugins = Onering::CLI::Plugin.registered_plugins.collect{|i| i.name.split('::').last.downcase }
10
+ exclude_plugins = %w{devices}
11
+
12
+ global = Trollop::options do
13
+ banner <<-EOS
14
+ onering command line client utility - version #{Onering::Client::VERSION}
15
+
16
+ Usage:
17
+ onering [global] [plugin] [subcommand] [options]
18
+
19
+ Plugins (onering <plugin> --help for usage details):
20
+ #{(plugins - exclude_plugins).sort.join(', ')}
21
+
22
+ where [global] options are:
23
+ EOS
24
+
25
+ opt :url, "The URL of the Onering server to connect to", :short => '-s', :type => :string
26
+ opt :path, "The base path to prepend to all requests (default: /api)", :type => :string
27
+ opt :source, "Specify the source IP address to use (i.e. which network interface the request should originate from)", :short => '-I', :type => :string
28
+ opt :param, "Additional query string parameters to include with the request in the format FIELD=VALUE. Can be specified multiple times.", :short => '-p', :type => :string, :multi => true
29
+ opt :format, "The output format for return values (i.e.: json, yaml, text)", :short => '-t', :type => :string
30
+ opt :sslkey, "Location of the SSL client key to use for authentication", :short => '-c', :type => :string
31
+ opt :nosslverify, "Disable verification of the server SSL certificate", :type => :boolean
32
+ opt :apikey, "The API token to use for authentication", :short => '-k', :type => :string
33
+ opt :quiet, "Suppress standard output", :short => '-q'
34
+ opt :separator, "A string used to separate output values of delimited tabular data", :short => '-S', :default => "\t"
35
+ opt :verbosity, "Set the log level (fatal, error, warn, info, debug)", :short => '-v', :type => :string, :default => 'warn'
36
+
37
+ stop_on plugins
38
+ end
39
+
40
+
41
+ plugin = ARGV.shift
42
+ Trollop::die("plugin argument is requried") if plugin.nil?
43
+
44
+ Onering::Logger.setup({
45
+ :destination => 'stdout',
46
+ :threshold => global[:verbosity]
47
+ })
48
+
49
+ if plugins.include?(plugin)
50
+ begin
51
+ plugin = Onering::CLI.const_get(plugin.capitalize)
52
+ plugin.configure(global)
53
+
54
+ Onering::Logger.debug("Executing plugin #{plugin}\#run()", $0)
55
+ rv = plugin.run(ARGV)
56
+ Onering::CLI.output(rv, (global[:format] || plugin.default_format(rv, ARGV) || 'text'))
57
+
58
+ rescue Onering::API::Errors::Exception => e
59
+ Onering::Logger.fatal(e.message, e.class.name.split('::').last) rescue nil
60
+ exit 1
61
+
62
+ rescue Onering::Client::FatalError => e
63
+ exit 255
64
+
65
+ end
66
+ else
67
+ Trollop::die("unknown plugin #{plugin}")
68
+ end
@@ -0,0 +1,19 @@
1
+ serialnumber:serial
2
+ manufacturer:make
3
+ productname:model
4
+ boardmanufacturer:mbmake
5
+ boardproductname:mbmodel
6
+ boardserialnumber:mbserial
7
+ hostname
8
+ fqdn
9
+ default_ipaddress:ip
10
+ default_macaddress:mac
11
+ default_gateway:gateway
12
+ kernelrelease:kernel
13
+ kernelarguments:@kernelarguments
14
+ architecture:arch
15
+ operatingsystem:distro
16
+ operatingsystemrelease:version
17
+ signature
18
+ boottime:booted_at
19
+ onering:@onering
data/lib/onering.rb ADDED
@@ -0,0 +1,32 @@
1
+ $: << File.expand_path(File.dirname(__FILE__))
2
+
3
+ module Onering
4
+ FULL_CLIENT = true
5
+ end
6
+
7
+ require 'onering/version'
8
+ require 'onering/logger'
9
+
10
+ if not ENV['ONERING_LOGLEVEL'].nil?
11
+ Onering::Logger.setup({
12
+ :destination => 'stderr',
13
+ :threshold => ENV['ONERING_LOGLEVEL']
14
+ })
15
+ end
16
+
17
+ require 'onering/util'
18
+ require 'onering/api'
19
+ require 'onering/cli'
20
+
21
+ # require plugins
22
+ Dir[File.join(File.expand_path(File.dirname(__FILE__)),"onering","plugins","*.rb")].each do |i|
23
+ require i
24
+ end
25
+
26
+ # require cli submodules
27
+ Dir[File.join(File.expand_path(File.dirname(__FILE__)),"onering","cli","*.rb")].each do |i|
28
+ require i
29
+ end
30
+
31
+ # you've loaded the library, now load the config
32
+ Onering::Config.load()
@@ -0,0 +1,322 @@
1
+ require 'openssl'
2
+ require 'yaml'
3
+ require 'hashlib'
4
+ require 'deep_merge'
5
+ require 'addressable/uri'
6
+ require 'httparty'
7
+ require 'onering/config'
8
+
9
+ module Onering
10
+ class API
11
+ module Actions
12
+ class Retry < ::Exception; end
13
+ end
14
+
15
+ module Errors
16
+ class Exception < ::Exception; end
17
+ class NotConnected < Exception; end
18
+
19
+ class ClientError < Exception; end
20
+ class Unauthorized < ClientError; end
21
+ class Forbidden < ClientError; end
22
+ class NotFound < ClientError; end
23
+
24
+ class ServerError < Exception; end
25
+ class ConnectionTimeout < Exception; end
26
+ class AuthenticationMissing < Exception; end
27
+ end
28
+
29
+ include Onering::Util
30
+ include ::HTTParty
31
+
32
+ attr_accessor :url
33
+ format :json
34
+
35
+ DEFAULT_BASE="https://onering"
36
+ DEFAULT_PATH="/api"
37
+ DEFAULT_CLIENT_PEM=["~/.onering/client.pem", "/etc/onering/client.pem"]
38
+ DEFAULT_CLIENT_KEY=["~/.onering/client.key", "/etc/onering/client.key"]
39
+ DEFAULT_VALIDATION_PEM="/etc/onering/validation.pem"
40
+
41
+
42
+ def initialize(options={})
43
+ @_plugins = {}
44
+ options = {} if options.nil?
45
+ @_connection_options = options
46
+
47
+ # load and merge all config file sources
48
+ Onering::Config.load(@_connection_options[:configfile], @_connection_options.get(:config, {}))
49
+
50
+ if options.get('config.nosslverify', false) == true
51
+ # deliberately break SSL verification
52
+ Onering::Logger.warn("Disabling SSL peer verification for #{options.get('config.url')}")
53
+ OpenSSL::SSL.send(:const_set, :OLD_VERIFY_PEER, OpenSSL::SSL::VERIFY_PEER)
54
+ OpenSSL::SSL.send(:remove_const, :VERIFY_PEER)
55
+ OpenSSL::SSL.send(:const_set, :VERIFY_PEER, OpenSSL::SSL::VERIFY_NONE)
56
+ else
57
+ # restore SSL verification if it's currently broken
58
+ if defined?(OpenSSL::SSL::OLD_VERIFY_PEER)
59
+ if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and OpenSSL::SSL::OLD_VERIFY_PEER != OpenSSL::SSL::VERIFY_NONE
60
+ OpenSSL::SSL.send(:remove_const, :VERIFY_PEER)
61
+ OpenSSL::SSL.send(:const_set, :VERIFY_PEER, OpenSSL::SSL::OLD_VERIFY_PEER)
62
+ end
63
+ end
64
+ end
65
+
66
+ if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE
67
+ Onering::Logger.warn("Disabling SSL peer verification for #{options.get('config.url')}")
68
+ end
69
+
70
+ # source interface specified
71
+ # !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !!
72
+ # Due to certain versions of Ruby's Net::HTTP not allowing you explicitly
73
+ # specify the source IP/interface to use, this horrific monkey patch is
74
+ # necessary, if not right.
75
+ #
76
+ # If at least some of your code doesn't make you feel bottomless shame
77
+ # then you aren't coding hard enough.
78
+ #
79
+ if options.get('config.source').is_a?(String)
80
+ if options.get('config.source') =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/
81
+ # insert firing pin into the hack
82
+ TCPSocket.instance_eval do
83
+ (class << self; self; end).instance_eval do
84
+ alias_method :_stock_open, :open
85
+ attr_writer :_hack_local_ip
86
+
87
+ define_method(:open) do |conn_address, conn_port|
88
+ _stock_open(conn_address, conn_port, @_hack_local_ip)
89
+ end
90
+ end
91
+ end
92
+
93
+ # arm the hack
94
+ TCPSocket._hack_local_ip = options.get('config.source')
95
+
96
+ # sound the siren
97
+ Onering::Logger.info("Using local interface #{options.get('config.source')} to connect", "Onering::API")
98
+
99
+ else
100
+ raise "Invalid source IP address #{options.get('config.source')}"
101
+ end
102
+ end
103
+
104
+ # set API connectivity details
105
+ Onering::API.base_uri(options.get('config.url', Onering::Config.get(:url, DEFAULT_BASE)))
106
+ Onering::Logger.info("Server URL is #{Onering::API.base_uri}", "Onering::API")
107
+
108
+ # add default parameters
109
+ options.get('config.params',{}).each do |k,v|
110
+ _default_param(k,v)
111
+ end
112
+
113
+ connect(options) if options.get(:autoconnect, true)
114
+ end
115
+
116
+ def connect(options={})
117
+ # setup authentication
118
+ _setup_auth()
119
+
120
+ Onering::Logger.debug("Connection setup complete", "Onering::API")
121
+ return self
122
+ end
123
+
124
+
125
+ def request(method, endpoint, options={})
126
+ endpoint = [Onering::Config.get(:path, DEFAULT_PATH).strip, endpoint.sub(/^\//,'')].join('/')
127
+
128
+ Onering::Logger.debug("#{method.to_s.upcase} #{endpoint}#{(options[:query] || {}).empty? ? '' : '?'+options[:query].join('=', '&')}", "Onering::API")
129
+ options.get(:headers,[]).each do |name, value|
130
+ next if name == 'Content-Type' and value == 'application/json'
131
+ Onering::Logger.debug("+#{name}: #{value}", "Onering::API")
132
+ end
133
+
134
+ begin
135
+ case (method.to_sym rescue method)
136
+ when :post
137
+ rv = Onering::API.post(endpoint, options)
138
+ when :put
139
+ rv = Onering::API.put(endpoint, options)
140
+ when :delete
141
+ rv = Onering::API.delete(endpoint, options)
142
+ when :head
143
+ rv = Onering::API.head(endpoint, options)
144
+ else
145
+ rv = Onering::API.get(endpoint, options)
146
+ end
147
+ rescue SocketError => e
148
+ Onering::Logger.fatal!("Unable to connect to #{Onering::API.base_uri}", "Onering::API")
149
+ end
150
+
151
+ if rv.code >= 500
152
+ raise Errors::ServerError.new("HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message','') rescue ''}")
153
+ elsif rv.code >= 400
154
+ message = "HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message', '') rescue ''}"
155
+
156
+ case rv.code
157
+ when 401
158
+ raise Errors::Unauthorized.new(message)
159
+ when 403
160
+ raise Errors::Forbidden.new(message)
161
+ when 404
162
+ raise Errors::NotFound.new(message)
163
+ else
164
+ raise Errors::ClientError.new(message)
165
+ end
166
+ else
167
+ rv
168
+ end
169
+ end
170
+
171
+
172
+ def get(endpoint, options={})
173
+ request(:get, endpoint, options)
174
+ end
175
+
176
+ def post(endpoint, options={}, &block)
177
+ if block_given?
178
+ request(:post, endpoint, options.merge({
179
+ :body => yield
180
+ }))
181
+ else
182
+ request(:post, endpoint, options)
183
+ end
184
+ end
185
+
186
+ def put(endpoint, options={}, &block)
187
+ if block_given?
188
+ request(:put, endpoint, options.merge({
189
+ :body => yield
190
+ }))
191
+ else
192
+ request(:put, endpoint, options)
193
+ end
194
+ end
195
+
196
+ def delete(endpoint, options={})
197
+ request(:delete, endpoint, options)
198
+ end
199
+
200
+
201
+ # I'm not a huge fan of what's happening here, but metaprogramming is hard...
202
+ #
203
+ # "Don't let the perfect be the enemy of the good."
204
+ #
205
+ def method_missing(method, *args, &block)
206
+ modname = method.to_s.split('_').map(&:capitalize).join
207
+
208
+ if not (plugin = (Onering::API.const_get(modname) rescue nil)).nil?
209
+ @_plugins[method] ||= plugin.new.connect(@_connection_options)
210
+ return @_plugins[method]
211
+ else
212
+ super
213
+ end
214
+ end
215
+
216
+ def status()
217
+ Onering::API.new.get("/").parsed_response
218
+ end
219
+
220
+ # -----------------------------------------------------------------------------
221
+ def _setup_auth()
222
+ type = Onering::Config.get('authentication.type', :auto)
223
+ _setup_auth_token()
224
+ end
225
+
226
+ # -----------------------------------------------------------------------------
227
+ def _default_param(key, value)
228
+ @_default_params ||= {}
229
+ @_default_params[key] = value
230
+ Onering::API.default_params(@_default_params)
231
+ end
232
+
233
+ # -----------------------------------------------------------------------------
234
+ def _setup_auth_token()
235
+ Onering::Logger.info("Using token authentication mechanism", "Onering::API")
236
+
237
+ # get first keyfile found
238
+ key = Onering::Config.get('authentication.key', Onering::Config.get('authentication.keyfile'))
239
+
240
+ if key.nil?
241
+ if Onering::Config.get('authentication.bootstrap.enabled', true)
242
+ Onering::Logger.warn("Authentication token not found, attempting to autoregister client", "Onering::API")
243
+
244
+ if not (bootstrap = Onering::Config.get('authentication.bootstrap.key')).nil?
245
+ if bootstrap.to_s =~ /[0-9a-f]{32,64}/
246
+ # attempt to create key.yml from least-specific to most, first writable path wins
247
+ clients = [{
248
+ :path => "/etc/onering",
249
+ :name => fact('hardwareid'),
250
+ :keyname => 'system',
251
+ :autodelete => true
252
+ },{
253
+ :path => "~/.onering",
254
+ :name => ENV['USER'],
255
+ :keyname => 'cli',
256
+ :autodelete => false
257
+ }]
258
+
259
+ # for each client attempt...
260
+ clients.each do |client|
261
+ # expand and assemble path
262
+ client[:path] = (File.expand_path(client[:path]) rescue client[:path])
263
+ keyfile = File.join(client[:path], 'key.yml')
264
+
265
+ # skip this if we can't write to the parent directory
266
+ next unless File.writable?(client[:path])
267
+ Dir.mkdir(client[:path]) unless File.directory?(client[:path])
268
+ next if File.exists?(keyfile)
269
+
270
+ self.class.headers({
271
+ 'X-Auth-Bootstrap-Token' => bootstrap
272
+ })
273
+
274
+ # attempt to create/download the keyfile
275
+ Onering::Logger.debug("Requesting authentication token for #{client[:name].strip}; #{bootstrap}", "Onering::API")
276
+ response = self.class.get("/api/users/#{client[:name].strip}/tokens/#{client[:keyname]}")
277
+
278
+ # if successful, write the file
279
+ if response.code < 400 and response.body
280
+ File.open(keyfile, 'w').puts(YAML.dump({
281
+ 'authentication' => {
282
+ 'key' => response.body.strip.chomp
283
+ }
284
+ }))
285
+
286
+ key = response.body.strip.chomp
287
+
288
+ else
289
+ # all errors are fatal at this stage
290
+ Onering::Logger.fatal!("Cannot autoregister client: HTTP #{response.code} - #{(response.parsed_response || {}).get('error.message', 'Unknown error')}", "Onering::API")
291
+ end
292
+
293
+ self.class.headers({})
294
+
295
+ # we're done here...
296
+ break
297
+ end
298
+ else
299
+ raise Errors::AuthenticationMissing.new("Autoregistration failed: invalid bootstrap token specified")
300
+ end
301
+
302
+ else
303
+ raise Errors::AuthenticationMissing.new("Autoregistration failed: no bootstrap token specified")
304
+ end
305
+
306
+ else
307
+ raise Errors::AuthenticationMissing.new("Authentication token not found, and autoregistration disabled")
308
+ end
309
+ end
310
+
311
+ raise Errors::AuthenticationMissing.new("Token authentication specified, but cannot find a token config or as a command line argument") if key.nil?
312
+
313
+ # set auth mechanism
314
+ Onering::API.headers({
315
+ 'X-Auth-Mechanism' => 'token'
316
+ })
317
+
318
+ # set default parameters
319
+ _default_param(:token, key)
320
+ end
321
+ end
322
+ end