ncio 1.0.1 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aae5414153f31d081b94ba2c7db8c2861c6ef295
4
- data.tar.gz: 70e4a75237aa6a37e768b6a8f67e6b1c4a245e64
3
+ metadata.gz: d3a023e01e8f30ad099628a652e23d8432a96e29
4
+ data.tar.gz: 26a22f56c26aa09821e7cc5af6b11ee92f2e9748
5
5
  SHA512:
6
- metadata.gz: 6de835b4da89a650b2a39b21317ca8e662937e1df2dea90c40dc38694e7ff3c3fb32e2d58814da8f296bad71fa6c74da070439d51532d408d136361edd7e8f59
7
- data.tar.gz: fba4dc609759ae67ef6c50f3f44f20c84507792482dcb3287a69e9f679e4a284af95c9669770befeff4e83a60f8e448bc649637335ea6102c8f79983cc11bf8d
6
+ metadata.gz: fbd161b5b74263feccfe972c9f0be4b1a12bd5e5e650d3e80a683177cd25615d9510974843c58689494442961ae712e09840f30a300336047d0017a1423d9bc5
7
+ data.tar.gz: 219f253cf24cbc5038160884ef0f55eecff340e3a30c92c9690b758b2e9d0885a23639108b7b76376f1f7c72d8f6d0fbf47ee0005e01b362611fada80928249c
@@ -0,0 +1,10 @@
1
+ Version 1.1.0
2
+ ===
3
+
4
+ * Add `--retry-connections` [Issue 6](https://github.com/jeffmccune/ncio/issues/6)
5
+ * Add better error handling [Issue 7](https://github.com/jeffmccune/ncio/issues/7)
6
+
7
+ Version 1.0.1
8
+ ===
9
+
10
+ * Initial release. Backup / Transform / Restore
data/README.md CHANGED
@@ -111,6 +111,70 @@ Log to the console using the `--no-syslog` command line option.
111
111
  The tool can only log to either syslog or the console at this time. Multiple
112
112
  log destinations are not currently supported.
113
113
 
114
+ ## Retrying Connections
115
+
116
+ It can take some time for the `pe-console-services` service to come online. In
117
+ an effort to make things are robust as possible consider using the
118
+ `--retry-connections` global option. This allows ncio to retry API connections
119
+ while the service comes online. This option has been added to address the
120
+ following use case:
121
+
122
+ service start pe-console-services.service
123
+ ncio --retry-connections backup
124
+
125
+ In this scenario ncio will retry API connections, eventually succeeding or
126
+ timing out. Ideally the service will come online before the timeout expires.
127
+
128
+ ## Replication
129
+
130
+ A simple way to replicate node classification data between a primary and a
131
+ secondary can be achieved with the following shell script. Call this from cron
132
+ on a periodic basis:
133
+
134
+ ```bash
135
+ #! /bin/bash
136
+ #
137
+ # This shell script is intended to be executed from cron on a periodic basis.
138
+ # The goal is to keep a Standby PE Monolithic master in sync with an active
139
+ # Primary. Pass the FQDN of the primary as ARG 1 and the FQDN of the secondary
140
+ # as ARG 2
141
+ #
142
+ # In a DR situation when the secondary becomes active, block replication by
143
+ # touching the lockfile. This will prevent any changes made to the standby from
144
+ # being clobbered as soon as the primary comes back online.
145
+ #
146
+ # Prior to re-enabling replication after a DR situation, replicate back to the
147
+ # primary by reversing the direction of this script.
148
+
149
+ set -euo pipefail
150
+
151
+ PRIMARY="$1"
152
+ STANDBY="$2"
153
+
154
+ SOURCE="https://${PRIMARY}:4433/classification-api/v1"
155
+ PATH="/opt/puppetlabs/puppet/bin:$PATH"
156
+ lockfile='/etc/ncio_do_not_replicate'
157
+
158
+ log() {
159
+ logger -t ncio-replicate -p daemon.warn -s "$1"
160
+ }
161
+
162
+ if [[ -e "$lockfile" ]]; then
163
+ log "WARN: Replication aborted, $lockfile exists!"
164
+ exit 1
165
+ fi
166
+
167
+ ncio --uri "$SOURCE" --retry-connections backup \
168
+ | ncio transform --hostname "${PRIMARY}:${STANDBY}" \
169
+ | ncio --retry-connections restore
170
+ rval=$?
171
+
172
+ [[ $rval -eq 0 ]] && STATUS='OK' || STATUS='ERROR'
173
+ msg="INFO: Finished replicating puppet classification groups."
174
+ log "$msg STATUS=${STATUS} EXITCODE=${rval} (touch $lockfile to disable)"
175
+ exit $rval
176
+ ```
177
+
114
178
  ## Contributing
115
179
 
116
180
  Bug reports and pull requests are welcome on GitHub at
@@ -0,0 +1,41 @@
1
+ #! /bin/bash
2
+ #
3
+ # This shell script is intended to be executed from cron on a periodic basis.
4
+ # The goal is to keep a Standby PE Monolithic master in sync with an active
5
+ # Primary. Pass the FQDN of the primary as ARG 1 and the FQDN of the secondary
6
+ # as ARG 2
7
+ #
8
+ # In a DR situation when the secondary becomes active, block replication by
9
+ # touching the lockfile. This will prevent any changes made to the standby from
10
+ # being clobbered as soon as the primary comes back online.
11
+ #
12
+ # Prior to re-enabling replication after a DR situation, replicate back to the
13
+ # primary by reversing the direction of this script.
14
+
15
+ set -euo pipefail
16
+
17
+ PRIMARY="$1"
18
+ STANDBY="$2"
19
+
20
+ SOURCE="https://${PRIMARY}:4433/classification-api/v1"
21
+ PATH="/opt/puppetlabs/puppet/bin:$PATH"
22
+ lockfile='/etc/ncio_do_not_replicate'
23
+
24
+ log() {
25
+ logger -t ncio-replicate -p daemon.warn -s "$1"
26
+ }
27
+
28
+ if [[ -e "$lockfile" ]]; then
29
+ log "WARN: Replication aborted, $lockfile exists!"
30
+ exit 1
31
+ fi
32
+
33
+ ncio --uri "$SOURCE" backup \
34
+ | ncio transform --hostname "${PRIMARY}:${STANDBY}" \
35
+ | ncio restore
36
+ rval=$?
37
+
38
+ [[ $rval -eq 0 ]] && STATUS='OK' || STATUS='ERROR'
39
+ msg="INFO: Finished replicating puppet classification groups."
40
+ log "$msg STATUS=${STATUS} EXITCODE=${rval} (touch $lockfile to disable)"
41
+ exit $rval
@@ -1,5 +1,7 @@
1
1
  require 'ncio/api'
2
2
  require 'ncio/http_client'
3
+ require 'ncio/support'
4
+ require 'ncio/support/retry_action'
3
5
  require 'uri'
4
6
  require 'socket'
5
7
  require 'json'
@@ -17,6 +19,7 @@ module Ncio
17
19
  attr_reader :opts
18
20
 
19
21
  class ApiError < RuntimeError; end
22
+ class ApiAuthenticationError < RuntimeError; end
20
23
 
21
24
  DEFAULT_HEADERS = {
22
25
  'Content-Type' => 'application/json',
@@ -35,6 +38,10 @@ module Ncio
35
38
  @port = uri.port
36
39
  end
37
40
 
41
+ def log
42
+ Ncio::Support.log
43
+ end
44
+
38
45
  ##
39
46
  # Return a memoized HTTP connection
40
47
  def connection
@@ -49,6 +56,37 @@ module Ncio
49
56
  @connection = Ncio::HttpClient.new(conn_opts)
50
57
  end
51
58
 
59
+ ##
60
+ # Make a request respecting the timeout global option
61
+ #
62
+ # Assumes the timeout value is available in opts[:connect_timeout]
63
+ def request_with_timeout(req)
64
+ params = {
65
+ timeout: opts[:connect_timeout],
66
+ retry_exceptions: [Errno::ECONNREFUSED],
67
+ log: self.log,
68
+ }
69
+ Ncio::Support::RetryAction.retry_action(params) do
70
+ connection.request(req)
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Make a request without a timeout
76
+ def request_without_timeout(req)
77
+ connection.request(req)
78
+ end
79
+
80
+ ##
81
+ # Make a request, return a response
82
+ def request(req)
83
+ if opts[:retry_connections]
84
+ request_with_timeout(req)
85
+ else
86
+ request_without_timeout(req)
87
+ end
88
+ end
89
+
52
90
  ##
53
91
  # Return all of the groups currently defined in the node classifier API.
54
92
  #
@@ -60,14 +98,27 @@ module Ncio
60
98
  def groups(inherited = false)
61
99
  uri = build_uri('groups', inherited: inherited.to_s)
62
100
  req = Net::HTTP::Get.new(uri, DEFAULT_HEADERS)
63
- resp = connection.request(req)
64
- if resp.code == '200'
101
+ resp = request(req)
102
+ obj = if resp.code == '200'
103
+ JSON.parse(resp.body)
104
+ else
105
+ raise_on_non_200(resp, 200)
106
+ end
107
+ obj
108
+ end
109
+
110
+ ##
111
+ # Handle a non 200 response.
112
+ def raise_on_non_200(resp, expected_code=200)
113
+ if resp.code == '401' && %r{rbac/user-unauthenticated}.match(resp.body)
65
114
  obj = JSON.parse(resp.body)
115
+ msg = obj['msg'] || '401 User Unauthenticated Error'
116
+ raise ApiAuthenticationError, msg
66
117
  else
67
- msg = "Expected 200 response, got #{resp.code} body: #{resp.body}"
118
+ msg = "Expected #{expected_code} response, got #{resp.code} "\
119
+ "body: #{resp.body}"
68
120
  raise ApiError, msg
69
121
  end
70
- obj
71
122
  end
72
123
 
73
124
  ##
@@ -80,10 +131,9 @@ module Ncio
80
131
  uri = build_uri('import-hierarchy')
81
132
  req = Net::HTTP::Post.new(uri, DEFAULT_HEADERS)
82
133
  req.body_stream = stream
83
- resp = connection.request(req)
134
+ resp = request(req)
84
135
  return true if resp.code == '204'
85
- msg = "Expected 204 response, got #{resp.code} body: #{resp.body}"
86
- raise ApiError, msg
136
+ raise_on_non_200(resp, 204)
87
137
  end
88
138
 
89
139
  ##
@@ -2,6 +2,7 @@
2
2
  require 'ncio'
3
3
  require 'ncio/support'
4
4
  require 'ncio/support/option_parsing'
5
+ require 'ncio/support/retry_action'
5
6
  require 'ncio/support/transform'
6
7
  require 'ncio/trollop'
7
8
  require 'ncio/version'
@@ -62,6 +63,11 @@ class App
62
63
  transform_groups
63
64
  return 0
64
65
  end
66
+ rescue Exception => e
67
+ msg = "ERROR: #{friendly_error(e)}"
68
+ fatal msg
69
+ $stderr.puts msg
70
+ return 1
65
71
  end
66
72
  # rubocop:enable Metrics/MethodLength
67
73
 
@@ -90,6 +96,7 @@ class App
90
96
  # Restore all groups in a manner suitable for the node classification
91
97
  # hierarchy import. See: [NC Import
92
98
  # Hierarchy](https://docs.puppet.com/pe/2016.1/nc_import-hierarchy.html)
99
+ # rubocop:disable Lint/RescueException
93
100
  def restore_groups
94
101
  warn 'Starting Node Classification Restore using '\
95
102
  "POST #{uri}/import-hierarchy"
@@ -105,6 +112,7 @@ class App
105
112
  fatal "ERROR Restoring backup: #{format_error e}"
106
113
  raise e
107
114
  end
115
+ # rubocop:enable Lint/RescueException
108
116
 
109
117
  ##
110
118
  # Transform a backup produced with backup_groups. The transformation is
@@ -1,6 +1,7 @@
1
+ require 'json'
1
2
  require 'logger'
3
+ require 'stringio'
2
4
  require 'syslog/logger'
3
- require 'json'
4
5
 
5
6
  module Ncio
6
7
  ##
@@ -11,6 +12,10 @@ module Ncio
11
12
  module Support
12
13
  attr_reader :opts
13
14
 
15
+ ##
16
+ # Reset the global logger instance and return it as an object.
17
+ #
18
+ # @return [Logger] initialized logging instance
14
19
  def self.reset_logging!(opts)
15
20
  logger = opts[:syslog] ? syslog_logger : stream_logger(opts)
16
21
  @log = logger
@@ -38,7 +43,7 @@ module Ncio
38
43
  # Logging is handled centrally, the helper methods will delegate to the
39
44
  # centrally configured logging instance.
40
45
  def self.log
41
- @log
46
+ @log || reset_logging!
42
47
  end
43
48
 
44
49
  ##
@@ -53,6 +58,7 @@ module Ncio
53
58
  when 'STDOUT' then $stdout
54
59
  when 'STDERR' then $stderr
55
60
  when 'STDIN' then $stdin
61
+ when 'STRING' then StringIO.new
56
62
  else File.expand_path(filepath)
57
63
  end
58
64
  end
@@ -155,10 +161,51 @@ module Ncio
155
161
  #
156
162
  # @param [Exception] e the exception to format
157
163
  def format_error(e)
158
- data = { error: "#{e.class}", message: e.message, backtrace: e.backtrace }
164
+ data = { error: e.class.to_s, message: e.message, backtrace: e.backtrace }
159
165
  JSON.pretty_generate(data)
160
166
  end
161
167
 
168
+ ##
169
+ # Top level exception handler and friendly error message handler.
170
+ def friendly_error(e)
171
+ case e
172
+ when Ncio::Support::RetryAction::RetryException::Timeout
173
+ 'Timeout expired connecting to the console service. Verify it is up and running.'
174
+ when OpenSSL::SSL::SSLError
175
+ friendly_ssl_error(e)
176
+ when Ncio::Api::V1::ApiAuthenticationError
177
+ 'Make sure the --cert option value is listed in the certificate whitelist, '\
178
+ 'and you are able to run puppet agent --test on the master. '\
179
+ 'The certificate whitelist on the master is located at '\
180
+ '/etc/puppetlabs/console-services/rbac-certificate-whitelist'
181
+ else
182
+ e.message
183
+ end
184
+ end
185
+
186
+ ##
187
+ # Handle SSL errors as a special case
188
+ def friendly_ssl_error(e)
189
+ case e.message
190
+ when %r{read server hello A}
191
+ 'The socket connected, but there is no SSL service on the other side. '\
192
+ 'This is often the case with TCP forwarding, e.g. in Vagrant '\
193
+ 'or with SSH tunnels.'
194
+ when %r{state=error: certificate verify failed}
195
+ 'The socket connected, but the certificate presented by the service could not '\
196
+ 'be verified. Make sure the value of the --cacert option points to an identical '\
197
+ 'copy of the /etc/puppetlabs/puppet/ssl/certs/ca.pem file from the master.'
198
+ when %r{returned=5 errno=0 state=SSLv3 read finished A}
199
+ "The socket connected, but got back SSL error: #{e.message} "\
200
+ 'This usually means the value of the --cert and --key options are certificates '\
201
+ 'which are not signed by the same CA the service trusts. This can often happen '\
202
+ 'if the service has recently been re-installed. Please obtain a valid cert and key '\
203
+ 'and try again.'
204
+ else
205
+ "SSL Error: The socket is listening but something went wrong: #{e.message}"
206
+ end
207
+ end
208
+
162
209
  ##
163
210
  # Return the application version as a Semantic Version encoded string
164
211
  #
@@ -75,6 +75,13 @@ module Ncio
75
75
  opt :syslog, 'Log to syslog', default: true, conflicts: :logto
76
76
  opt :verbose, 'Set log level to INFO'
77
77
  opt :debug, 'Set log level to DEBUG'
78
+ opt :retry_connections, 'Retry API connections, '\
79
+ 'e.g. waiting for the service to come online. '\
80
+ '{NCIO_RETRY_CONNECTIONS}',
81
+ default: (env['NCIO_RETRY_CONNECTIONS'] == 'true') || false
82
+ opt :connect_timeout, 'Retry <i> seconds if --retry-connections=true '\
83
+ '{NCIO_CONNECT_TIMEOUT}',
84
+ default: env['NCIO_CONNECT_TIMEOUT'] || CONNECT_TIMEOUT_DEFAULT
78
85
  end
79
86
  end
80
87
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
@@ -198,6 +205,8 @@ Global options: (Note, command line arguments supersede ENV vars in {}'s)
198
205
 
199
206
  # Map is indexed by the subcommand
200
207
  FILE_DEFAULT_MAP = { 'backup' => 'STDOUT', 'restore' => 'STDIN' }.freeze
208
+
209
+ CONNECT_TIMEOUT_DEFAULT = 120
201
210
  end
202
211
  end
203
212
  end
@@ -0,0 +1,79 @@
1
+ require 'ncio/support'
2
+ module Ncio
3
+ module Support
4
+ ##
5
+ # Provide a method to retry arbitrary code blocks with the ability to rescue
6
+ # certain exceptions and retry rather than failing hard on the exception.
7
+ #
8
+ # Copied from:
9
+ # https://github.com/puppetlabs/puppetlabs-cloud_provisioner/blob/f6cbac3/lib/puppet/cloudpack/utils.rb
10
+ module RetryAction
11
+ class RetryException < RuntimeError
12
+ class NoBlockGiven < RetryException; end
13
+ class NoTimeoutGiven < RetryException; end
14
+ class Timeout < RetryException; end
15
+ end
16
+
17
+ def self.timedout?(start, timeout)
18
+ return true if timeout.nil?
19
+ (Time.now - start) >= timeout
20
+ end
21
+
22
+ ##
23
+ # Retry an action, catching exceptions and retrying if the exception has
24
+ # been specified.
25
+ #
26
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength
27
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
28
+ def self.retry_action(params = { retry_exceptions: nil, timeout: nil, log: nil })
29
+ # Retry actions for a specified amount of time. This method will allow
30
+ # the final retry to complete even if that extends beyond the timeout
31
+ # period.
32
+ raise RetryException::NoBlockGiven unless block_given?
33
+
34
+ raise RetryException::NoTimeoutGiven if params[:timeout].nil?
35
+ params[:retry_exceptions] ||= []
36
+
37
+ # Assumes reset_logging! has been called. This happens in the Ncio::App
38
+ # initialization.
39
+ log = params[:log] || Ncio::Support.log
40
+
41
+ start = Time.now
42
+ failures = 0
43
+
44
+ # rubocop:disable Lint/RescueException
45
+ begin
46
+ yield
47
+
48
+ rescue Exception => e
49
+ # If we were giving exceptions to catch,
50
+ # catch the exceptions we care about and retry.
51
+ # All others fail hard
52
+
53
+ raise RetryException::Timeout if timedout?(start, params[:timeout])
54
+
55
+ retry_exceptions = params[:retry_exceptions]
56
+
57
+ unless retry_exceptions.empty?
58
+ if retry_exceptions.include?(e.class)
59
+ log.warn("Retrying: #{e.class}: #{e}")
60
+ else
61
+ # If the exceptions is not in the list of retry_exceptions
62
+ # re-raise.
63
+ raise e
64
+ end
65
+ end
66
+ # rubocop:enable Lint/RescueException
67
+
68
+ failures += 1
69
+ # Increase the amount of time that we sleep after every
70
+ # failed retry attempt.
71
+ sleep(((2**failures) - 1) * 0.1)
72
+
73
+ retry
74
+ end
75
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,3 @@
1
1
  module Ncio
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ncio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff McCune
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-06-29 00:00:00.000000000 Z
11
+ date: 2016-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -136,6 +136,7 @@ files:
136
136
  - ".rubocop.yml"
137
137
  - ".travis.yml"
138
138
  - ".yardopts"
139
+ - CHANGELOG.md
139
140
  - Gemfile
140
141
  - LICENSE.txt
141
142
  - README.md
@@ -143,6 +144,7 @@ files:
143
144
  - bin/console
144
145
  - bin/setup
145
146
  - exe/ncio
147
+ - ext/ncio-replicate
146
148
  - lib/ncio.rb
147
149
  - lib/ncio/api.rb
148
150
  - lib/ncio/api/v1.rb
@@ -150,6 +152,7 @@ files:
150
152
  - lib/ncio/http_client.rb
151
153
  - lib/ncio/support.rb
152
154
  - lib/ncio/support/option_parsing.rb
155
+ - lib/ncio/support/retry_action.rb
153
156
  - lib/ncio/support/transform.rb
154
157
  - lib/ncio/trollop.rb
155
158
  - lib/ncio/version.rb
@@ -174,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
177
  version: '0'
175
178
  requirements: []
176
179
  rubyforge_project:
177
- rubygems_version: 2.4.5.1
180
+ rubygems_version: 2.2.5
178
181
  signing_key:
179
182
  specification_version: 4
180
183
  summary: Puppet Node Classifier backup / restore / transform