ncio 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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