vmpooler-provider-ec2 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: af857a566941f0bb8a3fc9493bc45f16aa070cf9e9779433215204cbb8470cc2
4
+ data.tar.gz: 4a6190906493b92cf4f8130e195bab242606f31cc2e334e62f1a78fd4a327ed5
5
+ SHA512:
6
+ metadata.gz: d616b402493feae8a8a4b20a829d3dc2ac2d3152e2e1b46e9a645d9a10672bd697c576a65e115fc670e135bac3e35605bb4c79dd0c17f0453b6d45bac74d95a7
7
+ data.tar.gz: 37ee3a21cc1b362ab32ef991f6d7061f6823f04f1d56e01141f305896c49ca93dcfdcc08043913fffd4e964a41e91d2a90052f6df697cb2a55a4b15d2a87c554
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ssh'
4
+ module Vmpooler
5
+ class PoolManager
6
+ # This class connects to existing running VMs via NET:SSH
7
+ # it uses a local key to do so and then setup SSHD on the hosts to enable
8
+ # dev and CI users to connect.
9
+ class AwsSetup
10
+ ROOT_KEYS_SCRIPT = ENV['ROOT_KEYS_SCRIPT']
11
+ ROOT_KEYS_SYNC_CMD = "curl -k -o - -L #{ROOT_KEYS_SCRIPT} | %s"
12
+
13
+ def initialize(logger, new_vmname)
14
+ @logger = logger
15
+ @key_file = ENV['AWS_KEY_FILE_LOCATION']
16
+ @vm_name = new_vmname
17
+ end
18
+
19
+ def setup_node_by_ssh(host, platform)
20
+ conn = check_ssh_accepting_connections(host, platform)
21
+ return unless conn
22
+
23
+ @logger.log('s', "[>] [#{platform}] '#{@vm_name}' net:ssh connected")
24
+ configure_host(host, platform, conn)
25
+ @logger.log('s', "[>] [#{platform}] '#{@vm_name}' configured")
26
+ end
27
+
28
+ # For an Amazon Linux AMI, the user name is ec2-user.
29
+ #
30
+ # For a Centos AMI, the user name is centos.
31
+ #
32
+ # For a Debian AMI, the user name is admin or root.
33
+ #
34
+ # For a Fedora AMI, the user name is ec2-user or fedora.
35
+ #
36
+ # For a RHEL AMI, the user name is ec2-user or root.
37
+ #
38
+ # For a SUSE AMI, the user name is ec2-user or root.
39
+ #
40
+ # For an Ubuntu AMI, the user name is ubuntu.
41
+
42
+ def get_user(platform)
43
+ if platform =~ /centos/
44
+ 'centos'
45
+ elsif platform =~ /ubuntu/
46
+ 'ubuntu'
47
+ elsif platform =~ /debian/
48
+ 'root'
49
+ else
50
+ 'ec2-user'
51
+ end
52
+ end
53
+
54
+ def check_ssh_accepting_connections(host, platform)
55
+ retries = 0
56
+ begin
57
+ user = get_user(platform)
58
+ netssh_jruby_workaround
59
+ Net::SSH.start(host, user, keys: @key_file, timeout: 10)
60
+ rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED => e
61
+ @logger.log('s', "[>] [#{platform}] '#{@vm_name}' net:ssh requested instances do not have sshd ready yet, try again for 300s (#{retries}/300): #{e}")
62
+ sleep 1
63
+ retry if (retries += 1) < 300
64
+ rescue Errno::EBADF => e
65
+ @logger.log('s', "[>] [#{platform}] '#{@vm_name}' net:ssh jruby error, try again for 300s (#{retries}/30): #{e}")
66
+ sleep 10
67
+ retry if (retries += 1) < 30
68
+ rescue StandardError => e
69
+ @logger.log('s', "[>] [#{platform}] '#{@vm_name}' net:ssh other error, skipping aws_setup: #{e}")
70
+ puts e.backtrace
71
+ end
72
+ end
73
+
74
+ # Configure the aws host by enabling root and setting the hostname
75
+ # @param host [String] the internal dns name of the instance
76
+ def configure_host(host, platform, ssh)
77
+ ssh.exec!('sudo cp -r .ssh /root/.')
78
+ ssh.exec!("sudo sed -ri 's/^#?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config")
79
+ ssh.exec!("sudo hostname #{host}")
80
+ if platform =~ /amazon/
81
+ # Amazon Linux requires this to preserve host name changes across reboots.
82
+ ssh.exec!("sudo sed -ie '/^HOSTNAME/ s/=.*/=#{host}/' /etc/sysconfig/network")
83
+ end
84
+ restart_sshd(host, platform, ssh)
85
+ sync_root_keys(host, platform)
86
+ end
87
+
88
+ def restart_sshd(host, platform, ssh)
89
+ ssh.open_channel do |channel|
90
+ channel.request_pty do |ch, success|
91
+ raise "can't get pty request" unless success
92
+
93
+ if platform =~ /centos|el-|redhat|fedora|eos|amazon/
94
+ ch.exec('sudo -E /sbin/service sshd reload')
95
+ elsif platform =~ /debian|ubuntu|cumulus/
96
+ ch.exec('sudo su -c \"service sshd restart\"')
97
+ elsif platform =~ /arch|centos-7|el-7|redhat-7|fedora-(1[4-9]|2[0-9])/
98
+ ch.exec('sudo -E systemctl restart sshd.service')
99
+ else
100
+ services.logger.error("Attempting to update ssh on non-supported platform: #{host}: #{platform}")
101
+ end
102
+ end
103
+ end
104
+ ssh.loop
105
+ end
106
+
107
+ def sync_root_keys(host, _platform)
108
+ return if ROOT_KEYS_SCRIPT.nil?
109
+
110
+ user = 'root'
111
+ netssh_jruby_workaround
112
+ Net::SSH.start(host, user, keys: @key_file) do |ssh|
113
+ ssh.exec!(ROOT_KEYS_SYNC_CMD % 'env PATH="/usr/gnu/bin:$PATH" bash')
114
+ end
115
+ end
116
+
117
+ # issue when using net ssh 6.1.0 with jruby
118
+ # https://github.com/jruby/jruby-openssl/issues/105
119
+ # this will turn off some algos that match /^ecd(sa|h)-sha2/
120
+ def netssh_jruby_workaround
121
+ Net::SSH::Transport::Algorithms::ALGORITHMS.each_value { |algs| algs.reject! { |a| a =~ /^ecd(sa|h)-sha2/ } }
122
+ Net::SSH::KnownHosts::SUPPORTED_TYPE.reject! { |t| t =~ /^ecd(sa|h)-sha2/ }
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,571 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+ require 'bigdecimal/util'
5
+ require 'vmpooler/providers/base'
6
+ require 'vmpooler/cloud_dns'
7
+ require 'aws-sdk-ec2'
8
+ require 'vmpooler/aws_setup'
9
+
10
+ module Vmpooler
11
+ class PoolManager
12
+ class Provider
13
+ # This class represent a GCE provider to CRUD resources in a gce cloud.
14
+ class Ec2 < Vmpooler::PoolManager::Provider::Base
15
+ # The connection_pool method is normally used only for testing
16
+ attr_reader :connection_pool
17
+
18
+ def initialize(config, logger, metrics, redis_connection_pool, name, options)
19
+ super(config, logger, metrics, redis_connection_pool, name, options)
20
+
21
+ @aws_access_key = ENV['ABS_AWS_ACCESS_KEY'] || provider_config['ABS_AWS_ACCESS_KEY']
22
+ @aws_secret_key = ENV['ABS_AWS_SECRET_KEY'] || provider_config['ABS_AWS_SECRET_KEY']
23
+
24
+ task_limit = global_config[:config].nil? || global_config[:config]['task_limit'].nil? ? 10 : global_config[:config]['task_limit'].to_i
25
+ # The default connection pool size is:
26
+ # Whatever is biggest from:
27
+ # - How many pools this provider services
28
+ # - Maximum number of cloning tasks allowed
29
+ # - Need at least 2 connections so that a pool can have inventory functions performed while cloning etc.
30
+ default_connpool_size = [provided_pools.count, task_limit, 2].max
31
+ connpool_size = provider_config['connection_pool_size'].nil? ? default_connpool_size : provider_config['connection_pool_size'].to_i
32
+ # The default connection pool timeout should be quite large - 60 seconds
33
+ connpool_timeout = provider_config['connection_pool_timeout'].nil? ? 60 : provider_config['connection_pool_timeout'].to_i
34
+ logger.log('d', "[#{name}] ConnPool - Creating a connection pool of size #{connpool_size} with timeout #{connpool_timeout}")
35
+ @logger = logger
36
+ @connection_pool = Vmpooler::PoolManager::GenericConnectionPool.new(
37
+ metrics: metrics,
38
+ connpool_type: 'provider_connection_pool',
39
+ connpool_provider: name,
40
+ size: connpool_size,
41
+ timeout: connpool_timeout
42
+ ) do
43
+ logger.log('d', "[#{name}] Connection Pool - Creating a connection object")
44
+ # Need to wrap the vSphere connection object in another object. The generic connection pooler will preserve
45
+ # the object reference for the connection, which means it cannot "reconnect" by creating an entirely new connection
46
+ # object. Instead by wrapping it in a Hash, the Hash object reference itself never changes but the content of the
47
+ # Hash can change, and is preserved across invocations.
48
+ new_conn = connect_to_aws
49
+ { connection: new_conn }
50
+ end
51
+ @redis = redis_connection_pool
52
+ end
53
+
54
+ # name of the provider class
55
+ def name
56
+ 'ec2'
57
+ end
58
+
59
+ def connection
60
+ @connection_pool.with_metrics do |pool_object|
61
+ return ensured_aws_connection(pool_object)
62
+ end
63
+ end
64
+
65
+ # main configuration options
66
+ def region
67
+ provider_config['region']
68
+ end
69
+
70
+ # main configuration options, overridable for each pool
71
+ def zone(pool_name)
72
+ return pool_config(pool_name)['zone'] if pool_config(pool_name)['zone']
73
+
74
+ provider_config['zone']
75
+ end
76
+
77
+ def amisize(pool_name)
78
+ return pool_config(pool_name)['amisize'] if pool_config(pool_name)['amisize']
79
+
80
+ provider_config['amisize']
81
+ end
82
+
83
+ def volume_size(pool_name)
84
+ return pool_config(pool_name)['volume_size'] if pool_config(pool_name)['volume_size']
85
+
86
+ provider_config['volume_size']
87
+ end
88
+
89
+ # dns
90
+ def project
91
+ provider_config['project']
92
+ end
93
+
94
+ def domain
95
+ provider_config['domain']
96
+ end
97
+
98
+ def dns_zone_resource_name
99
+ provider_config['dns_zone_resource_name']
100
+ end
101
+
102
+ # subnets
103
+ def get_subnet_id(pool_name)
104
+ case zone(pool_name)
105
+ when 'us-west-2b'
106
+ 'subnet-0fe90a688844f6f26'
107
+ when 'us-west-2a'
108
+ 'subnet-091b436f'
109
+ end
110
+ end
111
+
112
+ def to_provision(pool_name)
113
+ pool_config(pool_name)['provision']
114
+ end
115
+
116
+ # Base methods that are implemented:
117
+
118
+ # vms_in_pool lists all the VM names in a pool, which is based on the VMs
119
+ # having a tag "pool" that match a pool config name.
120
+ # inputs
121
+ # [String] pool_name : Name of the pool
122
+ # returns
123
+ # empty array [] if no VMs found in the pool
124
+ # [Array]
125
+ # [Hashtable]
126
+ # [String] name : the name of the VM instance (unique for whole project)
127
+ def vms_in_pool(pool_name)
128
+ debug_logger('vms_in_pool')
129
+ vms = []
130
+ pool = pool_config(pool_name)
131
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
132
+
133
+ filters = [
134
+ {
135
+ name: 'tag:pool',
136
+ values: [pool_name]
137
+ },
138
+ {
139
+ name: 'instance-state-name',
140
+ values: %w[pending running shutting-down stopping stopped]
141
+ }
142
+ ]
143
+ instance_list = connection.instances(filters: filters)
144
+
145
+ return vms if instance_list.first.nil?
146
+
147
+ instance_list.each do |vm|
148
+ vms << { 'name' => vm.tags.detect { |f| f.key == 'vm_name' }&.value || 'vm_name not found in tags' }
149
+ end
150
+ debug_logger(vms)
151
+ vms
152
+ end
153
+
154
+ # inputs
155
+ # [String] pool_name : Name of the pool
156
+ # [String] vm_name : Name of the VM to find
157
+ # returns
158
+ # nil if VM doesn't exist name, template, poolname, boottime, status, image_size, private_ip_address
159
+ # [Hastable] of the VM
160
+ # [String] name : The name of the resource, provided by the client when initially creating the resource
161
+ # [String] template : This is the name of template
162
+ # [String] poolname : Name of the pool the VM
163
+ # [Time] boottime : Time when the VM was created/booted
164
+ # [String] status : One of the following values: pending, running, shutting-down, terminated, stopping, stopped
165
+ # [String] image_size : The EC2 image size eg a1.large
166
+ # [String] private_ip_address: The private IPv4 address
167
+ def get_vm(pool_name, vm_name)
168
+ debug_logger('get_vm')
169
+ vm_hash = nil
170
+
171
+ filters = [{
172
+ name: 'tag:vm_name',
173
+ values: [vm_name]
174
+ }]
175
+ instances = connection.instances(filters: filters).first
176
+ return vm_hash if instances.nil?
177
+
178
+ vm_hash = generate_vm_hash(instances, pool_name)
179
+ debug_logger("vm_hash #{vm_hash}")
180
+ vm_hash
181
+ end
182
+
183
+ # create_vm creates a new VM with a default network from the config,
184
+ # a initial disk named #{new_vmname}-disk0 that uses the 'template' as its source image
185
+ # and labels added for vm and pool
186
+ # and an instance configuration for machine_type from the config and
187
+ # labels vm and pool
188
+ # having a label "pool" that match a pool config name.
189
+ # inputs
190
+ # [String] pool : Name of the pool
191
+ # [String] new_vmname : Name to give the new VM
192
+ # returns
193
+ # [Hashtable] of the VM as per get_vm(pool_name, vm_name)
194
+ def create_vm(pool_name, new_vmname)
195
+ debug_logger('create_vm')
196
+ pool = pool_config(pool_name)
197
+ raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil?
198
+ raise("Instance creation not attempted, #{new_vmname} already exists") if get_vm(pool_name, new_vmname)
199
+
200
+ subnet_id = get_subnet_id(pool_name)
201
+ domain_set = domain
202
+ name_to_use = if domain_set.nil?
203
+ new_vmname
204
+ else
205
+ "#{new_vmname}.#{domain_set}"
206
+ end
207
+
208
+ tag = [
209
+ {
210
+ resource_type: 'instance', # accepts capacity-reservation, client-vpn-endpoint, customer-gateway, carrier-gateway, dedicated-host, dhcp-options, egress-only-internet-gateway, elastic-ip, elastic-gpu, export-image-task, export-instance-task, fleet, fpga-image, host-reservation, image, import-image-task, import-snapshot-task, instance, instance-event-window, internet-gateway, ipam, ipam-pool, ipam-scope, ipv4pool-ec2, ipv6pool-ec2, key-pair, launch-template, local-gateway, local-gateway-route-table, local-gateway-virtual-interface, local-gateway-virtual-interface-group, local-gateway-route-table-vpc-association, local-gateway-route-table-virtual-interface-group-association, natgateway, network-acl, network-interface, network-insights-analysis, network-insights-path, network-insights-access-scope, network-insights-access-scope-analysis, placement-group, prefix-list, replace-root-volume-task, reserved-instances, route-table, security-group, security-group-rule, snapshot, spot-fleet-request, spot-instances-request, subnet, subnet-cidr-reservation, traffic-mirror-filter, traffic-mirror-session, traffic-mirror-target, transit-gateway, transit-gateway-attachment, transit-gateway-connect-peer, transit-gateway-multicast-domain, transit-gateway-route-table, volume, vpc, vpc-endpoint, vpc-endpoint-service, vpc-peering-connection, vpn-connection, vpn-gateway, vpc-flow-log
211
+ tags: [
212
+ {
213
+ key: 'vm_name',
214
+ value: new_vmname
215
+ },
216
+ {
217
+ key: 'pool',
218
+ value: pool_name
219
+ },
220
+ {
221
+ key: 'lifetime', # required by AWS reaper
222
+ value: max_lifetime
223
+ },
224
+ {
225
+ key: 'created_by', # required by AWS reaper
226
+ value: get_current_user(new_vmname)
227
+ },
228
+ {
229
+ key: 'job_url',
230
+ value: get_current_job_url(new_vmname)
231
+ },
232
+ {
233
+ key: 'organization', # required by AWS reaper
234
+ value: 'engineering'
235
+ },
236
+ {
237
+ key: 'portfolio', # required by AWS reaper
238
+ value: 'ds-ci'
239
+ },
240
+ {
241
+ key: 'Name',
242
+ value: name_to_use
243
+ }
244
+ ]
245
+ }
246
+ ]
247
+ config = {
248
+ min_count: 1,
249
+ max_count: 1,
250
+ image_id: pool['template'],
251
+ monitoring: { enabled: true },
252
+ key_name: 'always-be-scheduling',
253
+ security_group_ids: ['sg-697fb015'],
254
+ instance_type: amisize(pool_name),
255
+ disable_api_termination: false,
256
+ instance_initiated_shutdown_behavior: 'terminate',
257
+ tag_specifications: tag,
258
+ subnet_id: subnet_id
259
+ }
260
+
261
+ config[:block_device_mappings] = get_block_device_mappings(config['image_id'], volume_size(pool_name)) if volume_size(pool_name)
262
+
263
+ debug_logger('trigger insert_instance')
264
+ batch_instance = connection.create_instances(config)
265
+ instance_id = batch_instance.first.instance_id
266
+ connection.client.wait_until(:instance_running, { instance_ids: [instance_id] })
267
+ @logger.log('s', "[>] [#{pool_name}] '#{new_vmname}' instance running")
268
+ created_instance = get_vm(pool_name, new_vmname)
269
+ dns_setup(created_instance) if domain
270
+
271
+ ### System status checks
272
+ # This check verifies that your instance is reachable. Amazon EC2 tests that network packets can get to your instance.
273
+ ### Instance status checks
274
+ # This check verifies that your instance's operating system is accepting traffic.
275
+ connection.client.wait_until(:instance_status_ok, { instance_ids: [instance_id] })
276
+ @logger.log('s', "[>] [#{pool_name}] '#{new_vmname}' instance ready to accept traffic")
277
+
278
+ @redis.with_metrics do |redis|
279
+ redis.hset("vmpooler__vm__#{new_vmname}", 'host', created_instance['private_dns_name'])
280
+ end
281
+
282
+ if domain
283
+ provision_node_aws(created_instance['name'], pool_name, new_vmname) if to_provision(pool_name) == 'true' || to_provision(pool_name) == true
284
+ elsif to_provision(pool_name) == 'true' || to_provision(pool_name) == true
285
+ provision_node_aws(created_instance['private_dns_name'], pool_name, new_vmname)
286
+ end
287
+
288
+ created_instance
289
+ end
290
+
291
+ def provision_node_aws(vm, pool_name, new_vmname)
292
+ aws_setup = AwsSetup.new(@logger, new_vmname)
293
+ aws_setup.setup_node_by_ssh(vm, pool_name)
294
+ end
295
+
296
+ def get_block_device_mappings(image_id, volume_size)
297
+ ec2_client = connection.client
298
+ image = ec2_client.describe_images(image_ids: [image_id]).images.first
299
+ raise "Image not found: #{image_id}" if image.nil?
300
+ raise "#{image_id} does not have an ebs root device type" unless image.root_device_type == 'ebs'
301
+
302
+ # Transform the images block_device_mappings output into a format
303
+ # ready for a create.
304
+ block_device_mappings = []
305
+ orig_bdm = image.block_device_mappings
306
+ orig_bdm.each do |block_device|
307
+ block_device_mappings << {
308
+ device_name: block_device.device_name,
309
+ ebs: {
310
+ # Change the default size of the root volume.
311
+ volume_size: volume_size,
312
+ # This is required to override the images default for
313
+ # delete_on_termination, forcing all volumes to be deleted once the
314
+ # instance is terminated.
315
+ delete_on_termination: true
316
+ }
317
+ }
318
+ end
319
+ block_device_mappings
320
+ end
321
+
322
+ # create_disk creates an additional disk for an existing VM. It will name the new
323
+ # disk #{vm_name}-disk#{number_disk} where number_disk is the next logical disk number
324
+ # starting with 1 when adding an additional disk to a VM with only the boot disk:
325
+ # #{vm_name}-disk0 == boot disk
326
+ # #{vm_name}-disk1 == additional disk added via create_disk
327
+ # #{vm_name}-disk2 == additional disk added via create_disk if run a second time etc
328
+ # the new disk has labels added for vm and pool
329
+ # The AWS lifecycle is to create a new disk (lives independently of the instance) then to attach
330
+ # it to the existing instance.
331
+ # inputs
332
+ # [String] pool_name : Name of the pool
333
+ # [String] vm_name : Name of the existing VM
334
+ # [String] disk_size : The new disk size in GB
335
+ # returns
336
+ # [boolean] true : once the operations are finished
337
+
338
+ # create_snapshot creates new snapshots with the unique name {new_snapshot_name}-#{disk.name}
339
+ # for one vm, and one create_snapshot() there could be multiple snapshots created, one for each drive.
340
+ # since the snapshot resource needs a unique name in the gce project,
341
+ # we create a unique name by concatenating {new_snapshot_name}-#{disk.name}
342
+ # the disk name is based on vm_name which makes it unique.
343
+ # The snapshot is added tags snapshot_name, vm, pool, diskname and boot
344
+ # inputs
345
+ # [String] pool_name : Name of the pool
346
+ # [String] vm_name : Name of the existing VM
347
+ # [String] new_snapshot_name : a unique name for this snapshot, which would be used to refer to it when reverting
348
+ # returns
349
+ # [boolean] true : once the operations are finished
350
+ # raises
351
+ # RuntimeError if the vm_name cannot be found
352
+ # RuntimeError if the snapshot_name already exists for this VM
353
+
354
+ # revert_snapshot reverts an existing VM's disks to an existing snapshot_name
355
+ # reverting in aws entails
356
+ # 1. shutting down the VM,
357
+ # 2. detaching and deleting the drives,
358
+ # 3. creating new disks with the same name from the snapshot for each disk
359
+ # 4. attach disks and start instance
360
+ # for one vm, there might be multiple snapshots in time. We select the ones referred to by the
361
+ # snapshot_name, but that may be multiple snapshots, one for each disks
362
+ # The new disk is added tags vm and pool
363
+ # inputs
364
+ # [String] pool_name : Name of the pool
365
+ # [String] vm_name : Name of the existing VM
366
+ # [String] snapshot_name : Name of an existing snapshot
367
+ # returns
368
+ # [boolean] true : once the operations are finished
369
+ # raises
370
+ # RuntimeError if the vm_name cannot be found
371
+ # RuntimeError if the snapshot_name already exists for this VM
372
+
373
+ # destroy_vm deletes an existing VM instance and any disks and snapshots via the labels
374
+ # in gce instances, disks and snapshots are resources that can exist independent of each other
375
+ # inputs
376
+ # [String] pool_name : Name of the pool
377
+ # [String] vm_name : Name of the existing VM
378
+ # returns
379
+ # [boolean] true : once the operations are finished
380
+ def destroy_vm(pool_name, vm_name)
381
+ debug_logger('destroy_vm')
382
+ deleted = false
383
+
384
+ filters = [{
385
+ name: 'tag:vm_name',
386
+ values: [vm_name]
387
+ }]
388
+ instances = connection.instances(filters: filters).first
389
+ return true if instances.nil?
390
+
391
+ instance_hash = get_vm(pool_name, vm_name)
392
+ debug_logger("trigger delete_instance #{vm_name}")
393
+ instances.terminate
394
+ begin
395
+ connection.client.wait_until(:instance_terminated, { instance_ids: [instances.id] })
396
+ deleted = true
397
+ rescue ::Aws::Waiters::Errors => e
398
+ debug_logger("failed waiting for instance terminated #{vm_name}: #{e}")
399
+ end
400
+
401
+ dns_teardown(instance_hash) if domain
402
+
403
+ deleted
404
+ end
405
+
406
+ # check if a vm is ready by opening a socket on port 22
407
+ # if a domain is set, it will use vn_name.domain,
408
+ # if not then it will use the private dns name directly (AWS workaround)
409
+ def vm_ready?(pool_name, vm_name)
410
+ begin
411
+ domain_set = domain
412
+ if domain_set.nil?
413
+ vm_ip = get_vm(pool_name, vm_name)['private_dns_name']
414
+ vm_name = vm_ip unless vm_ip.nil?
415
+ end
416
+ open_socket(vm_name, domain_set)
417
+ rescue StandardError => e
418
+ @logger.log('s', "[!] [#{pool_name}] '#{vm_name}' instance cannot be reached by vmpooler on tcp port 22; #{e}")
419
+ return false
420
+ end
421
+ true
422
+ end
423
+
424
+ # tag_vm_user This method is called once we know who is using the VM (it is running). This method enables seeing
425
+ # who is using what in the provider pools.
426
+ #
427
+ # inputs
428
+ # [String] pool_name : Name of the pool
429
+ # [String] vm_name : Name of the VM to check if ready
430
+ # returns
431
+ # [Boolean] : true if successful, false if an error occurred and it should retry
432
+ def tag_vm_user(pool, vm_name)
433
+ user = get_current_user(vm_name)
434
+ vm_hash = get_vm(pool, vm_name)
435
+ return false if vm_hash.nil?
436
+
437
+ filters = [{
438
+ name: 'tag:vm_name',
439
+ values: [vm_name]
440
+ }]
441
+ instances = connection.instances(filters: filters).first
442
+ return false if instances.nil?
443
+
444
+ # add new label called token-user, with value as user
445
+ instances.create_tags(tags: [key: 'token-user', value: user])
446
+ true
447
+ rescue StandardError => _e
448
+ false
449
+ end
450
+
451
+ # END BASE METHODS
452
+
453
+ def dns_setup(created_instance)
454
+ dns = Vmpooler::PoolManager::CloudDns.new(project, dns_zone_resource_name)
455
+ dns.dns_create_or_replace(created_instance)
456
+ end
457
+
458
+ def dns_teardown(created_instance)
459
+ dns = Vmpooler::PoolManager::CloudDns.new(project, dns_zone_resource_name)
460
+ dns.dns_teardown(created_instance)
461
+ end
462
+
463
+ def get_current_user(vm_name)
464
+ @redis.with_metrics do |redis|
465
+ user = redis.hget("vmpooler__vm__#{vm_name}", 'token:user')
466
+ return '' if user.nil?
467
+
468
+ # cleanup so it's a valid label value
469
+ # can't have upercase
470
+ user = user.downcase
471
+ # replace invalid chars with dash
472
+ user = user.gsub(/[^0-9a-z_-]/, '-')
473
+ return user
474
+ end
475
+ end
476
+
477
+ # returns lifetime in hours in the format Xh defaults to 1h
478
+ def get_current_lifetime(vm_name)
479
+ @redis.with_metrics do |redis|
480
+ lifetime = redis.hget("vmpooler__vm__#{vm_name}", 'lifetime') || '1'
481
+ return "#{lifetime}h"
482
+ end
483
+ end
484
+
485
+ # returns max_lifetime_upper_limit in hours in the format Xh defaults to 12h
486
+ def max_lifetime
487
+ max_hours = global_config[:config]['max_lifetime_upper_limit'] || '12'
488
+ "#{max_hours}h"
489
+ end
490
+
491
+ def get_current_job_url(vm_name)
492
+ @redis.with_metrics do |redis|
493
+ job = redis.hget("vmpooler__vm__#{vm_name}", 'tag:jenkins_build_url') || ''
494
+ return job
495
+ end
496
+ end
497
+
498
+ # Return a hash of VM data
499
+ # Provides name, template, poolname, boottime, status, image_size, private_ip_address
500
+ def generate_vm_hash(vm_object, pool_name)
501
+ pool_configuration = pool_config(pool_name)
502
+ return nil if pool_configuration.nil?
503
+
504
+ {
505
+ 'name' => vm_object.tags.detect { |f| f.key == 'Name' }&.value,
506
+ # 'hostname' => vm_object.hostname,
507
+ 'template' => pool_configuration&.key?('template') ? pool_configuration['template'] : nil, # was expecting to get it from API, not from config, but this is what vSphere does too!
508
+ 'poolname' => vm_object.tags.detect { |f| f.key == 'pool' }&.value,
509
+ 'boottime' => vm_object.launch_time,
510
+ 'status' => vm_object.state&.name, # One of the following values: pending, running, shutting-down, terminated, stopping, stopped
511
+ # 'zone' => vm_object.zone,
512
+ 'image_size' => vm_object.instance_type,
513
+ 'ip' => vm_object.private_ip_address, # used by the cloud dns class to set the record to this value
514
+ 'private_ip_address' => vm_object.private_ip_address,
515
+ 'private_dns_name' => vm_object.private_dns_name
516
+ }
517
+ end
518
+
519
+ def ensured_aws_connection(connection_pool_object)
520
+ connection_pool_object[:connection] = connect_to_aws unless connection_pool_object[:connection]
521
+ connection_pool_object[:connection]
522
+ end
523
+
524
+ def connect_to_aws
525
+ max_tries = global_config[:config]['max_tries'] || 3
526
+ retry_factor = global_config[:config]['retry_factor'] || 10
527
+ try = 1
528
+ begin
529
+ compute = ::Aws::EC2::Resource.new(
530
+ region: region,
531
+ credentials: ::Aws::Credentials.new(@aws_access_key, @aws_secret_key),
532
+ log_level: :debug
533
+ )
534
+
535
+ metrics.increment('connect.open')
536
+ compute
537
+ rescue StandardError => e # is that even a thing?
538
+ metrics.increment('connect.fail')
539
+ raise e if try >= max_tries
540
+
541
+ sleep(try * retry_factor)
542
+ try += 1
543
+ retry
544
+ end
545
+ end
546
+
547
+ # This should supercede the open_socket method in the Pool Manager
548
+ def open_socket(host, domain = nil, timeout = 5, port = 22, &_block)
549
+ Timeout.timeout(timeout) do
550
+ target_host = host
551
+ target_host = "#{host}.#{domain}" if domain
552
+ sock = TCPSocket.new target_host, port
553
+ begin
554
+ yield sock if block_given?
555
+ ensure
556
+ sock.close
557
+ end
558
+ end
559
+ end
560
+
561
+ # used in local dev environment, set DEBUG_FLAG=true
562
+ # this way the upstream vmpooler manager does not get polluted with logs
563
+ def debug_logger(message, send_to_upstream: false)
564
+ # the default logger is simple and does not enforce debug levels (the first argument)
565
+ puts message if ENV['DEBUG_FLAG']
566
+ @logger.log('[g]', message) if send_to_upstream
567
+ end
568
+ end
569
+ end
570
+ end
571
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VmpoolerProviderEc2
4
+ VERSION = '0.0.2'
5
+ end
metadata ADDED
@@ -0,0 +1,252 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vmpooler-provider-ec2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Puppet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-08-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1'
19
+ name: aws-sdk-ec2
20
+ prerelease: false
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.2'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '7.1'
36
+ name: net-ssh
37
+ prerelease: false
38
+ type: :runtime
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '6.2'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.1'
47
+ - !ruby/object:Gem::Dependency
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.3.0
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.3'
56
+ name: vmpooler
57
+ prerelease: false
58
+ type: :development
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.3.0
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.3'
67
+ - !ruby/object:Gem::Dependency
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 0.4.0
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.4'
76
+ name: vmpooler-provider-gce
77
+ prerelease: false
78
+ type: :development
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.4.0
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '0.4'
87
+ - !ruby/object:Gem::Dependency
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 0.2.0
93
+ name: climate_control
94
+ prerelease: false
95
+ type: :development
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 0.2.0
101
+ - !ruby/object:Gem::Dependency
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 0.17.0
107
+ name: mock_redis
108
+ prerelease: false
109
+ type: :development
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 0.17.0
115
+ - !ruby/object:Gem::Dependency
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ name: pry
122
+ prerelease: false
123
+ type: :development
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ - !ruby/object:Gem::Dependency
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0.6'
135
+ name: rack-test
136
+ prerelease: false
137
+ type: :development
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0.6'
143
+ - !ruby/object:Gem::Dependency
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '3.2'
149
+ name: rspec
150
+ prerelease: false
151
+ type: :development
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '3.2'
157
+ - !ruby/object:Gem::Dependency
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - "~>"
161
+ - !ruby/object:Gem::Version
162
+ version: 1.28.2
163
+ name: rubocop
164
+ prerelease: false
165
+ type: :development
166
+ version_requirements: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - "~>"
169
+ - !ruby/object:Gem::Version
170
+ version: 1.28.2
171
+ - !ruby/object:Gem::Dependency
172
+ requirement: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: 0.11.2
177
+ name: simplecov
178
+ prerelease: false
179
+ type: :development
180
+ version_requirements: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: 0.11.2
185
+ - !ruby/object:Gem::Dependency
186
+ requirement: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - "~>"
189
+ - !ruby/object:Gem::Version
190
+ version: '1.0'
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: 1.0.1
194
+ name: thor
195
+ prerelease: false
196
+ type: :development
197
+ version_requirements: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '1.0'
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: 1.0.1
205
+ - !ruby/object:Gem::Dependency
206
+ requirement: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '2.0'
211
+ name: yarjuf
212
+ prerelease: false
213
+ type: :development
214
+ version_requirements: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ version: '2.0'
219
+ description:
220
+ email:
221
+ - support@puppet.com
222
+ executables: []
223
+ extensions: []
224
+ extra_rdoc_files: []
225
+ files:
226
+ - lib/vmpooler-provider-ec2/version.rb
227
+ - lib/vmpooler/aws_setup.rb
228
+ - lib/vmpooler/providers/ec2.rb
229
+ homepage: https://github.com/puppetlabs/vmpooler-provider-ec2
230
+ licenses:
231
+ - Apache-2.0
232
+ metadata: {}
233
+ post_install_message:
234
+ rdoc_options: []
235
+ require_paths:
236
+ - lib
237
+ required_ruby_version: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: 2.3.0
242
+ required_rubygems_version: !ruby/object:Gem::Requirement
243
+ requirements:
244
+ - - ">="
245
+ - !ruby/object:Gem::Version
246
+ version: '0'
247
+ requirements: []
248
+ rubygems_version: 3.2.29
249
+ signing_key:
250
+ specification_version: 4
251
+ summary: EC2 provider for VMPooler
252
+ test_files: []