bcome 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bcome.rb +7 -0
  3. data/lib/objects/bcome/version.rb +3 -3
  4. data/lib/objects/bootup.rb +4 -3
  5. data/lib/objects/driver/base.rb +16 -2
  6. data/lib/objects/driver/ec2.rb +11 -2
  7. data/lib/objects/driver/gcp.rb +49 -5
  8. data/lib/objects/driver/gcp/authentication/base.rb +36 -0
  9. data/lib/objects/driver/gcp/authentication/oauth.rb +24 -29
  10. data/lib/objects/driver/gcp/authentication/oauth_client_config.rb +22 -0
  11. data/lib/objects/driver/gcp/authentication/oauth_session_store.rb +22 -0
  12. data/lib/objects/driver/gcp/authentication/service_account.rb +57 -2
  13. data/lib/objects/driver/gcp/authentication/signet/service_account.rb +27 -0
  14. data/lib/objects/driver/gcp/authentication/utilities.rb +42 -0
  15. data/lib/objects/encryptor.rb +83 -0
  16. data/lib/objects/exception/base.rb +10 -3
  17. data/lib/objects/exception/ec2_driver_missing_authorization_keys.rb +11 -0
  18. data/lib/objects/exception/empty_namespace_tree.rb +11 -0
  19. data/lib/objects/exception/gcp_auth_service_account_missing_credentials.rb +11 -0
  20. data/lib/objects/exception/invalid_metadata_encryption_key.rb +1 -1
  21. data/lib/objects/exception/missing_gcp_service_account_credentials_filename.rb +11 -0
  22. data/lib/objects/exception/user_orchestration_error.rb +11 -0
  23. data/lib/objects/initialization/factory.rb +36 -0
  24. data/lib/objects/initialization/structure.rb +18 -0
  25. data/lib/objects/initialization/utils.rb +20 -0
  26. data/lib/objects/loading_bar/handler.rb +1 -1
  27. data/lib/objects/loading_bar/indicator/base.rb +1 -0
  28. data/lib/objects/modules/draw.rb +49 -0
  29. data/lib/objects/modules/tree.rb +157 -0
  30. data/lib/objects/modules/workspace_commands.rb +2 -32
  31. data/lib/objects/modules/workspace_menu.rb +113 -48
  32. data/lib/objects/node/attributes.rb +6 -0
  33. data/lib/objects/node/base.rb +27 -7
  34. data/lib/objects/node/cache_handler.rb +1 -1
  35. data/lib/objects/node/factory.rb +15 -11
  36. data/lib/objects/node/inventory/base.rb +9 -3
  37. data/lib/objects/node/inventory/defined.rb +18 -15
  38. data/lib/objects/node/inventory/merge.rb +9 -1
  39. data/lib/objects/node/inventory/subselect.rb +6 -4
  40. data/lib/objects/node/meta_data_factory.rb +1 -1
  41. data/lib/objects/node/meta_data_loader.rb +2 -2
  42. data/lib/objects/node/resources/inventory.rb +19 -0
  43. data/lib/objects/node/resources/merged.rb +23 -14
  44. data/lib/objects/node/resources/sub_inventory.rb +6 -5
  45. data/lib/objects/node/server/base.rb +35 -22
  46. data/lib/objects/node/server/dynamic/ec2.rb +0 -1
  47. data/lib/objects/node/server/dynamic/gcp.rb +0 -1
  48. data/lib/objects/node/server/static.rb +22 -9
  49. data/lib/objects/orchestration/base.rb +7 -1
  50. data/lib/objects/orchestration/interactive_terraform.rb +10 -16
  51. data/lib/objects/registry/command/external.rb +6 -2
  52. data/lib/objects/registry/command/group.rb +5 -1
  53. data/lib/objects/registry/loader.rb +3 -0
  54. data/lib/objects/ssh/command.rb +4 -8
  55. data/lib/objects/ssh/command_exec.rb +3 -1
  56. data/lib/objects/ssh/connection_wrangler.rb +34 -17
  57. data/lib/objects/ssh/connector.rb +17 -9
  58. data/lib/objects/ssh/driver.rb +7 -18
  59. data/lib/objects/ssh/driver_concerns/connection.rb +3 -11
  60. data/lib/objects/ssh/driver_concerns/functions.rb +7 -7
  61. data/lib/objects/ssh/proxy_chain.rb +19 -0
  62. data/lib/objects/ssh/proxy_chain_link.rb +26 -0
  63. data/lib/objects/ssh/proxy_hop.rb +47 -18
  64. data/lib/objects/ssh/script_exec.rb +9 -11
  65. data/lib/objects/startup.rb +7 -1
  66. data/lib/objects/terraform/output.rb +5 -1
  67. data/lib/objects/workspace.rb +10 -0
  68. data/patches/irb.rb +35 -1
  69. data/patches/string.rb +13 -0
  70. metadata +71 -25
  71. data/lib/objects/driver/static.rb +0 -6
@@ -7,13 +7,17 @@ module Bcome::Node::Server
7
7
  end
8
8
 
9
9
  def initialize(params)
10
- config = params[:views]
11
- @identifier = config[:identifier]
12
- @public_ip_address = config[:public_ip_address]
13
- @internal_ip_address = config[:internal_ip_address]
14
- @cloud_tags = config[:cloud_tags]
15
- @description = config[:description]
16
- verify_we_have_at_least_one_interface(config)
10
+ @view_config = params[:views]
11
+
12
+ set_cloud_tags
13
+
14
+ @identifier = @view_config[:identifier]
15
+ @public_ip_address = @view_config[:public_ip_address]
16
+ @internal_ip_address = @view_config[:internal_ip_address]
17
+ @cloud_tags = @view_config[:cloud_tags]
18
+ @description = @view_config[:description]
19
+ verify_we_have_at_least_one_interface
20
+ verify_identifier_and_description
17
21
  super
18
22
  end
19
23
 
@@ -27,8 +31,17 @@ module Bcome::Node::Server
27
31
 
28
32
  attr_reader :description
29
33
 
30
- def verify_we_have_at_least_one_interface(config)
31
- raise Bcome::Exception::MissingIpaddressOnServer, config unless has_at_least_one_interface?
34
+ def set_cloud_tags
35
+ @view_config[:cloud_tags] = ::Bcome::Node::Meta::Cloud.new(@view_config[:cloud_tags]) unless @view_config[:cloud_tags].is_a?(::Bcome::Node::Meta::Cloud)
36
+ end
37
+
38
+ def verify_we_have_at_least_one_interface
39
+ raise Bcome::Exception::MissingIpaddressOnServer, @view_config unless has_at_least_one_interface?
40
+ end
41
+
42
+ def verify_identifier_and_description
43
+ raise Bcome::Exception::Generic, "Your static server defined by #{@view_config} is missing a description" unless @description
44
+ raise Bcome::Exception::Generic, "Your static server defined by #{@view_config} is missing an identifier" unless @identifier
32
45
  end
33
46
 
34
47
  def has_at_least_one_interface?
@@ -10,7 +10,13 @@ module Bcome::Orchestration
10
10
  def do_execute
11
11
  raise Bcome::Exception::MissingExecuteOnRegistryObject, self.class.to_s unless respond_to?(:execute)
12
12
 
13
- execute
13
+ begin
14
+ execute
15
+ rescue ::Bcome::Exception::Base => bcome_exception
16
+ show_backtrace = true unless bcome_exception.is_a?(::Bcome::Exception::InvalidMetaDataEncryptionKey)
17
+ bcome_exception.pretty_display(show_backtrace)
18
+ raise ::Bcome::Exception::UserOrchestrationError, self.class.to_s
19
+ end
14
20
  end
15
21
 
16
22
  def method_missing(method_sym, *_arguments)
@@ -11,14 +11,19 @@ module Bcome::Orchestration
11
11
  QUIT = '\\q'
12
12
  COMMAND_PROMPT = "enter command or '#{QUIT}' to quit: " + 'terraform'.informational + "\s"
13
13
 
14
+ def initialize(*params)
15
+ super
16
+ raise ::Bcome::Exception::Generic, "Missing terraform configuration directory #{path_to_env_config}" unless File.exist?(path_to_env_config)
17
+ end
18
+
14
19
  def execute
15
20
  show_intro_text
16
21
  wait_for_command_input
17
22
  end
18
23
 
19
24
  def show_intro_text
20
- puts "\n"
21
- puts "INTERACTIVE TERRAFORM\n".underline
25
+ puts "\n\n"
26
+ puts "Interactive Terraform\n".underline
22
27
  puts "Namespace:\s" + @node.namespace.to_s.informational
23
28
  puts "Configuration Path:\s" + "#{path_to_env_config}/*".informational
24
29
  puts "\nConfigured metadata:\s" + terraform_metadata.inspect.informational
@@ -29,13 +34,6 @@ module Bcome::Orchestration
29
34
  # PROCESSING INTERACTIVE COMMANDS
30
35
  #
31
36
  def process_command(raw_command)
32
- if raw_command =~ /destroy/
33
- are_you_sure_message = "Are you SURE you want to 'destroy'? Make sure you know what will be destroyed before you continue. (y/n):".warning
34
- response = wait_for_input(are_you_sure_message)
35
- response = wait_for_input(are_you_sure_message) until %w[y n].include?(response)
36
- return if response == 'n'
37
- end
38
-
39
37
  full_command = command(raw_command)
40
38
  @node.execute_local(full_command)
41
39
  wait_for_command_input
@@ -78,8 +76,6 @@ module Bcome::Orchestration
78
76
  end
79
77
 
80
78
  all_vars[:ssh_user] = @node.ssh_driver.user
81
- all_vars[:ssh_key_path] = @node.ssh_driver.ssh_keys.first
82
-
83
79
  all_vars.collect { |key, value| "-var #{key}=\"#{value}\"" }.join("\s")
84
80
  end
85
81
 
@@ -105,11 +101,9 @@ module Bcome::Orchestration
105
101
 
106
102
  # Formulate a terraform command
107
103
  def command(raw_command)
108
- # if raw_command == "init"
109
- # "cd #{path_to_env_config} ; terraform #{raw_command} #{backend_config_parameter_string}"
110
- # else
111
- "cd #{path_to_env_config} ; terraform #{raw_command} #{var_string}"
112
- # end
104
+ cmd = "cd #{path_to_env_config} ; terraform #{raw_command}"
105
+ cmd = "#{cmd} #{var_string}" if raw_command =~ Regexp.new(/^apply$|plan|destroy|refresh/)
106
+ cmd
113
107
  end
114
108
  end
115
109
  end
@@ -31,14 +31,18 @@ module Bcome::Registry::Command
31
31
  raise Bcome::Exception::MissingArgumentForRegistryCommand, error_message_suffix
32
32
  end
33
33
 
34
- substitute_with = [TrueClass, FalseClass].include?(substitute_with.class) ? (substitute_with ? 'true' : 'false') : substitute_with
34
+ substitute_with = if [TrueClass, FalseClass].include?(substitute_with.class)
35
+ substitute_with ? 'true' : 'false'
36
+ else
37
+ substitute_with
38
+ end
35
39
  substituted_command.gsub!("%#{substitution}%", substitute_with)
36
40
  end
37
41
  substituted_command
38
42
  end
39
43
 
40
44
  def namespace_command(node, command)
41
- "#{command} bcome_context=\"#{node.keyed_namespace}\""
45
+ "bcome_context=\"#{node.keyed_namespace}\" #{command}"
42
46
  end
43
47
 
44
48
  def local_command_substitutions
@@ -62,7 +62,11 @@ module Bcome::Registry::Command
62
62
 
63
63
  puts tab_spacing + command_key.resource_key + item_spacing(command_key) + description.resource_value
64
64
 
65
- usage_string = in_console_session? ? command_key.to_s : "bcome #{@node.keyed_namespace.empty? ? '' : "#{@node.keyed_namespace}:"}#{command_key}"
65
+ usage_string = if in_console_session?
66
+ command_key.to_s
67
+ else
68
+ "bcome #{@node.keyed_namespace.empty? ? '' : "#{@node.keyed_namespace}:"}#{command_key}"
69
+ end
66
70
  puts tab_spacing + ("\s" * menu_item_spacing_length) + 'usage: '.instructional + usage_string
67
71
 
68
72
  if defaults.keys.any?
@@ -20,6 +20,9 @@ module Bcome::Registry
20
20
  data.each do |key, commands|
21
21
  begin
22
22
  if /^#{key}$/.match(node.keyed_namespace)
23
+
24
+ next if commands.nil?
25
+
23
26
  commands.each do |c|
24
27
  unless c[:console_command]
25
28
  error_message = "Registry method is missing key 'console_command'."
@@ -16,17 +16,13 @@ module ::Bcome::Ssh
16
16
  @node = nil
17
17
  end
18
18
 
19
- def pretty_result
20
- is_success? ? 'success'.success : 'failure'.error
21
- end
22
-
23
19
  def output
24
20
  cmd_output = @stdout
25
21
 
26
- unless is_success?
27
- cmd_output += "\nExit code: #{@exit_code}"
28
- cmd_output += "\nSTDERR: #{@stderr}" unless @stderr.empty?
29
- end
22
+ cmd_output += "\nExit code:" + "\s#{@exit_code}"
23
+
24
+ cmd_output += "\nSTDERR: #{@stderr}" if exit_code == 1 && !@stderr.empty?
25
+
30
26
  "\n#{cmd_output}"
31
27
  end
32
28
 
@@ -18,6 +18,8 @@ module ::Bcome::Ssh
18
18
 
19
19
  def print_output
20
20
  print "#{@output_string}\n\n"
21
+ rescue StandardError => e
22
+ puts "Could not print #{@output_string.inspect}"
21
23
  end
22
24
 
23
25
  def execute!
@@ -33,7 +35,7 @@ module ::Bcome::Ssh
33
35
  ssh_exec!(ssh, command) # retry, once
34
36
  end
35
37
 
36
- output_append("\n(#{node.namespace})$".terminal_prompt + ">\s#{command.raw} (#{command.pretty_result})\n")
38
+ output_append("\n(#{node.namespace})$".terminal_prompt + ">\s#{command.raw}\n")
37
39
  output_append(command.output.to_s)
38
40
  end
39
41
 
@@ -4,19 +4,17 @@ require 'net/ssh/proxy/jump'
4
4
 
5
5
  module Bcome::Ssh
6
6
  class ConnectionWrangler
7
+ attr_accessor :proxy_details
8
+
7
9
  def initialize(ssh_driver)
8
10
  @ssh_driver = ssh_driver
9
11
  @config = ssh_driver.config[:proxy]
10
12
  @context_node = ssh_driver.context_node
11
13
  @user = ssh_driver.user
14
+ set_proxy_details
12
15
  end
13
16
 
14
17
  ## Accessors --
15
-
16
- def proxy_details
17
- hops.reverse.collect(&:proxy_details)
18
- end
19
-
20
18
  def first_hop
21
19
  hops.reverse.first
22
20
  end
@@ -41,7 +39,7 @@ module Bcome::Ssh
41
39
  def get_ssh_command(config = {}, _proxy_only = false)
42
40
  cmd = has_hop? ? 'ssh -J' : 'ssh'
43
41
  cmd += "\s" + hops.collect(&:get_ssh_string).join(',') if has_hop?
44
- cmd += "\s#{@ssh_driver.node_level_ssh_key_connection_string}\s#{@ssh_driver.user}@#{target_machine_ingress_ip}"
42
+ cmd += "\s#{@ssh_driver.user}@#{target_machine_ingress_ip}"
45
43
 
46
44
  config[:as_pseudo_tty] ? "#{cmd} -t" : cmd
47
45
  end
@@ -54,19 +52,29 @@ module Bcome::Ssh
54
52
  end
55
53
 
56
54
  def get_local_port_forward_command(start_port, end_port)
57
- # TODO: - below check is not actaully true... you might still want to proxy over SSH...
55
+ # TODO: - below check is not actually true... you might still want to proxy over SSH...
58
56
  raise ::Bcome::Exception::InvalidPortForwardRequest, 'Connections to this node are not via a proxy. Rather than port forward, try connecting directly.' unless has_hop?
59
57
 
60
58
  cmd = "ssh -N -L #{start_port}:localhost:#{end_port} -J"
61
59
  cmd += "\s" + hops.collect(&:get_ssh_string).join(',') if has_hop?
62
- cmd += "\s#{@ssh_driver.node_level_ssh_key_connection_string}\s#{@ssh_driver.user}@#{target_machine_ingress_ip}"
60
+ cmd += "\s#{@ssh_driver.user}@#{target_machine_ingress_ip}"
63
61
 
64
62
  cmd
65
63
  end
66
64
 
65
+ def hops
66
+ @hops ||= set_hops
67
+ end
68
+
67
69
  protected
68
70
 
71
+ def set_proxy_details
72
+ @proxy_details ||= hops.compact.collect(&:proxy_details)
73
+ end
74
+
69
75
  def target_machine_ingress_ip
76
+ return @context_node.internal_ip_address if @context_node.local_network?
77
+
70
78
  unless has_hop?
71
79
  raise ::Bcome::Exception::InvalidProxyConfig, "missing target ip address for #{@context_node.identifier}. Perhaps you meant to configure a proxy?" unless @context_node.public_ip_address
72
80
  end
@@ -74,10 +82,6 @@ module Bcome::Ssh
74
82
  has_hop? ? @context_node.internal_ip_address : @context_node.public_ip_address
75
83
  end
76
84
 
77
- def hops
78
- @hops ||= set_hops
79
- end
80
-
81
85
  private
82
86
 
83
87
  def set_hops
@@ -85,21 +89,34 @@ module Bcome::Ssh
85
89
 
86
90
  parent = nil
87
91
  iterable_configs.each do |config|
88
- hop = set_proxy_hop(config, parent)
92
+ hop = get_proxy_hop(config, parent)
93
+
94
+ if @context_node.is_same_machine?(hop.bcome_proxy_node)
95
+ # We don't hop through ourselves. If we're reached ourselves in the proxy chain,
96
+ # then we'll break the chain at that point.
97
+ break
98
+ end
99
+
100
+ # Set proxy hop
89
101
  hop_collection << hop
90
102
  parent = hop
91
103
  end
92
104
 
93
- hop_collection
105
+ hop_collection.compact
94
106
  end
95
107
 
96
- def set_proxy_hop(config, parent)
108
+ def get_proxy_hop(config, parent)
97
109
  config[:fallback_bastion_host_user] = @ssh_driver.fallback_bastion_host_user
98
- ::Bcome::Ssh::ProxyHop.new(config, @context_node, parent)
110
+ h = ::Bcome::Ssh::ProxyHop.new(config, @context_node, parent)
111
+ return h
99
112
  end
100
113
 
101
114
  def iterable_configs
102
- @iterable ||= @config ? (@config.is_a?(Hash) ? [@config] : @config) : []
115
+ @iterable ||= if @config
116
+ @config.is_a?(Hash) ? [@config] : @config
117
+ else
118
+ []
119
+ end
103
120
  end
104
121
  end
105
122
  end
@@ -32,6 +32,7 @@ module Bcome
32
32
  return if number_unconnected_machines == 0 && !ping?
33
33
 
34
34
  if show_progress?
35
+ print "\n"
35
36
  wrap_indicator type: :progress, size: @servers_to_connect.size, title: 'Opening connections' do
36
37
  open_connections
37
38
  end
@@ -66,18 +67,19 @@ module Bcome
66
67
  def open_connections
67
68
  @servers_to_connect.pmap do |machine|
68
69
  begin
69
- machine.open_ssh_connection(ping?)
70
- if machine.has_ssh_connection?
71
- @servers_to_connect -= [machine]
72
- @connected_machines << machine
73
- signal_success if show_progress?
74
- else
75
- signal_failure if show_progress?
76
- end
70
+ machine.open_ssh_connection(ping?)
71
+
72
+ if machine.has_ssh_connection?
73
+ @servers_to_connect -= [machine]
74
+ @connected_machines << machine
75
+ signal_success if show_progress?
76
+ else
77
+ signal_failure if show_progress?
78
+ end
77
79
  rescue Errno::EPIPE, Bcome::Exception::CouldNotInitiateSshConnection, ::Bcome::Exception::InvalidProxyConfig => e
78
80
  signal_failure if show_progress?
79
81
  @connection_exceptions[machine] = e
80
- end
82
+ end
81
83
  end
82
84
  end
83
85
 
@@ -85,6 +87,12 @@ module Bcome
85
87
 
86
88
  def set_servers
87
89
  @servers_to_connect = machines.dup
90
+
91
+ # Ensure that all connections are loaded. A machine might need to proxy through another that
92
+ # has not yet been loaded. Here we ensure that we've traversed the tree for all required nodes.
93
+ @servers_to_connect.each do |server|
94
+ server.ssh_driver.set_connection_wrangler
95
+ end
88
96
  end
89
97
 
90
98
  def number_unconnected_machines
@@ -20,21 +20,24 @@ module Bcome::Ssh
20
20
  @connection_wrangler ||= set_connection_wrangler
21
21
  end
22
22
 
23
+ def proxy_chain
24
+ @proxy_chain ||= ::Bcome::Ssh::ProxyChain.new(connection_wrangler)
25
+ end
26
+
23
27
  def set_connection_wrangler
24
- @set_connection_wrangler ||= ::Bcome::Ssh::ConnectionWrangler.new(self)
28
+ @connection_wrangler = ::Bcome::Ssh::ConnectionWrangler.new(self)
25
29
  end
26
30
 
27
31
  def pretty_ssh_config
28
32
  config = {
29
33
  user: user,
30
- ssh_keys: ssh_keys,
31
34
  timeout: timeout_in_seconds
32
35
  }
33
36
 
34
37
  if has_proxy?
35
38
  config[:proxy] = connection_wrangler.proxy_details
36
39
  else
37
- config[:host_or_ip] = @context_node.public_ip_address
40
+ config[:host_or_ip] = node_host_or_ip
38
41
  end
39
42
 
40
43
  config
@@ -48,26 +51,12 @@ module Bcome::Ssh
48
51
  @config[:multi_hop_proxy]
49
52
  end
50
53
 
51
- def node_level_ssh_key_connection_string
52
- key_specified_at_node_level? ? "-i #{node_level_ssh_key}\s" : ''
53
- end
54
-
55
- def key_specified_at_node_level?
56
- !node_level_ssh_key.nil?
57
- end
58
-
59
- def node_level_ssh_key
60
- @config[:ssh_keys] ? @config[:ssh_keys].first : nil
61
- end
62
-
63
54
  def has_multi_hop_proxy?
64
55
  !multi_hop_proxy_config.nil?
65
56
  end
66
57
 
67
58
  def has_proxy?
68
- return false if proxy_config_value && proxy_config_value == -1
69
-
70
- !@config[:proxy].nil?
59
+ return connection_wrangler.has_hop?
71
60
  end
72
61
  end
73
62
  end
@@ -12,9 +12,8 @@ module ::Bcome::Ssh
12
12
  @connection = nil
13
13
  begin
14
14
  raise ::Bcome::Exception::InvalidProxyConfig, "missing target ip address for #{@context_node.identifier}. Perhaps you meant to configure a proxy?" unless node_host_or_ip
15
-
16
15
  @connection = ::Net::SSH.start(node_host_or_ip, user, net_ssh_params)
17
- rescue Net::SSH::Proxy::ConnectError, Net::SSH::ConnectionTimeout => e
16
+ rescue Net::SSH::AuthenticationFailed, Net::SSH::Proxy::ConnectError, Net::SSH::ConnectionTimeout => e
18
17
  raise Bcome::Exception::CouldNotInitiateSshConnection, @context_node.namespace + "\s-\s#{e.message}"
19
18
  end
20
19
  @connection
@@ -45,13 +44,12 @@ module ::Bcome::Ssh
45
44
  end
46
45
 
47
46
  def node_host_or_ip
47
+ return @context_node.internal_ip_address if @context_node.local_network?
48
48
  has_proxy? ? @context_node.internal_ip_address : @context_node.public_ip_address
49
49
  end
50
50
 
51
51
  def net_ssh_params
52
- raise Bcome::Exception::InvalidSshConfig, "Missing ssh keys for #{@context_node.namespace}" unless ssh_keys
53
-
54
- params = { keys: ssh_keys, paranoid: false }
52
+ params = { paranoid: false }
55
53
  params[:proxy] = proxy if has_proxy?
56
54
  params[:timeout] = timeout_in_seconds
57
55
  params[:verbose] = :fatal # All but silent
@@ -63,12 +61,6 @@ module ::Bcome::Ssh
63
61
  @config[:timeout_in_seconds] ||= DEFAULT_TIMEOUT_IN_SECONDS
64
62
  end
65
63
 
66
- ## SSH KEYS
67
-
68
- def ssh_keys
69
- @config[:ssh_keys]
70
- end
71
-
72
64
  ## PROXYING --
73
65
 
74
66
  def proxy