vmpooler-provider-ec2 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []