bolt 2.1.0 → 2.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.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +5 -5
  3. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +1 -1
  4. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +1 -1
  5. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +1 -1
  6. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +1 -1
  7. data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +1 -1
  8. data/lib/bolt/applicator.rb +2 -2
  9. data/lib/bolt/bolt_option_parser.rb +1 -1
  10. data/lib/bolt/cli.rb +1 -1
  11. data/lib/bolt/config.rb +201 -206
  12. data/lib/bolt/config/transport/base.rb +142 -0
  13. data/lib/bolt/config/transport/docker.rb +42 -0
  14. data/lib/bolt/config/transport/local.rb +73 -0
  15. data/lib/bolt/config/transport/orch.rb +47 -0
  16. data/lib/bolt/config/transport/remote.rb +25 -0
  17. data/lib/bolt/config/transport/ssh.rb +105 -0
  18. data/lib/bolt/config/transport/winrm.rb +80 -0
  19. data/lib/bolt/executor.rb +17 -0
  20. data/lib/bolt/inventory.rb +12 -5
  21. data/lib/bolt/inventory/group.rb +1 -1
  22. data/lib/bolt/inventory/inventory.rb +16 -22
  23. data/lib/bolt/inventory/target.rb +26 -29
  24. data/lib/bolt/plugin.rb +5 -5
  25. data/lib/bolt/plugin/module.rb +1 -1
  26. data/lib/bolt/plugin/pkcs7.rb +1 -1
  27. data/lib/bolt/result.rb +1 -1
  28. data/lib/bolt/target.rb +5 -2
  29. data/lib/bolt/transport/base.rb +0 -18
  30. data/lib/bolt/transport/docker.rb +0 -26
  31. data/lib/bolt/transport/local.rb +0 -30
  32. data/lib/bolt/transport/local_windows.rb +4 -36
  33. data/lib/bolt/transport/orch.rb +0 -20
  34. data/lib/bolt/transport/remote.rb +0 -20
  35. data/lib/bolt/transport/ssh.rb +0 -85
  36. data/lib/bolt/transport/sudoable.rb +0 -7
  37. data/lib/bolt/transport/winrm.rb +0 -66
  38. data/lib/bolt/util.rb +11 -0
  39. data/lib/bolt/version.rb +1 -1
  40. data/lib/bolt_server/transport_app.rb +1 -0
  41. data/lib/bolt_spec/plans.rb +1 -1
  42. data/lib/bolt_spec/plans/action_stubs/command_stub.rb +1 -1
  43. data/lib/bolt_spec/plans/action_stubs/script_stub.rb +1 -1
  44. data/lib/bolt_spec/plans/action_stubs/task_stub.rb +2 -2
  45. data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +1 -1
  46. data/lib/bolt_spec/run.rb +1 -1
  47. data/libexec/apply_catalog.rb +1 -1
  48. data/libexec/custom_facts.rb +1 -1
  49. data/libexec/query_resources.rb +1 -1
  50. metadata +9 -2
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/util'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Base
10
+ attr_reader :input
11
+
12
+ def initialize(data = {}, boltdir = nil)
13
+ assert_hash_or_config(data)
14
+ @input = data
15
+ @resolved = !Bolt::Util.references?(input)
16
+ @config = resolved? ? Bolt::Util.deep_merge(defaults, filter(input)) : defaults
17
+ @boltdir = boltdir
18
+
19
+ validate if resolved?
20
+ end
21
+
22
+ # Accessor methods
23
+ # These are mostly all wrappers for same-named Hash methods, but they all
24
+ # require that the config options be fully-resolved before accessing data
25
+ def [](key)
26
+ resolved_config[key]
27
+ end
28
+
29
+ def to_h
30
+ resolved_config
31
+ end
32
+
33
+ def fetch(*args)
34
+ resolved_config.fetch(*args)
35
+ end
36
+
37
+ def include?(args)
38
+ resolved_config.include?(args)
39
+ end
40
+
41
+ def dig(*keys)
42
+ resolved_config.dig(*keys)
43
+ end
44
+
45
+ private def resolved_config
46
+ unless resolved?
47
+ raise Bolt::Error.new(
48
+ "Unable to access transport config, #{self.class} has unresolved config: #{input.inspect}",
49
+ 'bolt/unresolved-transport-config'
50
+ )
51
+ end
52
+
53
+ @config
54
+ end
55
+
56
+ # Merges the original input data with the provided data, which is either a hash
57
+ # or transport config object. Accepts multiple inputs.
58
+ def merge(*data)
59
+ merged = data.compact.inject(@input) do |acc, layer|
60
+ assert_hash_or_config(layer)
61
+ layer_data = layer.is_a?(self.class) ? layer.input : layer
62
+ Bolt::Util.deep_merge(acc, layer_data)
63
+ end
64
+
65
+ self.class.new(merged, @boltdir)
66
+ end
67
+
68
+ # Resolve any references in the input data, then remerge it with the defaults
69
+ # and validate all values
70
+ def resolve(plugins)
71
+ @input = plugins.resolve_references(input)
72
+ @config = Bolt::Util.deep_merge(defaults, filter(input))
73
+ @resolved = true
74
+
75
+ validate
76
+ end
77
+
78
+ def resolved?
79
+ @resolved
80
+ end
81
+
82
+ def self.options
83
+ unless defined? self::OPTIONS
84
+ raise NotImplementedError,
85
+ "Constant OPTIONS must be implemented by the transport config class"
86
+ end
87
+ self::OPTIONS
88
+ end
89
+
90
+ private def defaults
91
+ unless defined? self.class::DEFAULTS
92
+ raise NotImplementedError,
93
+ "Constant DEFAULTS must be implemented by the transport config class"
94
+ end
95
+ self.class::DEFAULTS
96
+ end
97
+
98
+ private def filter(unfiltered)
99
+ unfiltered.slice(*self.class.options.keys)
100
+ end
101
+
102
+ private def assert_hash_or_config(data)
103
+ return if data.is_a?(Hash) || data.is_a?(self.class)
104
+ raise Bolt::ValidationError,
105
+ "Transport config must be a Hash or #{self.class}, received #{data.class} #{data.inspect}"
106
+ end
107
+
108
+ private def normalize_interpreters(interpreters)
109
+ Bolt::Util.walk_keys(interpreters) do |key|
110
+ key.chars[0] == '.' ? key : '.' + key
111
+ end
112
+ end
113
+
114
+ # Validation defaults to just asserting the option types
115
+ private def validate
116
+ assert_type
117
+ end
118
+
119
+ # Validates that each option is the correct type. Types are loaded from the OPTIONS hash.
120
+ private def assert_type
121
+ @config.each_pair do |opt, val|
122
+ next unless (type = self.class.options.dig(opt, :type))
123
+
124
+ # Options that accept a Boolean value are indicated by the type TrueClass, so we
125
+ # need some special handling here to check if the value is either true or false.
126
+ if type == TrueClass
127
+ unless [true, false].include?(val)
128
+ raise Bolt::ValidationError,
129
+ "#{opt} must be a Boolean true or false, received #{val.class} #{val.inspect}"
130
+ end
131
+ else
132
+ unless val.nil? || val.is_a?(type)
133
+ raise Bolt::ValidationError,
134
+ "#{opt} must be a #{type}, received #{val.class} #{val.inspect}"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Docker < Base
10
+ OPTIONS = {
11
+ "host" => { type: String,
12
+ desc: "Host name." },
13
+ "interpreters" => { type: Hash,
14
+ desc: "A map of an extension name to the absolute path of an executable, "\
15
+ "enabling you to override the shebang defined in a task executable. The "\
16
+ "extension can optionally be specified with the `.` character (`.py` and "\
17
+ "`py` both map to a task executable `task.py`) and the extension is case "\
18
+ "sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
19
+ "Bolt Ruby interpreter by default." },
20
+ "service-url" => { type: String,
21
+ desc: "URL of the Docker host used for API requests." },
22
+ "shell-command" => { type: String,
23
+ desc: "A shell command to wrap any Docker exec commands in, such as `bash -lc`." },
24
+ "tmpdir" => { type: String,
25
+ desc: "The directory to upload and execute temporary files on the target." },
26
+ "tty" => { type: TrueClass,
27
+ desc: "Whether to enable tty on exec commands." }
28
+ }.freeze
29
+
30
+ DEFAULTS = {}.freeze
31
+
32
+ private def validate
33
+ super
34
+
35
+ if @config['interpreters']
36
+ @config['interpreters'] = normalize_interpreters(@config['interpreters'])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Local < Base
10
+ OPTIONS = {
11
+ "interpreters" => { type: Hash,
12
+ desc: "A map of an extension name to the absolute path of an executable, "\
13
+ "enabling you to override the shebang defined in a task executable. The "\
14
+ "extension can optionally be specified with the `.` character (`.py` and "\
15
+ "`py` both map to a task executable `task.py`) and the extension is case "\
16
+ "sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
17
+ "Bolt Ruby interpreter by default." },
18
+ "run-as" => { type: String,
19
+ desc: "A different user to run commands as after login." },
20
+ "run-as-command" => { type: Array,
21
+ desc: "The command to elevate permissions. Bolt appends the user and command "\
22
+ "strings to the configured `run-as-command` before running it on the target. "\
23
+ "This command must not require an interactive password prompt, and the "\
24
+ "`sudo-password` option is ignored when `run-as-command` is specified. The "\
25
+ "`run-as-command` must be specified as an array." },
26
+ "sudo-executable" => { type: String,
27
+ desc: "The executable to use when escalating to the configured `run-as` user. This "\
28
+ "is useful when you want to escalate using the configured `sudo-password`, "\
29
+ "since `run-as-command` does not use `sudo-password` or support prompting. "\
30
+ "The command executed on the target is `<sudo-executable> -S -u <user> -p "\
31
+ "custom_bolt_prompt <command>`. **This option is experimental.**" },
32
+ "sudo-password" => { type: String,
33
+ desc: "Password to use when changing users via `run-as`." },
34
+ "tmpdir" => { type: String,
35
+ desc: "The directory to copy and execute temporary files." }
36
+ }.freeze
37
+
38
+ WINDOWS_OPTIONS = {
39
+ "interpreters" => { type: Hash,
40
+ desc: "A map of an extension name to the absolute path of an executable, "\
41
+ "enabling you to override the shebang defined in a task executable. The "\
42
+ "extension can optionally be specified with the `.` character (`.py` and "\
43
+ "`py` both map to a task executable `task.py`) and the extension is case "\
44
+ "sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
45
+ "Bolt Ruby interpreter by default." },
46
+ "tmpdir" => { type: String,
47
+ desc: "The directory to copy and execute temporary files." }
48
+ }.freeze
49
+
50
+ DEFAULTS = {}.freeze
51
+
52
+ def self.options
53
+ Bolt::Util.windows? ? WINDOWS_OPTIONS : OPTIONS
54
+ end
55
+
56
+ private def validate
57
+ super
58
+
59
+ if @config['interpreters']
60
+ @config['interpreters'] = normalize_interpreters(@config['interpreters'])
61
+ end
62
+
63
+ if (run_as_cmd = @config['run-as-command'])
64
+ unless run_as_cmd.all? { |n| n.is_a?(String) }
65
+ raise Bolt::ValidationError,
66
+ "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Orch < Base
10
+ OPTIONS = {
11
+ "cacert" => { type: String,
12
+ desc: "The path to the CA certificate." },
13
+ "host" => { type: String,
14
+ desc: "Host name." },
15
+ "job-poll-interval" => { type: Integer,
16
+ desc: "Set interval to poll orchestrator for job status." },
17
+ "job-poll-timeout" => { type: Integer,
18
+ desc: "Set time to wait for orchestrator job status." },
19
+ "service-url" => { type: String,
20
+ desc: "The URL of the orchestrator API." },
21
+ "task-environment" => { type: String,
22
+ desc: "The environment the orchestrator loads task code from." },
23
+ "token-file" => { type: String,
24
+ desc: "The path to the token file." }
25
+ }.freeze
26
+
27
+ DEFAULTS = {
28
+ "task-environment" => "production"
29
+ }.freeze
30
+
31
+ private def validate
32
+ super
33
+
34
+ if @config['cacert']
35
+ @config['cacert'] = File.expand_path(@config['cacert'], @boltdir)
36
+ Bolt::Util.validate_file('cacert', @config['cacert'])
37
+ end
38
+
39
+ if @config['token-file']
40
+ @config['token-file'] = File.expand_path(@config['token-file'], @boltdir)
41
+ Bolt::Util.validate_file('token-file', @config['token-file'])
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class Remote < Base
10
+ OPTIONS = {
11
+ "run-on" => { type: String,
12
+ desc: "The proxy target that the task executes on." }
13
+ }.freeze
14
+
15
+ DEFAULTS = {
16
+ "run-on" => "localhost"
17
+ }.freeze
18
+
19
+ private def filter(unfiltered)
20
+ unfiltered
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class SSH < Base
10
+ OPTIONS = {
11
+ "connect-timeout" => { type: Integer,
12
+ desc: "How long to wait when establishing connections." },
13
+ "disconnect-timeout" => { type: Integer,
14
+ desc: "How long to wait before force-closing a connection." },
15
+ "host" => { type: String,
16
+ desc: "Host name." },
17
+ "host-key-check" => { type: TrueClass,
18
+ desc: "Whether to perform host key validation when connecting." },
19
+ "interpreters" => { type: Hash,
20
+ desc: "A map of an extension name to the absolute path of an executable, "\
21
+ "enabling you to override the shebang defined in a task executable. The "\
22
+ "extension can optionally be specified with the `.` character (`.py` and "\
23
+ "`py` both map to a task executable `task.py`) and the extension is case "\
24
+ "sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
25
+ "Bolt Ruby interpreter by default." },
26
+ "load-config" => { type: TrueClass,
27
+ desc: "Whether to load system SSH configuration." },
28
+ "password" => { type: String,
29
+ desc: "Login password." },
30
+ "port" => { type: Integer,
31
+ desc: "Connection port." },
32
+ "private-key" => { desc: "Either the path to the private key file to use for authentication, or a "\
33
+ "hash with the key `key-data` and the contents of the private key." },
34
+ "proxyjump" => { type: String,
35
+ desc: "A jump host to proxy connections through, and an optional user to "\
36
+ "connect with." },
37
+ "run-as" => { type: String,
38
+ desc: "A different user to run commands as after login." },
39
+ "run-as-command" => { type: Array,
40
+ desc: "The command to elevate permissions. Bolt appends the user and command "\
41
+ "strings to the configured `run-as-command` before running it on the "\
42
+ "target. This command must not require an interactive password prompt, "\
43
+ "and the `sudo-password` option is ignored when `run-as-command` is "\
44
+ "specified. The `run-as-command` must be specified as an array." },
45
+ "script-dir" => { type: String,
46
+ desc: "The subdirectory of the tmpdir to use in place of a randomized "\
47
+ "subdirectory for uploading and executing temporary files on the "\
48
+ "target. It's expected that this directory already exists as a subdir "\
49
+ "of tmpdir, which is either configured or defaults to `/tmp`." },
50
+ "sudo-executable" => { type: String,
51
+ desc: "The executable to use when escalating to the configured `run-as` "\
52
+ "user. This is useful when you want to escalate using the configured "\
53
+ "`sudo-password`, since `run-as-command` does not use `sudo-password` "\
54
+ "or support prompting. The command executed on the target is "\
55
+ "`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
56
+ "**This option is experimental.**" },
57
+ "sudo-password" => { type: String,
58
+ desc: "Password to use when changing users via `run-as`." },
59
+ "tmpdir" => { type: String,
60
+ desc: "The directory to upload and execute temporary files on the target." },
61
+ "tty" => { type: TrueClass,
62
+ desc: "Request a pseudo tty for the session. This option is generally "\
63
+ "only used in conjunction with the `run-as` option when the sudoers "\
64
+ "policy requires a `tty`." },
65
+ "user" => { type: String,
66
+ desc: "Login user." }
67
+ }.freeze
68
+
69
+ DEFAULTS = {
70
+ "connect-timeout" => 10,
71
+ "tty" => false,
72
+ "load-config" => true,
73
+ "disconnect-timeout" => 5
74
+ }.freeze
75
+
76
+ private def validate
77
+ super
78
+
79
+ if (key_opt = @config['private-key'])
80
+ unless key_opt.instance_of?(String) || (key_opt.instance_of?(Hash) && key_opt.include?('key-data'))
81
+ raise Bolt::ValidationError,
82
+ "private-key option must be a path to a private key file or a Hash containing the 'key-data', "\
83
+ "received #{key_opt.class} #{key_opt}"
84
+ end
85
+
86
+ if key_opt.instance_of?(String)
87
+ @config['private-key'] = File.expand_path(key_opt, @boltdir)
88
+ end
89
+ end
90
+
91
+ if @config['interpreters']
92
+ @config['interpreters'] = normalize_interpreters(@config['interpreters'])
93
+ end
94
+
95
+ if (run_as_cmd = @config['run-as-command'])
96
+ unless run_as_cmd.all? { |n| n.is_a?(String) }
97
+ raise Bolt::ValidationError,
98
+ "run-as-command must be an Array of Strings, received #{run_as_cmd.class} #{run_as_cmd.inspect}"
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/error'
4
+ require 'bolt/config/transport/base'
5
+
6
+ module Bolt
7
+ class Config
8
+ module Transport
9
+ class WinRM < Base
10
+ OPTIONS = {
11
+ "cacert" => { type: String,
12
+ desc: "The path to the CA certificate." },
13
+ "connect-timeout" => { type: Integer,
14
+ desc: "How long Bolt should wait when establishing connections." },
15
+ "extensions" => { type: Array,
16
+ desc: "List of file extensions that are accepted for scripts or tasks. "\
17
+ "Scripts with these file extensions rely on the target's file type "\
18
+ "association to run. For example, if Python is installed on the system, "\
19
+ "a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and "\
20
+ "`.pp` are always allowed and run via hard-coded executables." },
21
+ "file-protocol" => { type: String,
22
+ desc: "Which file transfer protocol to use. Either `winrm` or `smb`. Using `smb` is "\
23
+ "recommended for large file transfers." },
24
+ "host" => { type: String,
25
+ desc: "Host name." },
26
+ "interpreters" => { type: Hash,
27
+ desc: "A map of an extension name to the absolute path of an executable, "\
28
+ "enabling you to override the shebang defined in a task executable. The "\
29
+ "extension can optionally be specified with the `.` character (`.py` and "\
30
+ "`py` both map to a task executable `task.py`) and the extension is case "\
31
+ "sensitive. When a target's name is `localhost`, Ruby tasks run with the "\
32
+ "Bolt Ruby interpreter by default." },
33
+ "password" => { type: String,
34
+ desc: "Login password. **Required unless using Kerberos.**" },
35
+ "port" => { type: Integer,
36
+ desc: "Connection port." },
37
+ "realm" => { type: String,
38
+ desc: "Kerberos realm (Active Directory domain) to authenticate against." },
39
+ "smb-port" => { type: Integer,
40
+ desc: "With file-protocol set to smb, this is the port to establish a "\
41
+ "connection on." },
42
+ "ssl" => { type: TrueClass,
43
+ desc: "When true, Bolt uses secure https connections for WinRM." },
44
+ "ssl-verify" => { type: TrueClass,
45
+ desc: "When true, verifies the targets certificate matches the cacert." },
46
+ "tmpdir" => { type: String,
47
+ desc: "The directory to upload and execute temporary files on the target." },
48
+ "user" => { type: String,
49
+ desc: "Login user. **Required unless using Kerberos.**" }
50
+ }.freeze
51
+
52
+ DEFAULTS = {
53
+ "connect-timeout" => 10,
54
+ "ssl" => true,
55
+ "ssl-verify" => true,
56
+ "file-protocol" => "winrm"
57
+ }.freeze
58
+
59
+ private def validate
60
+ super
61
+
62
+ if @config['ssl']
63
+ if @config['file-protocol'] == 'smb'
64
+ raise Bolt::ValidationError, "SMB file transfers are not allowed with SSL enabled"
65
+ end
66
+
67
+ if @config['cacert']
68
+ @config['cacert'] = File.expand_path(@config['cacert'], @boltdir)
69
+ Bolt::Util.validate_file('cacert', @config['cacert'])
70
+ end
71
+ end
72
+
73
+ if @config['interpreters']
74
+ @config['interpreters'] = normalize_interpreters(@config['interpreters'])
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end