beaker-google 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +46 -0
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/release.yml +32 -0
- data/.rubocop.yml +519 -0
- data/.simplecov +1 -1
- data/CHANGELOG.md +59 -0
- data/Gemfile +8 -12
- data/README.md +48 -19
- data/Rakefile +49 -39
- data/beaker-google.gemspec +11 -13
- data/bin/beaker-google +1 -3
- data/lib/beaker/hypervisor/google_compute.rb +152 -113
- data/lib/beaker/hypervisor/google_compute_helper.rb +445 -754
- data/lib/beaker-google/version.rb +1 -1
- metadata +49 -17
- data/.github/workflows/snyk_scan.yaml +0 -23
- data/CODEOWNERS +0 -2
@@ -1,37 +1,52 @@
|
|
1
1
|
require 'time'
|
2
2
|
|
3
3
|
module Beaker
|
4
|
-
|
5
4
|
# Beaker support for the Google Compute Engine.
|
6
5
|
class GoogleCompute < Beaker::Hypervisor
|
7
|
-
|
8
6
|
SLEEPWAIT = 5
|
9
7
|
|
10
8
|
# Hours before an instance is considered a zombie
|
11
9
|
ZOMBIE = 3
|
12
10
|
|
13
11
|
# Do some reasonable sleuthing on the SSH public key for GCE
|
14
|
-
def find_google_ssh_public_key
|
15
|
-
keyfile = ENV.fetch('BEAKER_gce_ssh_public_key', File.join(ENV['HOME'], '.ssh', 'google_compute_engine.pub'))
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
##
|
14
|
+
# Try to find the private ssh key file
|
15
|
+
#
|
16
|
+
# @return [String] The file path for the private key file
|
17
|
+
#
|
18
|
+
# @raise [Error] if the private key can not be found
|
19
|
+
def find_google_ssh_private_key
|
20
|
+
private_keyfile = ENV.fetch('BEAKER_gce_ssh_public_key',
|
21
|
+
File.join(ENV.fetch('HOME', nil), '.ssh', 'google_compute_engine'))
|
22
|
+
private_keyfile = @options[:gce_ssh_private_key] if @options[:gce_ssh_private_key] && !File.exist?(private_keyfile)
|
23
|
+
raise("Could not find GCE Private SSH key at '#{keyfile}'") unless File.exist?(private_keyfile)
|
24
|
+
@options[:gce_ssh_private_key] = private_keyfile
|
25
|
+
private_keyfile
|
26
|
+
end
|
22
27
|
|
23
|
-
|
28
|
+
##
|
29
|
+
# Try to find the public key file based on the location of the private key or provided data
|
30
|
+
#
|
31
|
+
# @return [String] The file path for the public key file
|
32
|
+
#
|
33
|
+
# @raise [Error] if the public key can not be found
|
34
|
+
def find_google_ssh_public_key
|
35
|
+
private_keyfile = find_google_ssh_private_key
|
36
|
+
public_keyfile = private_keyfile << '.pub'
|
37
|
+
public_keyfile = @options[:gce_ssh_public_key] if @options[:gce_ssh_public_key] && !File.exist?(public_keyfile)
|
38
|
+
raise("Could not find GCE Public SSH key at '#{keyfile}'") unless File.exist?(public_keyfile)
|
39
|
+
@options[:gce_ssh_public_key] = public_keyfile
|
40
|
+
public_keyfile
|
24
41
|
end
|
25
42
|
|
26
|
-
#
|
27
|
-
# :
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
{:key => :jenkins_build_url, :value => @options[:jenkins_build_url]},
|
32
|
-
{:key => :sshKeys, :value => "google_compute:#{File.read(find_google_ssh_public_key).strip}" }
|
33
|
-
].delete_if { |member| member[:value].nil? or member[:value].empty?}
|
43
|
+
# IP is the only way we can be sure to connect
|
44
|
+
# TODO: This isn't being called
|
45
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
46
|
+
def connection_preference(host)
|
47
|
+
[:ip]
|
34
48
|
end
|
49
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
35
50
|
|
36
51
|
# Create a new instance of the Google Compute Engine hypervisor object
|
37
52
|
#
|
@@ -60,6 +75,7 @@ module Beaker
|
|
60
75
|
def initialize(google_hosts, options)
|
61
76
|
require 'beaker/hypervisor/google_compute_helper'
|
62
77
|
|
78
|
+
super
|
63
79
|
@options = options
|
64
80
|
@logger = options[:logger]
|
65
81
|
@hosts = google_hosts
|
@@ -70,67 +86,147 @@ module Beaker
|
|
70
86
|
# Create and configure virtual machines in the Google Compute Engine,
|
71
87
|
# including their associated disks and firewall rules
|
72
88
|
def provision
|
73
|
-
attempts = @options[:timeout].to_i / SLEEPWAIT
|
74
89
|
start = Time.now
|
75
|
-
|
76
90
|
test_group_identifier = "beaker-#{start.to_i}-"
|
77
91
|
|
78
|
-
# get machineType resource, used by all instances
|
79
|
-
machineType = @gce_helper.get_machineType(start, attempts)
|
80
|
-
|
81
92
|
# set firewall to open pe ports
|
82
|
-
network = @gce_helper.get_network
|
93
|
+
network = @gce_helper.get_network
|
94
|
+
|
83
95
|
@firewall = test_group_identifier + generate_host_name
|
84
|
-
@gce_helper.create_firewall(@firewall, network, start, attempts)
|
85
96
|
|
86
|
-
@
|
97
|
+
@gce_helper.create_firewall(@firewall, network)
|
87
98
|
|
99
|
+
@logger.debug("Created Google Compute firewall #{@firewall}")
|
88
100
|
|
89
101
|
@hosts.each do |host|
|
102
|
+
|
103
|
+
machine_type_name = ENV.fetch('BEAKER_gce_machine_type', host['gce_machine_type'])
|
104
|
+
raise "Must provide a machine type name in 'gce_machine_type'." if machine_type_name.nil?
|
105
|
+
# Get the GCE machine type object for this host
|
106
|
+
machine_type = @gce_helper.get_machine_type(machine_type_name)
|
107
|
+
raise "Unable to find machine type named #{machine_type_name} in region #{@compute.default_zone}" if machine_type.nil?
|
108
|
+
|
109
|
+
# Find the image to use to create the new VM.
|
110
|
+
# Either `image` or `family` must be set in the configuration. Accepted formats
|
111
|
+
# for the image and family:
|
112
|
+
# - {project}/{image}
|
113
|
+
# - {project}/{family}
|
114
|
+
# - {image}
|
115
|
+
# - {family}
|
116
|
+
#
|
117
|
+
# If a {project} is not specified, default to the project provided in the
|
118
|
+
# BEAKER_gce_project environment variable
|
90
119
|
if host[:image]
|
91
|
-
|
92
|
-
|
93
|
-
|
120
|
+
image_selector = host[:image]
|
121
|
+
# Do we have a project name?
|
122
|
+
if %r{/}.match?(image_selector)
|
123
|
+
image_project, image_name = image_selector.split('/')[0..1]
|
124
|
+
else
|
125
|
+
image_project = @gce_helper.options[:gce_project]
|
126
|
+
image_name = image_selector
|
127
|
+
end
|
128
|
+
img = @gce_helper.get_image(image_project, image_name)
|
129
|
+
raise "Unable to find image #{image_name} from project #{image_project}" if img.nil?
|
130
|
+
elsif host[:family]
|
131
|
+
image_selector = host[:family]
|
132
|
+
# Do we have a project name?
|
133
|
+
if %r{/}.match?(image_selector)
|
134
|
+
image_project, family_name = image_selector.split('/')
|
135
|
+
else
|
136
|
+
image_project = @gce_helper.options[:gce_project]
|
137
|
+
family_name = image_selector
|
138
|
+
end
|
139
|
+
img = @gce_helper.get_latest_image_from_family(image_project, family_name)
|
140
|
+
raise "Unable to find image in family #{family_name} from project #{image_project}" if img.nil?
|
94
141
|
else
|
95
|
-
raise('You must specify either :image or :
|
142
|
+
raise('You must specify either :image or :family')
|
96
143
|
end
|
97
144
|
|
98
|
-
img = @gce_helper.get_latest_image(gplatform, start, attempts)
|
99
|
-
|
100
145
|
unique_host_id = test_group_identifier + generate_host_name
|
101
146
|
|
102
|
-
host['
|
103
|
-
|
104
|
-
|
147
|
+
boot_size = host['volume_size'] || img.disk_size_gb
|
148
|
+
|
149
|
+
# The boot disk is created as part of the instance creation
|
150
|
+
# TODO: Allow creation of other disks
|
151
|
+
# disk = @gce_helper.create_disk(host["diskname"], img, size)
|
152
|
+
# @logger.debug("Created Google Compute disk for #{host.name}: #{host["diskname"]}")
|
105
153
|
|
106
154
|
# create new host name
|
107
155
|
host['vmhostname'] = unique_host_id
|
108
|
-
|
109
|
-
|
156
|
+
|
157
|
+
# add a new instance of the image
|
158
|
+
operation = @gce_helper.create_instance(host['vmhostname'], img, machine_type, boot_size)
|
159
|
+
unless operation.error.nil?
|
160
|
+
raise "Unable to create Google Compute Instance #{host.name}: [#{operation.error.errors[0].code}] #{operation.error.errors[0].message}"
|
161
|
+
end
|
110
162
|
@logger.debug("Created Google Compute instance for #{host.name}: #{host['vmhostname']}")
|
163
|
+
instance = @gce_helper.get_instance(host['vmhostname'])
|
164
|
+
|
165
|
+
# Make sure we have a non root/Adminsitor user to log in as
|
166
|
+
if host['user'] == "root" || host['user'] == "Administrator" || host['user'].empty?
|
167
|
+
initial_user = 'google_compute'
|
168
|
+
else
|
169
|
+
initial_user = host['user']
|
170
|
+
end
|
111
171
|
|
112
172
|
# add metadata to instance, if there is any to set
|
113
|
-
mdata = format_metadata
|
173
|
+
# mdata = format_metadata
|
174
|
+
# TODO: Set a configuration option for this to allow disabeling oslogin
|
175
|
+
mdata = [
|
176
|
+
{
|
177
|
+
key: 'ssh-keys',
|
178
|
+
value: "#{initial_user}:#{File.read(find_google_ssh_public_key).strip}"
|
179
|
+
},
|
180
|
+
# For now oslogin needs to be disabled as there's no way to log in as root and it would
|
181
|
+
# take too much work on beaker to add sudo support to everything
|
182
|
+
{
|
183
|
+
key: 'enable-oslogin',
|
184
|
+
value: 'FALSE'
|
185
|
+
},
|
186
|
+
]
|
187
|
+
|
188
|
+
# Check for google's default windows images and turn on ssh if found
|
189
|
+
if image_project == "windows-cloud" || image_project == "windows-sql-cloud"
|
190
|
+
# Turn on SSH on GCP's default windows images
|
191
|
+
mdata << {
|
192
|
+
key: 'enable-windows-ssh',
|
193
|
+
value: 'TRUE',
|
194
|
+
}
|
195
|
+
mdata << {
|
196
|
+
key: 'sysprep-specialize-script-cmd',
|
197
|
+
value: 'googet -noconfirm=true update && googet -noconfirm=true install google-compute-engine-ssh',
|
198
|
+
}
|
199
|
+
# Some versions of windows don't seem to add the OpenSSH directory to the path which prevents scp from working
|
200
|
+
mdata << {
|
201
|
+
key: 'sysprep-specialize-script-ps1',
|
202
|
+
value: '[Environment]::SetEnvironmentVariable( "PATH", "$ENV:PATH;C:\Program Files\OpenSSH", [EnvironmentVariableTarget]::Machine )',
|
203
|
+
}
|
204
|
+
end
|
114
205
|
unless mdata.empty?
|
115
|
-
|
116
|
-
|
117
|
-
start, attempts)
|
206
|
+
# Add the metadata to the host
|
207
|
+
@gce_helper.set_metadata_on_instance(host['vmhostname'], mdata)
|
118
208
|
@logger.debug("Added tags to Google Compute instance #{host.name}: #{host['vmhostname']}")
|
119
209
|
end
|
120
210
|
|
121
|
-
|
122
|
-
host['ip'] = instance['networkInterfaces'][0]['accessConfigs'][0]['natIP']
|
123
|
-
|
124
|
-
# configure ssh
|
125
|
-
default_user = host['user']
|
126
|
-
host['user'] = 'google_compute'
|
211
|
+
host['ip'] = instance.network_interfaces[0].access_configs[0].nat_ip
|
127
212
|
|
128
|
-
|
129
|
-
|
130
|
-
host['user'] = default_user
|
213
|
+
# Add the new host to the firewall
|
214
|
+
@gce_helper.add_firewall_tag(@firewall, host['vmhostname'])
|
131
215
|
|
132
|
-
|
133
|
-
|
216
|
+
if host['disable_root_ssh'] == true
|
217
|
+
@logger.info('Not enabling root ssh as disable_root_ssh is true')
|
218
|
+
else
|
219
|
+
real_user = host['user']
|
220
|
+
host['user'] = initial_user
|
221
|
+
# Set the ssh private key we need to use
|
222
|
+
host.options['ssh']['keys'] = [find_google_ssh_private_key]
|
223
|
+
|
224
|
+
copy_ssh_to_root(host, @options)
|
225
|
+
enable_root_login(host, @options)
|
226
|
+
host['user'] = real_user
|
227
|
+
# shut down connection, will reconnect on next exec
|
228
|
+
host.close
|
229
|
+
end
|
134
230
|
|
135
231
|
@logger.debug("Instance ready: #{host['vmhostname']} for #{host.name}}")
|
136
232
|
end
|
@@ -138,70 +234,13 @@ module Beaker
|
|
138
234
|
|
139
235
|
# Shutdown and destroy virtual machines in the Google Compute Engine,
|
140
236
|
# including their associated disks and firewall rules
|
141
|
-
def cleanup
|
142
|
-
|
143
|
-
start = Time.now
|
144
|
-
|
145
|
-
@gce_helper.delete_firewall(@firewall, start, attempts)
|
237
|
+
def cleanup
|
238
|
+
@gce_helper.delete_firewall(@firewall)
|
146
239
|
|
147
240
|
@hosts.each do |host|
|
148
|
-
|
241
|
+
# TODO: Delete any other disks attached during the instance creation
|
242
|
+
@gce_helper.delete_instance(host['vmhostname'])
|
149
243
|
@logger.debug("Deleted Google Compute instance #{host['vmhostname']} for #{host.name}")
|
150
|
-
@gce_helper.delete_disk(host['diskname'], start, attempts)
|
151
|
-
@logger.debug("Deleted Google Compute disk #{host['diskname']} for #{host.name}")
|
152
|
-
end
|
153
|
-
|
154
|
-
end
|
155
|
-
|
156
|
-
# Shutdown and destroy Google Compute instances (including their associated
|
157
|
-
# disks and firewall rules) that have been alive longer than ZOMBIE hours.
|
158
|
-
def kill_zombies(max_age = ZOMBIE)
|
159
|
-
now = start = Time.now
|
160
|
-
attempts = @options[:timeout].to_i / SLEEPWAIT
|
161
|
-
|
162
|
-
# get rid of old instances
|
163
|
-
instances = @gce_helper.list_instances(start, attempts)
|
164
|
-
if instances
|
165
|
-
instances.each do |instance|
|
166
|
-
created = Time.parse(instance['creationTimestamp'])
|
167
|
-
alive = (now - created )/60/60
|
168
|
-
if alive >= max_age
|
169
|
-
#kill it with fire!
|
170
|
-
@logger.debug("Deleting zombie instance #{instance['name']}")
|
171
|
-
@gce_helper.delete_instance( instance['name'], start, attempts )
|
172
|
-
end
|
173
|
-
end
|
174
|
-
else
|
175
|
-
@logger.debug("No zombie instances found")
|
176
|
-
end
|
177
|
-
|
178
|
-
# get rid of old disks
|
179
|
-
disks = @gce_helper.list_disks(start, attempts)
|
180
|
-
if disks
|
181
|
-
disks.each do |disk|
|
182
|
-
created = Time.parse(disk['creationTimestamp'])
|
183
|
-
alive = (now - created )/60/60
|
184
|
-
if alive >= max_age
|
185
|
-
|
186
|
-
# kill it with fire!
|
187
|
-
@logger.debug("Deleting zombie disk #{disk['name']}")
|
188
|
-
@gce_helper.delete_disk( disk['name'], start, attempts )
|
189
|
-
end
|
190
|
-
end
|
191
|
-
else
|
192
|
-
@logger.debug("No zombie disks found")
|
193
|
-
end
|
194
|
-
|
195
|
-
# get rid of non-default firewalls
|
196
|
-
firewalls = @gce_helper.list_firewalls( start, attempts)
|
197
|
-
|
198
|
-
if firewalls && !firewalls.empty?
|
199
|
-
firewalls.each do |firewall|
|
200
|
-
@logger.debug("Deleting non-default firewall #{firewall['name']}")
|
201
|
-
@gce_helper.delete_firewall( firewall['name'], start, attempts )
|
202
|
-
end
|
203
|
-
else
|
204
|
-
@logger.debug("No zombie firewalls found")
|
205
244
|
end
|
206
245
|
end
|
207
246
|
end
|