Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bolt.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "net-ssh", ">= 4.0"
spec.add_dependency "orchestrator_client", "~> 0.3.1"
spec.add_dependency "puppet", [">= 6.0.1", "< 7"]
spec.add_dependency "puppet-resource_api"
spec.add_dependency "r10k", "~> 2.6"
spec.add_dependency "terminal-table", "~> 1.8"
spec.add_dependency "winrm", "~> 2.0"
Expand Down
10 changes: 8 additions & 2 deletions lib/bolt/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
require 'bolt/transport/orch'
require 'bolt/transport/local'
require 'bolt/transport/docker'
require 'bolt/transport/remote'

module Bolt
TRANSPORTS = {
ssh: Bolt::Transport::SSH,
winrm: Bolt::Transport::WinRM,
pcp: Bolt::Transport::Orch,
local: Bolt::Transport::Local,
docker: Bolt::Transport::Docker
docker: Bolt::Transport::Docker,
remote: Bolt::Transport::Remote,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect to use this for anything other than devices? If not I think 'Device' is a more precise name, since ssh and winrm are also remote transports. But if so this might be the best name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a larger issue(or two closely related issues) I think we need to discuss and/or research.

Are all remote targets devices(ie physical devices or vms with limited control APIs)?
No, remote tasks should be used for cloud resources, notification APIs etc.

Do they use the puppet-device framework?
Usually but not always. I would expect most ruby implementations to use the puppet-device framework but they could very will be written without that especially to enable non-ruby implementations.

My general hypothesis is that module authors especially ruby authors will want to think of the modules they write as "device" modules since they use the device framework and renaming the framework is probably not worth the effort. On the other hand, I think bolt users, and eventually PE GUI admins should not think of these things as devices but rather remote targets that rely on a proxy since it's weird to call an aws account a device.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming is hard :) This seems to echo us not knowing how to refer to ephemeral, cloudy | container "nodes".

Definitely worth more discussion.

}.freeze

class UnknownTransportError < Bolt::Error
Expand All @@ -41,6 +43,7 @@ class Config
'tty' => false
}.freeze

# TODO: move these to the transport themselves
TRANSPORT_SPECIFIC_DEFAULTS = {
ssh: {
'host-key-check' => true
Expand All @@ -53,7 +56,10 @@ class Config
'task-environment' => 'production'
},
local: {},
docker: {}
docker: {},
remote: {
'run-on' => 'localhost'
}
}.freeze

def self.default
Expand Down
10 changes: 8 additions & 2 deletions lib/bolt/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ def initialize(concurrency = 1,
@load_config = load_config

@transports = Bolt::TRANSPORTS.each_with_object({}) do |(key, val), coll|
coll[key.to_s] = Concurrent::Delay.new do
val.new
if key == :remote
coll[key.to_s] = Concurrent::Delay.new do
val.new(self)
end
else
coll[key.to_s] = Concurrent::Delay.new do
val.new
end
end
end
@reported_transports = Set.new
Expand Down
12 changes: 12 additions & 0 deletions lib/bolt/inventory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ def data_hash
}
end

# TODO: This does two things because the applicator bypasses run_task
# It would probably be cleaner to give the exectutor access to inventory
# and handle this there.
def run_on_target(target, params)
if target.remote?
[get_target(target.run_on || 'localhost'), params.merge('_target' => target.hash)]
else
[target, params]
end
end

#### PRIVATE ####
#
# For debugging only now
Expand Down Expand Up @@ -207,6 +218,7 @@ def resolve_name(target)
private :resolve_name

def expand_targets(targets)
# TODO: inventory may not be set in this case?
if targets.is_a? Bolt::Target
targets
elsif targets.is_a? Array
Expand Down
9 changes: 8 additions & 1 deletion lib/bolt/node/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class Node
class BaseError < Bolt::Error
attr_reader :issue_code

def initialize(message, issue_code)
# TODO can we just drop issue code here?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

def initialize(message, issue_code = nil)
super(message, kind, nil, issue_code)
end

Expand All @@ -34,6 +35,12 @@ def kind
end
end

class RemoteError < BaseError
def kind
'puppetlabs.tasks/remote-task-error'
end
end

class EnvironmentVarError < BaseError
def initialize(var, val)
message = "Could not set environment variable '#{var}' to '#{val}'"
Expand Down
12 changes: 12 additions & 0 deletions lib/bolt/target.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ def to_s
"Target('#{@uri}', #{opts})"
end

def to_h
options.merge({
'name' => name,
'uri' => uri,
'protocol' => protocol,
'user' => user,
'password' => password,
'host' => host,
'port' => port,
})
end

def host
@uri_obj.hostname
end
Expand Down
4 changes: 4 additions & 0 deletions lib/bolt/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def module_name
name.split('::').first
end

def remote?
!!metadata['remote']
end

def tasks_dir
File.join(module_name, 'tasks')
end
Expand Down
28 changes: 28 additions & 0 deletions lib/bolt/transport/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Transport
class Base
STDIN_METHODS = %w[both stdin].freeze
ENVIRONMENT_METHODS = %w[both environment].freeze
DEFAULT_INPUT_METHOD = 'both'

attr_reader :logger

Expand Down Expand Up @@ -68,6 +69,33 @@ def with_events(target, callback)
result
end

def provided_features
[]
end

def default_input_method
'both'
end

def select_implementation(target, task)
impl =task.select_implementation(target, provided_features)
impl['input_method'] ||= default_input_method
impl
end

def add_target_param(target, task, params)
# TODO: How should metadata impact this?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata seems to be more "controllable" (and visible) to the user, so I think "prioritizing" that makes the most sense:

if !target.remote? && task.remote?
    raise Bolt::Node::RemoteError.new('Remote tasks can only be run on remote targets')
end

Raising an error if the target is remotable and the task doesn't specify it could be confusing if users don't know where the 'remotable' target is coming from.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on the other hand running a non-remote task against a remote target seems incredibly dangerous.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what a task be "remotable" is enough to comment. For right now it just seems like "the user said in the metadata it can be run on remote targets", and if they don't specify that they still might be able to run on remote targets.

I think we can raise a warning in that case, but limiting the user to only tasks that are remote seems like it makes using remote tasks much harder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the general statement is that a remoteable task is one that does something meaningful with the _target metaparam.

IE the service task is not remoteable. If I ran it targeting an aws account it would stop a service on the run-on node rather then do anything to the real device.

#if target.remote? && !task.remote?
# raise Bolt::Node::RemoteError.new('Remote targets can only run Remotable tasks')
#end
#if !target.remote? && task.remote?
# raise Bolt::Node::RemoteError.new('Remote tasks can only be run on remote targets')
#end

# TODO: wrap sensitive values
target.remote? ? params.merge('_target' => target.to_h) : params
end

def filter_options(target, options)
if target.options['run-as']
options.reject { |k, _v| k == '_run_as' }
Expand Down
8 changes: 5 additions & 3 deletions lib/bolt/transport/local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def self.options
%w[tmpdir]
end

PROVIDED_FEATURES = ['shell'].freeze
def provided_features
['shell']
end

def self.validate(_options); end

Expand Down Expand Up @@ -82,9 +84,9 @@ def run_script(target, script, arguments, _options = {})
end

def run_task(target, task, arguments, _options = {})
implementation = task.select_implementation(target, PROVIDED_FEATURES)
implementation = select_implementation(target, task)
executable = implementation['path']
input_method = implementation['input_method'] || 'both'
input_method = implementation['input_method']
extra_files = implementation['files']

in_tmpdir(target.options['tmpdir']) do |dir|
Expand Down
4 changes: 3 additions & 1 deletion lib/bolt/transport/orch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def self.options
%w[service-url cacert token-file task-environment]
end

PROVIDED_FEATURES = ['puppet-agent'].freeze
def provided_features
['puppet-agent']
end

def self.validate(options); end

Expand Down
52 changes: 52 additions & 0 deletions lib/bolt/transport/remote.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require 'bolt/transport/base'


module Bolt
module Transport
class Remote < Base
def self.options
# TODO: We should accept arbitrary options here
%w[host port user password connnect-timeout device-type run-on]
end

def self.validate(options)
# This will fail when validating global config
#unless options['device-type']
# raise Bolt::ValidationError, 'Must specify device-type for devices'
#end
end

# TODO: this should have access to inventory so target doesn't have to
def initialize(executor)
super()

@executor = executor
end

def get_proxy(target)
# TODO: This needs to have access to the inventory
inventory = target.instance_variable_get(:@inventory)
raise "Target was creates without inventory? Not get_targets?" unless inventory
inventory.get_targets(target.options['run-on'] || 'localhost').first
end

# Cannot batch because arugments differ
def run_task(target, task, arguments, options = {})
proxy_target = get_proxy(target)
transport = @executor.transport(proxy_target.protocol)
arguments = arguments.merge('_target' => target.to_h.reject {|_, v| v.nil?})

# TODO: add support for device-type and feature checking here.
# * tasks/task_implementations must have the same device-type as the target
# * Does one implementation need to support multiple device types?
# * Do we need multiple implementations for the same device based on proxy features?
# TODO doesn't support transports with only batch exec(orchestrator). update orch
result = transport.run_task(proxy_target, task, arguments, options)

Bolt::Result.new(target, value: result.value)
end
end
end
end
7 changes: 4 additions & 3 deletions lib/bolt/transport/ssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ def self.options
%w[port user password sudo-password private-key host-key-check connect-timeout tmpdir run-as tty run-as-command]
end

PROVIDED_FEATURES = ['shell'].freeze
def provided_features
['shell']
end

def self.validate(options)
logger = Logging.logger[self]
Expand Down Expand Up @@ -122,11 +124,10 @@ def run_script(target, script, arguments, options = {})
end

def run_task(target, task, arguments, options = {})
implementation = task.select_implementation(target, PROVIDED_FEATURES)
implementation = select_implementation(target, task)
executable = implementation['path']
input_method = implementation['input_method']
extra_files = implementation['files']
input_method ||= 'both'

# unpack any Sensitive data
arguments = unwrap_sensitive_args(arguments)
Expand Down
10 changes: 8 additions & 2 deletions lib/bolt/transport/winrm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ def self.options
%w[port user password connect-timeout ssl ssl-verify tmpdir cacert extensions]
end

PROVIDED_FEATURES = ['powershell'].freeze
def provided_features
['powershell']
end

def default_input_method
'powershell'
end

def self.validate(options)
ssl_flag = options['ssl']
Expand Down Expand Up @@ -108,7 +114,7 @@ def run_script(target, script, arguments, _options = {})
end

def run_task(target, task, arguments, _options = {})
implementation = task.select_implementation(target, PROVIDED_FEATURES)
implementation = select_implementation(target, task)
executable = implementation['path']
input_method = implementation['input_method']
extra_files = implementation['files']
Expand Down
25 changes: 23 additions & 2 deletions libexec/apply_catalog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@

Puppet[:default_file_terminus] = :file_server

if args['_target']
# TODO: move some of this logic to puppet
special_keys = ['type', 'debug']
connection = conn_info.reject { |k, _| special_keys.include?(k) }
device = OpenStruct.new(connection)
device.provider = conn_info['type']
device.options[:debug] = true if conn_info['debug']
Puppet[:certname] = device.name
Puppet::Util::NetworkDevice.init(device)

Puppet[:facts_terminus] = :network_device
Puppet[:node_terminus] = :plain
Puppet[:catalog_terminus] = :compiler
# TODO this shouldn't be device speciifc
Puppet[:catalog_cache_terminus] = nil
end

exit_code = 0
begin
# This happens implicitly when running the Configurer, but we make it explicit here. It creates the
Expand All @@ -49,6 +66,7 @@
end

# Ensure custom facts are available for provider suitability tests
# TODO: skip this for devices?
facts = Puppet::Node::Facts.indirection.find(SecureRandom.uuid, environment: env)

report = if Puppet::Util::Package.versioncmp(Puppet.version, '5.0.0') > 0
Expand All @@ -57,8 +75,11 @@
Puppet::Transaction::Report.new('apply')
end

Puppet.override(current_environment: env,
loaders: Puppet::Pops::Loaders.new(env)) do
overrides = { current_environment: env,
loaders: Puppet::Pops::Loaders.new(env) }
ovierrides[:network_device] = true if args['_target']

Puppet.override(overrides) do
catalog = Puppet::Resource::Catalog.from_data_hash(args['catalog'])
catalog.environment = env.name.to_s
catalog.environment_instance = env
Expand Down
Loading