bcome 1.3.6 → 1.4.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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/bin/bcome +13 -8
  3. data/lib/bcome.rb +7 -11
  4. data/lib/objects/bcome/version.rb +19 -1
  5. data/lib/objects/bootup.rb +13 -5
  6. data/lib/objects/command/local.rb +2 -0
  7. data/lib/objects/config_factory.rb +3 -0
  8. data/lib/objects/driver/base.rb +36 -4
  9. data/lib/objects/driver/bucket.rb +6 -4
  10. data/lib/objects/driver/ec2.rb +35 -4
  11. data/lib/objects/driver/gcp.rb +124 -0
  12. data/lib/objects/driver/gcp/authentication/api_key.rb +6 -0
  13. data/lib/objects/driver/gcp/authentication/oauth.rb +101 -0
  14. data/lib/objects/driver/gcp/authentication/service_account.rb +7 -0
  15. data/lib/objects/driver/static.rb +2 -0
  16. data/lib/objects/encryptor.rb +26 -24
  17. data/lib/objects/exception/argument_error_invoking_method_from_command_line.rb +8 -4
  18. data/lib/objects/exception/base.rb +14 -10
  19. data/lib/objects/exception/can_only_subselect_on_inventory.rb +8 -4
  20. data/lib/objects/exception/cannot_authenticate_to_gcp.rb +11 -0
  21. data/lib/objects/exception/cannot_find_internal_registry_klass.rb +8 -4
  22. data/lib/objects/exception/cannot_find_inventory.rb +11 -0
  23. data/lib/objects/exception/cannot_find_subselection_parent.rb +8 -4
  24. data/lib/objects/exception/cant_find_key_in_cloud_tags.rb +8 -4
  25. data/lib/objects/exception/cant_find_key_in_metadata.rb +8 -4
  26. data/lib/objects/exception/cant_find_proxy_host_by_identifier.rb +8 -4
  27. data/lib/objects/exception/cant_find_proxy_host_by_namespace.rb +8 -4
  28. data/lib/objects/exception/could_not_initiate_ssh_connection.rb +8 -4
  29. data/lib/objects/exception/could_not_initiate_ssh_connection_through_backend_proxy.rb +8 -4
  30. data/lib/objects/exception/could_not_retrieve_terraform_output.rb +11 -0
  31. data/lib/objects/exception/deprecation_warning.rb +9 -7
  32. data/lib/objects/exception/duplicate_command_line_argument_key.rb +8 -4
  33. data/lib/objects/exception/ec2_driver_missing_provisioning_region.rb +8 -4
  34. data/lib/objects/exception/failed_to_run_local_command.rb +8 -4
  35. data/lib/objects/exception/generic.rb +11 -0
  36. data/lib/objects/exception/interactive_session_halt.rb +6 -2
  37. data/lib/objects/exception/invalid_bcome_breadcrumb.rb +8 -4
  38. data/lib/objects/exception/invalid_breadcrumb.rb +8 -4
  39. data/lib/objects/exception/invalid_context_command.rb +8 -4
  40. data/lib/objects/exception/invalid_gcp_authentication_scheme.rb +11 -0
  41. data/lib/objects/exception/invalid_identifier.rb +8 -4
  42. data/lib/objects/exception/invalid_machines_cache_config.rb +8 -4
  43. data/lib/objects/exception/invalid_matcher_query.rb +8 -4
  44. data/lib/objects/exception/invalid_meta_data_config.rb +8 -4
  45. data/lib/objects/exception/invalid_metadata_encryption_key.rb +8 -4
  46. data/lib/objects/exception/invalid_network_config.rb +8 -4
  47. data/lib/objects/exception/invalid_network_driver_type.rb +8 -4
  48. data/lib/objects/exception/invalid_port_forward_request.rb +11 -0
  49. data/lib/objects/exception/invalid_proxy_config.rb +8 -4
  50. data/lib/objects/exception/invalid_regexp_matcher_in_registry.rb +8 -4
  51. data/lib/objects/exception/invalid_registry_arguments_type.rb +8 -4
  52. data/lib/objects/exception/invalid_registry_command_name_length.rb +8 -4
  53. data/lib/objects/exception/invalid_registry_data_config.rb +8 -4
  54. data/lib/objects/exception/invalid_restriction_key_in_registry.rb +8 -4
  55. data/lib/objects/exception/invalid_ssh_config.rb +8 -4
  56. data/lib/objects/exception/inventories_cannot_have_subviews.rb +8 -4
  57. data/lib/objects/exception/malformed_command_line_arguments.rb +8 -4
  58. data/lib/objects/exception/method_invocation_requires_parameter.rb +8 -4
  59. data/lib/objects/exception/method_name_conflict_in_registry.rb +8 -4
  60. data/lib/objects/exception/missing_argument_for_registry_command.rb +8 -4
  61. data/lib/objects/exception/missing_description_on_view.rb +8 -4
  62. data/lib/objects/exception/missing_execute_on_registry_object.rb +8 -4
  63. data/lib/objects/exception/missing_gcp_authentication_scheme.rb +11 -0
  64. data/lib/objects/exception/missing_gcp_service_scopes.rb +11 -0
  65. data/lib/objects/exception/missing_identifier_on_view.rb +8 -4
  66. data/lib/objects/exception/missing_inventory_contributors.rb +11 -0
  67. data/lib/objects/exception/missing_ip_address_on_server.rb +8 -4
  68. data/lib/objects/exception/missing_network_config.rb +8 -4
  69. data/lib/objects/exception/missing_or_invalid_client_secrets.rb +11 -0
  70. data/lib/objects/exception/missing_params_for_rsync.rb +8 -4
  71. data/lib/objects/exception/missing_params_for_scp.rb +8 -4
  72. data/lib/objects/exception/missing_subselection_key.rb +8 -4
  73. data/lib/objects/exception/missing_type_on_view.rb +8 -4
  74. data/lib/objects/exception/no_node_found_for_breadcrumb.rb +8 -4
  75. data/lib/objects/exception/no_node_named_by_identifier.rb +8 -4
  76. data/lib/objects/exception/node_identifiers_must_be_unique.rb +8 -4
  77. data/lib/objects/exception/orchestration_script_does_not_exist.rb +8 -4
  78. data/lib/objects/exception/proxy_host_node_does_not_have_public_ip_address.rb +8 -4
  79. data/lib/objects/exception/unknown_dynamic_server_type.rb +11 -0
  80. data/lib/objects/exception/unknown_method_for_namespace.rb +8 -4
  81. data/lib/objects/interactive/session.rb +4 -1
  82. data/lib/objects/interactive/session_item/base.rb +2 -0
  83. data/lib/objects/interactive/session_item/capture_input.rb +2 -0
  84. data/lib/objects/interactive/session_item/transparent_ssh.rb +29 -23
  85. data/lib/objects/loading_bar/handler.rb +80 -0
  86. data/lib/objects/loading_bar/indicator/base.rb +64 -0
  87. data/lib/objects/loading_bar/indicator/basic.rb +34 -0
  88. data/lib/objects/loading_bar/indicator/progress.rb +26 -0
  89. data/lib/objects/loading_bar/pid_bucket.rb +27 -0
  90. data/lib/objects/modules/context.rb +13 -9
  91. data/lib/objects/modules/registry_management.rb +16 -10
  92. data/lib/objects/modules/ui_output.rb +10 -6
  93. data/lib/objects/modules/workspace_commands.rb +159 -155
  94. data/lib/objects/modules/workspace_menu.rb +129 -130
  95. data/lib/objects/node/attributes.rb +13 -21
  96. data/lib/objects/node/base.rb +113 -71
  97. data/lib/objects/node/cache_handler.rb +2 -0
  98. data/lib/objects/node/collection.rb +10 -9
  99. data/lib/objects/node/factory.rb +35 -28
  100. data/lib/objects/node/inventory/base.rb +100 -100
  101. data/lib/objects/node/inventory/defined.rb +110 -89
  102. data/lib/objects/node/inventory/merge.rb +43 -0
  103. data/lib/objects/node/inventory/subselect.rb +64 -46
  104. data/lib/objects/node/kube/base.rb +51 -0
  105. data/lib/objects/node/kube/container.rb +9 -0
  106. data/lib/objects/node/kube/estate.rb +19 -0
  107. data/lib/objects/node/kube/namespace.rb +24 -0
  108. data/lib/objects/node/kube/pod.rb +24 -0
  109. data/lib/objects/node/kube_wrap.rb +26 -0
  110. data/lib/objects/node/meta/base.rb +8 -1
  111. data/lib/objects/node/meta/cloud.rb +2 -0
  112. data/lib/objects/node/meta/local.rb +2 -0
  113. data/lib/objects/node/meta_data_factory.rb +3 -1
  114. data/lib/objects/node/meta_data_loader.rb +27 -28
  115. data/lib/objects/node/resources/base.rb +5 -1
  116. data/lib/objects/node/resources/inventory.rb +7 -5
  117. data/lib/objects/node/resources/merged.rb +38 -0
  118. data/lib/objects/node/resources/sub_inventory.rb +7 -4
  119. data/lib/objects/node/server/base.rb +88 -66
  120. data/lib/objects/node/server/dynamic/base.rb +23 -0
  121. data/lib/objects/node/server/{dynamic.rb → dynamic/ec2.rb} +14 -13
  122. data/lib/objects/node/server/dynamic/gcp.rb +47 -0
  123. data/lib/objects/node/server/static.rb +13 -2
  124. data/lib/objects/orchestration/base.rb +10 -0
  125. data/lib/objects/orchestration/interactive_terraform.rb +62 -27
  126. data/lib/objects/orchestrator.rb +22 -0
  127. data/lib/objects/parser/bread_crumb.rb +3 -1
  128. data/lib/objects/registry/arguments/base.rb +3 -1
  129. data/lib/objects/registry/arguments/command_line.rb +6 -1
  130. data/lib/objects/registry/arguments/console.rb +4 -1
  131. data/lib/objects/registry/command/base.rb +3 -0
  132. data/lib/objects/registry/command/external.rb +4 -2
  133. data/lib/objects/registry/command/group.rb +6 -3
  134. data/lib/objects/registry/command/internal.rb +3 -1
  135. data/lib/objects/registry/command/shortcut.rb +17 -9
  136. data/lib/objects/registry/command_list.rb +2 -0
  137. data/lib/objects/registry/loader.rb +10 -10
  138. data/lib/objects/ssh/bootstrap.rb +3 -1
  139. data/lib/objects/ssh/command.rb +10 -5
  140. data/lib/objects/ssh/command_exec.rb +13 -9
  141. data/lib/objects/ssh/connection_wrangler.rb +105 -0
  142. data/lib/objects/ssh/connector.rb +100 -0
  143. data/lib/objects/ssh/driver.rb +27 -230
  144. data/lib/objects/ssh/driver_concerns/command_strings.rb +17 -0
  145. data/lib/objects/ssh/driver_concerns/connection.rb +78 -0
  146. data/lib/objects/ssh/driver_concerns/functions.rb +89 -0
  147. data/lib/objects/ssh/driver_concerns/user.rb +32 -0
  148. data/lib/objects/ssh/{proxy_data.rb → proxy_hop.rb} +52 -7
  149. data/lib/objects/ssh/script_exec.rb +4 -1
  150. data/lib/objects/ssh/tunnel/local_port_forward.rb +5 -6
  151. data/lib/objects/ssh/tunnel_keeper.rb +21 -0
  152. data/lib/objects/ssh/window.rb +31 -0
  153. data/lib/objects/startup.rb +52 -0
  154. data/lib/objects/system/local.rb +3 -0
  155. data/lib/objects/terraform/output.rb +41 -0
  156. data/lib/objects/workspace.rb +3 -14
  157. data/patches/irb.rb +27 -3
  158. data/patches/string-encrypt.rb +20 -23
  159. data/patches/string.rb +5 -1
  160. data/patches/string_stylesheet.rb +2 -0
  161. metadata +95 -18
  162. data/lib/objects/progress_bar.rb +0 -30
  163. data/lib/objects/ssh/connection_handler.rb +0 -101
  164. data/lib/objects/terraform/parser.rb +0 -23
  165. data/lib/objects/terraform/state.rb +0 -40
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bcome::Registry::Command
2
4
  class Internal < Base
3
- # In which the bcome context is an external (extended framework) call
5
+ # In which the bcome context is an internal (extended framework) call
4
6
 
5
7
  def execute(node, arguments)
6
8
  merged_arguments = process_arguments(arguments)
@@ -1,24 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bcome::Registry::Command
2
4
  class Shortcut < Base
3
5
  # In which the bcome context is a shortcut to a more complex command
4
6
 
5
- def execute(node, arguments) ## We'll add in arguments later
6
- if run_as_pseudo_tty?
7
- node.pseudo_tty command
8
- else
9
- node.run command
7
+ def execute(node, _arguments) ## We'll add in arguments later
8
+ begin
9
+ if run_as_pseudo_tty?
10
+ node.pseudo_tty command
11
+ else
12
+ puts "\n(#{node.namespace})$".terminal_prompt + ">\s#{command}"
13
+ ::Bcome::Orchestrator.instance.tail_all_command_output!(node)
14
+ node.run command
15
+ ::Bcome::Orchestrator.instance.reset!
16
+ end
17
+ rescue Interrupt
18
+ ::Bcome::Orchestrator.instance.reset!
19
+ puts "\nExiting gracefully from interrupt\n".warning
10
20
  end
11
- rescue Interrupt
12
- puts "\nExiting gracefully from interrupt\n".warning
21
+ nil
13
22
  end
14
23
 
15
24
  def command
16
25
  @data[:shortcut_command]
17
26
  end
18
-
27
+
19
28
  def run_as_pseudo_tty?
20
29
  @data[:run_as_pseudo_tty]
21
30
  end
22
-
23
31
  end
24
32
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bcome::Registry
2
4
  class CommandList
3
5
  include Singleton
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bcome::Registry
2
4
  class Loader
3
5
  include ::Singleton
4
6
 
5
- FILE_PATH = 'bcome/registry.yml'.freeze
7
+ FILE_PATH = 'bcome/registry.yml'
6
8
 
7
9
  def data
8
10
  @data ||= do_load
@@ -17,19 +19,17 @@ module Bcome::Registry
17
19
 
18
20
  data.each do |key, commands|
19
21
  begin
20
- if /^#{key.to_s}$/.match(node.keyed_namespace)
22
+ if /^#{key}$/.match(node.keyed_namespace)
21
23
  commands.each do |c|
22
-
23
24
  unless c[:console_command]
24
25
  error_message = "Registry method is missing key 'console_command'."
25
26
  error_message += "\n\n#{c.inspect}"
26
- raise Bcome::Exception::InvalidRegistryDataConfig.new error_message
27
+ raise Bcome::Exception::InvalidRegistryDataConfig, error_message
27
28
  end
28
29
 
29
30
  # Verify that the proposed user registered method does not conflict with either an existing method name, instance var, or other registry command name for this node
30
- if node.is_node_level_method?(c[:console_command]) || command_group.console_method_name_exists?(c[:console_command])
31
- raise Bcome::Exception::MethodNameConflictInRegistry, "'#{c[:console_command]}'"
32
- end
31
+ raise Bcome::Exception::MethodNameConflictInRegistry, "'#{c[:console_command]}'" if node.is_node_level_method?(c[:console_command]) || command_group.console_method_name_exists?(c[:console_command])
32
+
33
33
  command_group << ::Bcome::Registry::Command::Base.new_from_raw_command(c) unless restrict_config?(node, c)
34
34
  ::Bcome::Registry::CommandList.instance.register(node, c[:console_command].to_sym)
35
35
  end
@@ -47,11 +47,10 @@ module Bcome::Registry
47
47
 
48
48
  def restrict_config?(node, command_config)
49
49
  return false unless command_config.key?(:restrict_to_node)
50
+
50
51
  node_klass_mapping = restriction_to_node_klass_mappings[command_config[:restrict_to_node].to_sym]
51
52
 
52
- unless node_klass_mapping
53
- raise Bcome::Exception::InvalidRestrictionKeyInRegistry, "'#{command_config[:restrict_to_node]}' is invalid. Valid keys: #{restriction_to_node_klass_mappings.keys.join(', ')}"
54
- end
53
+ raise Bcome::Exception::InvalidRestrictionKeyInRegistry, "'#{command_config[:restrict_to_node]}' is invalid. Valid keys: #{restriction_to_node_klass_mappings.keys.join(', ')}" unless node_klass_mapping
55
54
 
56
55
  !node.is_a?(node_klass_mapping)
57
56
  end
@@ -66,6 +65,7 @@ module Bcome::Registry
66
65
 
67
66
  def do_load
68
67
  return {} unless File.exist?(FILE_PATH)
68
+
69
69
  begin
70
70
  file_data = YAML.load_file(FILE_PATH).deep_symbolize_keys
71
71
  rescue Psych::SyntaxError => e
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bcome::Ssh
2
4
  class Bootstrap
3
5
  def initialize(config)
@@ -7,7 +9,7 @@ module Bcome::Ssh
7
9
  def ssh_key_path
8
10
  key_path = `eval directory=#{@config[:ssh_key_path]}; echo $directory`
9
11
  key_path =~ /(.+)\n/
10
- $1
12
+ Regexp.last_match(1)
11
13
  end
12
14
 
13
15
  def user
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ::Bcome::Ssh
2
4
  class Command
3
- attr_reader :raw, :stdout, :stderr, :exit_code, :node, :bootstrap
5
+ attr_reader :raw, :stdout, :stderr, :exit_code, :node
4
6
 
5
7
  def initialize(params)
6
8
  @raw = params[:raw]
@@ -18,11 +20,14 @@ module ::Bcome::Ssh
18
20
  is_success? ? 'success'.success : 'failure'.error
19
21
  end
20
22
 
21
- attr_writer :bootstrap
22
-
23
23
  def output
24
- command_output = is_success? ? @stdout : "Exit code: #{@exit_code}\n\nSTDERR: #{@stderr}"
25
- "\n#{command_output}"
24
+ cmd_output = @stdout
25
+
26
+ unless is_success?
27
+ cmd_output += "\nExit code: #{@exit_code}"
28
+ cmd_output += "\nSTDERR: #{@stderr}" unless @stderr.empty?
29
+ end
30
+ "\n#{cmd_output}"
26
31
  end
27
32
 
28
33
  def is_success?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ::Bcome::Ssh
2
4
  class CommandExec
3
5
  attr_reader :commands
@@ -10,14 +12,15 @@ module ::Bcome::Ssh
10
12
  @output_string = "#{@output_string}#{output_string}"
11
13
  end
12
14
 
15
+ def log_window
16
+ ::Bcome::Ssh::Window.instance
17
+ end
18
+
13
19
  def print_output
14
20
  print "#{@output_string}\n\n"
15
21
  end
16
22
 
17
23
  def execute!
18
- # TODO: - catch IOError: closed stream here and re-connect gracefully
19
- # to reproduce: open connections, and let them timeout then re-enter command (interactive mode)
20
-
21
24
  @commands.each do |command|
22
25
  node = command.node
23
26
  ssh = node.ssh_driver.ssh_connection
@@ -25,24 +28,25 @@ module ::Bcome::Ssh
25
28
  begin
26
29
  ssh_exec!(ssh, command)
27
30
  rescue IOError # Typically occurs after a timeout if the session has been left idle
28
- node.open_ssh_connection
31
+ node.reopen_ssh_connection
32
+ ssh = node.ssh_driver.ssh_connection
29
33
  ssh_exec!(ssh, command) # retry, once
30
34
  end
31
35
 
32
36
  output_append("\n(#{node.namespace})$".terminal_prompt + ">\s#{command.raw} (#{command.pretty_result})\n")
33
37
  output_append(command.output.to_s)
34
38
  end
35
- print_output unless ::Bcome::Orchestrator.instance.command_output_silenced?
39
+
40
+ print_output unless ::Bcome::Orchestrator.instance.command_output_silenced? || ::Bcome::Orchestrator.instance.tail_all_command_output?
36
41
  end
37
42
 
38
43
  def ssh_exec!(ssh, command) #  NON PTY (i.e no pseudo-terminal)
39
44
  ssh.open_channel do |channel|
40
45
  channel.exec(command.raw) do |_cha, success|
41
- unless success
42
- abort "FAILED: couldn't execute command (ssh.channel.exec)"
43
- end
46
+ abort "FAILED: couldn't execute command (ssh.channel.exec)" unless success
44
47
 
45
48
  channel.on_data do |_ch, data|
49
+ log_window.add(command.node, data) if ::Bcome::Orchestrator.instance.tail_all_command_output?
46
50
  command.stdout += data
47
51
  end
48
52
 
@@ -55,7 +59,7 @@ module ::Bcome::Ssh
55
59
  end
56
60
 
57
61
  channel.on_request('exit-signal') do |_ch, data|
58
- command.exit_signal = data.read_long
62
+ command.exit_code = data.read_long
59
63
  end
60
64
  end
61
65
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ssh/proxy/jump'
4
+
5
+ module Bcome::Ssh
6
+ class ConnectionWrangler
7
+ def initialize(ssh_driver)
8
+ @ssh_driver = ssh_driver
9
+ @config = ssh_driver.config[:proxy]
10
+ @context_node = ssh_driver.context_node
11
+ @user = ssh_driver.user
12
+ end
13
+
14
+ ## Accessors --
15
+
16
+ def proxy_details
17
+ hops.reverse.collect(&:proxy_details)
18
+ end
19
+
20
+ def first_hop
21
+ hops.reverse.first
22
+ end
23
+
24
+ def has_hop?
25
+ hops.any?
26
+ end
27
+
28
+ def single_hop?
29
+ has_hop? && hops.size == 1
30
+ end
31
+
32
+ def proxy
33
+ @proxy ||= create_proxy
34
+ end
35
+
36
+ def create_proxy
37
+ proxy = Net::SSH::Proxy::Jump.new(hops.reverse.collect(&:get_ssh_string).join(','))
38
+ proxy
39
+ end
40
+
41
+ def get_ssh_command(config = {}, _proxy_only = false)
42
+ cmd = has_hop? ? 'ssh -J' : 'ssh'
43
+ 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}"
45
+
46
+ config[:as_pseudo_tty] ? "#{cmd} -t" : cmd
47
+ end
48
+
49
+ def get_rsync_command(local_path, remote_path)
50
+ cmd = 'rsync -azv'
51
+ cmd += "\s-e 'ssh\s-A -J\s" + hops.collect(&:get_ssh_string).join(',') + "'" if has_hop?
52
+ cmd += "\s#{local_path}\s#{@ssh_driver.user}@#{target_machine_ingress_ip}:#{remote_path}"
53
+ cmd
54
+ end
55
+
56
+ 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...
58
+ raise ::Bcome::Exception::InvalidPortForwardRequest, 'Connections to this node are not via a proxy. Rather than port forward, try connecting directly.' unless has_hop?
59
+
60
+ cmd = "ssh -N -L #{start_port}:localhost:#{end_port} -J"
61
+ 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}"
63
+
64
+ cmd
65
+ end
66
+
67
+ protected
68
+
69
+ def target_machine_ingress_ip
70
+ unless has_hop?
71
+ 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
+ end
73
+
74
+ has_hop? ? @context_node.internal_ip_address : @context_node.public_ip_address
75
+ end
76
+
77
+ def hops
78
+ @hops ||= set_hops
79
+ end
80
+
81
+ private
82
+
83
+ def set_hops
84
+ hop_collection = []
85
+
86
+ parent = nil
87
+ iterable_configs.each do |config|
88
+ hop = set_proxy_hop(config, parent)
89
+ hop_collection << hop
90
+ parent = hop
91
+ end
92
+
93
+ hop_collection
94
+ end
95
+
96
+ def set_proxy_hop(config, parent)
97
+ config[:fallback_bastion_host_user] = @ssh_driver.fallback_bastion_host_user
98
+ ::Bcome::Ssh::ProxyHop.new(config, @context_node, parent)
99
+ end
100
+
101
+ def iterable_configs
102
+ @iterable ||= @config ? (@config.is_a?(Hash) ? [@config] : @config) : []
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bcome
4
+ module Ssh
5
+ class Connector
6
+ include ::Bcome::LoadingBar::Handler
7
+
8
+ class << self
9
+ def connect(node, config = {})
10
+ handler = new(node, config)
11
+ handler.connect
12
+ end
13
+ end
14
+
15
+ def initialize(node, config)
16
+ @node = node
17
+ @config = config
18
+ set_servers
19
+ @connected_machines = []
20
+ @connection_exceptions = {}
21
+ end
22
+
23
+ def show_progress?
24
+ @config[:show_progress] ? true : false
25
+ end
26
+
27
+ def ping?
28
+ @config[:is_ping] ? true : false
29
+ end
30
+
31
+ def connect
32
+ return if number_unconnected_machines == 0 && !ping?
33
+
34
+ if show_progress?
35
+ wrap_indicator type: :progress, size: @servers_to_connect.size, title: 'Opening connections' do
36
+ open_connections
37
+ end
38
+ else
39
+ open_connections
40
+ end
41
+
42
+ report_connection_outcome
43
+ end
44
+
45
+ def report_connection_outcome
46
+ print "\n"
47
+
48
+ if ping?
49
+ @connected_machines.pmap do |machine|
50
+ puts machine.print_ping_result
51
+ end
52
+
53
+ # If any machines remain, then we couldn't connect to them
54
+ @servers_to_connect.each do |machine|
55
+ ping_result = {
56
+ success: false,
57
+ error: @connection_exceptions[machine]
58
+ }
59
+ puts machine.print_ping_result(ping_result)
60
+ end
61
+ end
62
+
63
+ puts "Failed to connect to #{@servers_to_connect.size} node#{@servers_to_connect.size > 1 ? 's' : ''}".error if @servers_to_connect.any?
64
+ end
65
+
66
+ def open_connections
67
+ @servers_to_connect.pmap do |machine|
68
+ 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
77
+ rescue Errno::EPIPE, Bcome::Exception::CouldNotInitiateSshConnection, ::Bcome::Exception::InvalidProxyConfig => e
78
+ signal_failure if show_progress?
79
+ @connection_exceptions[machine] = e
80
+ end
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def set_servers
87
+ @servers_to_connect = machines.dup
88
+ end
89
+
90
+ def number_unconnected_machines
91
+ @servers_to_connect.reject(&:has_ssh_connection?).size
92
+ end
93
+
94
+ def machines
95
+ skip_for_hidden = true # Skip servers with hidden namespaces
96
+ @node.server? ? [@node] : @node.machines(skip_for_hidden)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,276 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/scp'
4
+
1
5
  module Bcome::Ssh
2
6
  class Driver
3
- attr_reader :config, :bootstrap_settings
7
+ attr_reader :config, :context_node
4
8
 
5
- DEFAULT_TIMEOUT_IN_SECONDS = 5
6
- PROXY_CONNECT_PREFIX = '-o StrictHostKeyChecking=no -W %h:%p'.freeze
7
- PROXY_SSH_PREFIX = '-o UserKnownHostsFile=/dev/null -o "ProxyCommand ssh -W %h:%p'.freeze
9
+ include Bcome::Ssh::DriverConnection
10
+ include Bcome::Ssh::DriverFunctions
11
+ include Bcome::Ssh::DriverUser
12
+ include Bcome::Ssh::DriverCommandStrings
8
13
 
9
14
  def initialize(config, context_node)
10
15
  @config = config
11
16
  @context_node = context_node
12
- @proxy_data = @config[:proxy] ? ::Bcome::Ssh::ProxyData.new(@config[:proxy], @context_node) : nil
13
- @bootstrap_settings = @config[:bootstrap_settings] ? ::Bcome::Ssh::Bootstrap.new(@config[:bootstrap_settings]) : nil
14
17
  end
15
18
 
16
- def bootstrap?
17
- @context_node.bootstrap? && has_bootstrap_settings?
19
+ def connection_wrangler
20
+ @connection_wrangler ||= set_connection_wrangler
18
21
  end
19
22
 
20
- def has_bootstrap_settings?
21
- !@bootstrap_settings.nil?
23
+ def set_connection_wrangler
24
+ @set_connection_wrangler ||= ::Bcome::Ssh::ConnectionWrangler.new(self)
22
25
  end
23
26
 
24
- def pretty_config_details
27
+ def pretty_ssh_config
25
28
  config = {
26
29
  user: user,
27
30
  ssh_keys: ssh_keys,
28
31
  timeout: timeout_in_seconds
29
32
  }
33
+
30
34
  if has_proxy?
31
- config[:host_or_ip] = @context_node.internal_ip_address
32
- config[:proxy] = {
33
- bastion_host: @proxy_data.host,
34
- bastion_host_user: bastion_host_user
35
- }
35
+ config[:proxy] = connection_wrangler.proxy_details
36
36
  else
37
37
  config[:host_or_ip] = @context_node.public_ip_address
38
38
  end
39
- config
40
- end
41
-
42
- def timeout_in_seconds
43
- @config[:timeout_in_seconds] ||= Bcome::Ssh::Driver::DEFAULT_TIMEOUT_IN_SECONDS
44
- end
45
-
46
- def proxy
47
- return nil unless has_proxy?
48
- connection_string = bootstrap? ? bootstrap_proxy_connection_string : proxy_connection_string
49
- ::Net::SSH::Proxy::Command.new(connection_string)
50
- end
51
39
 
52
- def has_proxy?
53
- return false if proxy_config_value && proxy_config_value == -1
54
- !@config[:proxy].nil?
40
+ config
55
41
  end
56
42
 
57
43
  def proxy_config_value
58
44
  @config[:proxy]
59
45
  end
60
46
 
61
- def proxy_connection_string
62
- "ssh #{PROXY_CONNECT_PREFIX} #{bastion_host_user}@#{@proxy_data.host}"
63
- end
64
-
65
- def bootstrap_proxy_connection_string
66
- "ssh -i #{@bootstrap_settings.ssh_key_path} -o StrictHostKeyChecking=no -W %h:%p #{@bootstrap_settings.bastion_host_user}@#{@proxy_data.host}"
67
- end
68
-
69
- def do_ssh
70
- cmd = ssh_command
71
- @context_node.execute_local(cmd)
72
- end
73
-
74
- def bastion_host_user
75
- bootstrap? && @bootstrap_settings.bastion_host_user ? @bootstrap_settings.bastion_host_user : @proxy_data.bastion_host_user ? @proxy_data.bastion_host_user : user
76
- end
77
-
78
- def ssh_command(as_pseudo_tty = false)
79
- return bootstrap_ssh_command if bootstrap? && @bootstrap_settings.ssh_key_path
80
-
81
- if has_proxy?
82
- "#{as_pseudo_tty ? "ssh -t" : "ssh"} #{PROXY_SSH_PREFIX} #{bastion_host_user}@#{@proxy_data.host}\" #{node_level_ssh_key_connection_string}#{user}@#{@context_node.internal_ip_address}"
83
- else
84
- "#{as_pseudo_tty ? "ssh -t" : "ssh"} #{node_level_ssh_key_connection_string}#{user}@#{@context_node.public_ip_address}"
85
- end
47
+ def multi_hop_proxy_config
48
+ @config[:multi_hop_proxy]
86
49
  end
87
50
 
88
51
  def node_level_ssh_key_connection_string
89
- key_specified_at_node_level? ? "-i #{node_level_ssh_key}\s" : ""
52
+ key_specified_at_node_level? ? "-i #{node_level_ssh_key}\s" : ''
90
53
  end
91
54
 
92
55
  def key_specified_at_node_level?
93
56
  !node_level_ssh_key.nil?
94
- end
95
-
96
- def node_level_ssh_key
97
- return (@config[:ssh_keys]) ? @config[:ssh_keys].first : nil
98
- end
99
-
100
- def local_port_forward(start_port, end_port)
101
- if has_proxy?
102
-
103
- if bootstrap?
104
- tunnel_command = "ssh -N -L #{start_port}:#{@context_node.internal_ip_address}:#{end_port} -i #{@bootstrap_settings.ssh_key_path} #{bastion_host_user}@#{@proxy_data.host}"
105
- else
106
- tunnel_command = "ssh -N -L #{start_port}:#{@context_node.internal_ip_address}:#{end_port} #{bastion_host_user}@#{@proxy_data.host}"
107
- end
108
- else
109
- if bootstrap?
110
- tunnel_command = "ssh -i #{@bootstrap_settings.ssh_key_path} -N -L #{start_port}:#{@context_node.public_ip_address}:#{end_port}"
111
- else
112
- tunnel_command = "ssh -N -L #{start_port}:#{@context_node.public_ip_address}:#{end_port}"
113
- end
114
- end
115
-
116
- tunnel = ::Bcome::Ssh::Tunnel::LocalPortForward.new(tunnel_command)
117
- tunnel.open!
118
- return tunnel
119
- end
120
-
121
- def bootstrap_ssh_command
122
- if has_proxy?
123
- "ssh -i #{@bootstrap_settings.ssh_key_path} -t #{bastion_host_user}@#{@proxy_data.host} ssh -t #{user}@#{@context_node.internal_ip_address}"
124
- else
125
- "ssh -i #{@bootstrap_settings.ssh_key_path} #{user}@#{@context_node.public_ip_address}"
126
- end
127
- end
128
-
129
- def user
130
- # If we're in bootstrapping mode and have a bootstrap user set, return it
131
- return @bootstrap_settings.user if (bootstrap? && @bootstrap_settings.user)
132
-
133
- # If we have a user explcitly set in the config, then return it
134
- return @config[:user] if @config[:user]
135
-
136
- # If the local user has explicitly overriden their user, return that
137
- return overriden_local_user if overriden_local_user
138
-
139
- # Else fall back to whichever local user is using bcome
140
- fallback_local_user
141
- end
142
-
143
- def overriden_local_user
144
- @overriden_local_user ||= get_overriden_local_user
145
- end
146
-
147
- def get_overriden_local_user
148
- ::Bcome::Node::Factory.instance.local_data[:ssh_user]
149
- end
150
-
151
- def fallback_local_user
152
- @fallback_local_user ||= ::Bcome::System::Local.instance.local_user
153
- end
154
-
155
- def node_host_or_ip
156
- has_proxy? ? @context_node.internal_ip_address : @context_node.public_ip_address
157
- end
158
-
159
- def net_ssh_params(verbose = false)
160
- raise Bcome::Exception::InvalidSshConfig, "Missing ssh keys for #{@context_node.namespace}" unless ssh_keys
161
- params = { keys: ssh_keys, paranoid: false }
162
- params[:proxy] = proxy if has_proxy?
163
- params[:timeout] = timeout_in_seconds
164
- params[:verbose] = :debug if verbose
165
- params
166
- end
167
-
168
- def ssh_keys
169
- if bootstrap?
170
- [@bootstrap_settings.ssh_key_path]
171
- else
172
- @config[:ssh_keys]
173
- end
174
- end
175
-
176
- def rsync(local_path, remote_path)
177
- raise Bcome::Exception::MissingParamsForRsync, "'rsync' requires a local_path and a remote_path" if local_path.to_s.empty? || remote_path.to_s.empty?
178
- command = rsync_command(local_path, remote_path)
179
- @context_node.execute_local(command)
180
- end
181
-
182
- def rsync_command(local_path, remote_path)
183
- return bootstrap_rsync_command(local_path, remote_path) if bootstrap? && @bootstrap_settings.ssh_key_path
184
- if has_proxy?
185
- "rsync -av -e \"ssh -A #{bastion_host_user}@#{@proxy_data.host} ssh -o StrictHostKeyChecking=no\" #{local_path} #{user}@#{@context_node.internal_ip_address}:#{remote_path}"
186
- else
187
- "rsync -av #{local_path} #{user}@#{@context_node.public_ip_address}:#{remote_path}"
188
- end
189
- end
190
-
191
- def bootstrap_rsync_command(local_path, remote_path)
192
- if has_proxy?
193
- "rsync -av -e \"ssh -i #{@bootstrap_settings.ssh_key_path} -A #{bastion_host_user}@#{@proxy_data.host} ssh -o StrictHostKeyChecking=no\" #{local_path} #{user}@#{@context_node.internal_ip_address}:#{remote_path}"
194
- else
195
- "rsync -i #{@bootstrap_settings.ssh_key_path} -av #{local_path} #{user}@#{@context_node.public_ip_address}:#{remote_path}"
196
- end
197
57
  end
198
58
 
199
- def ssh_connect!(_verbose = false)
200
- @connection = nil
201
- begin
202
- @connection = ::Net::SSH.start(node_host_or_ip, user, net_ssh_params)
203
- rescue Net::SSH::Proxy::ConnectError, Net::SSH::ConnectionTimeout, Errno::EPIPE => e
204
- raise Bcome::Exception::CouldNotInitiateSshConnection, @context_node.namespace + "\s-\s#{e.message}"
205
- end
206
- @connection
207
- end
208
-
209
- def ping
210
- ssh_connect!
211
- return { success: true }
212
- rescue Exception => e
213
- return { success: false, error: e }
214
- end
215
-
216
- def scp
217
- ssh_connection.scp
218
- end
219
-
220
- def put(local_path, remote_path)
221
- raise Bcome::Exception::MissingParamsForScp, "'put' requires a local_path and a remote_path" if local_path.to_s.empty? || remote_path.to_s.empty?
222
- puts "\n(#{@context_node.namespace})\s".namespace + "Uploading #{local_path} to #{remote_path}\n".informational
223
-
224
- begin
225
- scp.upload!(local_path, remote_path, recursive: true) do |_ch, name, sent, total|
226
- puts "#{name}: #{sent}/#{total}".progress
227
- end
228
- rescue Exception => e # scp just throws generic exceptions :-/
229
- puts e.message.error
230
- end
231
- nil
232
- end
233
-
234
- def put_str(string, remote_path)
235
- raise Bcome::Exception::MissingParamsForScp, "'put' requires a string and a remote_path" if string.to_s.empty? || remote_path.to_s.empty?
236
- puts "\n(#{@context_node.namespace})\s".namespace + "Uploading from string to #{remote_path}\n".informational
237
-
238
- begin
239
- scp.upload!(StringIO.new(string), remote_path) do |_ch, name, sent, total|
240
- puts "#{name}: #{sent}/#{total}".progress
241
- end
242
- rescue Exception => e # scp just throws generic exceptions :-/
243
- puts e.message.error
244
- end
245
- nil
246
- end
247
-
248
-
249
- def get(remote_path, local_path)
250
- raise Bcome::Exception::MissingParamsForScp, "'get' requires a local_path and a remote_path" if local_path.to_s.empty? || remote_path.to_s.empty?
251
- puts "\n(#{@context_node.namespace})\s".namespace + "Downloading #{remote_path} to #{local_path}\n".informational
252
-
253
- begin
254
- scp.download!(remote_path, local_path, recursive: true) do |_ch, name, sent, total|
255
- puts "#{name}: #{sent}/#{total}".progress
256
- end
257
- rescue Exception => e # scp just throws generic exceptions :-/
258
- puts e.message.error
259
- end
59
+ def node_level_ssh_key
60
+ @config[:ssh_keys] ? @config[:ssh_keys].first : nil
260
61
  end
261
62
 
262
- def close_ssh_connection
263
- return unless @connection
264
- @connection.close unless @connection.closed?
265
- @connection = nil
63
+ def has_multi_hop_proxy?
64
+ !multi_hop_proxy_config.nil?
266
65
  end
267
66
 
268
- def ssh_connection(_bootstrap = false)
269
- has_open_ssh_con? ? @connection : ssh_connect!
270
- end
67
+ def has_proxy?
68
+ return false if proxy_config_value && proxy_config_value == -1
271
69
 
272
- def has_open_ssh_con?
273
- !@connection.nil? && !@connection.closed?
70
+ !@config[:proxy].nil?
274
71
  end
275
72
  end
276
73
  end