Class: CemAcpt::Platform::Gcp::Cmd

Inherits:
CmdBase
  • Object
show all
Defined in:
lib/cem_acpt/platform/gcp/cmd.rb

Overview

This class provides methods to run gcloud commands. It allows for default values to be set for the project, zone, and user and can also find these values from the local config. Additionally, this class provides a way to run SSH commands against GCP VMs using IAP.

Instance Method Summary collapse

Methods inherited from CmdBase

#trim_output, #with_timed_retry

Methods included from Logging

current_log_config, #current_log_config, current_log_format, current_log_level, #current_log_level, included, #logger, new_log_config, #new_log_config, new_log_formatter, new_log_level, #new_log_level

Constructor Details

#initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil) ⇒ Cmd

Returns a new instance of Cmd.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 12

def initialize(project: nil, zone: nil, out_format: nil, filter: nil, user_name: nil, local_port: nil)
  super
  require 'net/ssh'
  require 'net/ssh/proxy/command'
  require 'net/scp'

  @project = project unless project.nil?
  @zone = zone unless zone.nil?
  @default_out_format = out_format
  @default_filter = filter
  @user_name = user_name
  @local_port = local_port
  raise 'gcloud command not available' unless gcloud?
  raise 'gcloud is not authenticated' unless authenticated?
end

Instance Method Details

#apply_manifest(instance_name, manifest, opts = {}) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 213

def apply_manifest(instance_name, manifest, opts = {})
  require 'tempfile'

  temp_manifest = Tempfile.new('acpt_manifest')
  temp_manifest.write(manifest)
  temp_manifest.close
  begin
    scp_upload(
      instance_name,
      temp_manifest.path,
      '/tmp/acpt_manifest.pp',
      opts: opts[:ssh_opts],
    )
  ensure
    temp_manifest.unlink
  end
  apply_cmd = [
    'sudo -n -u root -i',
    "#{opts[:puppet_path]} apply /tmp/acpt_manifest.pp"
  ]
  apply_cmd << '--trace' if opts[:apply][:trace]
  apply_cmd << "--hiera_config=#{opts[:apply][:hiera_config]}" if opts[:apply][:hiera_config]
  apply_cmd << '--debug' if opts[:apply][:debug]
  apply_cmd << '--noop' if opts[:apply][:noop]
  apply_cmd << '--detailed-exitcodes' if opts[:apply][:detailed_exitcodes]

  ssh(
    instance_name,
    apply_cmd.join(' '),
    ignore_command_errors: true,
    opts: opts[:ssh_opts]
  )
end

#delete_instance(instance_name) ⇒ Object

Deletes a GCP VM instance.



100
101
102
103
104
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 100

def delete_instance(instance_name)
  local_exec("compute instances delete #{instance_name} --quiet")
rescue StandardError
  # Ignore errors when deleting instances.
end

#filter(out_filter = nil) ⇒ Object

Returns the filter string passed in during object initialization or the default filter string.



54
55
56
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 54

def filter(out_filter = nil)
  out_filter&.chomp || @default_filter&.chomp
end

#format(out_format = nil) ⇒ Object

Returns the format string passed in during object initialization or the default format string.



48
49
50
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 48

def format(out_format = nil)
  out_format&.chomp || @default_out_format&.chomp
end

#local_exec(command, out_format: 'json', out_filter: nil, project_flag: true) ⇒ Object

Runs `gcloud` commands locally.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 76

def local_exec(command, out_format: 'json', out_filter: nil, project_flag: true)
  cmd_parts = ['gcloud', command]
  cmd_parts << "--project=#{project.chomp}" unless !project_flag || project.nil?
  cmd_parts << "--format=#{format(out_format)}" if format(out_format)
  cmd_parts << "--filter=\"#{filter(out_filter)}\"" if filter(out_filter)
  final_command = cmd_parts.join(' ')
  stdout, stderr, status = Open3.capture3(final_command)
  raise "gcloud command '#{final_command}' failed: #{stderr}" unless status.success?

  begin
    return ::JSON.parse(stdout) if format(out_format) == 'json'
  rescue ::JSON::ParserError
    return stdout
  end

  stdout
end

#local_portObject



58
59
60
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 58

def local_port
  @local_port ||= rand(49_512..65_535)
end

#projectObject

Returns either the project name passed in during object initialization or the project name from the local config.



30
31
32
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 30

def project
  @project ||= project_from_config&.chomp
end

#project_from_configObject



190
191
192
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 190

def project_from_config
  local_exec('config get-value project', out_format: nil, project_flag: false)
end

#run_shell(instance_name, cmd, opts = {}) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 198

def run_shell(instance_name, cmd, opts = {})
  ssh_opts = opts.key?(:ssh_opts) ? opts[:ssh_opts] : {}
  command = [
    'sudo -n -u root -i',
    cmd,
  ]
  ssh(
    instance_name,
    command.join(' '),
    ignore_command_errors: true,
    use_proxy_command: false,
    opts: ssh_opts,
  )
end

#scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object

Downloads a file from the specified VM.



136
137
138
139
140
141
142
143
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 136

def scp_download(instance_name, remote, local, use_proxy_command: true, scp_opts: {}, opts: {})
  raise "Local file #{local} does not exist" unless File.exist?(local)

  hostname = use_proxy_command ? vm_alias(instance_name) : instance_name
  Net::SCP.start(hostname, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |scp|
    scp.download(remote, local, scp_opts).wait
  end
end

#scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {}) ⇒ Object

Uploads a file to the specified VM.



126
127
128
129
130
131
132
133
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 126

def scp_upload(instance_name, local, remote, use_proxy_command: true, scp_opts: {}, opts: {})
  raise "Local file #{local} does not exist" unless File.exist?(local)

  hostname = use_proxy_command ? vm_alias(instance_name) : instance_name
  Net::SCP.start(hostname, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |scp|
    scp.upload(local, remote, scp_opts).wait
  end
end

#ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {}) ⇒ Object

This function runs the specified command as the currently authenticated user on the given CGP VM via SSH. Using `ssh` does not invoke the gcloud command and is dependent on the system's SSH configuration.



114
115
116
117
118
119
120
121
122
123
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 114

def ssh(instance_name, cmd, ignore_command_errors: false, use_proxy_command: true, opts: {})
  Net::SSH.start(instance_name, user_name, ssh_opts(instance_name: instance_name, use_proxy_command: use_proxy_command, opts: opts)) do |ssh|
    result = ssh.exec!(cmd)
    if result.exitstatus != 0 && !ignore_command_errors
      abridged_cmd = cmd.length > 100 ? "#{cmd[0..100]}..." : cmd
      raise "Failed to run SSH command \"#{abridged_cmd}\" on #{instance_name}: #{result}"
    end
    result
  end
end

#ssh_opts(instance_name: nil, use_proxy_command: true, opts: {}) ⇒ Object

Returns a formatted hash of ssh options to be used with Net::SSH.start. If you pass in a GCP VM instance name, this method will configure the IAP tunnel ProxyCommand to use. If you pass in an opts hash, it will merge the options with the default options.



66
67
68
69
70
71
72
73
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 66

def ssh_opts(instance_name: nil, use_proxy_command: true, opts: {})
  base_opts = default_ssh_opts
  if use_proxy_command
    base_opts[:proxy] = proxy_command(instance_name, port: base_opts[:port])
    base_opts[:host_key_alias] = vm_alias(instance_name)
  end
  base_opts.merge(opts).reject { |_, v| v.nil? }
end

#ssh_ready?(instance_name, timeout = 300, opts: {}) ⇒ Boolean

Returns:

  • (Boolean)


145
146
147
148
149
150
151
152
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 145

def ssh_ready?(instance_name, timeout = 300, opts: {})
  with_timed_retry(timeout) do
    logger.debug("Testing SSH connection to #{instance_name}")
    Net::SSH.start(instance_name, user_name, ssh_opts(instance_name: instance_name, opts: opts)) do |_|
      true
    end
  end
end

#stop_instance(instance_name) ⇒ Object

Stops a GCP VM instance.



95
96
97
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 95

def stop_instance(instance_name)
  local_exec("compute instances stop #{instance_name}")
end

#user_nameObject

Returns either the user name passed in during object initialization or queries the current active, authenticated user from gcloud and returns the name.



42
43
44
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 42

def user_name
  @user_name ||= authenticated_user_name&.chomp
end

#vm_describe(instance_name, out_format: 'json', out_filter: nil) ⇒ Object

Returns a Hash describing a GCP VM instance.



107
108
109
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 107

def vm_describe(instance_name, out_format: 'json', out_filter: nil)
  local_exec("compute instances describe #{instance_name}", out_format: out_format, out_filter: out_filter)
end

#with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false) ⇒ Object

This function spawns a background thread to run a GCP IAP tunnel, run the given code block, then kill the thread. The code block will be yielded ssh_opts that are used to configure SSH connections over the IAP tunnel. The IAP tunnel is run in it's own thread and will not block the main thread. The IAP tunnel is killed when the code block exits. All SSH connections made in the code block must be made to the host '127.0.0.1' using the yielded ssh_opts and not the VM name.

Parameters:

  • instance_name (String)

    The name of the GCP VM instance to connect to.

  • instance_port (Integer) (defaults to: 22)

    The port to connect to on the GCP VM instance.

  • disable_connection_check (Boolean) (defaults to: false)

    If true, the connection check will be disabled.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 163

def with_iap_tunnel(instance_name, instance_port = 22, disable_connection_check: false)
  return unless block_given?

  cmd = [
    'compute start-iap-tunnel',
    "#{instance_name} #{instance_port}",
    "--local-host-port=localhost:#{local_port}",
  ]
  cmd << '--disable-connection-check' if disable_connection_check
  tunnel_ssh_opts = {
    proxy: nil,
    port: local_port,
    forward_agent: false,
  }
  begin
    thread = Thread.new do
      local_exec(cmd.join(' '))
    end
    yield ssh_opts(use_proxy_command: false, opts: tunnel_ssh_opts)
    thread.exit
  rescue IOError
    # Ignore errors when killing the thread.
  ensure
    thread.exit
  end
end

#zoneObject

Returns either the zone name passed in during object initialization or the zone name from the local config.



36
37
38
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 36

def zone
  @zone ||= zone_from_config&.chomp
end

#zone_from_configObject



194
195
196
# File 'lib/cem_acpt/platform/gcp/cmd.rb', line 194

def zone_from_config
  local_exec('config get-value compute/zone', out_format: nil, project_flag: false)
end