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 +7 -0
- data/lib/vmpooler/aws_setup.rb +126 -0
- data/lib/vmpooler/providers/ec2.rb +571 -0
- data/lib/vmpooler-provider-ec2/version.rb +5 -0
- metadata +252 -0
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
|
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: []
|