bolt 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/bolt/applicator.rb +6 -0
- data/lib/bolt/bolt_option_parser.rb +17 -0
- data/lib/bolt/catalog.rb +2 -2
- data/lib/bolt/cli.rb +65 -22
- data/lib/bolt/error.rb +2 -2
- data/lib/bolt/inventory/group.rb +10 -10
- data/lib/bolt/outputter/human.rb +6 -0
- data/lib/bolt/outputter/json.rb +4 -0
- data/lib/bolt/pal.rb +10 -0
- data/lib/bolt/target.rb +4 -1
- data/lib/bolt/task/puppet_server.rb +27 -0
- data/lib/bolt/task.rb +22 -8
- data/lib/bolt/transport/base.rb +0 -4
- data/lib/bolt/transport/local.rb +15 -7
- data/lib/bolt/transport/ssh.rb +13 -22
- data/lib/bolt/transport/winrm.rb +13 -22
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/acl.rb +39 -0
- data/lib/bolt_server/config.rb +105 -0
- data/lib/bolt_server/file_cache.rb +177 -0
- data/lib/{bolt_ext → bolt_server}/schemas/ssh-run_task.json +0 -0
- data/lib/{bolt_ext → bolt_server}/schemas/task.json +24 -14
- data/lib/{bolt_ext → bolt_server}/schemas/winrm-run_task.json +0 -0
- data/lib/bolt_server/transport_app.rb +105 -0
- data/lib/bolt_spec/run.rb +15 -1
- data/libexec/bolt_catalog +1 -1
- metadata +24 -8
- data/lib/bolt_ext/server.rb +0 -101
- data/lib/bolt_ext/server_acl.rb +0 -37
- data/lib/bolt_ext/server_config.rb +0 -88
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hocon'
|
4
|
+
require 'bolt/error'
|
5
|
+
|
6
|
+
module BoltServer
|
7
|
+
class Config
|
8
|
+
CONFIG_KEYS = ['host', 'port', 'ssl-cert', 'ssl-key', 'ssl-ca-cert',
|
9
|
+
'ssl-cipher-suites', 'loglevel', 'logfile', 'whitelist', 'concurrency',
|
10
|
+
'cache-dir', 'file-server-conn-timeout', 'file-server-uri'].freeze
|
11
|
+
|
12
|
+
DEFAULTS = {
|
13
|
+
'host' => '127.0.0.1',
|
14
|
+
'port' => 62658,
|
15
|
+
'ssl-cipher-suites' => ['ECDHE-ECDSA-AES256-GCM-SHA384',
|
16
|
+
'ECDHE-RSA-AES256-GCM-SHA384',
|
17
|
+
'ECDHE-ECDSA-CHACHA20-POLY1305',
|
18
|
+
'ECDHE-RSA-CHACHA20-POLY1305',
|
19
|
+
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
20
|
+
'ECDHE-RSA-AES128-GCM-SHA256',
|
21
|
+
'ECDHE-ECDSA-AES256-SHA384',
|
22
|
+
'ECDHE-RSA-AES256-SHA384',
|
23
|
+
'ECDHE-ECDSA-AES128-SHA256',
|
24
|
+
'ECDHE-RSA-AES128-SHA256'],
|
25
|
+
'loglevel' => 'notice',
|
26
|
+
'concurrency' => 100,
|
27
|
+
'cache-dir' => "/opt/puppetlabs/server/data/bolt-server/cache",
|
28
|
+
'file-server-conn-timeout' => 120
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
CONFIG_KEYS.each do |key|
|
32
|
+
define_method(key.tr('-', '_').to_sym) do
|
33
|
+
@data[key]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(config = nil)
|
38
|
+
@data = DEFAULTS.clone
|
39
|
+
@data = @data.merge(config.select { |key, _| CONFIG_KEYS.include?(key) }) if config
|
40
|
+
@config_path = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_config(path)
|
44
|
+
@config_path = path
|
45
|
+
begin
|
46
|
+
parsed_hocon = Hocon.load(path)['bolt-server']
|
47
|
+
rescue Hocon::ConfigError => e
|
48
|
+
raise "Hocon data in '#{path}' failed to load.\n Error: '#{e.message}'"
|
49
|
+
rescue Errno::EACCES
|
50
|
+
raise "Your user doesn't have permission to read #{path}"
|
51
|
+
end
|
52
|
+
|
53
|
+
raise "Could not find bolt-server config at #{path}" if parsed_hocon.nil?
|
54
|
+
|
55
|
+
parsed_hocon = parsed_hocon.select { |key, _| CONFIG_KEYS.include?(key) }
|
56
|
+
@data = @data.merge(parsed_hocon)
|
57
|
+
|
58
|
+
validate
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def natural?(num)
|
63
|
+
num.is_a?(Integer) && num.positive?
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate
|
67
|
+
ssl_keys = ['ssl-cert', 'ssl-key', 'ssl-ca-cert']
|
68
|
+
required_keys = ssl_keys + ['file-server-uri']
|
69
|
+
|
70
|
+
required_keys.each do |k|
|
71
|
+
next unless @data[k].nil?
|
72
|
+
raise Bolt::ValidationError, "You must configure #{k} in #{@config_path}"
|
73
|
+
end
|
74
|
+
|
75
|
+
unless natural?(port)
|
76
|
+
raise Bolt::ValidationError, "Configured 'port' must be a valid integer greater than 0"
|
77
|
+
end
|
78
|
+
ssl_keys.each do |sk|
|
79
|
+
unless File.file?(@data[sk]) && File.readable?(@data[sk])
|
80
|
+
raise Bolt::ValidationError, "Configured #{sk} must be a valid filepath"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
unless ssl_cipher_suites.is_a?(Array)
|
85
|
+
raise Bolt::ValidationError, "Configured 'ssl-cipher-suites' must be an array of cipher suite names"
|
86
|
+
end
|
87
|
+
|
88
|
+
unless whitelist.nil? || whitelist.is_a?(Array)
|
89
|
+
raise Bolt::ValidationError, "Configured 'whitelist' must be an array of names"
|
90
|
+
end
|
91
|
+
|
92
|
+
unless natural?(concurrency)
|
93
|
+
raise Bolt::ValidationError, "Configured 'concurrency' must be a positive integer"
|
94
|
+
end
|
95
|
+
|
96
|
+
unless natural?(file_server_conn_timeout)
|
97
|
+
raise Bolt::ValidationError, "Configured 'file-server-conn-timeout' must be a positive integer"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def [](key)
|
102
|
+
@data[key]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
require 'digest'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'net/http'
|
7
|
+
require 'logging'
|
8
|
+
|
9
|
+
require 'bolt/error'
|
10
|
+
|
11
|
+
module BoltServer
|
12
|
+
class FileCache
|
13
|
+
class Error < Bolt::Error
|
14
|
+
def initialize(msg)
|
15
|
+
super(msg, 'bolt-server/file-cache-error')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
PURGE_TIMEOUT = 60 * 60
|
20
|
+
PURGE_INTERVAL = 24 * PURGE_TIMEOUT
|
21
|
+
PURGE_TTL = 7 * PURGE_INTERVAL
|
22
|
+
|
23
|
+
def initialize(config,
|
24
|
+
executor: Concurrent::SingleThreadExecutor.new,
|
25
|
+
purge_interval: PURGE_INTERVAL,
|
26
|
+
purge_timeout: PURGE_TIMEOUT,
|
27
|
+
purge_ttl: PURGE_TTL)
|
28
|
+
@executor = executor
|
29
|
+
@cache_dir = config.cache_dir
|
30
|
+
@config = config
|
31
|
+
@logger = Logging.logger[self]
|
32
|
+
@cache_dir_mutex = Concurrent::ReadWriteLock.new
|
33
|
+
|
34
|
+
@purge = Concurrent::TimerTask.new(execution_interval: purge_interval,
|
35
|
+
timeout_interval: purge_timeout,
|
36
|
+
run_now: true) { expire(purge_ttl) }
|
37
|
+
@purge.execute
|
38
|
+
end
|
39
|
+
|
40
|
+
def tmppath
|
41
|
+
File.join(@cache_dir, 'tmp')
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup
|
45
|
+
FileUtils.mkdir_p(@cache_dir)
|
46
|
+
FileUtils.mkdir_p(tmppath)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def ssl_cert
|
51
|
+
@ssl_cert ||= File.read(@config.ssl_cert)
|
52
|
+
end
|
53
|
+
|
54
|
+
def ssl_key
|
55
|
+
@ssl_key ||= File.read(@config.ssl_key)
|
56
|
+
end
|
57
|
+
|
58
|
+
def client
|
59
|
+
@client ||= begin
|
60
|
+
uri = URI(@config.file_server_uri)
|
61
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
62
|
+
https.use_ssl = true
|
63
|
+
https.ssl_version = :TLSv1_2
|
64
|
+
https.ca_file = @config.ssl_ca_cert
|
65
|
+
https.cert = OpenSSL::X509::Certificate.new(ssl_cert)
|
66
|
+
https.key = OpenSSL::PKey::RSA.new(ssl_key)
|
67
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
68
|
+
https.open_timeout = @config.file_server_conn_timeout
|
69
|
+
https
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def request_file(path, params, file)
|
74
|
+
uri = "#{@config.file_server_uri.chomp('/')}#{path}"
|
75
|
+
uri = URI(uri)
|
76
|
+
uri.query = URI.encode_www_form(params)
|
77
|
+
|
78
|
+
req = Net::HTTP::Get.new(uri)
|
79
|
+
|
80
|
+
begin
|
81
|
+
client.request(req) do |resp|
|
82
|
+
if resp.code != "200"
|
83
|
+
msg = "Failed to download task: #{resp.body}"
|
84
|
+
@logger.warn resp.body
|
85
|
+
raise Error, msg
|
86
|
+
end
|
87
|
+
resp.read_body do |chunk|
|
88
|
+
file.write(chunk)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue StandardError => e
|
92
|
+
if e.is_a?(Bolt::Error)
|
93
|
+
raise e
|
94
|
+
else
|
95
|
+
@logger.warn e
|
96
|
+
raise Error, "Failed to download task: #{e.message}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
file.close
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_file(file_path, sha)
|
104
|
+
File.exist?(file_path) && Digest::SHA256.file(file_path) == sha
|
105
|
+
end
|
106
|
+
|
107
|
+
def serial_execute(&block)
|
108
|
+
promise = Concurrent::Promise.new(executor: @executor, &block).execute.wait
|
109
|
+
raise promise.reason if promise.state == :rejected
|
110
|
+
promise.value
|
111
|
+
end
|
112
|
+
|
113
|
+
# Create a cache dir if necessary and update it's last write time. Returns the dir.
|
114
|
+
# Acquires @cache_dir_mutex to ensure we don't try to purge the directory at the same time.
|
115
|
+
# Uses the directory mtime because it's simpler to ensure the directory exists and update
|
116
|
+
# mtime in a single place than with a file in a directory that may not exist.
|
117
|
+
def create_cache_dir(sha)
|
118
|
+
file_dir = File.join(@cache_dir, sha)
|
119
|
+
@cache_dir_mutex.with_read_lock do
|
120
|
+
# mkdir_p doesn't error if the file exists
|
121
|
+
FileUtils.mkdir_p(file_dir, mode: 0o750)
|
122
|
+
FileUtils.touch(file_dir)
|
123
|
+
end
|
124
|
+
file_dir
|
125
|
+
end
|
126
|
+
|
127
|
+
def download_file(file_path, sha, uri)
|
128
|
+
if check_file(file_path, sha)
|
129
|
+
@logger.debug("File was downloaded while queued: #{file_path}")
|
130
|
+
return file_path
|
131
|
+
end
|
132
|
+
|
133
|
+
@logger.debug("Downloading file: #{file_path}")
|
134
|
+
|
135
|
+
tmpfile = Tempfile.new(sha, tmppath)
|
136
|
+
request_file(uri['path'], uri['params'], tmpfile)
|
137
|
+
|
138
|
+
if Digest::SHA256.file(tmpfile.path) == sha
|
139
|
+
# mv doesn't error if the file exists
|
140
|
+
FileUtils.mv(tmpfile.path, file_path)
|
141
|
+
@logger.debug("Downloaded file: #{file_path}")
|
142
|
+
file_path
|
143
|
+
else
|
144
|
+
msg = "Downloaded file did not match checksum for: #{file_path}"
|
145
|
+
@logger.warn msg
|
146
|
+
raise Error, msg
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# If the file doesn't exist or is invalid redownload it
|
151
|
+
# This downloads, validates and moves into place
|
152
|
+
def update_file(file_data)
|
153
|
+
sha = file_data['sha256']
|
154
|
+
file_dir = create_cache_dir(file_data['sha256'])
|
155
|
+
file_path = File.join(file_dir, File.basename(file_data['filename']))
|
156
|
+
if check_file(file_path, sha)
|
157
|
+
@logger.debug("Using prexisting task file: #{file_path}")
|
158
|
+
return file_path
|
159
|
+
end
|
160
|
+
|
161
|
+
@logger.debug("Queueing download for: #{file_path}")
|
162
|
+
serial_execute { download_file(file_path, sha, file_data['uri']) }
|
163
|
+
end
|
164
|
+
|
165
|
+
def expire(purge_ttl)
|
166
|
+
expired_time = Time.now - purge_ttl
|
167
|
+
@cache_dir_mutex.with_write_lock do
|
168
|
+
Dir.glob(File.join(@cache_dir, '*')).select { |f| File.directory?(f) }.each do |dir|
|
169
|
+
if (mtime = File.mtime(dir)) < expired_time
|
170
|
+
@logger.debug("Removing #{dir}, last used at #{mtime}")
|
171
|
+
FileUtils.remove_dir(dir)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
File without changes
|
@@ -42,23 +42,33 @@
|
|
42
42
|
}
|
43
43
|
}
|
44
44
|
},
|
45
|
-
"
|
46
|
-
"type": "
|
47
|
-
"description": "
|
48
|
-
"
|
49
|
-
"
|
50
|
-
|
51
|
-
"
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
"
|
45
|
+
"files": {
|
46
|
+
"type": "array",
|
47
|
+
"description": "Description of task files",
|
48
|
+
"items": {
|
49
|
+
"type": "object",
|
50
|
+
"properties": {
|
51
|
+
"uri": {
|
52
|
+
"type": "object",
|
53
|
+
"description": "Where is the file"
|
54
|
+
},
|
55
|
+
"sha256": {
|
56
|
+
"type": "string",
|
57
|
+
"description": "checksum of file"
|
58
|
+
},
|
59
|
+
"filename": {
|
60
|
+
"type": "string",
|
61
|
+
"description": "Name of file"
|
62
|
+
},
|
63
|
+
"size": {
|
64
|
+
"type": "number",
|
65
|
+
"description": "Size of file"
|
66
|
+
}
|
56
67
|
}
|
57
68
|
},
|
58
|
-
"required": ["filename", "
|
59
|
-
"additionalProperties": false
|
69
|
+
"required": ["filename", "uri", "sha256"]
|
60
70
|
}
|
61
71
|
},
|
62
|
-
"required": ["name", "
|
72
|
+
"required": ["name", "files"],
|
63
73
|
"additionalProperties": false
|
64
74
|
}
|
File without changes
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
require 'bolt'
|
5
|
+
require 'bolt/target'
|
6
|
+
require 'bolt/task/puppet_server'
|
7
|
+
require 'bolt_server/file_cache'
|
8
|
+
require 'json'
|
9
|
+
require 'json-schema'
|
10
|
+
|
11
|
+
module BoltServer
|
12
|
+
class TransportApp < Sinatra::Base
|
13
|
+
# This disables Sinatra's error page generation
|
14
|
+
set :show_exceptions, false
|
15
|
+
|
16
|
+
def initialize(config)
|
17
|
+
@config = config
|
18
|
+
@schemas = {
|
19
|
+
"ssh-run_task" => JSON.parse(File.read(File.join(__dir__, 'schemas', 'ssh-run_task.json'))),
|
20
|
+
"winrm-run_task" => JSON.parse(File.read(File.join(__dir__, 'schemas', 'winrm-run_task.json')))
|
21
|
+
}
|
22
|
+
shared_schema = JSON::Schema.new(JSON.parse(File.read(File.join(__dir__, 'schemas', 'task.json'))),
|
23
|
+
Addressable::URI.parse("file:task"))
|
24
|
+
JSON::Validator.add_schema(shared_schema)
|
25
|
+
|
26
|
+
@executor = Bolt::Executor.new(0, load_config: false)
|
27
|
+
|
28
|
+
@file_cache = BoltServer::FileCache.new(@config).setup
|
29
|
+
|
30
|
+
super(nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
get '/' do
|
34
|
+
200
|
35
|
+
end
|
36
|
+
|
37
|
+
if ENV['RACK_ENV'] == 'dev'
|
38
|
+
get '/admin/gc' do
|
39
|
+
GC.start
|
40
|
+
200
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
get '/admin/gc_stat' do
|
45
|
+
[200, GC.stat.to_json]
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/500_error' do
|
49
|
+
raise 'Unexpected error'
|
50
|
+
end
|
51
|
+
|
52
|
+
post '/ssh/run_task' do
|
53
|
+
content_type :json
|
54
|
+
|
55
|
+
body = JSON.parse(request.body.read)
|
56
|
+
schema_error = JSON::Validator.fully_validate(@schemas["ssh-run_task"], body)
|
57
|
+
return [400, schema_error.join] if schema_error.any?
|
58
|
+
|
59
|
+
opts = body['target']
|
60
|
+
if opts['private-key-content']
|
61
|
+
opts['private-key'] = { 'key-data' => opts['private-key-content'] }
|
62
|
+
opts.delete('private-key-content')
|
63
|
+
end
|
64
|
+
|
65
|
+
target = [Bolt::Target.new(body['target']['hostname'], opts)]
|
66
|
+
|
67
|
+
task = Bolt::Task::PuppetServer.new(body['task'], @file_cache)
|
68
|
+
|
69
|
+
parameters = body['parameters'] || {}
|
70
|
+
|
71
|
+
# Since this will only be on one node we can just return the first result
|
72
|
+
results = @executor.run_task(target, task, parameters)
|
73
|
+
[200, results.first.to_json]
|
74
|
+
end
|
75
|
+
|
76
|
+
post '/winrm/run_task' do
|
77
|
+
content_type :json
|
78
|
+
|
79
|
+
body = JSON.parse(request.body.read)
|
80
|
+
schema_error = JSON::Validator.fully_validate(@schemas["winrm-run_task"], body)
|
81
|
+
return [400, schema_error.join] if schema_error.any?
|
82
|
+
|
83
|
+
opts = body['target'].merge('protocol' => 'winrm')
|
84
|
+
|
85
|
+
target = [Bolt::Target.new(body['target']['hostname'], opts)]
|
86
|
+
|
87
|
+
task = Bolt::Task::PuppetServer.new(body['task'], @file_cache)
|
88
|
+
|
89
|
+
parameters = body['parameters'] || {}
|
90
|
+
|
91
|
+
# Since this will only be on one node we can just return the first result
|
92
|
+
results = @executor.run_task(target, task, parameters)
|
93
|
+
[200, results.first.to_json]
|
94
|
+
end
|
95
|
+
|
96
|
+
error 404 do
|
97
|
+
[404, "Could not find route #{request.path}"]
|
98
|
+
end
|
99
|
+
|
100
|
+
error 500 do
|
101
|
+
e = env['sinatra.error']
|
102
|
+
[500, "500: Unknown error: #{e.message}"]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/bolt_spec/run.rb
CHANGED
@@ -9,7 +9,7 @@ require 'bolt/puppetdb'
|
|
9
9
|
require 'bolt/util'
|
10
10
|
|
11
11
|
# This is intended to provide a relatively stable method of executing bolt in process from tests.
|
12
|
-
# Currently it provides run_task, run_plan and run_command helpers.
|
12
|
+
# Currently it provides run_task, run_plan, run_script and run_command helpers.
|
13
13
|
module BoltSpec
|
14
14
|
module Run
|
15
15
|
def run_task(task_name, targets, params = nil, config: nil, inventory: nil)
|
@@ -41,6 +41,14 @@ module BoltSpec
|
|
41
41
|
Bolt::Util.walk_keys(result, &:to_s)
|
42
42
|
end
|
43
43
|
|
44
|
+
def run_script(script, targets, arguments = nil, options = {}, config: nil, inventory: nil)
|
45
|
+
result = BoltRunner.with_runner(config, inventory) do |runner|
|
46
|
+
runner.run_script(script, targets, arguments, options)
|
47
|
+
end
|
48
|
+
result = result.to_a
|
49
|
+
Bolt::Util.walk_keys(result, &:to_s)
|
50
|
+
end
|
51
|
+
|
44
52
|
class BoltRunner
|
45
53
|
# Creates a temporary boltdir so no settings are picked up
|
46
54
|
# WARNING: puppetdb config and orch config which do not use the boltdir may
|
@@ -93,6 +101,12 @@ module BoltSpec
|
|
93
101
|
targets = inventory.get_targets(targets)
|
94
102
|
executor.run_command(targets, command, params || {})
|
95
103
|
end
|
104
|
+
|
105
|
+
def run_script(script, targets, arguments = nil, options = {})
|
106
|
+
executor = Bolt::Executor.new(config.concurrency, @analytics)
|
107
|
+
targets = inventory.get_targets(targets)
|
108
|
+
executor.run_script(targets, script, arguments, options)
|
109
|
+
end
|
96
110
|
end
|
97
111
|
end
|
98
112
|
end
|
data/libexec/bolt_catalog
CHANGED
@@ -34,7 +34,7 @@ require 'json'
|
|
34
34
|
command = ARGV[0]
|
35
35
|
if command == "parse"
|
36
36
|
code = File.open(ARGV[1], &:read)
|
37
|
-
puts JSON.pretty_generate(Bolt::Catalog.new.generate_ast(code))
|
37
|
+
puts JSON.pretty_generate(Bolt::Catalog.new.generate_ast(code, ARGV[1]))
|
38
38
|
elsif command == "compile"
|
39
39
|
request = if ARGV[1]
|
40
40
|
File.open(ARGV[1]) { |fh| JSON.parse(fh.read) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bolt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-10-
|
11
|
+
date: 2018-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: CFPropertyList
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: concurrent-ruby
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -307,6 +321,7 @@ files:
|
|
307
321
|
- lib/bolt/result_set.rb
|
308
322
|
- lib/bolt/target.rb
|
309
323
|
- lib/bolt/task.rb
|
324
|
+
- lib/bolt/task/puppet_server.rb
|
310
325
|
- lib/bolt/transport/base.rb
|
311
326
|
- lib/bolt/transport/local.rb
|
312
327
|
- lib/bolt/transport/local/shell.rb
|
@@ -320,12 +335,13 @@ files:
|
|
320
335
|
- lib/bolt/util/puppet_log_level.rb
|
321
336
|
- lib/bolt/version.rb
|
322
337
|
- lib/bolt_ext/puppetdb_inventory.rb
|
323
|
-
- lib/
|
324
|
-
- lib/
|
325
|
-
- lib/
|
326
|
-
- lib/
|
327
|
-
- lib/
|
328
|
-
- lib/
|
338
|
+
- lib/bolt_server/acl.rb
|
339
|
+
- lib/bolt_server/config.rb
|
340
|
+
- lib/bolt_server/file_cache.rb
|
341
|
+
- lib/bolt_server/schemas/ssh-run_task.json
|
342
|
+
- lib/bolt_server/schemas/task.json
|
343
|
+
- lib/bolt_server/schemas/winrm-run_task.json
|
344
|
+
- lib/bolt_server/transport_app.rb
|
329
345
|
- lib/bolt_spec/plans.rb
|
330
346
|
- lib/bolt_spec/plans/mock_executor.rb
|
331
347
|
- lib/bolt_spec/run.rb
|
data/lib/bolt_ext/server.rb
DELETED
@@ -1,101 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'sinatra'
|
4
|
-
require 'bolt'
|
5
|
-
require 'bolt/target'
|
6
|
-
require 'bolt/task'
|
7
|
-
require 'json'
|
8
|
-
require 'json-schema'
|
9
|
-
|
10
|
-
class TransportAPI < Sinatra::Base
|
11
|
-
# This disables Sinatra's error page generation
|
12
|
-
set :show_exceptions, false
|
13
|
-
|
14
|
-
def initialize(app = nil)
|
15
|
-
@schemas = {
|
16
|
-
"ssh-run_task" => JSON.parse(File.read(File.join(__dir__, 'schemas', 'ssh-run_task.json'))),
|
17
|
-
"winrm-run_task" => JSON.parse(File.read(File.join(__dir__, 'schemas', 'winrm-run_task.json')))
|
18
|
-
}
|
19
|
-
shared_schema = JSON::Schema.new(JSON.parse(File.read(File.join(__dir__, 'schemas', 'task.json'))),
|
20
|
-
Addressable::URI.parse("file:task"))
|
21
|
-
JSON::Validator.add_schema(shared_schema)
|
22
|
-
|
23
|
-
@executor = Bolt::Executor.new(0, load_config: false)
|
24
|
-
|
25
|
-
super(app)
|
26
|
-
end
|
27
|
-
|
28
|
-
get '/' do
|
29
|
-
200
|
30
|
-
end
|
31
|
-
|
32
|
-
if ENV['RACK_ENV'] == 'dev'
|
33
|
-
get '/admin/gc' do
|
34
|
-
GC.start
|
35
|
-
200
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
get '/admin/gc_stat' do
|
40
|
-
[200, GC.stat.to_json]
|
41
|
-
end
|
42
|
-
|
43
|
-
get '/500_error' do
|
44
|
-
raise 'Unexpected error'
|
45
|
-
end
|
46
|
-
|
47
|
-
post '/ssh/run_task' do
|
48
|
-
content_type :json
|
49
|
-
|
50
|
-
body = JSON.parse(request.body.read)
|
51
|
-
schema_error = JSON::Validator.fully_validate(@schemas["ssh-run_task"], body)
|
52
|
-
return [400, schema_error.join] if schema_error.any?
|
53
|
-
|
54
|
-
# CODEREVIEW: the schema is additionalProperties false do we need this?
|
55
|
-
keys = %w[user password port connect-timeout run-as-command run-as
|
56
|
-
tmpdir host-key-check known-hosts-content private-key-content sudo-password
|
57
|
-
tty]
|
58
|
-
opts = body['target'].select { |k, _| keys.include? k }
|
59
|
-
|
60
|
-
if opts['private-key-content']
|
61
|
-
opts['private-key'] = { 'key-data' => opts['private-key-content'] }
|
62
|
-
opts.delete('private-key-content')
|
63
|
-
end
|
64
|
-
|
65
|
-
target = [Bolt::Target.new(body['target']['hostname'], opts)]
|
66
|
-
task = Bolt::Task.new(body['task'])
|
67
|
-
parameters = body['parameters'] || {}
|
68
|
-
|
69
|
-
# Since this will only be on one node we can just return the first result
|
70
|
-
results = @executor.run_task(target, task, parameters)
|
71
|
-
[200, results.first.to_json]
|
72
|
-
end
|
73
|
-
|
74
|
-
post '/winrm/run_task' do
|
75
|
-
content_type :json
|
76
|
-
|
77
|
-
body = JSON.parse(request.body.read)
|
78
|
-
schema_error = JSON::Validator.fully_validate(@schemas["winrm-run_task"], body)
|
79
|
-
return [400, schema_error.join] if schema_error.any?
|
80
|
-
|
81
|
-
keys = %w[user password port connect-timeout ssl ssl-verify tmpdir cacert extensions]
|
82
|
-
opts = body['target'].select { |k, _| keys.include? k }
|
83
|
-
opts['protocol'] = 'winrm'
|
84
|
-
target = [Bolt::Target.new(body['target']['hostname'], opts)]
|
85
|
-
task = Bolt::Task.new(body['task'])
|
86
|
-
parameters = body['parameters'] || {}
|
87
|
-
|
88
|
-
# Since this will only be on one node we can just return the first result
|
89
|
-
results = @executor.run_task(target, task, parameters)
|
90
|
-
[200, results.first.to_json]
|
91
|
-
end
|
92
|
-
|
93
|
-
error 404 do
|
94
|
-
[404, "Could not find route #{request.path}"]
|
95
|
-
end
|
96
|
-
|
97
|
-
error 500 do
|
98
|
-
e = env['sinatra.error']
|
99
|
-
[500, "500: Unknown error: #{e.message}"]
|
100
|
-
end
|
101
|
-
end
|
data/lib/bolt_ext/server_acl.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rails/auth/rack'
|
4
|
-
|
5
|
-
class TransportACL < Rails::Auth::ErrorPage::Middleware
|
6
|
-
class X509Matcher
|
7
|
-
def initialize(options)
|
8
|
-
@options = options.freeze
|
9
|
-
end
|
10
|
-
|
11
|
-
def match(env)
|
12
|
-
certificate = Rails::Auth::X509::Certificate.new(env['puma.peercert'])
|
13
|
-
# This can be extended fairly easily to search OpenSSL::X509::Certificate#extensions for subjectAltNames.
|
14
|
-
@options.all? { |name, value| certificate[name] == value }
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def initialize(app, whitelist)
|
19
|
-
acls = []
|
20
|
-
whitelist.each do |entry|
|
21
|
-
acls << {
|
22
|
-
'resources' => [
|
23
|
-
{
|
24
|
-
'method' => 'ALL',
|
25
|
-
'path' => '/.*'
|
26
|
-
}
|
27
|
-
],
|
28
|
-
'allow_x509_subject' => {
|
29
|
-
'cn' => entry
|
30
|
-
}
|
31
|
-
}
|
32
|
-
end
|
33
|
-
acl = Rails::Auth::ACL.new(acls, matchers: { allow_x509_subject: X509Matcher })
|
34
|
-
mid = Rails::Auth::ACL::Middleware.new(app, acl: acl)
|
35
|
-
super(mid, page_body: 'Access denied')
|
36
|
-
end
|
37
|
-
end
|