ncio 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +95 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/exe/ncio +6 -0
- data/lib/ncio.rb +9 -0
- data/lib/ncio/api.rb +7 -0
- data/lib/ncio/api/v1.rb +109 -0
- data/lib/ncio/app.rb +114 -0
- data/lib/ncio/http_client.rb +149 -0
- data/lib/ncio/support.rb +116 -0
- data/lib/ncio/support/option_parsing.rb +200 -0
- data/lib/ncio/support/transform.rb +73 -0
- data/lib/ncio/trollop.rb +863 -0
- data/lib/ncio/version.rb +3 -0
- data/ncio.gemspec +33 -0
- metadata +181 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module Ncio
|
6
|
+
##
|
7
|
+
# HttpClient provides a Net::HTTP instance pre-configured to communicate with
|
8
|
+
# the Puppet Node Classification Service. The client will return Ruby native
|
9
|
+
# objects where possible, parsing JSON responses from the service.
|
10
|
+
#
|
11
|
+
# This client implements v1 of the [Node Classification
|
12
|
+
# API](https://docs.puppet.com/pe/2016.1/nc_index.html).
|
13
|
+
class HttpClient
|
14
|
+
attr_reader :host
|
15
|
+
attr_reader :port
|
16
|
+
attr_reader :use_ssl
|
17
|
+
attr_reader :cert
|
18
|
+
attr_reader :key
|
19
|
+
attr_reader :cacert
|
20
|
+
attr_reader :protocol
|
21
|
+
|
22
|
+
# ApiError is raised when there are errors in the REST API repsonse.
|
23
|
+
class ApiError < RuntimeError; end
|
24
|
+
|
25
|
+
ssldir = '/etc/puppetlabs/puppet/ssl'
|
26
|
+
OPTION_DEFAULTS = {
|
27
|
+
host: Socket.gethostname,
|
28
|
+
port: 4433,
|
29
|
+
use_ssl: true,
|
30
|
+
cert: ssldir + '/certs/pe-internal-orchestrator.pem',
|
31
|
+
key: ssldir + '/private_keys/pe-internal-orchestrator.pem',
|
32
|
+
cacert: ssldir + '/certs/ca.pem'
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
##
|
36
|
+
# initialize a new HttpClient instance
|
37
|
+
#
|
38
|
+
# @param [Hash] opts Options
|
39
|
+
#
|
40
|
+
# @option opts [String] :host The API host, e.g. `"master1.puppet.vm"`.
|
41
|
+
# Defaults to the local hostname returned by `Socket.gethostname`
|
42
|
+
#
|
43
|
+
# @option opts [Fixnum] :port The API tcp port, Defaults to `4433`
|
44
|
+
#
|
45
|
+
# @option opts [String] :cert The path to the PEM encoded client
|
46
|
+
# certificate. Defaults to
|
47
|
+
# `"/etc/puppetlabs/puppet/ssl/certs/pe-internal-orchestrator.pem"`
|
48
|
+
#
|
49
|
+
# @option opts [String] :key The path to the PEM encoded RSA private key
|
50
|
+
# used for the SSL client connection. Defaults to
|
51
|
+
# `"/etc/puppetlabs/puppet/ssl/private_keys/pe-internal-orchestrator.pem"`
|
52
|
+
#
|
53
|
+
# @option opts [String] :cacert The path to the PEM encoded CA certificate
|
54
|
+
# used to authenticate the service URL. Defaults to
|
55
|
+
# `"/etc/puppetlabs/puppet/ssl/certs/ca.pem"`
|
56
|
+
def initialize(opts = {})
|
57
|
+
opts = OPTION_DEFAULTS.merge(opts)
|
58
|
+
@use_ssl = opts[:use_ssl]
|
59
|
+
@host = opts[:host]
|
60
|
+
@port = opts[:port]
|
61
|
+
@cert = opts[:cert]
|
62
|
+
@key = opts[:key]
|
63
|
+
@cacert = opts[:cacert]
|
64
|
+
@protocol = use_ssl ? 'https' : 'http'
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# make a request, pass through to Net::HTTP#request
|
69
|
+
#
|
70
|
+
# @param [Net::HTTPRequest] req The HTTP request, e.g. an instance of
|
71
|
+
# `Net::HTTP::Get`, `Net::HTTP::Post`, or `Net::HTTP::Head`.
|
72
|
+
#
|
73
|
+
# @param [String] body The request body, if any.
|
74
|
+
#
|
75
|
+
# @return [Net::HTTPResponse] response
|
76
|
+
def request(req, body = nil)
|
77
|
+
http.request(req, body)
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Provide a URL to the endpoint this client connects to. This is intended
|
82
|
+
# to construct URL's and add query parameters easily.
|
83
|
+
#
|
84
|
+
# @return [URI] the URI of the server this client connects to.
|
85
|
+
def uri
|
86
|
+
return @uri if @uri
|
87
|
+
@uri = URI("#{protocol}://#{host}:#{port}")
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
##
|
93
|
+
# return a memoized HTTP object instance configured with the SSL client
|
94
|
+
# certificate and ready to authorize the peer service
|
95
|
+
#
|
96
|
+
# TODO: Add revocation checking. See: [puppet/ssl/host.rb line
|
97
|
+
# 263](https://github.com/puppetlabs/puppet/blob/4.5.2/lib/puppet/ssl/host.rb#L263)
|
98
|
+
#
|
99
|
+
# @return [Net::HTTP]
|
100
|
+
def http
|
101
|
+
return @http if @http
|
102
|
+
client = Net::HTTP.new(uri.host, uri.port)
|
103
|
+
@http = if use_ssl
|
104
|
+
setup_ssl(client)
|
105
|
+
else
|
106
|
+
client
|
107
|
+
end
|
108
|
+
@http
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Configure this client to use SSL
|
113
|
+
#
|
114
|
+
# @param [Net::HTTP] http The http instance to configure to use SSL
|
115
|
+
#
|
116
|
+
# @return [Net::HTTP] configured with SSL certificates passed to
|
117
|
+
# initializer.
|
118
|
+
def setup_ssl(http)
|
119
|
+
http.use_ssl = true
|
120
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
121
|
+
# Setup the SSL store used for this connection
|
122
|
+
ssl_store = ssl_store()
|
123
|
+
ssl_store.purpose = OpenSSL::X509::PURPOSE_ANY
|
124
|
+
ssl_store.add_file(cacert)
|
125
|
+
http.cert_store = ssl_store
|
126
|
+
# PEM files
|
127
|
+
http.cert = OpenSSL::X509::Certificate.new(read_cert)
|
128
|
+
http.key = OpenSSL::PKey::RSA.new(read_key)
|
129
|
+
http.ca_file = cacert
|
130
|
+
http
|
131
|
+
end
|
132
|
+
|
133
|
+
# helper method to stub the OpenSSL Store
|
134
|
+
def ssl_store
|
135
|
+
OpenSSL::X509::Store.new
|
136
|
+
end
|
137
|
+
##
|
138
|
+
|
139
|
+
# helper method to stub the cert in the tests
|
140
|
+
def read_cert
|
141
|
+
File.read(cert)
|
142
|
+
end
|
143
|
+
|
144
|
+
# helper method to stub the cert in the tests
|
145
|
+
def read_key
|
146
|
+
File.read(key)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/lib/ncio/support.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
module Ncio
|
2
|
+
##
|
3
|
+
# Support module to mix into other classes, particularly the application and
|
4
|
+
# API classes. This support module provides a centralized logging
|
5
|
+
# configuration, and common methods to access configuration options about the
|
6
|
+
# behavior of the program.
|
7
|
+
module Support
|
8
|
+
attr_reader :opts
|
9
|
+
|
10
|
+
def self.reset_logging!(opts)
|
11
|
+
out = map_file_option(opts[:logto])
|
12
|
+
logger = Logger.new(out)
|
13
|
+
logger.level = opts[:debug] ? Logger::DEBUG : Logger::INFO
|
14
|
+
@log = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Logging is handled centrally, the helper methods will delegate to the
|
19
|
+
# centrally configured logging instance.
|
20
|
+
def self.log
|
21
|
+
@log
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Map a file option to STDOUT, STDERR or a fully qualified file path.
|
26
|
+
#
|
27
|
+
# @param [String] filepath A relative or fully qualified file path, or the
|
28
|
+
# keyword strings 'STDOUT' or 'STDERR'
|
29
|
+
#
|
30
|
+
# @return [String] file path or $stdout or $sederr
|
31
|
+
def self.map_file_option(filepath)
|
32
|
+
case filepath
|
33
|
+
when 'STDOUT' then $stdout
|
34
|
+
when 'STDERR' then $stderr
|
35
|
+
when 'STDIN' then $stdin
|
36
|
+
else File.expand_path(filepath)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_file_option(filepath)
|
41
|
+
Ncio::Support.map_file_option(filepath)
|
42
|
+
end
|
43
|
+
|
44
|
+
def log
|
45
|
+
Ncio::Support.log
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Reset the logging system, requires command line options to have been
|
50
|
+
# parsed.
|
51
|
+
#
|
52
|
+
# @param [Hash<Symbol, String>] Options hash, passed to the support module
|
53
|
+
def reset_logging!(opts)
|
54
|
+
Ncio::Support.reset_logging!(opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Memoized helper method to instantiate an API instance assuming options
|
59
|
+
# are available.
|
60
|
+
def api
|
61
|
+
@api ||= Ncio::Api::V1.new(opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Log an info message
|
66
|
+
def info(msg)
|
67
|
+
log.info msg
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Log a debug message
|
72
|
+
def debug(msg)
|
73
|
+
log.debug msg
|
74
|
+
end
|
75
|
+
|
76
|
+
def uri
|
77
|
+
opts[:uri]
|
78
|
+
end
|
79
|
+
|
80
|
+
def file
|
81
|
+
opts[:file]
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Helper method to write output, used for stubbing out the tests.
|
86
|
+
#
|
87
|
+
# @param [String, IO] output the output path or a IO stream
|
88
|
+
def write_output(str, output)
|
89
|
+
if output.is_a?(IO)
|
90
|
+
output.puts(str)
|
91
|
+
else
|
92
|
+
File.open(output, 'w+') { |f| f.puts(str) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Helper method to read from STDIN, or a file and execute an arbitrary block
|
98
|
+
# of code. A block must be passed which will recieve an IO object in the
|
99
|
+
# event input is a readable file path.
|
100
|
+
def input_stream(input)
|
101
|
+
if input.is_a?(IO)
|
102
|
+
yield input
|
103
|
+
else
|
104
|
+
File.open(input, 'r') { |stream| yield stream }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Return the application version as a Semantic Version encoded string
|
110
|
+
#
|
111
|
+
# @return [String] the version
|
112
|
+
def version
|
113
|
+
Ncio::VERSION
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'ncio/version'
|
2
|
+
# rubocop:disable Metrics/ModuleLength
|
3
|
+
module Ncio
|
4
|
+
module Support
|
5
|
+
##
|
6
|
+
# Support methods intended to be mixed into the application class. These
|
7
|
+
# methods are specific to command line parsing. The model is [GLOBAL
|
8
|
+
# OPTIONS] SUBCOMMAND [SUBCOMMAND OPTIONS]
|
9
|
+
#
|
10
|
+
# Configuration state parsed from options is intended to be stored in a
|
11
|
+
# @opts hash and injected into dependencies, like API instances.
|
12
|
+
module OptionParsing
|
13
|
+
attr_reader :argv, :env, :opts
|
14
|
+
|
15
|
+
##
|
16
|
+
# Reset the @opts instance variable by parsing @argv and @env. Operates
|
17
|
+
# against duplicate copies of the argument vector avoid side effects.
|
18
|
+
#
|
19
|
+
# @return [Hash<Symbol, String>] Options hash
|
20
|
+
def reset_options!
|
21
|
+
@opts = parse_options(argv, env)
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Parse options using the argument vector and the environment hash as
|
26
|
+
# input. Option parsing occurs in two phases, first the global options are
|
27
|
+
# parsed. These are the options specified before the subcommand. The
|
28
|
+
# subcommand, if any, is matched, and subcommand specific options are then
|
29
|
+
# parsed from the remainder of the argument vector.
|
30
|
+
#
|
31
|
+
# @param [Array] argv The argument vector, passed to the option parser.
|
32
|
+
#
|
33
|
+
# @param [Hash] env The environment hash, passed to the option parser to
|
34
|
+
# supply defaults not specified on the command line argument vector.
|
35
|
+
#
|
36
|
+
# @return [Hash<Symbol, String>] options hash
|
37
|
+
def parse_options(argv, env)
|
38
|
+
argv_copy = argv.dup
|
39
|
+
opts = parse_global_options!(argv_copy, env)
|
40
|
+
subcommand = parse_subcommand!(argv_copy)
|
41
|
+
opts[:subcommand] = subcommand
|
42
|
+
sub_opts = parse_subcommand_options!(subcommand, argv_copy, env)
|
43
|
+
opts.merge!(sub_opts)
|
44
|
+
opts
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Parse out the global options, the ones specified between the main
|
49
|
+
# executable and the subcommand argument.
|
50
|
+
#
|
51
|
+
# Modifies argv as a side effect, shifting elements from the array until
|
52
|
+
# the first unknown option is found, which is assumed to be the subcommand
|
53
|
+
# name.
|
54
|
+
#
|
55
|
+
# @return [Hash<Symbol, String>] Global options
|
56
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
57
|
+
def parse_global_options!(argv, env)
|
58
|
+
semver = Ncio::VERSION
|
59
|
+
host = Socket.gethostname
|
60
|
+
Ncio::Trollop.options(argv) do
|
61
|
+
stop_on_unknown
|
62
|
+
version "ncio #{semver} (c) 2016 Jeff McCune"
|
63
|
+
banner BANNER
|
64
|
+
uri_dfl = env['NCIO_URI'] || "https://#{host}:4433/classifier-api/v1"
|
65
|
+
opt :uri, 'Node Classifier service uri '\
|
66
|
+
'{NCIO_URI}', default: uri_dfl
|
67
|
+
opt :cert, CERT_MSG, default: env['NCIO_CERT'] || CERT_DEFAULT
|
68
|
+
opt :key, KEY_MSG, default: env['NCIO_KEY'] || KEY_DEFAULT
|
69
|
+
opt :cacert, CACERT_MSG, default: env['NCIO_CACERT'] || CACERT_DEFAULT
|
70
|
+
log_msg = 'Log file to write to or keywords '\
|
71
|
+
'STDOUT, STDERR {NCIO_LOGTO}'
|
72
|
+
opt :logto, log_msg, default: env['NCIO_LOGTO'] || 'STDERR'
|
73
|
+
opt :debug
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
77
|
+
|
78
|
+
##
|
79
|
+
# Extract the subcommand, if any, from the arguments provided. Modifies
|
80
|
+
# argv as a side effect, shifting the subcommand name if it is present.
|
81
|
+
#
|
82
|
+
# @return [String] The subcommand name, e.g. 'backup' or 'restore', or
|
83
|
+
# false if no arguments remain in the argument vector.
|
84
|
+
def parse_subcommand!(argv)
|
85
|
+
argv.shift || false
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Parse the subcommand options. This method branches out because each
|
90
|
+
# subcommand can have quite different options, unlike global options which
|
91
|
+
# are consistent across all invocations of the application.
|
92
|
+
#
|
93
|
+
# Modifies argv as a side effect, shifting all options as things are
|
94
|
+
# parsed.
|
95
|
+
#
|
96
|
+
# @return [Hash<Symbol, String>] Subcommand specific options hash
|
97
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
98
|
+
def parse_subcommand_options!(subcommand, argv, env)
|
99
|
+
case subcommand
|
100
|
+
when 'backup', 'restore'
|
101
|
+
Ncio::Trollop.options(argv) do
|
102
|
+
banner "Node Classification #{subcommand} options:"
|
103
|
+
groups_msg = 'Operate against NC groups. See: https://goo.gl/QD6ZdW'
|
104
|
+
opt :groups, groups_msg, default: true
|
105
|
+
file_msg = 'File to operate against {NCIO_FILE} or STDOUT, STDERR'
|
106
|
+
file_default = FILE_DEFAULT_MAP[subcommand]
|
107
|
+
opt :file, file_msg, default: env['NCIO_FILE'] || file_default
|
108
|
+
end
|
109
|
+
when 'transform'
|
110
|
+
opts = Ncio::Trollop.options(argv) do
|
111
|
+
banner "Node Classification transformations\n"\
|
112
|
+
'Note: Currently only Monolithic (All-in-one) deployments are '\
|
113
|
+
"supported.\n\nTransformation matches against class names "\
|
114
|
+
'assigned to groups. Transformation of hostnames happen '\
|
115
|
+
'against rules assigned to groups and class parameters for '\
|
116
|
+
"matching classes.\n\nOptions:"
|
117
|
+
hostname_msg = 'Replace the fully qualified domain name on the '\
|
118
|
+
'left with the right, separated with a : '\
|
119
|
+
'e.g --hostname master1.acme.com:master2.acme.com'
|
120
|
+
opt :class_matcher, 'Regexp matching classes assigned to groups. '\
|
121
|
+
'Passed to Regexp.new()',
|
122
|
+
default: '^puppet_enterprise'
|
123
|
+
opt :input, 'Input file path or keywords STDIN, STDOUT, STDERR',
|
124
|
+
default: 'STDIN'
|
125
|
+
opt :output, 'Output file path or keywords STDIN, STDOUT, STDERR',
|
126
|
+
default: 'STDOUT'
|
127
|
+
opt :hostname, hostname_msg, type: :strings, multi: true,
|
128
|
+
required: true
|
129
|
+
end
|
130
|
+
opts[:matcher] = Regexp.new(opts[:class_matcher])
|
131
|
+
if opts[:hostname_given]
|
132
|
+
hsh = map_hostnames(opts[:hostname].flatten)
|
133
|
+
opts.merge!(hostname_map: hsh)
|
134
|
+
end
|
135
|
+
else
|
136
|
+
Ncio::Trollop.die "Unknown subcommand: #{subcommand.inspect}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
140
|
+
|
141
|
+
##
|
142
|
+
# Map an array of hostnames separated by a colon. The left is the key of
|
143
|
+
# the map, the right is the value. The Hash returned is "smart" in that a
|
144
|
+
# key that does not exist will return a value matching the key.
|
145
|
+
#
|
146
|
+
# @param [Array<String>] hostnames
|
147
|
+
#
|
148
|
+
# @return [Hash<String, String>] Hash of hostnames, left as key, right as
|
149
|
+
# value.
|
150
|
+
def map_hostnames(hostnames)
|
151
|
+
smart_map = Hash.new { |_, key| key }
|
152
|
+
hostnames.each_with_object(smart_map) do |pair, hsh|
|
153
|
+
(k, v) = pair.split(':')
|
154
|
+
hsh[k] = v
|
155
|
+
hsh
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
BANNER = <<-'EOBANNER'.freeze
|
160
|
+
usage: ncio [GLOBAL OPTIONS] SUBCOMMAND [ARGS]
|
161
|
+
Sub Commands:
|
162
|
+
|
163
|
+
backup Backup Node Classification resources
|
164
|
+
restore Restore Node Classification resources
|
165
|
+
transform Transform a backup, replacing hostnames
|
166
|
+
|
167
|
+
Quick Start: On the host of the Node Classifier service, as root or pe-puppet
|
168
|
+
(to read certs and keys)
|
169
|
+
|
170
|
+
/opt/puppetlabs/puppet/bin/ncio backup > groups.$(date +%s).json
|
171
|
+
/opt/puppetlabs/puppet/bin/ncio restore < groups.1467151827.json
|
172
|
+
|
173
|
+
Transformation:
|
174
|
+
|
175
|
+
ncio --uri https://master1.puppet.vm:4433/classification-api/v1 backup \
|
176
|
+
| ncio transform --hostname master1.puppet.vm:master2.puppet.vm \
|
177
|
+
| ncio --uri https://master2.puppet.vm:4433/classification-api/v1 restore
|
178
|
+
|
179
|
+
Global options: (Note, command line arguments supersede ENV vars in {}'s)
|
180
|
+
EOBANNER
|
181
|
+
|
182
|
+
SSLDIR = '/etc/puppetlabs/puppet/ssl'.freeze
|
183
|
+
CERT_MSG = 'White listed client SSL cert {NCIO_CERT} '\
|
184
|
+
'See: https://goo.gl/zCjncC'.freeze
|
185
|
+
CERT_DEFAULT = (SSLDIR + '/certs/'\
|
186
|
+
'pe-internal-orchestrator.pem').freeze
|
187
|
+
KEY_MSG = 'Client RSA key, must match certificate '\
|
188
|
+
'{NCIO_KEY}'.freeze
|
189
|
+
KEY_DEFAULT = (SSLDIR + '/private_keys/'\
|
190
|
+
'pe-internal-orchestrator.pem').freeze
|
191
|
+
CACERT_MSG = 'CA Cert to authenticate the service uri '\
|
192
|
+
'{NCIO_CACERT}'.freeze
|
193
|
+
CACERT_DEFAULT = (SSLDIR + '/certs/ca.pem').freeze
|
194
|
+
|
195
|
+
# Map is indexed by the subcommand
|
196
|
+
FILE_DEFAULT_MAP = { 'backup' => 'STDOUT', 'restore' => 'STDIN' }.freeze
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
# rubocop:enable Metrics/ModuleLength
|