beaker-google 0.3.0 → 0.5.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.
- 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
|