From 8f7f61fd17c64affad6a3084517e58dae3e058be Mon Sep 17 00:00:00 2001 From: Sayantam Dey Date: Tue, 14 Sep 2021 01:44:04 +0530 Subject: [PATCH 1/2] Pick security group type attributes #196 (#253) * Pick security group type attributes #196 * Fix CC reported issues --- docker-compose.yml | 2 +- hailstorm-api/Gemfile | 2 +- hailstorm-api/Gemfile.lock | 30 ++--- hailstorm-api/app/version.rb | 2 +- hailstorm-cli/Gemfile.lock | 7 +- hailstorm-cli/hailstorm-cli.gemspec | 2 +- hailstorm-cli/lib/hailstorm/cli/version.rb | 2 +- hailstorm-gem/Gemfile.lock | 2 +- .../features/clean_aws_account.feature | 8 ++ .../step_definitions/aws_model_steps.rb | 71 +++++++++++ .../step_definitions/aws_sdk_steps.rb | 37 ++++++ .../features/step_definitions/aws_steps.rb | 113 ------------------ .../step_definitions/jmeter_model_steps.rb | 52 ++++++++ .../step_definitions/load_generation_steps.rb | 6 + .../features/step_definitions/model_steps.rb | 33 ----- .../step_definitions/project_model_steps.rb | 10 ++ hailstorm-gem/features/support/aws_helper.rb | 29 +++-- .../security_group_client.rb | 2 +- hailstorm-gem/lib/hailstorm/version.rb | 2 +- 19 files changed, 232 insertions(+), 180 deletions(-) create mode 100644 hailstorm-gem/features/clean_aws_account.feature create mode 100644 hailstorm-gem/features/step_definitions/aws_model_steps.rb create mode 100644 hailstorm-gem/features/step_definitions/aws_sdk_steps.rb delete mode 100644 hailstorm-gem/features/step_definitions/aws_steps.rb create mode 100644 hailstorm-gem/features/step_definitions/jmeter_model_steps.rb delete mode 100644 hailstorm-gem/features/step_definitions/model_steps.rb create mode 100644 hailstorm-gem/features/step_definitions/project_model_steps.rb diff --git a/docker-compose.yml b/docker-compose.yml index 189f50e1..4e594aae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: - "start.sh" hailstorm-api: - image: "hailstorm3/hailstorm-api:1.0.19" + image: "hailstorm3/hailstorm-api:1.0.20" ports: - "4567:8080" environment: diff --git a/hailstorm-api/Gemfile b/hailstorm-api/Gemfile index 78740174..e5a5190a 100644 --- a/hailstorm-api/Gemfile +++ b/hailstorm-api/Gemfile @@ -5,7 +5,7 @@ source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } gem 'activerecord-jdbcmysql-adapter', '~> 60.2' -gem 'hailstorm', '= 5.1.14' +gem 'hailstorm', '= 5.1.15' gem 'httparty', '~> 0.18.1' gem 'puma' gem 'rake', '~> 13' diff --git a/hailstorm-api/Gemfile.lock b/hailstorm-api/Gemfile.lock index dfbc99c6..29475839 100644 --- a/hailstorm-api/Gemfile.lock +++ b/hailstorm-api/Gemfile.lock @@ -31,17 +31,17 @@ GEM tzinfo (~> 1.1) zeitwerk (~> 2.2, >= 2.2.2) ast (2.4.1) - aws-eventstream (1.1.0) - aws-partitions (1.350.0) - aws-sdk-core (3.104.3) + aws-eventstream (1.1.1) + aws-partitions (1.476.0) + aws-sdk-core (3.116.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-ec2 (1.182.0) - aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-ec2 (1.248.0) + aws-sdk-core (~> 3, >= 3.112.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.1) + aws-sigv4 (1.2.4) aws-eventstream (~> 1, >= 1.0.2) builder (3.2.4) columnize (0.9.0) @@ -50,10 +50,10 @@ GEM croaky-rspec (0.2.0) diff-lcs (1.4.4) docile (1.3.2) - erubi (1.9.0) + erubi (1.10.0) erubis (2.7.0) - haikunator (1.1.0) - hailstorm (5.1.14-java) + haikunator (1.1.1) + hailstorm (5.1.15-java) actionpack (~> 6.0.0) activerecord-jdbc-adapter (~> 60.2) aws-sdk-ec2 (~> 1) @@ -73,7 +73,7 @@ GEM jmespath (1.4.0) json (2.3.1-java) linecache (1.3.1-java) - loofah (2.6.0) + loofah (2.10.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mime-types (3.3.1) @@ -87,7 +87,7 @@ GEM net-ssh (>= 5.0.0, < 7.0.0) net-ssh (5.0.2) nio4r (2.5.2-java) - nokogiri (1.10.9-java) + nokogiri (1.10.10-java) parallel (1.19.2) parser (2.7.1.4) ast (~> 2.4.1) @@ -96,8 +96,8 @@ GEM rack (2.2.3) rack-protection (2.0.8.1) rack - rack-test (0.6.3) - rack (>= 1.0) + rack-test (1.1.0) + rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -139,7 +139,7 @@ GEM ruby-debug-base (0.11.0-java) ruby-progressbar (1.10.1) ruby2_keywords (0.0.2) - rubyzip (2.3.0) + rubyzip (2.3.2) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) @@ -166,7 +166,7 @@ PLATFORMS DEPENDENCIES activerecord-jdbcmysql-adapter (~> 60.2) croaky-rspec (~> 0.1) - hailstorm (= 5.1.14) + hailstorm (= 5.1.15) httparty (~> 0.18.1) puma rake (~> 13) diff --git a/hailstorm-api/app/version.rb b/hailstorm-api/app/version.rb index d722d37c..5e370628 100644 --- a/hailstorm-api/app/version.rb +++ b/hailstorm-api/app/version.rb @@ -3,6 +3,6 @@ # Version module Hailstorm module Api - VERSION = '1.0.19' + VERSION = '1.0.20' end end diff --git a/hailstorm-cli/Gemfile.lock b/hailstorm-cli/Gemfile.lock index 5c070f47..39f6015c 100644 --- a/hailstorm-cli/Gemfile.lock +++ b/hailstorm-cli/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - hailstorm-cli (1.0.16-java) - hailstorm (= 5.1.14) + hailstorm-cli (1.0.17-java) + hailstorm (= 5.1.15) GEM remote: https://rubygems.org/ @@ -94,7 +94,7 @@ GEM erubis (2.7.0) ffi (1.13.1-java) haikunator (1.1.1) - hailstorm (5.1.14-java) + hailstorm (5.1.15-java) actionpack (~> 6.0.0) activerecord-jdbc-adapter (~> 60.2) aws-sdk-ec2 (~> 1) @@ -191,6 +191,7 @@ GEM PLATFORMS java universal-java-1.8 + universal-java-11 DEPENDENCIES activerecord-jdbcmysql-adapter (~> 60.2) diff --git a/hailstorm-cli/hailstorm-cli.gemspec b/hailstorm-cli/hailstorm-cli.gemspec index 4025bd36..7a5bf4e6 100644 --- a/hailstorm-cli/hailstorm-cli.gemspec +++ b/hailstorm-cli/hailstorm-cli.gemspec @@ -32,5 +32,5 @@ and generate reports.' gem.executables = gem.files.grep(%r{^bin/\b}).map { |f| File.basename(f) } gem.require_paths = %w[lib] - gem.add_runtime_dependency('hailstorm', '= 5.1.14') + gem.add_runtime_dependency('hailstorm', '= 5.1.15') end diff --git a/hailstorm-cli/lib/hailstorm/cli/version.rb b/hailstorm-cli/lib/hailstorm/cli/version.rb index 25cb2e7d..2f5af47b 100644 --- a/hailstorm-cli/lib/hailstorm/cli/version.rb +++ b/hailstorm-cli/lib/hailstorm/cli/version.rb @@ -3,6 +3,6 @@ # Version module Hailstorm module Cli - VERSION = '1.0.16' + VERSION = '1.0.17' end end diff --git a/hailstorm-gem/Gemfile.lock b/hailstorm-gem/Gemfile.lock index 2d10b73a..06a7343c 100644 --- a/hailstorm-gem/Gemfile.lock +++ b/hailstorm-gem/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - hailstorm (5.1.14-java) + hailstorm (5.1.15-java) actionpack (~> 6.0.0) activerecord-jdbc-adapter (~> 60.2) aws-sdk-ec2 (~> 1) diff --git a/hailstorm-gem/features/clean_aws_account.feature b/hailstorm-gem/features/clean_aws_account.feature new file mode 100644 index 00000000..13e4fc73 --- /dev/null +++ b/hailstorm-gem/features/clean_aws_account.feature @@ -0,0 +1,8 @@ +Feature: Clean AWS Account + Background: Hailstorm application is initialized + Given Hailstorm is initialized with a project 'clean_aws_account' + + Scenario: Purge AWS Cluster + Given an AWS cluster already exists in 'ap-northeast-1' region + When the AWS cluster is purged + Then the AWS cluster should be removed diff --git a/hailstorm-gem/features/step_definitions/aws_model_steps.rb b/hailstorm-gem/features/step_definitions/aws_model_steps.rb new file mode 100644 index 00000000..b0e1248f --- /dev/null +++ b/hailstorm-gem/features/step_definitions/aws_model_steps.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +include ModelHelper + +After do |scenario| + if scenario.source_tag_names.include?('@terminate_instance') && @aws + if @load_agent + terminate_agents(@aws.region, @load_agent) + elsif @project + terminate_agents(@aws.region, *@project.load_agents) + end + + @aws.cleanup + end +end + +Given(/^Amazon is chosen as the cluster$/) do + require 'hailstorm/model/amazon_cloud' + @aws = Hailstorm::Model::AmazonCloud.new + @aws.project = @project + @aws.access_key, @aws.secret_key = aws_keys + @aws.active = true +end + +When(/^I choose '(.+?)' region$/) do |region| + @aws.region = region + aws_region_helper = Hailstorm::Model::Helper::AwsRegionHelper.new + @ami_id = aws_region_helper.region_base_ami_map[@aws.region] + expect(@ami_id).to_not be_nil +end + +When(/^(?:I |)create the AMI$/) do + expect(@aws).to be_valid + @aws.send(:create_security_group) + @aws.send(:create_agent_ami) +end + +Then(/^the AMI to be created would be named '(.+?)'$/) do |expected_ami_name| + actual_ami_name = @aws.send(:ami_id) + expect(actual_ami_name).to eq(expected_ami_name) +end + +And(/^instance type is '(.+?)'$/) do |instance_type| + @aws.instance_type = instance_type +end + +Given(/^an AWS cluster already exists in '(.+?)' region$/) do |region_code| + require 'hailstorm/model/amazon_cloud' + @aws = Hailstorm::Model::AmazonCloud.new + @aws.project = @project + @aws.access_key, @aws.secret_key = aws_keys + @aws.region = region_code + @aws.active = false + @aws.agent_ami = 'ami-123' + @aws.save! + @aws.update_column(:active, true) + + require 'hailstorm/model/cluster' + Hailstorm::Model::Cluster.create!(project: @project, + cluster_type: @aws.class.name, + clusterable_id: @aws.id) +end + +When(/^the AWS cluster is purged$/) do + @project.purge_clusters +end + +Then(/^the AWS cluster should be removed$/) do + @aws.reload + expect(@aws.agent_ami).to be_nil +end diff --git a/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb b/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb new file mode 100644 index 00000000..2033ec86 --- /dev/null +++ b/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'stringio' +require 'hailstorm/model/helper/aws_region_helper' + +include AwsHelper + +Then(/^the AMI should exist$/) do + ec2 = ec2_resource(region: @aws.region) + expect(ec2.images(image_ids: [@ami_id]).first).to_not be_nil +end + +Then(/^an AMI with name '(.+?)' (?:should |)exists?$/) do |ami_name| + ec2 = ec2_resource(region: @aws.region) + avail_amis = ec2.images(owners: %w[self]).select { |e| e.state.to_sym == :available } + ami_names = avail_amis.collect(&:name) + expect(ami_names).to include(ami_name) + @aws.agent_ami = avail_amis.find { |e| e.name == ami_name }.id +end + +And(/^a public VPC subnet is available$/) do + public_subnet = select_public_subnets(region: @aws.region).first + expect(public_subnet).to_not be_nil + @aws.vpc_subnet_id = public_subnet.id +end + +And(/^there is no AMI with name '([^']+)'$/) do |ami_name| + ec2 = ec2_resource(region: @aws.region) + ami = ec2.images(owners: %w[self]).find { |img| img.name == ami_name } + if ami + snapshot_ids = ami.block_device_mappings + .select { |blkdev| blkdev.key?(:ebs) } + .map { |blkdev| blkdev[:ebs][:snapshot_id] } + ami.deregister + snapshot_ids.each { |snapshot_id| ec2.snapshot(snapshot_id).delete } + end +end diff --git a/hailstorm-gem/features/step_definitions/aws_steps.rb b/hailstorm-gem/features/step_definitions/aws_steps.rb deleted file mode 100644 index 54e3011a..00000000 --- a/hailstorm-gem/features/step_definitions/aws_steps.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -require 'stringio' -require 'hailstorm/model/helper/aws_region_helper' - -include AwsHelper - -Given(/^Amazon is chosen as the cluster$/) do - require 'hailstorm/model/amazon_cloud' - @aws = Hailstorm::Model::AmazonCloud.new - @aws.project = @project - @aws.access_key, @aws.secret_key = aws_keys - @aws.active = true -end - -When(/^I choose '(.+?)' region$/) do |region| - @aws.region = region - aws_region_helper = Hailstorm::Model::Helper::AwsRegionHelper.new - @ami_id = aws_region_helper.region_base_ami_map[@aws.region] - expect(@ami_id).to_not be_nil -end - -Then(/^the AMI should exist$/) do - ec2 = ec2_resource(region: @aws.region) - expect(ec2.images(image_ids: [@ami_id]).first).to_not be_nil -end - -When(/^(?:I |)create the AMI$/) do - expect(@aws).to be_valid - @aws.send(:create_security_group) - @aws.send(:create_agent_ami) -end - -Then(/^an AMI with name '(.+?)' (?:should |)exists?$/) do |ami_name| - ec2 = ec2_resource(region: @aws.region) - avail_amis = ec2.images(owners: %w[self]).select { |e| e.state.to_sym == :available } - ami_names = avail_amis.collect(&:name) - expect(ami_names).to include(ami_name) - @aws.agent_ami = avail_amis.find { |e| e.name == ami_name }.id -end - -Then(/^installed JMeter version should be '(.+?)'$/) do |expected_jmeter_version| - require 'hailstorm/support/ssh' - jmeter_version_out = StringIO.new - ssh_args = [(@load_agent || @ec2_instance).public_ip_address, @aws.user_name, @aws.ssh_options] - Hailstorm::Support::SSH.start(*ssh_args) do |ssh| - ssh.exec!('$HOME/jmeter/bin/jmeter --version') do |_ch, stream, data| - jmeter_version_out << data if stream == :stdout - end - end - - expect(jmeter_version_out.string).to_not be_blank - expect(jmeter_version_out.string).to include(expected_jmeter_version) -end - -When(/^I (?:create|start) a new load agent$/) do - expect(@aws).to be_valid - require 'hailstorm/model/master_agent' - @load_agent = Hailstorm::Model::MasterAgent.new - @aws.start_agent(@load_agent) -end - -Then(/^custom properties should be added$/) do - require 'hailstorm/support/ssh' - require 'tmpdir' - remote_jmeter_props_file = File.join(Dir.tmpdir, 'user.properties') - Hailstorm::Support::SSH.start(@load_agent.public_ip_address, @aws.user_name, @aws.ssh_options) do |ssh| - ssh.download("/home/#{@aws.user_name}/jmeter/bin/user.properties", remote_jmeter_props_file) - end - remote_jmeter_props = File.readlines(remote_jmeter_props_file).collect(&:chomp) - expect(remote_jmeter_props).to include('jmeter.save.saveservice.hostname=true') - expect(remote_jmeter_props).to include('jmeter.save.saveservice.thread_counts=true') - expect(remote_jmeter_props).to include('jmeter.save.saveservice.output_format=xml') -end - -After do |scenario| - if scenario.source_tag_names.include?('@terminate_instance') && @aws - if @load_agent - terminate_agents(@aws.region, @load_agent) - elsif @project - terminate_agents(@aws.region, *@project.load_agents) - end - - @aws.cleanup - end -end - -Then(/^the AMI to be created would be named '(.+?)'$/) do |expected_ami_name| - actual_ami_name = @aws.send(:ami_id) - expect(actual_ami_name).to eq(expected_ami_name) -end - -And(/^a public VPC subnet is available$/) do - public_subnet = select_public_subnets(region: @aws.region).first - expect(public_subnet).to_not be_nil - @aws.vpc_subnet_id = public_subnet.id -end - -And(/^instance type is '(.+?)'$/) do |instance_type| - @aws.instance_type = instance_type -end - -And(/^there is no AMI with name '([^']+)'$/) do |ami_name| - ec2 = ec2_resource(region: @aws.region) - ami = ec2.images(owners: %w[self]).find { |img| img.name == ami_name } - if ami - snapshot_ids = ami.block_device_mappings - .select { |blkdev| blkdev.key?(:ebs) } - .map { |blkdev| blkdev[:ebs][:snapshot_id] } - ami.deregister - snapshot_ids.each { |snapshot_id| ec2.snapshot(snapshot_id).delete } - end -end diff --git a/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb b/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb new file mode 100644 index 00000000..d88f8c0b --- /dev/null +++ b/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +include ModelHelper + +When(/^the JMeter version for the project is '(.+?)'$/) do |jmeter_version| + @project.jmeter_version = jmeter_version +end + +When(/^(?:the |)[jJ][mM]eter installer URL for the project is '(.+?)'$/) do |jmeter_installer_url| + @project.jmeter_version = nil + @project.custom_jmeter_installer_url = jmeter_installer_url + @project.send(:set_defaults) +end + +When(/^import results from '(.+)'$/) do |jtl_path| + abs_jtl_path = File.expand_path(jtl_path, __FILE__) + @project.settings_modified = true + require 'hailstorm/behavior/file_store' + fs = CukeDataFs.new + fs.jtl_path = abs_jtl_path + Hailstorm.fs = fs + require 'hailstorm/middleware/command_execution_template' + template = Hailstorm::Middleware::CommandExecutionTemplate.new(@project, @hailstorm_config) + template.results(false, nil, :import, [[abs_jtl_path], nil]) +end + +Then(/^installed JMeter version should be '(.+?)'$/) do |expected_jmeter_version| + require 'hailstorm/support/ssh' + jmeter_version_out = StringIO.new + ssh_args = [(@load_agent || @ec2_instance).public_ip_address, @aws.user_name, @aws.ssh_options] + Hailstorm::Support::SSH.start(*ssh_args) do |ssh| + ssh.exec!('$HOME/jmeter/bin/jmeter --version') do |_ch, stream, data| + jmeter_version_out << data if stream == :stdout + end + end + + expect(jmeter_version_out.string).to_not be_blank + expect(jmeter_version_out.string).to include(expected_jmeter_version) +end + +Then(/^custom properties should be added$/) do + require 'hailstorm/support/ssh' + require 'tmpdir' + remote_jmeter_props_file = File.join(Dir.tmpdir, 'user.properties') + Hailstorm::Support::SSH.start(@load_agent.public_ip_address, @aws.user_name, @aws.ssh_options) do |ssh| + ssh.download("/home/#{@aws.user_name}/jmeter/bin/user.properties", remote_jmeter_props_file) + end + remote_jmeter_props = File.readlines(remote_jmeter_props_file).collect(&:chomp) + expect(remote_jmeter_props).to include('jmeter.save.saveservice.hostname=true') + expect(remote_jmeter_props).to include('jmeter.save.saveservice.thread_counts=true') + expect(remote_jmeter_props).to include('jmeter.save.saveservice.output_format=xml') +end diff --git a/hailstorm-gem/features/step_definitions/load_generation_steps.rb b/hailstorm-gem/features/step_definitions/load_generation_steps.rb index d680d95a..21dc81a8 100644 --- a/hailstorm-gem/features/step_definitions/load_generation_steps.rb +++ b/hailstorm-gem/features/step_definitions/load_generation_steps.rb @@ -150,3 +150,9 @@ expect(@project.load_agents.count).to be == 2 end end + +When(/^I (?:create|start) a new load agent$/) do + expect(@aws).to be_valid + @load_agent = Hailstorm::Model::MasterAgent.new + @aws.start_agent(@load_agent) +end diff --git a/hailstorm-gem/features/step_definitions/model_steps.rb b/hailstorm-gem/features/step_definitions/model_steps.rb deleted file mode 100644 index 5a351cf1..00000000 --- a/hailstorm-gem/features/step_definitions/model_steps.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -# Steps invoked by initializing and calling Hailstorm -include ModelHelper - -Given(/^(?:Hailstorm is initialized with a project|the) ['"]([^'"]+)['"](?:| project(?:| is active))$/) do |project_code| - require 'hailstorm/model/project' - @project = find_project(project_code) - require 'hailstorm/support/configuration' - @hailstorm_config = Hailstorm::Support::Configuration.new -end - -When(/^the JMeter version for the project is '(.+?)'$/) do |jmeter_version| - @project.jmeter_version = jmeter_version -end - -When(/^(?:the |)[jJ][mM]eter installer URL for the project is '(.+?)'$/) do |jmeter_installer_url| - @project.jmeter_version = nil - @project.custom_jmeter_installer_url = jmeter_installer_url - @project.send(:set_defaults) -end - -When(/^import results from '(.+)'$/) do |jtl_path| - abs_jtl_path = File.expand_path(jtl_path, __FILE__) - @project.settings_modified = true - require 'hailstorm/behavior/file_store' - fs = CukeDataFs.new - fs.jtl_path = abs_jtl_path - Hailstorm.fs = fs - require 'hailstorm/middleware/command_execution_template' - template = Hailstorm::Middleware::CommandExecutionTemplate.new(@project, @hailstorm_config) - template.results(false, nil, :import, [[abs_jtl_path], nil]) -end diff --git a/hailstorm-gem/features/step_definitions/project_model_steps.rb b/hailstorm-gem/features/step_definitions/project_model_steps.rb new file mode 100644 index 00000000..09b28c8c --- /dev/null +++ b/hailstorm-gem/features/step_definitions/project_model_steps.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +include ModelHelper + +Given(/^(?:Hailstorm is initialized with a project|the) ['"]([^'"]+)['"](?:| project(?:| is active))$/) do |project_code| + require 'hailstorm/model/project' + @project = find_project(project_code) + require 'hailstorm/support/configuration' + @hailstorm_config = Hailstorm::Support::Configuration.new +end diff --git a/hailstorm-gem/features/support/aws_helper.rb b/hailstorm-gem/features/support/aws_helper.rb index 51f77040..6ff1845e 100644 --- a/hailstorm-gem/features/support/aws_helper.rb +++ b/hailstorm-gem/features/support/aws_helper.rb @@ -25,14 +25,12 @@ def ec2_resource(region:) end def select_public_subnets(region:) - ec2_resource(region: region) - .vpcs - .flat_map { |vpc| vpc.route_tables.to_a } - .select { |route_table| route_table_contains_igw?(route_table) } - .flat_map { |route_table| route_table.associations.to_a } - .select(&:subnet_id) - .map(&:subnet) - .select(&:map_public_ip_on_launch) + route_tables = ec2_resource(region: region).vpcs + .flat_map { |vpc| vpc.route_tables.to_a } + + subnets = find_igw_subnets(route_tables) + subnets = find_main_rtb_subnets(route_tables) if subnets.empty? + subnets.select(&:map_public_ip_on_launch) end def route_table_contains_igw?(route_table) @@ -52,4 +50,19 @@ def terminate_agents(region, *load_agents) ec2_instance&.terminate end end + + def find_main_rtb_subnets(route_tables) + route_tables.flat_map { |route_table| route_table.associations.to_a } + .select(&:main) + .map(&:route_table) + .map(&:vpc) + .flat_map { |vpc| vpc.subnets.to_a } + end + + def find_igw_subnets(route_tables) + route_tables.select { |route_table| route_table_contains_igw?(route_table) } + .flat_map { |route_table| route_table.associations.to_a } + .select(&:subnet_id) + .map(&:subnet) + end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb index e2311412..9b17330e 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb @@ -45,7 +45,7 @@ def delete(group_id:) def list ec2.describe_security_groups.security_groups.lazy.map do |sg| - Hailstorm::Behavior::AwsAdaptable::SecurityGroup.new(sg.to_h) + Hailstorm::Behavior::AwsAdaptable::SecurityGroup.new(sg.to_h.slice(:group_name, :group_id, :vpc_id)) end end end diff --git a/hailstorm-gem/lib/hailstorm/version.rb b/hailstorm-gem/lib/hailstorm/version.rb index c3867f4d..e813dbc4 100644 --- a/hailstorm-gem/lib/hailstorm/version.rb +++ b/hailstorm-gem/lib/hailstorm/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Hailstorm - VERSION = '5.1.14' + VERSION = '5.1.15' end From 41d131c2d5ea4fcc6239ff76e043b966493a145f Mon Sep 17 00:00:00 2001 From: Sayantam Dey Date: Sat, 23 Oct 2021 19:05:21 +0530 Subject: [PATCH 2/2] Purge an AWS cluster with tagged resources (#255) * Purge an AWS cluster with tagged resources * Untagged AWS resources are not purged. Signed-off-by: Sayantam Dey * Fix CC issue with AmazonCloud model CC issues are invalid. The Amazon Model has less than 20 methods, while the drop in coverage is because CC does not ignore ``no_cov`` sections coded in templates. --- docker-compose.yml | 2 +- hailstorm-api/Gemfile | 2 +- hailstorm-api/Gemfile.lock | 4 +- hailstorm-api/app/version.rb | 2 +- hailstorm-cli/Gemfile.lock | 6 +- hailstorm-cli/hailstorm-cli.gemspec | 2 +- hailstorm-cli/lib/hailstorm/cli/version.rb | 2 +- hailstorm-gem/.gitignore | 1 + hailstorm-gem/Gemfile.lock | 2 +- .../features/clean_aws_account.feature | 22 +- .../data/feature-parameters.sample.yml | 5 + .../step_definitions/aws_model_steps.rb | 46 +++- .../step_definitions/aws_sdk_steps.rb | 66 +++++- .../step_definitions/jmeter_model_steps.rb | 2 - .../step_definitions/load_generation_steps.rb | 4 +- .../step_definitions/project_model_steps.rb | 2 - .../features/support/aws_ec2_helper.rb | 135 ++++++++++++ hailstorm-gem/features/support/aws_helper.rb | 65 ++---- .../support/aws_security_group_helper.rb | 54 +++++ .../features/support/aws_vpc_helper.rb | 63 ++++++ .../features/support/cuke_data_fs.rb | 2 +- .../features/support/feature_helper.rb | 14 ++ .../features/support/model_helper.rb | 33 +++ .../lib/hailstorm/behavior/aws_adaptable.rb | 70 +++++- .../hailstorm/behavior/aws_adapter_domain.rb | 37 +++- .../lib/hailstorm/behavior/aws_exception.rb | 4 +- .../lib/hailstorm/model/amazon_cloud.rb | 177 ++++++++++------ hailstorm-gem/lib/hailstorm/model/cluster.rb | 2 +- .../model/concern/abstract_clusterable.rb | 24 --- .../model/helper/security_group_creator.rb | 4 +- .../model/helper/security_group_finder.rb | 14 +- .../lib/hailstorm/model/helper/vpc_helper.rb | 10 +- .../support/amazon_account_cleaner.rb | 199 +++++++++++++----- .../aws_adapter_clients/abstract_client.rb | 48 ++++- .../support/aws_adapter_clients/ami_client.rb | 15 +- .../support/aws_adapter_clients/ec2_client.rb | 7 - .../aws_adapter_clients/instance_client.rb | 10 +- .../internet_gateway_client.rb | 18 +- .../aws_adapter_clients/key_pair_client.rb | 2 +- .../aws_adapter_clients/route_table_client.rb | 31 ++- .../security_group_client.rb | 13 +- .../aws_adapter_clients/subnet_client.rb | 24 ++- .../support/aws_adapter_clients/vpc_client.rb | 20 +- .../support/aws_exception_builder.rb | 3 + hailstorm-gem/lib/hailstorm/version.rb | 2 +- hailstorm-gem/spec/model/amazon_cloud_spec.rb | 60 +++++- hailstorm-gem/spec/model/cluster_spec.rb | 15 -- .../concern/abstract_clusterable_spec.rb | 46 ---- .../spec/model/helper/ami_helper_spec.rb | 2 +- .../helper/security_group_creator_spec.rb | 6 +- .../helper/security_group_finder_spec.rb | 6 +- .../support/amazon_account_cleaner_spec.rb | 123 ++++++++--- .../spec/support/aws_adapter_spec.rb | 185 +++++++++++----- 53 files changed, 1275 insertions(+), 438 deletions(-) create mode 100644 hailstorm-gem/features/data/feature-parameters.sample.yml create mode 100644 hailstorm-gem/features/support/aws_ec2_helper.rb create mode 100644 hailstorm-gem/features/support/aws_security_group_helper.rb create mode 100644 hailstorm-gem/features/support/aws_vpc_helper.rb diff --git a/docker-compose.yml b/docker-compose.yml index 4e594aae..0e0c904a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: - "start.sh" hailstorm-api: - image: "hailstorm3/hailstorm-api:1.0.20" + image: "hailstorm3/hailstorm-api:1.0.21" ports: - "4567:8080" environment: diff --git a/hailstorm-api/Gemfile b/hailstorm-api/Gemfile index e5a5190a..2018d3be 100644 --- a/hailstorm-api/Gemfile +++ b/hailstorm-api/Gemfile @@ -5,7 +5,7 @@ source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } gem 'activerecord-jdbcmysql-adapter', '~> 60.2' -gem 'hailstorm', '= 5.1.15' +gem 'hailstorm', '= 5.1.16' gem 'httparty', '~> 0.18.1' gem 'puma' gem 'rake', '~> 13' diff --git a/hailstorm-api/Gemfile.lock b/hailstorm-api/Gemfile.lock index 29475839..24d1390a 100644 --- a/hailstorm-api/Gemfile.lock +++ b/hailstorm-api/Gemfile.lock @@ -53,7 +53,7 @@ GEM erubi (1.10.0) erubis (2.7.0) haikunator (1.1.1) - hailstorm (5.1.15-java) + hailstorm (5.1.16-java) actionpack (~> 6.0.0) activerecord-jdbc-adapter (~> 60.2) aws-sdk-ec2 (~> 1) @@ -166,7 +166,7 @@ PLATFORMS DEPENDENCIES activerecord-jdbcmysql-adapter (~> 60.2) croaky-rspec (~> 0.1) - hailstorm (= 5.1.15) + hailstorm (= 5.1.16) httparty (~> 0.18.1) puma rake (~> 13) diff --git a/hailstorm-api/app/version.rb b/hailstorm-api/app/version.rb index 5e370628..b3392e8e 100644 --- a/hailstorm-api/app/version.rb +++ b/hailstorm-api/app/version.rb @@ -3,6 +3,6 @@ # Version module Hailstorm module Api - VERSION = '1.0.20' + VERSION = '1.0.21' end end diff --git a/hailstorm-cli/Gemfile.lock b/hailstorm-cli/Gemfile.lock index 39f6015c..ad63786a 100644 --- a/hailstorm-cli/Gemfile.lock +++ b/hailstorm-cli/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - hailstorm-cli (1.0.17-java) - hailstorm (= 5.1.15) + hailstorm-cli (1.0.18-java) + hailstorm (= 5.1.16) GEM remote: https://rubygems.org/ @@ -94,7 +94,7 @@ GEM erubis (2.7.0) ffi (1.13.1-java) haikunator (1.1.1) - hailstorm (5.1.15-java) + hailstorm (5.1.16-java) actionpack (~> 6.0.0) activerecord-jdbc-adapter (~> 60.2) aws-sdk-ec2 (~> 1) diff --git a/hailstorm-cli/hailstorm-cli.gemspec b/hailstorm-cli/hailstorm-cli.gemspec index 7a5bf4e6..ee273ad7 100644 --- a/hailstorm-cli/hailstorm-cli.gemspec +++ b/hailstorm-cli/hailstorm-cli.gemspec @@ -32,5 +32,5 @@ and generate reports.' gem.executables = gem.files.grep(%r{^bin/\b}).map { |f| File.basename(f) } gem.require_paths = %w[lib] - gem.add_runtime_dependency('hailstorm', '= 5.1.15') + gem.add_runtime_dependency('hailstorm', '= 5.1.16') end diff --git a/hailstorm-cli/lib/hailstorm/cli/version.rb b/hailstorm-cli/lib/hailstorm/cli/version.rb index 2f5af47b..1723f882 100644 --- a/hailstorm-cli/lib/hailstorm/cli/version.rb +++ b/hailstorm-cli/lib/hailstorm/cli/version.rb @@ -3,6 +3,6 @@ # Version module Hailstorm module Cli - VERSION = '1.0.17' + VERSION = '1.0.18' end end diff --git a/hailstorm-gem/.gitignore b/hailstorm-gem/.gitignore index cd06a9f6..9a99b79d 100644 --- a/hailstorm-gem/.gitignore +++ b/hailstorm-gem/.gitignore @@ -10,3 +10,4 @@ coverage/ features/data/data-center-machines.yml features/data/site_server.txt /pkg/ +/features/data/feature-parameters.yml diff --git a/hailstorm-gem/Gemfile.lock b/hailstorm-gem/Gemfile.lock index 06a7343c..b4116876 100644 --- a/hailstorm-gem/Gemfile.lock +++ b/hailstorm-gem/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - hailstorm (5.1.15-java) + hailstorm (5.1.16-java) actionpack (~> 6.0.0) activerecord-jdbc-adapter (~> 60.2) aws-sdk-ec2 (~> 1) diff --git a/hailstorm-gem/features/clean_aws_account.feature b/hailstorm-gem/features/clean_aws_account.feature index 13e4fc73..f60e5632 100644 --- a/hailstorm-gem/features/clean_aws_account.feature +++ b/hailstorm-gem/features/clean_aws_account.feature @@ -2,7 +2,23 @@ Feature: Clean AWS Account Background: Hailstorm application is initialized Given Hailstorm is initialized with a project 'clean_aws_account' - Scenario: Purge AWS Cluster - Given an AWS cluster already exists in 'ap-northeast-1' region + Scenario: Purge a previously deleted AWS cluster + Given a previously deleted AWS cluster is configured in any region When the AWS cluster is purged - Then the AWS cluster should be removed + Then the AMI is deleted from the AWS cluster model + + @delete_untagged_resources + Scenario: Purge an AWS cluster with untagged resources + Given AWS untagged resources with prefix 'purge-aws-account-feature' in a region with a public subnet + And an AWS cluster configured with these resources + When the AWS cluster is purged + Then the AMI is deleted from the AWS cluster model + But the AWS resources are not deleted + + Scenario: Purge an AWS cluster with tagged resources + Given JMeter is correctly configured + And an AWS cluster and resources created by Hailstorm in a clean region + And the created resources are tagged + When the AWS cluster is purged + Then the AMI is deleted from the AWS cluster model + And the AWS resources are deleted as well diff --git a/hailstorm-gem/features/data/feature-parameters.sample.yml b/hailstorm-gem/features/data/feature-parameters.sample.yml new file mode 100644 index 00000000..796baa5c --- /dev/null +++ b/hailstorm-gem/features/data/feature-parameters.sample.yml @@ -0,0 +1,5 @@ +# Copy me to feature-parameters.yml +--- +clean_aws_account: + region_with_public_subnet: ap-south-1 + clean_region: ap-south-1 diff --git a/hailstorm-gem/features/step_definitions/aws_model_steps.rb b/hailstorm-gem/features/step_definitions/aws_model_steps.rb index b0e1248f..1bfd0a20 100644 --- a/hailstorm-gem/features/step_definitions/aws_model_steps.rb +++ b/hailstorm-gem/features/step_definitions/aws_model_steps.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -include ModelHelper - After do |scenario| if scenario.source_tag_names.include?('@terminate_instance') && @aws if @load_agent @@ -44,12 +42,12 @@ @aws.instance_type = instance_type end -Given(/^an AWS cluster already exists in '(.+?)' region$/) do |region_code| +Given(/^a(?:n| previously deleted) AWS cluster is configured in (?:any|'(.+?)') region$/) do |region_code| require 'hailstorm/model/amazon_cloud' @aws = Hailstorm::Model::AmazonCloud.new @aws.project = @project @aws.access_key, @aws.secret_key = aws_keys - @aws.region = region_code + @aws.region = region_code || 'ap-northeast-1' @aws.active = false @aws.agent_ami = 'ami-123' @aws.save! @@ -65,7 +63,43 @@ @project.purge_clusters end -Then(/^the AWS cluster should be removed$/) do - @aws.reload +Then(/^the AMI is deleted from the AWS cluster model$/) do + if @aws + @aws.reload + else + @aws = @project.clusters[0].cluster_instance + end + expect(@aws.agent_ami).to be_nil end + +And(/^an AWS cluster configured with these resources$/) do + access_key, secret_key = aws_keys + aws = create_aws_cluster(@project, + access_key: access_key, + secret_key: secret_key, + ssh_identity: @untagged_resource_group[:key_pair].key_name, + region: @untagged_resource_group[:region], + agent_ami: @untagged_resource_group[:agent_ami].id, + user_name: 'ec2-user', + security_group: @untagged_resource_group[:security_group].group_name, + instance_type: 't3a.nano', + max_threads_per_agent: 5, + vpc_subnet_id: @untagged_resource_group[:subnet_id]) + + create_load_agent(aws, @untagged_resource_group[:instance]) + @project.reload +end + +Given(/^an AWS cluster and resources created by Hailstorm in a clean region$/) do + region_code = feature_parameter(feature: 'clean_aws_account', param: 'clean_region') + @hailstorm_config.clusters('amazon_cloud') do |aws| + aws.access_key, aws.secret_key = aws_keys + aws.region = region_code + aws.active = true + aws.instance_type = 't3a.nano' + end + + @project.settings_modified = true + @project.setup(config: @hailstorm_config) +end diff --git a/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb b/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb index 2033ec86..64725870 100644 --- a/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb +++ b/hailstorm-gem/features/step_definitions/aws_sdk_steps.rb @@ -3,7 +3,13 @@ require 'stringio' require 'hailstorm/model/helper/aws_region_helper' -include AwsHelper +After do |scenario| + if scenario.source_tag_names.include?('@delete_untagged_resources') && @untagged_resource_group + @untagged_resource_group[:instance]&.terminate + @untagged_resource_group[:key_pair]&.delete + @untagged_resource_group[:security_group]&.delete + end +end Then(/^the AMI should exist$/) do ec2 = ec2_resource(region: @aws.region) @@ -35,3 +41,61 @@ snapshot_ids.each { |snapshot_id| ec2.snapshot(snapshot_id).delete } end end + +Given(/^AWS untagged resources with prefix '(.+?)' in a region with a public subnet$/) do |prefix| + region_code = feature_parameter(feature: 'clean_aws_account', param: 'region_with_public_subnet') + public_subnet = select_public_subnets(region: region_code).first + expect(public_subnet).to_not be_nil + @untagged_resource_group = { + region: region_code, + security_group: create_security_group(region: region_code, group_name: prefix, vpc_id: public_subnet.vpc_id), + key_pair: create_key_pair(region: region_code, key_name: prefix), + agent_ami: find_most_recent_amazon_ami(region: region_code), + subnet_id: public_subnet.id + } + + expect(@untagged_resource_group[:agent_ami]).to_not be_nil + @untagged_resource_group[:instance] = create_instances( + params: { + region: region_code, + image_id: @untagged_resource_group[:agent_ami].id, + key_name: @untagged_resource_group[:key_pair].name, + security_group_id: @untagged_resource_group[:security_group].id, + subnet_id: public_subnet.id + } + ).first +end + +And(/^the AWS resources are not deleted$/) do + raise('Expected @untagged_resource_group to be defined') if @untagged_resource_group.blank? + + expect(@untagged_resource_group[:instance].reload.state.name).to_not be == 'running' + expect(key_pair_exists?(region: @untagged_resource_group[:region], + key_pair_id: @untagged_resource_group[:key_pair].key_pair_id)).to be(true) + expect(@untagged_resource_group[:agent_ami].reload.state).to be == 'available' + expect(security_group_exists?(region: @untagged_resource_group[:region], + security_group_id: @untagged_resource_group[:security_group].id)).to be(true) +end + +And(/^the AWS resources are deleted as well$/) do + args = { region: @aws.region, tags: { hailstorm: { created: true } } } + expect(tagged_ec2_instances(args)).to be_empty + expect(tagged_key_pairs(args)).to be_empty + expect(tagged_images(args)).to be_empty + expect(tagged_security_groups(args)).to be_empty + expect(tagged_subnets(args)).to be_empty + expect(tagged_internet_gws(args)).to be_empty + expect(tagged_vpcs(args)).to be_empty +end + +And(/^the created resources are tagged$/) do + region_code = feature_parameter(feature: 'clean_aws_account', param: 'clean_region') + args = { region: region_code, tags: { hailstorm: { created: true } } } + expect(tagged_ec2_instances(args)).to_not be_empty + expect(tagged_key_pairs(args)).to_not be_empty + expect(tagged_images(args)).to_not be_empty + expect(tagged_security_groups(args)).to_not be_empty + expect(tagged_subnets(args)).to_not be_empty + expect(tagged_internet_gws(args)).to_not be_empty + expect(tagged_vpcs(args)).to_not be_empty +end diff --git a/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb b/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb index d88f8c0b..272f882e 100644 --- a/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb +++ b/hailstorm-gem/features/step_definitions/jmeter_model_steps.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -include ModelHelper - When(/^the JMeter version for the project is '(.+?)'$/) do |jmeter_version| @project.jmeter_version = jmeter_version end diff --git a/hailstorm-gem/features/step_definitions/load_generation_steps.rb b/hailstorm-gem/features/step_definitions/load_generation_steps.rb index 21dc81a8..0dd693b6 100644 --- a/hailstorm-gem/features/step_definitions/load_generation_steps.rb +++ b/hailstorm-gem/features/step_definitions/load_generation_steps.rb @@ -5,13 +5,11 @@ require 'hailstorm/model/master_agent' require 'hailstorm/support/aws_exception_builder' -include AwsHelper - Given(/^JMeter is correctly configured$/) do Hailstorm.fs = CukeDataFs.new if Hailstorm.fs.nil? @hailstorm_config.jmeter do |jmeter| jmeter.properties do |map| - map['NumUsers'] = 10 + map['NumUsers'] = 3 map['Duration'] = 180 end end diff --git a/hailstorm-gem/features/step_definitions/project_model_steps.rb b/hailstorm-gem/features/step_definitions/project_model_steps.rb index 09b28c8c..1f6e4253 100644 --- a/hailstorm-gem/features/step_definitions/project_model_steps.rb +++ b/hailstorm-gem/features/step_definitions/project_model_steps.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -include ModelHelper - Given(/^(?:Hailstorm is initialized with a project|the) ['"]([^'"]+)['"](?:| project(?:| is active))$/) do |project_code| require 'hailstorm/model/project' @project = find_project(project_code) diff --git a/hailstorm-gem/features/support/aws_ec2_helper.rb b/hailstorm-gem/features/support/aws_ec2_helper.rb new file mode 100644 index 00000000..897f3e2e --- /dev/null +++ b/hailstorm-gem/features/support/aws_ec2_helper.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +# Methods for AWS EC2 resource usage +module AwsEc2Helper + + # @param [String] region + # @return [Aws::EC2::Client] + def ec2_client(region:) + Aws::EC2::Client.new( + region: region, + credentials: Aws::Credentials.new(*aws_keys) + ) + end + + # @param [String] region + # @return [Aws::EC2::Resource] + def ec2_resource(region:) + Aws::EC2::Resource.new(client: ec2_client(region: region)) + end + + def terminate_agents(region, *load_agents) + ec2 = ec2_resource(region: region) + load_agents.each do |agent| + ec2_instance = ec2.instances(instance_ids: [agent.identifier]).first + ec2_instance&.terminate + end + end + + # @param [String] region + # @param [String] key_name + # @return [Aws::EC2::KeyPair] + def create_key_pair(region:, key_name:) + ec2 = ec2_resource(region: region) + key_pair = ec2.key_pairs(filters: [{ name: 'key-name', values: [key_name] }]).to_a.first + return key_pair if key_pair + + ec2.create_key_pair(key_name: key_name) + end + + # @param [String] region + # @param [Aws::EC2::Resource] ec2 + # @return [Aws::EC2::Image, nil] + def find_most_recent_amazon_ami(region:, ec2: nil) + ec2 ||= ec2_resource(region: region) + params = { + owners: ['amazon'], + filters: [ + { name: 'is-public', values: [true.to_s] }, + { name: 'architecture', values: ['x86_64'] }, + { name: 'image-type', values: ['machine'] }, + { name: 'root-device-type', values: ['ebs'] } + ] + } + + most_recent_image = proc do |a, b| + t1 = Time.parse(a.creation_date) + t2 = Time.parse(b.creation_date) + t2 <=> t1 + end + + collection = ec2.images(params) + .select { |image| image.name =~ /amzn2-ami-hvm-2\.0/ } + .sort { |a, b| most_recent_image.call(a, b) } + + collection.first + end + + # @param [Hash] params + # @param [String (frozen)] instance_type + # @param [Integer] count + # @return [Enumerable] + def create_instances(params: {}, + instance_type: 't3a.nano', + count: 1) + + ec2 = ec2_resource(region: params[:region]) + collection = ec2.create_instances( + image_id: params[:image_id], + subnet_id: params[:subnet_id], + key_name: params[:key_name], + security_group_ids: [params[:security_group_id]], + instance_type: instance_type, + min_count: count, + max_count: count + ) + + %i[instance_running instance_status_ok system_status_ok].each do |waiter_name| + ec2.client.wait_until(waiter_name, instance_ids: collection.map(&:id)) + end + + collection + end + + # @param [String] region + # @param [String] key_pair_id + # @return [Boolean] + def key_pair_exists?(region:, key_pair_id:) + ec2 = ec2_resource(region: region) + ec2.key_pairs(key_pair_ids: [key_pair_id]).to_a.empty? == false + end + + # AWS EC2 instances tagged with provided tags. + # Provided tags can be namespaced with Hash values as a Hash. + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_ec2_instances(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.instances(filters: to_tag_filters(tags).push({ name: 'instance-state-name', values: ['running'] })).to_a + end + + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_key_pairs(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.key_pairs(filters: to_tag_filters(tags)).to_a + end + + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_images(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.images(filters: to_tag_filters(tags).push({ name: 'state', values: ['available'] })).to_a + end + + # @param [String] region + # @param [String] image_name + # @return [Enumerable] + def search_owned_images(region:, image_name:) + ec2 = ec2_resource(region: region) + ec2.images(owners: ['self'], filters: [{ name: 'name', values: [image_name] }]).to_a + end +end diff --git a/hailstorm-gem/features/support/aws_helper.rb b/hailstorm-gem/features/support/aws_helper.rb index 6ff1845e..862a5c6a 100644 --- a/hailstorm-gem/features/support/aws_helper.rb +++ b/hailstorm-gem/features/support/aws_helper.rb @@ -2,6 +2,10 @@ require 'aws-sdk-ec2' +require_relative './aws_ec2_helper' +require_relative './aws_vpc_helper' +require_relative './aws_security_group_helper' + # Methods for AWS resource usage module AwsHelper @@ -15,54 +19,23 @@ def aws_keys end end - def ec2_resource(region:) - ec2_client = Aws::EC2::Client.new( - region: region, - credentials: Aws::Credentials.new(*aws_keys) - ) - - Aws::EC2::Resource.new(client: ec2_client) - end - - def select_public_subnets(region:) - route_tables = ec2_resource(region: region).vpcs - .flat_map { |vpc| vpc.route_tables.to_a } - - subnets = find_igw_subnets(route_tables) - subnets = find_main_rtb_subnets(route_tables) if subnets.empty? - subnets.select(&:map_public_ip_on_launch) - end - - def route_table_contains_igw?(route_table) - !fetch_routes(route_table).find { |route| route && route.gateway_id != 'local' }.nil? - end - - def fetch_routes(route_table) - route_table.routes - rescue Aws::Errors::ServiceError, ArgumentError - [] - end + # Converts a 1-level hash to an array of AWS tags. + # > to_tag_array(hailstorm: {project: 'abc', created: true}) + # > [{name: 'hailstorm:project', values: ['abc']}, {name: 'hailstorm:created', values: ['true']}] + # @param [Hash] tags + # @return [Array] + def to_tag_filters(tags) + key_namespace = tags.keys.first + return [] if key_namespace.nil? - def terminate_agents(region, *load_agents) - ec2 = ec2_resource(region: region) - load_agents.each do |agent| - ec2_instance = ec2.instances(instance_ids: [agent.identifier]).first - ec2_instance&.terminate + tags[key_namespace].map do |key, value| + { name: "tag:#{key_namespace}:#{key}", values: [value.to_s] } end end - def find_main_rtb_subnets(route_tables) - route_tables.flat_map { |route_table| route_table.associations.to_a } - .select(&:main) - .map(&:route_table) - .map(&:vpc) - .flat_map { |vpc| vpc.subnets.to_a } - end - - def find_igw_subnets(route_tables) - route_tables.select { |route_table| route_table_contains_igw?(route_table) } - .flat_map { |route_table| route_table.associations.to_a } - .select(&:subnet_id) - .map(&:subnet) - end + include AwsEc2Helper + include AwsVpcHelper + include AwsSecurityGroupHelper end + +World(AwsHelper) diff --git a/hailstorm-gem/features/support/aws_security_group_helper.rb b/hailstorm-gem/features/support/aws_security_group_helper.rb new file mode 100644 index 00000000..799c8386 --- /dev/null +++ b/hailstorm-gem/features/support/aws_security_group_helper.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Methods for AWS Security Group resource usage +module AwsSecurityGroupHelper + + # @param [String] region + # @param [String] group_name + # @param [String] vpc_id + # @return [Aws::EC2::SecurityGroup] + def create_security_group(region:, group_name:, vpc_id:) + security_group = search_security_groups(region: region, group_name: group_name, vpc_id: vpc_id).to_a.first + return security_group if security_group + + ec2 = ec2_resource(region: region) + ec2.create_security_group(group_name: group_name, description: group_name, vpc_id: vpc_id) + end + + # @param [String] region + # @param [String] security_group_id + # @return [Boolean] + # @param [String, nil] group_name + # @param [String, nil] vpc_id + def security_group_exists?(region:, security_group_id: nil, group_name: nil, vpc_id: nil) + search_security_groups( + region: region, + security_group_id: security_group_id, + group_name: group_name, + vpc_id: vpc_id + ).to_a.empty? == false + end + + # @param [String] region + # @param [String, nil] security_group_id + # @param [String, nil] group_name + # @param [String, nil] vpc_id + # @return [Aws::EC2::SecurityGroup::Collection] + def search_security_groups(region:, security_group_id: nil, group_name: nil, vpc_id: nil) + ec2 = ec2_resource(region: region) + params = {} + params[:group_ids] = [security_group_id] if security_group_id + params[:filters] = [] if group_name || vpc_id + params[:filters].push({ name: 'vpc-id', values: [vpc_id] }) if vpc_id + params[:filters].push({ name: 'group-name', values: [group_name] }) if group_name + ec2.security_groups(params) + end + + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_security_groups(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.security_groups(filters: to_tag_filters(tags)).to_a + end +end diff --git a/hailstorm-gem/features/support/aws_vpc_helper.rb b/hailstorm-gem/features/support/aws_vpc_helper.rb new file mode 100644 index 00000000..1a4b0a42 --- /dev/null +++ b/hailstorm-gem/features/support/aws_vpc_helper.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Methods for AWS VPC resource usage +module AwsVpcHelper + + def select_public_subnets(region:) + route_tables = ec2_resource(region: region).vpcs + .flat_map { |vpc| vpc.route_tables.to_a } + + subnets = find_igw_subnets(route_tables) + subnets = find_main_rtb_subnets(route_tables) if subnets.empty? + subnets.select(&:map_public_ip_on_launch) + end + + def route_table_contains_igw?(route_table) + !fetch_routes(route_table).find { |route| route && route.gateway_id != 'local' }.nil? + end + + def fetch_routes(route_table) + route_table.routes + rescue Aws::Errors::ServiceError, ArgumentError + [] + end + + def find_main_rtb_subnets(route_tables) + route_tables.flat_map { |route_table| route_table.associations.to_a } + .select(&:main) + .map(&:route_table) + .map(&:vpc) + .flat_map { |vpc| vpc.subnets.to_a } + end + + def find_igw_subnets(route_tables) + route_tables.select { |route_table| route_table_contains_igw?(route_table) } + .flat_map { |route_table| route_table.associations.to_a } + .select(&:subnet_id) + .map(&:subnet) + end + + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_subnets(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.subnets(filters: to_tag_filters(tags)).to_a + end + + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_vpcs(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.vpcs(filters: to_tag_filters(tags)).to_a + end + + # @param [String] region + # @param [Hash] tags + # @return Enumerable + def tagged_internet_gws(region:, tags:) + ec2 = ec2_resource(region: region) + ec2.internet_gateways(filters: to_tag_filters(tags)).to_a + end +end diff --git a/hailstorm-gem/features/support/cuke_data_fs.rb b/hailstorm-gem/features/support/cuke_data_fs.rb index e539ab64..793ca81a 100644 --- a/hailstorm-gem/features/support/cuke_data_fs.rb +++ b/hailstorm-gem/features/support/cuke_data_fs.rb @@ -16,7 +16,7 @@ def fetch_jmeter_plans(*_args) end def app_dir_tree(*_args) - { data: nil }.stringify_keys + { data: nil, jmeter: nil }.stringify_keys end def transfer_jmeter_artifacts(_project_code, to_dir_path) diff --git a/hailstorm-gem/features/support/feature_helper.rb b/hailstorm-gem/features/support/feature_helper.rb index 2c657ce7..2bf86e05 100644 --- a/hailstorm-gem/features/support/feature_helper.rb +++ b/hailstorm-gem/features/support/feature_helper.rb @@ -52,3 +52,17 @@ def data_path def tmp_path @tmp_path ||= BUILD_PATH end + +# @param [String] feature +# @param [String] param +def feature_parameter(feature:, param:) + file_path = File.join(data_path, 'feature-parameters.yml') + begin + params = YAML.load_file(file_path) + rescue Errno::ENOENT + sample_path = File.join(data_path, 'feature-parameters.sample.yml') + raise("Missing configuration file expected at #{file_path}. See #{sample_path}") + end + + params[feature.to_s][param.to_s] +end diff --git a/hailstorm-gem/features/support/model_helper.rb b/hailstorm-gem/features/support/model_helper.rb index 3f320ec2..afeac5d5 100644 --- a/hailstorm-gem/features/support/model_helper.rb +++ b/hailstorm-gem/features/support/model_helper.rb @@ -3,7 +3,40 @@ # Model Helper module ModelHelper + # @return [Hailstorm::Model::Project] def find_project(project_code) Hailstorm::Model::Project.where(project_code: project_code).first_or_create! end + + # @param [Hailstorm::Model::Project] project + # @param [Hash] attributes + # @return [Hailstorm::Model::AmazonCloud] + def create_aws_cluster(project, attributes = {}) + require 'hailstorm/model/amazon_cloud' + + aws = Hailstorm::Model::AmazonCloud.where(attributes.merge(project_id: project.id)).first_or_create!(active: false) + aws.update_column(:active, true) unless aws.active + Hailstorm::Model::Cluster.where(project_id: project.id, cluster_type: aws.class.name, clusterable_id: aws.id) + .first_or_create! + aws + end + + # @param [Hailstorm::Model::AmazonCloud] aws + # @param [Aws::EC2::Instance] ec2_instance + def create_load_agent(aws, ec2_instance) + jmeter_plan = Hailstorm::Model::JmeterPlan.where(project_id: aws.project.id, + test_plan_name: 'hailstorm-site-basic', + content_hash: 'A', + properties: '{}').first_or_create!(active: false) + jmeter_plan.update_column(:active, true) unless jmeter_plan.active? + + require 'hailstorm/model/master_agent' + master_agent = Hailstorm::Model::MasterAgent.where(clusterable_id: aws.id, + clusterable_type: aws.class.name, + jmeter_plan_id: jmeter_plan.id, + identifier: ec2_instance.id).first_or_create!(active: false) + master_agent.update_column(:active, true) unless master_agent.active? + end end + +World(ModelHelper) diff --git a/hailstorm-gem/lib/hailstorm/behavior/aws_adaptable.rb b/hailstorm-gem/lib/hailstorm/behavior/aws_adaptable.rb index e762994d..7824d907 100644 --- a/hailstorm-gem/lib/hailstorm/behavior/aws_adaptable.rb +++ b/hailstorm-gem/lib/hailstorm/behavior/aws_adaptable.rb @@ -29,12 +29,6 @@ def first_available_zone raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end - # @param [String] subnet_id - # @return [String] vpc_id - def find_vpc(subnet_id:) - raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") - end - # @return [Enumerator] def find_self_owned_snapshots raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") @@ -84,8 +78,9 @@ module SecurityGroupClient # :nocov: # @param [String] name # @param [String] vpc_id + # @param [Array] filters accepts :created (tag:created) or Hash#keys name, values # @return [SecurityGroup] - def find(name:, vpc_id: nil) + def find(name:, vpc_id: nil, filters: []) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end @@ -197,8 +192,9 @@ def create(_instance_attrs, min_count: 1, max_count: 1) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end + # @param [Array] instance_ids # @return [Enumerator] - def list + def list(instance_ids: nil) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end # :nocov: @@ -237,8 +233,9 @@ def deregister(ami_id:) # Find AMI by ami_id # @param [String] ami_id + # @param [Array] filters accepts :created (tag:created) or Hash#keys name, values # @return [Ami] - def find(ami_id:) + def find(ami_id:, filters: []) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end @@ -257,8 +254,9 @@ module SubnetClient # :nocov: # @param [String] subnet_id # @param [String] name_tag + # @param [Array] filters accepts :created (tag:created) or Hash#keys name, values # @return [String] subnet_id - def find(subnet_id: nil, name_tag: nil) + def find(subnet_id: nil, name_tag: nil, filters: []) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end @@ -281,6 +279,17 @@ def available?(subnet_id:) def modify_attribute(subnet_id:, **_kwargs) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end + + # @param [String] subnet_id + def delete(subnet_id:) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end + + # @param [String] subnet_id + # @return [String, NilClass] + def find_vpc(subnet_id:) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end # :nocov: end @@ -307,6 +316,18 @@ def create(cidr:) def available?(vpc_id:) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end + + # @param [String] vpc_id + # @param [Array] filters accepts :created (tag:created) or Hash#keys name, values + # @return [Hailstorm::Behavior::AwsAdapterDomain::Vpc, Nil] Vpc + def find(vpc_id:, filters: []) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end + + # @param [String] vpc_id + def delete(vpc_id:) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end # :nocov: end @@ -325,6 +346,23 @@ def create def attach(igw_id:, vpc_id:) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end + + # @param [String] vpc_id + # @param [Array] filters accepted :created + def select(vpc_id:, filters: []) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end + + # @param [String] igw_id + def delete(igw_id:) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end + + # @param [String] igw_id + # @param [String] vpc_id + def detach_from_vpc(igw_id:, vpc_id:) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end # :nocov: end @@ -364,6 +402,18 @@ def create_route(route_table_id:, cidr:, internet_gateway_id:) def routes(route_table_id:) raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") end + + # @param [String] vpc_id + # @param [Array] filters known filters :created + # @return [Enumerable] + def route_tables(vpc_id:, filters: []) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end + + # @param [String] route_table_id + def delete(route_table_id:) + raise(NotImplementedError, "#{self.class}##{__method__} implementation not found.") + end # :nocov: end end diff --git a/hailstorm-gem/lib/hailstorm/behavior/aws_adapter_domain.rb b/hailstorm-gem/lib/hailstorm/behavior/aws_adapter_domain.rb index 51306b2d..a926875d 100644 --- a/hailstorm-gem/lib/hailstorm/behavior/aws_adapter_domain.rb +++ b/hailstorm-gem/lib/hailstorm/behavior/aws_adapter_domain.rb @@ -72,25 +72,46 @@ def status # StateReason (code: String, message: String) StateReason = Struct.new(:code, :message, keyword_init: true) - # Ami(id|image_id|ami_id: String, state: Symbol, name: String, state_reason: StateReason) + # VPC (id|vpc_id: String, state: String, status: Symbol) + Vpc = Struct.new(:vpc_id, :state, keyword_init: true) do + def id + vpc_id + end + + def status + state.to_sym + end + + def available? + status == :available + end + end + + # Ami(id|image_id|ami_id: String, state: Symbol, name: String, state_reason: StateReason, snapshot_id: String) class Ami - attr_reader :image_id, :name, :state, :state_reason + attr_reader :image_id, :name, :state, :state_reason, :snapshot_id - # @param [String] image_id | ami_id + # @param [String] image_id # @param [String] state # @param [String] name # @param [StateReason] state_reason - def initialize(image_id: nil, state: nil, name: nil, ami_id: nil, state_reason: nil) - @image_id = image_id || ami_id + # @param [String] snapshot_id + def initialize(image_id: nil, state: nil, name: nil, state_reason: nil, snapshot_id: nil) + @image_id = image_id @state = state ? state.to_sym : nil @name = name + @snapshot_id = snapshot_id end def id image_id end + def ami_id + image_id + end + def available? state == :available end @@ -111,6 +132,12 @@ def active? end end + # RouteTable(id: String, main: Boolean) + RouteTable = Struct.new(:id, :main, keyword_init: true) + + # InternetGateway(id: String) + InternetGateway = Struct.new(:id, keyword_init: true) + CLIENT_KEYS = %i[ec2_client key_pair_client security_group_client instance_client ami_client subnet_client vpc_client internet_gateway_client route_table_client].freeze diff --git a/hailstorm-gem/lib/hailstorm/behavior/aws_exception.rb b/hailstorm-gem/lib/hailstorm/behavior/aws_exception.rb index 32e5ace9..f6ab0268 100644 --- a/hailstorm-gem/lib/hailstorm/behavior/aws_exception.rb +++ b/hailstorm-gem/lib/hailstorm/behavior/aws_exception.rb @@ -4,5 +4,7 @@ # Hailstorm Aws Exception wrapper class Hailstorm::AwsException < Hailstorm::Exception - attr_accessor :data + attr_accessor :data, + :code, + :context end diff --git a/hailstorm-gem/lib/hailstorm/model/amazon_cloud.rb b/hailstorm-gem/lib/hailstorm/model/amazon_cloud.rb index d91172e9..aa5aadf9 100644 --- a/hailstorm-gem/lib/hailstorm/model/amazon_cloud.rb +++ b/hailstorm-gem/lib/hailstorm/model/amazon_cloud.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'fileutils' require 'hailstorm' require 'hailstorm/model' require 'hailstorm/behavior/loggable' @@ -18,6 +19,7 @@ require 'hailstorm/model/helper/security_group_creator' require 'hailstorm/model/helper/ec2_instance_helper' require 'hailstorm/model/helper/ami_helper' +require 'hailstorm/support/amazon_account_cleaner' # Represents state and behavior for Amazon Web Services (AWS) cluster # @author Sayantam Dey @@ -65,31 +67,57 @@ def ami_prefix DEFAULTS::AMI_ID end - # @return [Hailstorm::Model::Helper::AmiHelper] - def ami_helper - helper_attrs = {} - add_method_attrs!(helper_attrs, :ec2_instance_helper, :security_group_finder) - helper_group = Hailstorm::Model::Helper::AmiHelper::MemberHelperGroup.new(helper_attrs) - - client_attrs = { ami_client: client_factory.ami_client } - add_method_attrs!(client_attrs, :instance_client) - client_group = Hailstorm::Model::Helper::AmiHelper::ClientGroup.new(client_attrs) - - Hailstorm::Model::Helper::AmiHelper.new(aws_clusterable: self, - helper_group: helper_group, - client_group: client_group) - end - include Hailstorm::Model::Concern::AbstractClusterable + + # AWS resource clean up + module CleanupMethods + # Delete SSH key-pair and identity once all load agents have been terminated + # (see Hailstorm::Behavior::Clusterable#cleanup) + def cleanup + logger.debug { "#{self.class}##{__method__}" } + return unless self.active? && self.autogenerated_ssh_key? && self.load_agents.reload.empty? + + key_pair_id = client_factory.key_pair_client.find(name: self.ssh_identity) + return unless key_pair_id + + client_factory.key_pair_client.delete(key_pair_id: key_pair_id) + FileUtils.safe_unlink(identity_file_path) + end + + # Purges the Amazon accounts used of Hailstorm related artifacts + def purge(cleaner = nil) + logger.debug { "#{self}.#{__method__}" } + cleaner ||= Hailstorm::Support::AmazonAccountCleaner.new(client_factory: client_factory, + region_code: self.region, + resource_group: resources_to_purge) + cleaner.cleanup + self.update_column(:agent_ami, nil) + self.update_column(:vpc_subnet_id, nil) + end + + def resources_to_purge + resource_group_attrs = { instance_ids: self.load_agents.map(&:identifier), + ami_id: self.agent_ami, + security_group_name: self.security_group } + + resource_group_attrs[:key_pair_name] = self.ssh_identity if self.autogenerated_ssh_key? + unless self.vpc_subnet_id.nil? + resource_group_attrs[:subnet_id] = self.vpc_subnet_id + vpc_helper = Hailstorm::Model::Helper::VpcHelper.new(subnet_client: subnet_client) + resource_group_attrs[:vpc_id] = vpc_helper.find_vpc_from_subnet(subnet_id: self.vpc_subnet_id) + end + + Hailstorm::Support::AmazonAccountCleaner::AccountResourceGroup.new(resource_group_attrs) + end + private :resources_to_purge + end + + include CleanupMethods include Hailstorm::Model::Concern::AbstractProvisionable ######################### PRIVATE METHODS #################################### private - def add_method_attrs!(attrs, *keys) - keys.each { |key| attrs[key] = send(key) } - end - def set_defaults self.security_group = DEFAULTS::SECURITY_GROUP if self.security_group.blank? self.user_name ||= DEFAULTS::SSH_USER @@ -102,42 +130,8 @@ def set_defaults self.autogenerated_ssh_key = true end - def client_factory - Hailstorm::Support::AwsAdapter.clients(aws_config) - end - - def aws_config - @aws_config ||= { - access_key_id: self.access_key, - secret_access_key: self.secret_key, - region: self.region, - logger: logger - } - end - - # @return [Hailstorm::Behavior::AwsAdaptable::InstanceClient] - def instance_client - @instance_client ||= client_factory.instance_client - end - - # @return [Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient] - def security_group_client - @security_group_client ||= client_factory.security_group_client - end - - # @return [Hailstorm::Behavior::AwsAdaptable::Ec2Client] - def ec2_client - @ec2_client ||= client_factory.ec2_client - end - - def security_group_finder - attrs = { aws_clusterable: self } - add_method_attrs!(attrs, :security_group_client, :ec2_client) - Hailstorm::Model::Helper::SecurityGroupFinder.new(attrs) - end - - def ec2_instance_helper - Hailstorm::Model::Helper::Ec2InstanceHelper.new(aws_clusterable: self, instance_client: instance_client) + def default_max_threads_per_agent + DEFAULTS.calc_max_threads_per_instance(instance_type: self.instance_type) end # Sets the first available zone based on configured region @@ -149,10 +143,6 @@ def set_availability_zone self.zone = ec2_client.first_available_zone end - def default_max_threads_per_agent - DEFAULTS.calc_max_threads_per_instance(instance_type: self.instance_type) - end - def assign_vpc_subnet attrs = {} keys = %i[internet_gateway_client route_table_client vpc_client subnet_client] @@ -167,6 +157,10 @@ def identity_file_name [File.basename(self.ssh_identity).gsub(/\.pem/, ''), self.region].join('_').concat('.pem', '') end + def add_method_attrs!(attrs, *keys) + keys.each { |key| attrs[key] = send(key) } + end + def identity_file_exists attrs = { key_pair_client: client_factory.key_pair_client } add_method_attrs!(attrs, :ssh_identity, :identity_file_path) @@ -177,15 +171,78 @@ def identity_file_exists def create_security_group logger.debug { "#{self.class}##{__method__}" } attrs = { aws_clusterable: self } - add_method_attrs!(attrs, :ec2_client, :security_group_client) + add_method_attrs!(attrs, :subnet_client, :security_group_client) sg_creator = Hailstorm::Model::Helper::SecurityGroupCreator.new(attrs) sg_creator.create_security_group end + # @return [Hailstorm::Model::Helper::AmiHelper] + def ami_helper + helper_attrs = {} + add_method_attrs!(helper_attrs, :ec2_instance_helper, :security_group_finder) + helper_group = Hailstorm::Model::Helper::AmiHelper::MemberHelperGroup.new(helper_attrs) + + client_attrs = { ami_client: client_factory.ami_client } + add_method_attrs!(client_attrs, :instance_client) + client_group = Hailstorm::Model::Helper::AmiHelper::ClientGroup.new(client_attrs) + + Hailstorm::Model::Helper::AmiHelper.new(aws_clusterable: self, + helper_group: helper_group, + client_group: client_group) + end + # creates the agent ami def create_agent_ami ami_helper.create_agent_ami! end + def client_factory + Hailstorm::Support::AwsAdapter.clients(aws_config) + end + + def aws_config + @aws_config ||= { + access_key_id: self.access_key, + secret_access_key: self.secret_key, + region: self.region, + logger: logger + } + end + + # Client methods for DI + module ClientMethods + # @return [Hailstorm::Behavior::AwsAdaptable::InstanceClient] + def instance_client + @instance_client ||= client_factory.instance_client + end + + # @return [Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient] + def security_group_client + @security_group_client ||= client_factory.security_group_client + end + + # @return [Hailstorm::Behavior::AwsAdaptable::Ec2Client] + def ec2_client + @ec2_client ||= client_factory.ec2_client + end + + # @return [Hailstorm::Behavior::AwsAdaptable::SubnetClient] + def subnet_client + @subnet_client ||= client_factory.subnet_client + end + end + + include ClientMethods + + def security_group_finder + attrs = { aws_clusterable: self } + add_method_attrs!(attrs, :security_group_client, :subnet_client) + Hailstorm::Model::Helper::SecurityGroupFinder.new(attrs) + end + + def ec2_instance_helper + Hailstorm::Model::Helper::Ec2InstanceHelper.new(aws_clusterable: self, instance_client: instance_client) + end + include Hailstorm::Support::Waiter end diff --git a/hailstorm-gem/lib/hailstorm/model/cluster.rb b/hailstorm-gem/lib/hailstorm/model/cluster.rb index d49e9b10..65e33ad9 100644 --- a/hailstorm-gem/lib/hailstorm/model/cluster.rb +++ b/hailstorm-gem/lib/hailstorm/model/cluster.rb @@ -256,7 +256,7 @@ def destroy_clusterable end def purge - cluster_instance.purge if cluster_instance.active + cluster_instance.purge end def set_cluster_code diff --git a/hailstorm-gem/lib/hailstorm/model/concern/abstract_clusterable.rb b/hailstorm-gem/lib/hailstorm/model/concern/abstract_clusterable.rb index 745ffa4e..1187a24b 100644 --- a/hailstorm-gem/lib/hailstorm/model/concern/abstract_clusterable.rb +++ b/hailstorm-gem/lib/hailstorm/model/concern/abstract_clusterable.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require 'hailstorm/model/concern' -require 'fileutils' -require 'hailstorm/support/amazon_account_cleaner' # Method implementation of `Clusterable` interface module Hailstorm::Model::Concern::AbstractClusterable @@ -16,26 +14,4 @@ def setup(force: false) provision_agents end - - # Delete SSH key-pair and identity once all load agents have been terminated - # (see Hailstorm::Behavior::Clusterable#cleanup) - def cleanup - logger.debug { "#{self.class}##{__method__}" } - return unless self.active? && self.autogenerated_ssh_key? && self.load_agents.reload.empty? - - key_pair_id = client_factory.key_pair_client.find(name: self.ssh_identity) - return unless key_pair_id - - client_factory.key_pair_client.delete(key_pair_id: key_pair_id) - FileUtils.safe_unlink(identity_file_path) - end - - # Purges the Amazon accounts used of Hailstorm related artifacts - def purge(cleaner = nil) - logger.debug { "#{self}.#{__method__}" } - cleaner ||= Hailstorm::Support::AmazonAccountCleaner.new(client_factory: client_factory, - region_code: self.region) - cleaner.cleanup - self.update_column(:agent_ami, nil) - end end diff --git a/hailstorm-gem/lib/hailstorm/model/helper/security_group_creator.rb b/hailstorm-gem/lib/hailstorm/model/helper/security_group_creator.rb index 46cf09a8..51e13d38 100644 --- a/hailstorm-gem/lib/hailstorm/model/helper/security_group_creator.rb +++ b/hailstorm-gem/lib/hailstorm/model/helper/security_group_creator.rb @@ -12,9 +12,9 @@ class Hailstorm::Model::Helper::SecurityGroupCreator < Hailstorm::Model::Helper: attr_reader :security_group_desc, :ssh_port, :region # @param [Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient] security_group_client - # @param [Hailstorm::Behavior::AwsAdaptable::Ec2Client] ec2_client + # @param [Hailstorm::Behavior::AwsAdaptable::SubnetClient] subnet_client # @param [Hailstorm::Model::AmazonCloud] aws_clusterable - def initialize(security_group_client:, ec2_client:, aws_clusterable:) + def initialize(security_group_client:, subnet_client:, aws_clusterable:) super @ssh_port = aws_clusterable.ssh_port || Hailstorm::Model::Helper::AmazonCloudDefaults::SSH_PORT @region = aws_clusterable.region diff --git a/hailstorm-gem/lib/hailstorm/model/helper/security_group_finder.rb b/hailstorm-gem/lib/hailstorm/model/helper/security_group_finder.rb index 0d5926b6..e8f55ae4 100644 --- a/hailstorm-gem/lib/hailstorm/model/helper/security_group_finder.rb +++ b/hailstorm-gem/lib/hailstorm/model/helper/security_group_finder.rb @@ -7,15 +7,17 @@ class Hailstorm::Model::Helper::SecurityGroupFinder include Hailstorm::Behavior::Loggable - attr_reader :ec2_client, :security_group_client, :vpc_subnet_id, :security_group + attr_reader :subnet_client, :security_group_client, :vpc_subnet_id, :security_group - # @param [Hailstorm::Behavior::AwsAdaptable::Ec2Client] ec2_client + # @param [Hailstorm::Behavior::AwsAdaptable::SubnetClient] subnet_client # @param [Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient] security_group_client # @param [Hailstorm::Model::AmazonCloud] aws_clusterable - def initialize(security_group_client:, aws_clusterable:, ec2_client: nil) - raise(ArgumentError, 'ec2_client needed if vpc_subnet_id provided') if aws_clusterable.vpc_subnet_id && !ec2_client + def initialize(security_group_client:, aws_clusterable:, subnet_client: nil) + if aws_clusterable.vpc_subnet_id && !subnet_client + raise(ArgumentError, 'ec2_client needed if vpc_subnet_id provided') + end - @ec2_client = ec2_client + @subnet_client = subnet_client @security_group_client = security_group_client @vpc_subnet_id = aws_clusterable.vpc_subnet_id @security_group = aws_clusterable.security_group @@ -29,6 +31,6 @@ def find_security_group(vpc_id: nil) protected def find_vpc - ec2_client.find_vpc(subnet_id: self.vpc_subnet_id) if self.vpc_subnet_id + subnet_client.find_vpc(subnet_id: self.vpc_subnet_id) if self.vpc_subnet_id end end diff --git a/hailstorm-gem/lib/hailstorm/model/helper/vpc_helper.rb b/hailstorm-gem/lib/hailstorm/model/helper/vpc_helper.rb index 64b8ce68..7e1ef235 100644 --- a/hailstorm-gem/lib/hailstorm/model/helper/vpc_helper.rb +++ b/hailstorm-gem/lib/hailstorm/model/helper/vpc_helper.rb @@ -11,13 +11,21 @@ class Hailstorm::Model::Helper::VpcHelper attr_reader :vpc_client, :subnet_client, :internet_gateway_client, :route_table_client - def initialize(vpc_client:, subnet_client:, internet_gateway_client:, route_table_client:) + # @param [Hailstorm::Behavior::AwsAdaptable::VpcClient] vpc_client + # @param [Hailstorm::Behavior::AwsAdaptable::SubnetClient] subnet_client + # @param [Hailstorm::Behavior::AwsAdaptable::InternetGatewayClient] internet_gateway_client + # @param [Hailstorm::Behavior::AwsAdaptable::RouteTableClient] route_table_client + def initialize(vpc_client: nil, subnet_client: nil, internet_gateway_client: nil, route_table_client: nil) @vpc_client = vpc_client @subnet_client = subnet_client @internet_gateway_client = internet_gateway_client @route_table_client = route_table_client end + def find_vpc_from_subnet(subnet_id:) + subnet_client.find_vpc(subnet_id: subnet_id) + end + # @param [String] subnet_name_tag # @param [String] vpc_name_tag # @param [String] cidr diff --git a/hailstorm-gem/lib/hailstorm/support/amazon_account_cleaner.rb b/hailstorm-gem/lib/hailstorm/support/amazon_account_cleaner.rb index e3d604fa..09546966 100644 --- a/hailstorm-gem/lib/hailstorm/support/amazon_account_cleaner.rb +++ b/hailstorm-gem/lib/hailstorm/support/amazon_account_cleaner.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require 'hailstorm/support' -require 'hailstorm/model/helper/amazon_cloud_defaults' require 'hailstorm/behavior/loggable' +require 'hailstorm/exceptions' # Standalone script to remove all artifacts associated with an Amazon account - # Instances, AMI, Snapshots, Key Pairs and Security Groups from every region @@ -12,91 +12,174 @@ class Hailstorm::Support::AmazonAccountCleaner attr_reader :doze_seconds, :region_code, - :ec2_client, - :key_pair_client, - :security_group_client, - :instance_client, :ami_client + :resource_group, + :client_factory + + # Resource group for resources to be removed + class AccountResourceGroup + attr_reader :instance_ids, + :ami_id, + :security_group_name, + :key_pair_name, + :subnet_id, + :vpc_id + + # @param [Hash] attrs Hash#keys instance_ids ami_id security_group_name key_pair_name subnet_id vpc_id + def initialize(attrs = {}) + @instance_ids = attrs[:instance_ids] || [] + @ami_id = attrs[:ami_id] + @security_group_name = attrs[:security_group_name] + @key_pair_name = attrs[:key_pair_name] + @subnet_id = attrs[:subnet_id] + @vpc_id = attrs[:vpc_id] + end + end - def initialize(client_factory:, region_code:, doze_seconds: 5) + CLEANUP_STEPS_ATTR_MAPS = [ + %i[terminate_instances instance_ids], + %i[delete_key_pairs key_pair_name], + %i[deregister_amis ami_id], + %i[delete_security_groups security_group_name], + %i[delete_subnet subnet_id], + %i[delete_vpc vpc_id] + ].freeze + + # @param [Hash] client_factory + # @param [String] region_code + # @param [AccountResourceGroup] resource_group + def initialize(client_factory:, + region_code:, + resource_group: AccountResourceGroup.new, + doze_seconds: 5) + @client_factory = client_factory @region_code = region_code + @resource_group = resource_group @doze_seconds = doze_seconds - @default_security_group = Hailstorm::Model::Helper::AmazonCloudDefaults::SECURITY_GROUP - @ec2_client = client_factory.ec2_client - @key_pair_client = client_factory.key_pair_client - @security_group_client = client_factory.security_group_client - @instance_client = client_factory.instance_client - @ami_client = client_factory.ami_client end - def cleanup(remove_key_pairs: false) - logger.info { "Scanning #{region_code} for running instances..." } - terminate_instances - - logger.info { "Scanning #{region_code} for available AMIs..." } - deregister_amis - - logger.info { "Scanning #{region_code} for completed Snapshots..." } - delete_snapshots - - logger.info { "Scanning #{region_code} for #{@default_security_group} security group..." } - delete_security_groups - - if remove_key_pairs - logger.info { "Scanning #{region_code} for key pairs..." } - delete_key_pairs + def cleanup + logger.info "Cleaning up in AWS Region #{region_code}..." + all_steps = determine_cleanup_steps + completed_steps = [] + begin + all_steps.each do |step| + send(step) + completed_steps.push(step) + end + logger.info "Cleanup Done in AWS Region #{region_code}!" + rescue Hailstorm::AwsException => error + cleanup_error = Hailstorm::AwsException.new("#{error.message}. All remaining steps were skipped.") + cleanup_error.retryable = error.retryable? + cleanup_error.data = all_steps - completed_steps + logger.debug do + "Cleanup in AWS Region #{region_code} failed with pending steps: #{cleanup_error.data.join(', ')}" + end + + raise(cleanup_error) end - - logger.info 'Cleanup Done!' end private + def determine_cleanup_steps + CLEANUP_STEPS_ATTR_MAPS.each_with_object([]) do |attr_map, steps_to_execute| + step, resource_attr = attr_map + steps_to_execute.push(step) unless resource_group.send(resource_attr).blank? + end + end + def terminate_instances - instance_client.list.each do |instance| + client_factory.instance_client.list(instance_ids: resource_group.instance_ids).each do |instance| next if instance.status == :terminated logger.info { "Terminating instance##{instance.id}..." } - instance_client.terminate(instance_id: instance.id) - sleep(doze_seconds) until instance_client.terminated?(instance_id: instance.id) + client_factory.instance_client.terminate(instance_id: instance.id) + sleep(doze_seconds) until client_factory.instance_client.terminated?(instance_id: instance.id) logger.info { "... instance##{instance.id} terminated" } end end + def delete_key_pairs + key_pair_id = client_factory.key_pair_client.find(name: resource_group.key_pair_name) + return if key_pair_id.blank? + + client_factory.key_pair_client.delete(key_pair_id: key_pair_id) + logger.info { "Key pair##{resource_group.key_pair_name} deleted" } + end + def deregister_amis - ami_client.select_self_owned( - ami_name_regexp: Regexp.new(Hailstorm::Model::Helper::AmazonCloudDefaults::AMI_ID) - ).each do |image| - next unless image.available? - - logger.info { "Deregistering AMI##{image.id}..." } - ami_client.deregister(ami_id: image.id) - logger.info { "... AMI##{image.id} deregistered" } - end + image = find_ami + return if image.nil? || !image.available? + + logger.info { "Deregistering AMI##{image.id}..." } + client_factory.ami_client.deregister(ami_id: image.id) + logger.info { "... AMI##{image.id} deregistered" } + + logger.info { "Deleting snapshot##{image.snapshot_id}" } + client_factory.ec2_client.delete_snapshot(snapshot_id: image.snapshot_id) + logger.info { "... snapshot##{image.snapshot_id} deleted" } end - def delete_snapshots - ec2_client.find_self_owned_snapshots.each do |snapshot| - next unless snapshot.completed? + def find_ami + client_factory.ami_client.find(ami_id: resource_group.ami_id, filters: [:created]) + rescue Hailstorm::AwsException + nil + end - logger.info { "Deleting snapshot##{snapshot.id}" } - ec2_client.delete_snapshot(snapshot_id: snapshot.id) - logger.info { "... snapshot##{snapshot.id} deleted" } - end + def delete_security_groups + security_group = client_factory.security_group_client.find(name: resource_group.security_group_name, + vpc_id: resource_group.vpc_id, + filters: [:created]) + return if security_group.nil? + + client_factory.security_group_client.delete(group_id: security_group.id) + logger.info { "Security group##{resource_group.security_group_name} deleted" } end - def delete_key_pairs - key_pair_client.list.each do |key_pair| - key_pair_client.delete(key_pair_id: key_pair.key_pair_id) - logger.info { "Key pair##{key_pair.key_name} deleted" } + def delete_subnet + subnet_id = begin + client_factory.subnet_client.find(subnet_id: resource_group.subnet_id, filters: [:created]) + rescue StandardError + nil + end + return if subnet_id.nil? + + client_factory.subnet_client.delete(subnet_id: subnet_id) + logger.info { "Subnet##{subnet_id} deleted" } + end + + def delete_vpc + vpc = client_factory.vpc_client.find(vpc_id: resource_group.vpc_id, filters: [:created]) + return if vpc.nil? + + # VPC can't be deleted till sub-resources are deleted + delete_routing_tables + delete_internet_gateways + client_factory.vpc_client.delete(vpc_id: vpc.id) + logger.info { "VPC##{vpc.id} deleted" } + end + + # Delete routing table created by Hailstorm + def delete_routing_tables + client_factory + .route_table_client + .route_tables(vpc_id: resource_group.vpc_id, filters: [:created]) + .reject(&:main) + .each do |route_table| + + client_factory.route_table_client.delete(route_table_id: route_table.id) end end - def delete_security_groups - security_group_client.list.each do |security_group| - next unless security_group.group_name == @default_security_group + # Delete internet gateway + def delete_internet_gateways + client_factory + .internet_gateway_client + .select(vpc_id: resource_group.vpc_id, filters: [:created]) + .each do |igw| - security_group_client.delete(group_id: security_group.group_id) - logger.info { "Security group##{security_group.group_name} deleted" } + client_factory.internet_gateway_client.detach_from_vpc(igw_id: igw.id, vpc_id: resource_group.vpc_id) + client_factory.internet_gateway_client.delete(igw_id: igw.id) end end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/abstract_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/abstract_client.rb index 52dae0c7..eb71f22a 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/abstract_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/abstract_client.rb @@ -15,7 +15,53 @@ def initialize(ec2_client:) @ec2 = ec2_client end + # @param [String] resource_id + # @param [String] name def tag_name(resource_id:, name:) - ec2.create_tags(resources: [resource_id], tags: [{ key: 'Name', value: name }]) + tag_resource(resource_id, key: 'Name', value: name) + end + + # @param [String] resource_id + # @param [String] key + # @param [Object] value + def tag_resource(resource_id, key:, value:) + ec2.create_tags(resources: [resource_id], tags: [{ key: key, value: value.to_s }]) + end + + CREATED_TAG = { key: 'hailstorm:created', value: true.to_s }.freeze + CREATED_FILTER = { name: "tag:#{CREATED_TAG[:key]}", values: [CREATED_TAG[:value]] }.freeze + + protected + + # Iterates through filters and adds to params[:filters] (mutates params). + # :filters key is initialized to an Array if not present in params and filters is not empty. + # If filters is empty, params is not mutated. + def add_filters_to_params(filters, params) + return if filters.empty? + + params[:filters] ||= [] + filters.each_with_object(params[:filters]) do |filter, object| + object << filter if filter.is_a?(Hash) + object << CREATED_FILTER if filter == :created + end + end + + # @param [String] resource_type + # @param [Hash, NilClass] params + # @return [Hash] + def created_tag_specifications(resource_type, params = nil) + tag_spec = { + tag_specifications: [{ + resource_type: resource_type, + tags: [CREATED_TAG] + }] + } + + params ? params.merge(tag_spec) : tag_spec + end + + # @return [Hash] + def created_tag + CREATED_TAG end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ami_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ami_client.rb index c30cf441..d6a0a982 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ami_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ami_client.rb @@ -15,10 +15,13 @@ def decorate(ami) Hailstorm::Behavior::AwsAdaptable::StateReason.new(code: ami.state_reason.code, message: ami.state_reason.message) end + ebs_mapping = ami.block_device_mappings.select { |device| device.ebs&.snapshot_id }.first + snapshot_id = ebs_mapping.ebs.snapshot_id if ebs_mapping Hailstorm::Behavior::AwsAdaptable::Ami.new(image_id: ami.image_id, name: ami.name, state: ami.state, - state_reason: state_reason) + state_reason: state_reason, + snapshot_id: snapshot_id) end private :decorate @@ -30,7 +33,9 @@ def available?(ami_id:) # @see Hailstorm::Behavior::AwsAdaptable::AmiClient#register_ami def register_ami(name:, instance_id:, description:) - resp = ec2.create_image(name: name, instance_id: instance_id, description: description) + params = { name: name, instance_id: instance_id, description: description } + resp = ec2.create_image(params) + tag_resource(resp.image_id, created_tag) resp.image_id end @@ -38,8 +43,10 @@ def deregister(ami_id:) ec2.deregister_image(image_id: ami_id) end - def find(ami_id:) - resp = ec2.describe_images(image_ids: [ami_id]) + def find(ami_id:, filters: []) + params = { image_ids: [ami_id] } + add_filters_to_params(filters, params) + resp = ec2.describe_images(params) return nil if resp.images.empty? decorate(resp.images[0]) diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ec2_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ec2_client.rb index 07d7473e..d0b0afc6 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ec2_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/ec2_client.rb @@ -12,13 +12,6 @@ def first_available_zone zone ? zone.zone_name : nil end - def find_vpc(subnet_id:) - resp = ec2.describe_subnets(subnet_ids: [subnet_id]) - return nil if resp.subnets.empty? - - resp.subnets[0].vpc_id - end - def find_self_owned_snapshots resp = ec2.describe_snapshots(owner_ids: ['self']) resp.snapshots.lazy.map do |snapshot| diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/instance_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/instance_client.rb index a65b47f8..abfe4cde 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/instance_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/instance_client.rb @@ -90,7 +90,7 @@ def create(instance_attrs, min_count: 1, max_count: 1) req_attrs[:min_count] = min_count req_attrs[:max_count] = max_count req_attrs[:placement] = instance_attrs.slice(:availability_zone) if instance_attrs.key?(:availability_zone) - instance = ec2.run_instances(req_attrs).instances[0] + instance = ec2.run_instances(created_tag_specifications('instance', req_attrs)).instances[0] decorate(instance: instance) end @@ -113,7 +113,11 @@ def systems_ok(instance) end private :systems_ok - def list - ec2.describe_instances.reservations.flat_map(&:instances).lazy.map { |instance| decorate(instance: instance) } + def list(instance_ids: nil) + params = {} + params[:instance_ids] = instance_ids unless instance_ids.nil? + ec2.describe_instances(params).reservations.flat_map(&:instances).lazy.map do |instance| + decorate(instance: instance) + end end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/internet_gateway_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/internet_gateway_client.rb index 0274687f..12c143fe 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/internet_gateway_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/internet_gateway_client.rb @@ -9,7 +9,23 @@ def attach(igw_id:, vpc_id:) end def create - resp = ec2.create_internet_gateway + resp = ec2.create_internet_gateway(created_tag_specifications('internet-gateway')) resp.internet_gateway.internet_gateway_id end + + def select(vpc_id:, filters: []) + params = { filters: [{ name: 'attachment.vpc-id', values: [vpc_id] }] } + add_filters_to_params(filters, params) + ec2.describe_internet_gateways(params) + .internet_gateways + .map { |igw| Hailstorm::Behavior::AwsAdaptable::InternetGateway.new(id: igw.internet_gateway_id) } + end + + def delete(igw_id:) + ec2.delete_internet_gateway(internet_gateway_id: igw_id) + end + + def detach_from_vpc(igw_id:, vpc_id:) + ec2.detach_internet_gateway(internet_gateway_id: igw_id, vpc_id: vpc_id) + end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/key_pair_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/key_pair_client.rb index 55042eae..94672ad9 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/key_pair_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/key_pair_client.rb @@ -19,7 +19,7 @@ def delete(key_pair_id:) end def create(name:) - resp = ec2.create_key_pair(key_name: name) + resp = ec2.create_key_pair(created_tag_specifications('key-pair', key_name: name)) attribute_keys = %i[key_fingerprint key_material key_name key_pair_id] Hailstorm::Behavior::AwsAdaptable::KeyPair.new(resp.to_h.slice(*attribute_keys)) end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/route_table_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/route_table_client.rb index b1d467c3..f8a3f9c5 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/route_table_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/route_table_client.rb @@ -10,7 +10,7 @@ def associate_with_subnet(route_table_id:, subnet_id:) end def create(vpc_id:) - resp = ec2.create_route_table(vpc_id: vpc_id) + resp = ec2.create_route_table(created_tag_specifications('route-table', vpc_id: vpc_id)) resp.route_table.route_table_id end @@ -21,8 +21,8 @@ def create_route(route_table_id:, cidr:, internet_gateway_id:) end def main_route_table(vpc_id:) - resp = ec2.describe_route_tables(filters: [{ name: 'vpc-id', values: [vpc_id] }]) - rtb = resp.route_tables.find do |route_table| + params = { filters: [to_vpc_filter(vpc_id)] } + rtb = describe_route_tables(params).find do |route_table| route_table.associations.any?(&:main) end @@ -36,4 +36,29 @@ def routes(route_table_id:) .routes .map { |route| Hailstorm::Behavior::AwsAdaptable::Route.new(state: route.state) } end + + def route_tables(vpc_id:, filters: []) + params = { filters: [to_vpc_filter(vpc_id)] } + add_filters_to_params(filters, params) + describe_route_tables(params).map do |route_table| + Hailstorm::Behavior::AwsAdaptable::RouteTable.new(id: route_table.route_table_id, + main: route_table.associations.any?(&:main)) + end + end + + def delete(route_table_id:) + ec2.delete_route_table(route_table_id: route_table_id) + end + + private + + def to_vpc_filter(vpc_id) + { name: 'vpc-id', values: [vpc_id] } + end + + # @param [Hash] params + # @return [Array] + def describe_route_tables(params) + ec2.describe_route_tables(params).route_tables + end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb index 9b17330e..b4656d8d 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/security_group_client.rb @@ -4,10 +4,12 @@ class Hailstorm::Support::AwsAdapter::SecurityGroupClient < Hailstorm::Support::AwsAdapter::AbstractClient include Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient - def find(name:, vpc_id: nil) - filters = [{ name: 'group-name', values: [name] }] - filters.push(name: 'vpc-id', values: [vpc_id]) if vpc_id - resp = ec2.describe_security_groups(filters: filters) + def find(name:, vpc_id: nil, filters: []) + sg_filters = [{ name: 'group-name', values: [name] }] + sg_filters.push(name: 'vpc-id', values: [vpc_id]) if vpc_id + params = { filters: sg_filters } + add_filters_to_params(filters, params) + resp = ec2.describe_security_groups(params) return if resp.security_groups.empty? security_group = resp.security_groups[0] @@ -17,7 +19,8 @@ def find(name:, vpc_id: nil) end def create(name:, description:, vpc_id: nil) - resp = ec2.create_security_group(group_name: name, description: description, vpc_id: vpc_id) + params = { group_name: name, description: description, vpc_id: vpc_id } + resp = ec2.create_security_group(created_tag_specifications('security-group', params)) Hailstorm::Behavior::AwsAdaptable::SecurityGroup.new(group_id: resp.group_id) end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/subnet_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/subnet_client.rb index 4d913696..c09da1f0 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/subnet_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/subnet_client.rb @@ -10,12 +10,14 @@ def available?(subnet_id:) end def create(vpc_id:, cidr:) - resp = ec2.create_subnet(cidr_block: cidr, vpc_id: vpc_id) + resp = ec2.create_subnet(created_tag_specifications('subnet', cidr_block: cidr, vpc_id: vpc_id)) resp.subnet.subnet_id end - def find(subnet_id: nil, name_tag: nil) - resp = query(subnet_id: subnet_id, name_tag: name_tag) + def find(subnet_id: nil, name_tag: nil, filters: []) + params = { subnet_id: subnet_id, name_tag: name_tag } + add_filters_to_params(filters, params) + resp = query(params) return nil if resp.subnets.empty? resp.subnets[0].subnet_id @@ -27,13 +29,25 @@ def modify_attribute(subnet_id:, **kwargs) ec2.modify_subnet_attribute(attrs) end + def delete(subnet_id:) + ec2.delete_subnet(subnet_id: subnet_id) + end + + def find_vpc(subnet_id:) + resp = ec2.describe_subnets(subnet_ids: [subnet_id]) + return nil if resp.subnets.empty? + + resp.subnets[0].vpc_id + end + private - def query(subnet_id: nil, name_tag: nil) + def query(subnet_id: nil, name_tag: nil, filters: []) params = {} params[:subnet_ids] = [subnet_id] if subnet_id + params[:filters] = filters unless filters.blank? if name_tag - params[:filters] = [] + params[:filters] = [] unless params.key?(:filters) params[:filters].push(name: 'tag:Name', values: [name_tag]) end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/vpc_client.rb b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/vpc_client.rb index 7e16cbb8..94a801e2 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/vpc_client.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_adapter_clients/vpc_client.rb @@ -5,7 +5,7 @@ class Hailstorm::Support::AwsAdapter::VpcClient < Hailstorm::Support::AwsAdapter include Hailstorm::Behavior::AwsAdaptable::VpcClient def create(cidr:) - resp = ec2.create_vpc(cidr_block: cidr) + resp = ec2.create_vpc(created_tag_specifications('vpc', cidr_block: cidr)) resp.vpc.vpc_id end @@ -14,7 +14,21 @@ def modify_attribute(vpc_id:, **kwargs) end def available?(vpc_id:) - resp = ec2.describe_vpcs(vpc_ids: [vpc_id]) - !resp.vpcs.blank? && resp.vpcs[0].state.to_sym == :available + vpc = find(vpc_id: vpc_id) + vpc&.available? + end + + def find(vpc_id:, filters: []) + params = { vpc_ids: [vpc_id] } + add_filters_to_params(filters, params) + resp = ec2.describe_vpcs(params) + return if resp.vpcs.empty? + + vpc = resp.vpcs.first + Hailstorm::Behavior::AwsAdaptable::Vpc.new(vpc_id: vpc.vpc_id, state: vpc.state) + end + + def delete(vpc_id:) + ec2.delete_vpc(vpc_id: vpc_id) end end diff --git a/hailstorm-gem/lib/hailstorm/support/aws_exception_builder.rb b/hailstorm-gem/lib/hailstorm/support/aws_exception_builder.rb index 9fdaa29d..9dd12234 100644 --- a/hailstorm-gem/lib/hailstorm/support/aws_exception_builder.rb +++ b/hailstorm-gem/lib/hailstorm/support/aws_exception_builder.rb @@ -9,6 +9,9 @@ module Hailstorm::Support::AwsExceptionBuilder def self.from(aws_error) exception = Hailstorm::AwsException.new(aws_error.message) exception.retryable = aws_error.retryable? + exception.data = aws_error.data + exception.code = aws_error.code + exception.context = aws_error.context exception end end diff --git a/hailstorm-gem/lib/hailstorm/version.rb b/hailstorm-gem/lib/hailstorm/version.rb index e813dbc4..544f0cc1 100644 --- a/hailstorm-gem/lib/hailstorm/version.rb +++ b/hailstorm-gem/lib/hailstorm/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Hailstorm - VERSION = '5.1.15' + VERSION = '5.1.16' end diff --git a/hailstorm-gem/spec/model/amazon_cloud_spec.rb b/hailstorm-gem/spec/model/amazon_cloud_spec.rb index 921c7132..2751d26c 100644 --- a/hailstorm-gem/spec/model/amazon_cloud_spec.rb +++ b/hailstorm-gem/spec/model/amazon_cloud_spec.rb @@ -163,11 +163,12 @@ def stub_aws!(aws) end it 'should delegate to helper' do - allow(@aws).to receive(:client_factory) - .and_return(Hailstorm::Behavior::AwsAdaptable::ClientFactory.new( - ec2_client: instance_double(Hailstorm::Behavior::AwsAdaptable::Ec2Client), - security_group_client: instance_double(Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient) - )) + allow(@aws).to receive(:client_factory).and_return( + Hailstorm::Behavior::AwsAdaptable::ClientFactory.new( + subnet_client: instance_double(Hailstorm::Behavior::AwsAdaptable::SubnetClient), + security_group_client: instance_double(Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient) + ) + ) expect_any_instance_of(Hailstorm::Model::Helper::SecurityGroupCreator).to receive(:create_security_group) @aws.send(:create_security_group) @@ -485,4 +486,53 @@ def stub_aws!(aws) allow(@aws).to receive(:client_factory).and_return(mock_client_factory) expect { @aws.send(:ec2_instance_helper) }.to_not raise_error end + + context '#cleanup' do + before(:each) do + @aws.active = true + @aws.autogenerated_ssh_key = true + allow(@aws).to receive(:identity_file_path).and_return('secure.pem') + @aws.ssh_identity = 'secure' + @mock_key_pair_client = instance_double(Hailstorm::Behavior::AwsAdaptable::KeyPairClient) + client_factory = Hailstorm::Behavior::AwsAdaptable::ClientFactory.new(key_pair_client: @mock_key_pair_client) + allow(@aws).to receive(:client_factory).and_return(client_factory) + end + + context 'key_pair exists' do + it 'should delete the key_pair' do + allow(@mock_key_pair_client).to receive(:find).and_return('key-pair-123') + expect(@mock_key_pair_client).to receive(:delete).with(key_pair_id: 'key-pair-123') + expect(FileUtils).to receive(:safe_unlink) + @aws.cleanup + end + end + + context 'key_pair does not exist' do + it 'should do nothing' do + allow(@mock_key_pair_client).to receive(:find).and_return(nil) + expect(@mock_key_pair_client).to_not receive(:delete) + expect(FileUtils).to_not receive(:safe_unlink) + @aws.cleanup + end + end + end + + context '#purge' do + it 'should clean the regions with active Amazon Cloud clusters' do + attrs = { access_key: 'foo-east-1', secret_key: 'bar-east-1', region: 'us-east-1', active: false } + clusterable = Hailstorm::Model::AmazonCloud.new(attrs) + clusterable.project = Hailstorm::Model::Project.new(project_code: Digest::SHA2.new.to_s[0..5]) + stub_aws!(clusterable) + clusterable.save! + clusterable.update_column(:active, true) + + mock_subnet_client = instance_double(Hailstorm::Behavior::AwsAdaptable::SubnetClient) + allow(mock_subnet_client).to receive(:find_vpc).and_return('vpc-123') + allow(clusterable).to receive(:subnet_client).and_return(mock_subnet_client) + mock_cleaner = instance_double(Hailstorm::Support::AmazonAccountCleaner) + expect(mock_cleaner).to receive(:cleanup) + clusterable.purge(mock_cleaner) + expect(clusterable.agent_ami).to be_nil + end + end end diff --git a/hailstorm-gem/spec/model/cluster_spec.rb b/hailstorm-gem/spec/model/cluster_spec.rb index 7f42af30..9ddf7633 100644 --- a/hailstorm-gem/spec/model/cluster_spec.rb +++ b/hailstorm-gem/spec/model/cluster_spec.rb @@ -490,21 +490,6 @@ def clusterables_stub! expect_any_instance_of(Hailstorm::Model::AmazonCloud).to receive(:purge) Hailstorm::Model::Cluster.first.purge end - - it 'should not purge inactive clusters' do - config = Hailstorm::Support::Configuration.new - config.clusters(:amazon_cloud) do |aws| - aws.access_key = 'key-1' - aws.secret_key = 'secret-1' - aws.region = 'us-east-1' - aws.active = false - end - - clusterables_stub! - Hailstorm::Model::Cluster.configure_all(@project, config) - expect_any_instance_of(Hailstorm::Model::AmazonCloud).to_not receive(:purge) - Hailstorm::Model::Cluster.first.purge - end end context '#cluster_klass' do diff --git a/hailstorm-gem/spec/model/concern/abstract_clusterable_spec.rb b/hailstorm-gem/spec/model/concern/abstract_clusterable_spec.rb index be59e7c6..34559830 100644 --- a/hailstorm-gem/spec/model/concern/abstract_clusterable_spec.rb +++ b/hailstorm-gem/spec/model/concern/abstract_clusterable_spec.rb @@ -67,50 +67,4 @@ def stub_aws!(aws) end end end - - context '#cleanup' do - before(:each) do - @aws.active = true - @aws.autogenerated_ssh_key = true - allow(@aws).to receive(:identity_file_path).and_return('secure.pem') - @aws.ssh_identity = 'secure' - @mock_key_pair_client = instance_double(Hailstorm::Behavior::AwsAdaptable::KeyPairClient) - client_factory = Hailstorm::Behavior::AwsAdaptable::ClientFactory.new(key_pair_client: @mock_key_pair_client) - allow(@aws).to receive(:client_factory).and_return(client_factory) - end - - context 'key_pair exists' do - it 'should delete the key_pair' do - allow(@mock_key_pair_client).to receive(:find).and_return('key-pair-123') - expect(@mock_key_pair_client).to receive(:delete).with(key_pair_id: 'key-pair-123') - expect(FileUtils).to receive(:safe_unlink) - @aws.cleanup - end - end - - context 'key_pair does not exist' do - it 'should do nothing' do - allow(@mock_key_pair_client).to receive(:find).and_return(nil) - expect(@mock_key_pair_client).to_not receive(:delete) - expect(FileUtils).to_not receive(:safe_unlink) - @aws.cleanup - end - end - end - - context '#purge' do - it 'should clean the regions with active Amazon Cloud clusters' do - attrs = { access_key: 'foo-east-1', secret_key: 'bar-east-1', region: 'us-east-1', active: false } - clusterable = Hailstorm::Model::AmazonCloud.new(attrs) - clusterable.project = Hailstorm::Model::Project.new(project_code: Digest::SHA2.new.to_s[0..5]) - stub_aws!(clusterable) - clusterable.save! - clusterable.update_column(:active, true) - - mock_cleaner = instance_double(Hailstorm::Support::AmazonAccountCleaner) - expect(mock_cleaner).to receive(:cleanup) - clusterable.purge(mock_cleaner) - expect(clusterable.agent_ami).to be_nil - end - end end diff --git a/hailstorm-gem/spec/model/helper/ami_helper_spec.rb b/hailstorm-gem/spec/model/helper/ami_helper_spec.rb index a7415cc0..65f1bdb8 100644 --- a/hailstorm-gem/spec/model/helper/ami_helper_spec.rb +++ b/hailstorm-gem/spec/model/helper/ami_helper_spec.rb @@ -118,7 +118,7 @@ @aws.project = Hailstorm::Model::Project.create!(project_code: __FILE__) mock_ec2_ami = Hailstorm::Behavior::AwsAdaptable::Ami.new(state: 'available', name: @helper.send(:ami_id), - ami_id: 'ami-123') + image_id: 'ami-123') allow(@mock_ami_client).to receive(:find_self_owned).and_return(mock_ec2_ami) @helper.send(:check_for_existing_ami) expect(@aws.agent_ami).to be == mock_ec2_ami.id diff --git a/hailstorm-gem/spec/model/helper/security_group_creator_spec.rb b/hailstorm-gem/spec/model/helper/security_group_creator_spec.rb index 1f16b1bf..2bf6411a 100644 --- a/hailstorm-gem/spec/model/helper/security_group_creator_spec.rb +++ b/hailstorm-gem/spec/model/helper/security_group_creator_spec.rb @@ -13,15 +13,15 @@ allow(mock_sg_client).to receive(:create).and_return(mock_sec_group) expect(mock_sg_client).to receive(:authorize_ingress).exactly(3).times expect(mock_sg_client).to receive(:allow_ping) - mock_ec2_client = instance_double(Hailstorm::Behavior::AwsAdaptable::Ec2Client) - allow(mock_ec2_client).to receive(:find_vpc).and_return('vpc-123') + mock_subnet_client = instance_double(Hailstorm::Behavior::AwsAdaptable::SubnetClient) + allow(mock_subnet_client).to receive(:find_vpc).and_return('vpc-123') mock_aws = Hailstorm::Model::AmazonCloud.new(region: 'us-east-1', security_group: 'hailstorm', ssh_port: 22, vpc_subnet_id: 'subnet-123') helper = Hailstorm::Model::Helper::SecurityGroupCreator.new(security_group_client: mock_sg_client, aws_clusterable: mock_aws, - ec2_client: mock_ec2_client) + subnet_client: mock_subnet_client) helper.create_security_group end end diff --git a/hailstorm-gem/spec/model/helper/security_group_finder_spec.rb b/hailstorm-gem/spec/model/helper/security_group_finder_spec.rb index 29517326..f1bd5008 100644 --- a/hailstorm-gem/spec/model/helper/security_group_finder_spec.rb +++ b/hailstorm-gem/spec/model/helper/security_group_finder_spec.rb @@ -20,10 +20,10 @@ it 'should find the security group if vpc_subnet_id is provided' do @mock_aws.vpc_subnet_id = 'subnet-123' - mock_ec2_client = instance_double(Hailstorm::Behavior::AwsAdaptable::Ec2Client) - allow(mock_ec2_client).to receive(:find_vpc).and_return('vpc-123') + mock_subnet_client = instance_double(Hailstorm::Behavior::AwsAdaptable::SubnetClient) + allow(mock_subnet_client).to receive(:find_vpc).and_return('vpc-123') finder = Hailstorm::Model::Helper::SecurityGroupFinder.new(security_group_client: @mock_sg_client, - ec2_client: mock_ec2_client, + subnet_client: mock_subnet_client, aws_clusterable: @mock_aws) expect(finder.find_security_group).to be == @mock_sec_group end diff --git a/hailstorm-gem/spec/support/amazon_account_cleaner_spec.rb b/hailstorm-gem/spec/support/amazon_account_cleaner_spec.rb index 6dd0ef0f..e9cbb54a 100644 --- a/hailstorm-gem/spec/support/amazon_account_cleaner_spec.rb +++ b/hailstorm-gem/spec/support/amazon_account_cleaner_spec.rb @@ -6,26 +6,59 @@ describe Hailstorm::Support::AmazonAccountCleaner do before(:each) do @client_factory = Hailstorm::Behavior::AwsAdaptable::ClientFactory.new( - ec2_client: instance_double(Hailstorm::Behavior::AwsAdaptable::Ec2Client), - key_pair_client: instance_double(Hailstorm::Behavior::AwsAdaptable::KeyPairClient), - security_group_client: instance_double(Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient), - instance_client: instance_double(Hailstorm::Behavior::AwsAdaptable::InstanceClient), - ami_client: instance_double(Hailstorm::Behavior::AwsAdaptable::AmiClient) + ec2_client: instance_double(Hailstorm::Behavior::AwsAdaptable::Ec2Client, 'ec2_client'), + key_pair_client: instance_double(Hailstorm::Behavior::AwsAdaptable::KeyPairClient, 'key_pair_client'), + security_group_client: instance_double(Hailstorm::Behavior::AwsAdaptable::SecurityGroupClient, + 'security_group_client'), + instance_client: instance_double(Hailstorm::Behavior::AwsAdaptable::InstanceClient, 'instance_client'), + ami_client: instance_double(Hailstorm::Behavior::AwsAdaptable::AmiClient, 'ami_client'), + route_table_client: instance_double(Hailstorm::Behavior::AwsAdaptable::RouteTableClient, 'route_table_client'), + internet_gateway_client: instance_double(Hailstorm::Behavior::AwsAdaptable::InternetGatewayClient, + 'internet_gateway_client') + ) + + @resource_group = Hailstorm::Support::AmazonAccountCleaner::AccountResourceGroup.new( + instance_ids: %w[id-1 id-2 id-3], + ami_id: 'ami-123', + vpc_id: 'vpc-123', + security_group_name: 'hailstorm', + key_pair_name: 'secure', + subnet_id: 'subnet-123' ) @account_cleaner = Hailstorm::Support::AmazonAccountCleaner.new(client_factory: @client_factory, region_code: 'us-east-1', + resource_group: @resource_group, doze_seconds: 0) end it 'clean an AWS account of Hailstorm artifacts' do expect(@account_cleaner).to receive(:terminate_instances) expect(@account_cleaner).to receive(:deregister_amis) - expect(@account_cleaner).to receive(:delete_snapshots) expect(@account_cleaner).to receive(:delete_security_groups) expect(@account_cleaner).to receive(:delete_key_pairs) + expect(@account_cleaner).to receive(:delete_subnet) + expect(@account_cleaner).to receive(:delete_vpc) + + @account_cleaner.cleanup + end - @account_cleaner.cleanup(remove_key_pairs: true) + context 'when interim steps fail' do + it 'should throw an exception with information on skipped steps' do + allow(@account_cleaner).to receive(:deregister_amis).and_raise(Hailstorm::AwsException, 'mock error') + allow(@client_factory.instance_client).to receive(:list).and_return([]) + expect(@account_cleaner).to receive(:delete_key_pairs) + begin + @account_cleaner.cleanup + raise('Control should not reach here') + rescue Hailstorm::AwsException => error + expect(error.message).to be == 'mock error. All remaining steps were skipped.' + expect(error.data).to be == %i[deregister_amis delete_security_groups delete_subnet delete_vpc] + end + + expect(@account_cleaner).to_not receive(:delete_subnet) + expect(@account_cleaner).to_not receive(:delete_vpc) + end end context '#terminate_instances' do @@ -60,36 +93,24 @@ context '#deregister_amis' do it 'should deregister all owned AMIs' do - images = [{ state: 'pending', image_id: 'ami-1' }, { state: 'available', image_id: 'ami-2' }] - .map { |attrs| Hailstorm::Behavior::AwsAdaptable::Ami.new(attrs) } - - expect(@client_factory.ami_client).to receive(:deregister).once - expect(@client_factory.ami_client).to_not receive(:deregister).with(ami_id: 'ami-1') - allow(@client_factory.ami_client).to receive(:select_self_owned).and_return(images) + expect(@client_factory.ami_client).to receive(:deregister).with(ami_id: 'ami-2') + expect(@client_factory.ec2_client).to receive(:delete_snapshot).with(snapshot_id: 'snap-2') + image = Hailstorm::Behavior::AwsAdaptable::Ami.new(state: 'available', image_id: 'ami-2', snapshot_id: 'snap-2') + allow(@client_factory.ami_client).to receive(:find).and_return(image) @account_cleaner.send(:deregister_amis) end end - context '#delete_snapshots' do - it 'should delete all owned snapshots' do - pending_snapshot = Hailstorm::Behavior::AwsAdaptable::Snapshot.new(state: 'pending', snapshot_id: 'snap-1') - completed_snapshot = Hailstorm::Behavior::AwsAdaptable::Snapshot.new(state: 'completed', snapshot_id: 'snap-2') - snapshots = [pending_snapshot, completed_snapshot] - - expect(@client_factory.ec2_client).to_not receive(:delete_snapshot).with(snapshot_id: pending_snapshot.id) - expect(@client_factory.ec2_client).to receive(:delete_snapshot).with(snapshot_id: completed_snapshot.id) - allow(@client_factory.ec2_client).to receive(:find_self_owned_snapshots).and_return(snapshots.each) - - @account_cleaner.send(:delete_snapshots) - end - end - context '#delete_key_pairs' do it 'should delete key_pairs' do - key_pair_info = Hailstorm::Behavior::AwsAdaptable::KeyPairInfo.new(key_name: 's', key_pair_id: 'kp-123') + key_pair_info = Hailstorm::Behavior::AwsAdaptable::KeyPairInfo.new( + key_name: @resource_group.key_pair_name, + key_pair_id: 'kp-123' + ) + expect(@client_factory.key_pair_client).to receive(:delete).with(key_pair_id: 'kp-123') - allow(@client_factory.key_pair_client).to receive(:list).and_return([key_pair_info].each) + allow(@client_factory.key_pair_client).to receive(:find).and_return(key_pair_info.key_pair_id) @account_cleaner.send(:delete_key_pairs) end end @@ -97,13 +118,53 @@ context '#delete_security_groups' do it 'should delete default Hailstorm security group' do sec_group = Hailstorm::Behavior::AwsAdaptable::SecurityGroup.new( - group_name: Hailstorm::Model::Helper::AmazonCloudDefaults::SECURITY_GROUP, + group_name: @resource_group.security_group_name, group_id: 'sg-123' ) expect(@client_factory.security_group_client).to receive(:delete).with(group_id: 'sg-123') - allow(@client_factory.security_group_client).to receive(:list).and_return([sec_group].each) + allow(@client_factory.security_group_client).to receive(:find).and_return(sec_group) @account_cleaner.send(:delete_security_groups) end end + + context '#delete_subnet' do + it 'should delete a Hailstorm tagged subnet' do + expect(@client_factory.subnet_client).to receive(:delete) + allow(@client_factory.subnet_client).to receive(:find).and_return('subnet-123') + @account_cleaner.send(:delete_subnet) + end + end + + context '#delete_vpc' do + it 'should delete a Hailstorm tagged vpc' do + vpc = Hailstorm::Behavior::AwsAdaptable::Vpc.new(vpc_id: 'vpc-123', state: 'available') + expect(@client_factory.vpc_client).to receive(:delete) + allow(@client_factory.vpc_client).to receive(:find).and_return(vpc) + allow(@account_cleaner).to receive(:delete_routing_tables) + allow(@account_cleaner).to receive(:delete_internet_gateways) + @account_cleaner.send(:delete_vpc) + end + end + + context '#delete_routing_tables' do + it 'should delete routing tables created by Hailstorm in a VPC' do + main_rtb = Hailstorm::Behavior::AwsAdaptable::RouteTable.new(id: 'rtb-123', main: true) + secondary_rtb = Hailstorm::Behavior::AwsAdaptable::RouteTable.new(id: 'rtb-456', main: false) + expect(@client_factory.route_table_client).to receive(:route_tables).and_return([main_rtb, secondary_rtb]) + expect(@client_factory.route_table_client).to_not receive(:delete).with(route_table_id: main_rtb.id) + expect(@client_factory.route_table_client).to receive(:delete).with(route_table_id: secondary_rtb.id) + @account_cleaner.send(:delete_routing_tables) + end + end + + context '#delete_internet_gateways' do + it 'should delete internet gateways created by Hailstorm in a VPC' do + igw = Hailstorm::Behavior::AwsAdaptable::InternetGateway.new(id: 'igw-123') + expect(@client_factory.internet_gateway_client).to receive(:select).and_return([igw]) + expect(@client_factory.internet_gateway_client).to receive(:delete).with(igw_id: igw.id) + expect(@client_factory.internet_gateway_client).to receive(:detach_from_vpc) + @account_cleaner.send(:delete_internet_gateways) + end + end end diff --git a/hailstorm-gem/spec/support/aws_adapter_spec.rb b/hailstorm-gem/spec/support/aws_adapter_spec.rb index ac1d2294..9409c1f7 100644 --- a/hailstorm-gem/spec/support/aws_adapter_spec.rb +++ b/hailstorm-gem/spec/support/aws_adapter_spec.rb @@ -8,7 +8,7 @@ include DeepHashStruct before(:each) do - @mock_ec2 = instance_double(Aws::EC2::Client) + @mock_ec2 = instance_double(Aws::EC2::Client, 'mock_ec2') end context Hailstorm::Support::AwsAdapter::KeyPairClient do @@ -45,7 +45,7 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should create a key_pair' do resp = deep_struct(key_fingerprint: 'AA:00', key_material: 'A', key_name: 'hailstorm', key_pair_id: 'kp-1') - expect(@mock_ec2).to receive(:create_key_pair).with(key_name: 'hailstorm').and_return(resp) + expect(@mock_ec2).to receive(:create_key_pair).with(hash_including(key_name: 'hailstorm')).and_return(resp) key_pair = @client.create(name: 'hailstorm') expect(key_pair.private_key).to be == resp.key_material expect(key_pair.key_pair_id).to be == resp.key_pair_id @@ -176,13 +176,13 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; instance_id: 'i-123456', state: { name: 'pending', code: 10 }, public_ip_address: nil, private_ip_address: nil } - expect(@mock_ec2).to receive(:run_instances).with(image_id: 'ami-1', - key_name: 's', - security_group_ids: %w[sg-1], - instance_type: 't3.small', - placement: { availability_zone: 'us-east-1a' }, - min_count: 1, - max_count: 1) + expect(@mock_ec2).to receive(:run_instances).with(hash_including(image_id: 'ami-1', + key_name: 's', + security_group_ids: %w[sg-1], + instance_type: 't3.small', + placement: { availability_zone: 'us-east-1a' }, + min_count: 1, + max_count: 1)) .and_return(deep_struct(instances: [instance_attrs])) resp = deep_struct({ reservations: [{ instances: [instance_attrs] }] }) @@ -409,12 +409,6 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; expect(@client.first_available_zone).to eq('us-east-1b') end - it 'should find vpc from subnet_id' do - resp = deep_struct(subnets: [{ vpc_id: 'vpc-123' }]) - expect(@mock_ec2).to receive(:describe_subnets).with(subnet_ids: ['subnet-123']).and_return(resp) - expect(@client.find_vpc(subnet_id: 'subnet-123')).to be == resp.subnets[0].vpc_id - end - it 'should find self owned snapshots' do resp = deep_struct(snapshots: [ { snapshot_id: 'snap-12345def0', state: 'pending' }, @@ -440,6 +434,18 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; end context Hailstorm::Support::AwsAdapter::AmiClient do + def fixture_method(name:, image_id:) + { + state: :available, + name: name, + image_id: image_id, + state_reason: { code: '12', message: '' }, + block_device_mappings: [ + { ebs: { snapshot_id: "snap-#{image_id}" } } + ] + } + end + before(:each) do @client = Hailstorm::Support::AwsAdapter::AmiClient.new(ec2_client: @mock_ec2) end @@ -447,15 +453,9 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should find the first instance of a self owned ami that matches a given pattern' do resp = deep_struct( images: [ - { - state: :available, name: 'hailstorm/vulcan', image_id: 'ami-123', state_reason: { code: '12', message: '' } - }, - { - state: :available, name: 'hailstorm/vulcan-2', image_id: 'ami-2', state_reason: { code: '12', message: '' } - }, - { - state: :available, name: 'hailstorm/romulan-1', image_id: 'ami-3', state_reason: { code: '12', message: '' } - } + fixture_method(name: 'hailstorm/vulcan', image_id: 'ami-123'), + fixture_method(name: 'hailstorm/vulcan-2', image_id: 'ami-2'), + fixture_method(name: 'hailstorm/romulan-1', image_id: 'ami-3') ] ) @@ -467,15 +467,9 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should select all AMIs matching a given pattern' do resp = deep_struct( images: [ - { - state: :available, name: 'hailstorm/vulcan-1', image_id: 'ami-1', state_reason: { code: '12', message: '' } - }, - { - state: :available, name: 'hailstorm/vulcan-2', image_id: 'ami-2', state_reason: { code: '12', message: '' } - }, - { - state: :available, name: 'hailstorm/romulan-1', image_id: 'ami-3', state_reason: { code: '12', message: '' } - } + fixture_method(name: 'hailstorm/vulcan-1', image_id: 'ami-1'), + fixture_method(name: 'hailstorm/vulcan-2', image_id: 'ami-2'), + fixture_method(name: 'hailstorm/romulan-1', image_id: 'ami-3') ] ) @@ -486,7 +480,9 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should register an instance as a new AMI' do image_id = 'ami-123' - allow(@mock_ec2).to receive(:create_image).and_return(deep_struct(image_id: image_id)) + expect(@mock_ec2).to receive(:create_image).and_return(deep_struct(image_id: image_id)) + create_tags_params = { resources: ['ami-123'], tags: [{ key: 'hailstorm:created', value: true.to_s }] } + expect(@mock_ec2).to receive(:create_tags).with(create_tags_params) actual_ami_id = @client.register_ami( name: 'hailstorm/brave', instance_id: 'i-67678', @@ -499,7 +495,7 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should query for image availability' do resp = { images: [ - { image_id: 'ami-123', state: 'available', state_reason: { code: '12', message: '' }, name: 'hailstorm' } + fixture_method(image_id: 'ami-123', name: 'hailstorm') ] } @@ -517,7 +513,7 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should find an AMI by ami_id' do resp = { images: [ - { image_id: 'ami-123', state: 'available', name: 'hailstorm', state_reason: { code: '12', message: '' } } + fixture_method(image_id: 'ami-123', name: 'hailstorm') ] } expect(@mock_ec2).to receive(:describe_images).with(image_ids: ['ami-123']) @@ -549,10 +545,15 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; end it 'should create a Subnet' do - expect(@mock_ec2).to receive(:create_subnet).with(cidr_block: '10.0.0.0/16', vpc_id: 'vpc-a01106c2') - .and_return(deep_struct(subnet: { subnet_id: 'subnet-9d4a7b6c' })) + resp = deep_struct(subnet: { subnet_id: 'subnet-9d4a7b6c' }) + create_subnet_params = { cidr_block: '10.0.0.0/16', + vpc_id: 'vpc-a01106c2', + tag_specifications: [{ resource_type: 'subnet', tags: [{ key: 'hailstorm:created', + value: true.to_s }] }] } + expect(@mock_ec2).to receive(:create_subnet).with(create_subnet_params) + .and_return(resp) - expect(@client.create(vpc_id: 'vpc-a01106c2', cidr: '10.0.0.0/16')).to be == 'subnet-9d4a7b6c' + expect(@client.create(vpc_id: 'vpc-a01106c2', cidr: '10.0.0.0/16')).to be == resp.subnet.subnet_id end it 'should modify subnet attribute' do @@ -569,6 +570,17 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; map_customer_owned_ip_on_launch: false ) end + + it 'should delete a subnet' do + expect(@mock_ec2).to receive(:delete_subnet).with(subnet_id: 'subnet-123') + @client.delete(subnet_id: 'subnet-123') + end + + it 'should find vpc from subnet_id' do + resp = deep_struct(subnets: [{ vpc_id: 'vpc-123' }]) + expect(@mock_ec2).to receive(:describe_subnets).with(subnet_ids: ['subnet-123']).and_return(resp) + expect(@client.find_vpc(subnet_id: 'subnet-123')).to be == resp.subnets[0].vpc_id + end end context Hailstorm::Support::AwsAdapter::VpcClient do @@ -591,17 +603,32 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; it 'should query its status' do expect(@mock_ec2).to receive(:describe_vpcs) .with(vpc_ids: ['vpc-a01106c2']) - .and_return(deep_struct(vpcs: [{ state: 'available' }])) + .and_return(deep_struct(vpcs: [{ state: 'available', vpc_id: 'vpc-123' }])) expect(@client.available?(vpc_id: 'vpc-a01106c2')).to be true end it 'should create a VPC' do + resp = deep_struct(vpc: { vpc_id: 'vpc-a01106c2', state: 'pending' }) + create_vpc_params = { cidr_block: '10.0.0.0/16', + tag_specifications: [{ resource_type: 'vpc', tags: [{ key: 'hailstorm:created', + value: true.to_s }] }] } expect(@mock_ec2).to receive(:create_vpc) - .with(cidr_block: '10.0.0.0/16') - .and_return(deep_struct(vpc: { vpc_id: 'vpc-a01106c2', state: 'pending' })) + .with(create_vpc_params) + .and_return(resp) + + expect(@client.create(cidr: '10.0.0.0/16')).to be == resp.vpc.vpc_id + end + + it 'should delete a VPC' do + expect(@mock_ec2).to receive(:delete_vpc).with(vpc_id: 'vpc-123') + @client.delete(vpc_id: 'vpc-123') + end - expect(@client.create(cidr: '10.0.0.0/16')).to be == 'vpc-a01106c2' + it 'should find a VPC with filters' do + resp = deep_struct(vpcs: [{ state: 'available', vpc_id: 'vpc-123' }]) + expect(@mock_ec2).to receive(:describe_vpcs).and_return(resp) + expect(@client.find(vpc_id: 'vpc-123', filters: [:created]).id).to be == 'vpc-123' end end @@ -621,11 +648,42 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; allow(@mock_ec2).to receive(:create_internet_gateway).and_return(resp) expect(@client.create).to be == 'igw-c0a643a9' end + + it 'should select internet gateways in a VPC' do + resp = deep_struct(internet_gateways: [ + { attachments: [{ state: 'attached', vpc_id: 'vpc-123' }], internet_gateway_id: 'igw-123' } + ]) + + expect(@mock_ec2).to receive(:describe_internet_gateways).and_return(resp) + igws = @client.select(vpc_id: 'vpc-123', filters: [:created]) + expect(igws).to_not be_empty + expect(igws[0].id).to be == resp.internet_gateways[0].internet_gateway_id + end + + it 'should delete an internet gateway' do + expect(@mock_ec2).to receive(:delete_internet_gateway).with(internet_gateway_id: 'igw-c0a643a9') + @client.delete(igw_id: 'igw-c0a643a9') + end + + it 'should detach an internet gateway' do + expect(@mock_ec2).to receive(:detach_internet_gateway).with(internet_gateway_id: 'igw-1', vpc_id: 'vpc-1') + @client.detach_from_vpc(igw_id: 'igw-1', vpc_id: 'vpc-1') + end end context Hailstorm::Support::AwsAdapter::RouteTableClient do before(:each) do @client = Hailstorm::Support::AwsAdapter::RouteTableClient.new(ec2_client: @mock_ec2) + @desc_rtbs_resp = deep_struct(route_tables: [ + { + associations: [{ main: true, route_table_id: 'rtb-1f382e7d' }], + route_table_id: 'rtb-1f382e7d' + }, + { + associations: [{ main: false, route_table_id: 'rtb-1g473f6f' }], + route_table_id: 'rtb-1g473f6f' + } + ]) end it 'should create a route' do @@ -649,20 +707,8 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; end it 'should fetch main route table for a VPC' do - route_tables = [ - { - associations: [{ main: true, route_table_id: 'rtb-1f382e7d' }], - route_table_id: 'rtb-1f382e7d' - }, - { - associations: [{ main: false, route_table_id: 'rtb-1g473f6f' }], - route_table_id: 'rtb-1g473f6f' - } - ] - - resp = deep_struct(route_tables: route_tables) expect(@mock_ec2).to receive(:describe_route_tables).with(filters: [{ name: 'vpc-id', values: ['vpc-123'] }]) - .and_return(resp) + .and_return(@desc_rtbs_resp) expect(@client.main_route_table(vpc_id: 'vpc-123')).to be == 'rtb-1f382e7d' end @@ -682,11 +728,36 @@ class Aws::EC2::Errors::InvalidKeyPairNotFound < Aws::EC2::Errors::ServiceError; expect(routes.first).to be_active end + it 'should filter route tables' do + expect(@mock_ec2).to receive(:describe_route_tables) + .with(filters: [{ name: 'vpc-id', values: ['vpc-123'] }, + { name: 'tag:hailstorm:created', values: [true.to_s] }]) + .and_return(@desc_rtbs_resp) + + route_tables = @client.route_tables(vpc_id: 'vpc-123', filters: [:created]) + expect(route_tables).to_not be_blank + route_tables.each do |route_table| + expect(route_table.id).to_not be_blank + end + end + it 'should create a route table' do response = deep_struct(route_table: { route_table_id: 'rtb-22574640' }) - expect(@mock_ec2).to receive(:create_route_table).with(vpc_id: 'vpc-a01106c2').and_return(response) + expect(@mock_ec2).to receive(:create_route_table).with( + { + tag_specifications: [ + { resource_type: 'route-table', tags: [{ key: 'hailstorm:created', value: true.to_s }] } + ], + vpc_id: 'vpc-a01106c2' + } + ).and_return(response) expect(@client.create(vpc_id: 'vpc-a01106c2')).to be == 'rtb-22574640' end + + it 'should delete a route table' do + expect(@mock_ec2).to receive(:delete_route_table).with(route_table_id: 'rb-123') + @client.delete(route_table_id: 'rb-123') + end end it 'should create clients' do