hashicorptools 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ module Hashicorptools
2
+ class Host < Thor
3
+
4
+ desc 'hosts', 'list running instances'
5
+ option :environment, required: false
6
+ option :role, required: false
7
+ option :name, required: false
8
+ def hosts
9
+ ec2 = Aws::EC2::Client.new(region: 'us-east-1')
10
+
11
+ resp = ec2.describe_instances(filters: filters)
12
+ resp.reservations.each do |reservation|
13
+ reservation.instances.each do |instance|
14
+ name = instance.tags.find{|t| t.key == 'Name'}.value
15
+ puts "#{name} #{instance.public_dns_name}"
16
+ end
17
+ end
18
+ end
19
+
20
+ desc 'ssh', 'ssh to the first matching instance'
21
+ option :environment, required: false
22
+ option :role, required: false
23
+ option :name, required: false
24
+ def ssh(role = '')
25
+ ec2 = Aws::EC2::Client.new(region: 'us-east-1')
26
+
27
+ resp = ec2.describe_instances(filters: filters(role))
28
+ if resp.reservations.any?
29
+ instance = resp.reservations.first.instances.first
30
+ dns = if instance.public_dns_name.present?
31
+ instance.public_dns_name
32
+ else
33
+ instance.private_dns_name
34
+ end
35
+
36
+ exec "ssh #{ssh_user_fragment}#{dns}"
37
+ else
38
+ puts "no instances with #{role} role found"
39
+ end
40
+ end
41
+
42
+ desc 'console', 'run the agra rails console'
43
+ option :environment, required: false
44
+ def console
45
+ ec2 = Aws::EC2::Client.new(region: 'us-east-1')
46
+
47
+ bastion_dns = dns_from_reservations(ec2.describe_instances(filters: filters('console', 'maintenance')))
48
+ agra_console_dns = dns_from_reservations(ec2.describe_instances(filters: filters('console', 'agra')))
49
+
50
+
51
+ if bastion_dns && agra_console_dns
52
+ exec "ssh -t #{ssh_user_fragment}#{bastion_dns} 'ssh -t #{ssh_user_fragment}#{agra_console_dns}'"
53
+ else
54
+ puts "no instances found"
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def dns_from_reservations(resp)
61
+ if resp.reservations.any?
62
+ instance = resp.reservations.first.instances.first
63
+ if instance.public_dns_name.present?
64
+ instance.public_dns_name
65
+ else
66
+ instance.private_dns_name
67
+ end
68
+ end
69
+ end
70
+
71
+
72
+ def application_environment
73
+ if options[:environment].present?
74
+ options[:environment]
75
+ elsif ENV['CHANGESPROUT_APP_ENVIRONMENT'].present?
76
+ ENV['CHANGESPROUT_APP_ENVIRONMENT']
77
+ else
78
+ 'staging'
79
+ end
80
+ end
81
+
82
+ def ssh_user_fragment
83
+ ENV['AWS_SSH_USERNAME'].present? ? "#{ENV['AWS_SSH_USERNAME']}@" : ''
84
+ end
85
+
86
+ def filters(role = '', kind='')
87
+ filters = []
88
+
89
+ filters << {name: 'instance-state-name', values: ['running']}
90
+
91
+ if application_environment.present?
92
+ filters << {name: 'tag:environment', values: [ application_environment ]}
93
+ end
94
+
95
+ if options[:name].present?
96
+ filters << {name: 'tag:Name', values: [ options[:name] ]}
97
+ end
98
+
99
+ if role.blank?
100
+ role = options[:role].present?
101
+ end
102
+
103
+ if role.present?
104
+ filters << {name: 'tag:role', values: [ role ]}
105
+ end
106
+
107
+ if kind.present?
108
+ filters << {name: 'tag:kind', values: [ kind ]}
109
+ end
110
+
111
+ filters
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,129 @@
1
+ module Hashicorptools
2
+ NUMBER_OF_AMIS_TO_KEEP = 2
3
+
4
+ class Packer < Thor
5
+ include Ec2Utilities
6
+ include Variables
7
+
8
+ desc "build", "creates an AMI from the current config"
9
+ option :debug, :required => false
10
+ def build
11
+ _build
12
+ end
13
+
14
+ desc "validate", "validates the packer config"
15
+ def validate
16
+ system "packer validate #{ami_config_path}"
17
+ end
18
+
19
+ desc "console", "interactive session"
20
+ def console
21
+ require 'byebug'
22
+ byebug
23
+ end
24
+
25
+ desc "list", "list all available telize amis"
26
+ def list
27
+ amis.each do |ami|
28
+ puts ami.image_id
29
+ end
30
+ end
31
+
32
+ desc "clean", "clean old AMIs that are no longer needed"
33
+ def clean
34
+ clean_amis
35
+ end
36
+
37
+ desc "boot", "start up an instance of the latest version of AMI"
38
+ def boot
39
+ run_instances_resp = ec2.run_instances(image_id: current_ami('base-image').image_id,
40
+ min_count: 1,
41
+ max_count: 1,
42
+ instance_type: "t2.micro")
43
+
44
+ ec2.create_tags( resources: run_instances_resp.instances.collect{|i| i.instance_id },
45
+ tags: [ {key: 'Name', value: "packer test boot #{tag_name}"}, {key: 'environment', value: 'packer-development'}, {key: 'temporary', value: 'kill me'}])
46
+
47
+ require 'byebug'
48
+ byebug
49
+ end
50
+
51
+ protected
52
+
53
+ def _build(settings_overrides={})
54
+ settings_overrides.merge!({source_ami: source_ami_id, ami_tag: tag_name, cookbook_name: cookbook_name})
55
+
56
+ if options[:debug]
57
+ puts "[DEBUG] Executing 'packer build -debug #{variables(settings_overrides)} #{ami_config_path}'"
58
+ system "packer build -debug \
59
+ #{variables(settings_overrides)} \
60
+ #{ami_config_path}"
61
+ else
62
+ system "packer build \
63
+ #{variables(settings_overrides)} \
64
+ #{ami_config_path}"
65
+ end
66
+
67
+ clean_amis
68
+ end
69
+
70
+ def source_ami_id
71
+ current_ami('base-image').image_id
72
+ end
73
+
74
+ def tag_name
75
+ raise 'implement me'
76
+ end
77
+
78
+ def cookbook_name
79
+ raise 'implement me'
80
+ end
81
+
82
+ def ami_config_path
83
+ datadir_path = Gem.datadir('hashicorptools')
84
+ File.join(datadir_path, 'standard-ami.json')
85
+ end
86
+
87
+ def auto_scaling
88
+ @auto_scaling ||= Aws::AutoScaling::Client.new(region: 'us-east-1')
89
+ end
90
+
91
+ def ec2_v2
92
+ @ec2 ||= Aws::EC2::Client.new(region: 'us-east-1')
93
+ end
94
+
95
+ def amis_in_use
96
+ launch_configs = auto_scaling.describe_launch_configurations
97
+ image_ids = launch_configs.data['launch_configurations'].collect{|lc| lc.image_id}.flatten
98
+
99
+ ec2_reservations = ec2_v2.describe_instances
100
+ image_ids << ec2_reservations.reservations.collect{|res| res.instances.collect{|r| r.image_id}}.flatten
101
+ image_ids.flatten
102
+ end
103
+
104
+ def clean_amis
105
+ ami_ids = amis.collect{|a| a.image_id}
106
+ ami_ids_to_remove = ami_ids - amis_in_use
107
+ potential_amis_to_remove = amis
108
+ potential_amis_to_remove.keep_if {|a| ami_ids_to_remove.include?(a.image_id) }
109
+
110
+ if potential_amis_to_remove.size > NUMBER_OF_AMIS_TO_KEEP
111
+ amis_to_remove = potential_amis_to_remove[NUMBER_OF_AMIS_TO_KEEP..-1]
112
+ amis_to_keep = potential_amis_to_remove[0..(NUMBER_OF_AMIS_TO_KEEP-1)]
113
+
114
+ puts "Deregistering old AMIs..."
115
+ amis_to_remove.each do |ami|
116
+ puts "Deregistering #{ami.image_id}"
117
+ ami.deregister
118
+ end
119
+
120
+ puts "Currently active AMIs..."
121
+ amis_to_keep.each do |ami|
122
+ puts ami.image_id
123
+ end
124
+ else
125
+ puts "no AMIs to clean."
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,283 @@
1
+ module Hashicorptools
2
+ class Terraform < Thor
3
+ TERRAFORM_VERSION = '0.6.8'
4
+
5
+ include Ec2Utilities
6
+ include Variables
7
+
8
+ desc 'bootstrap', 'terraform a new infrastructure from scratch'
9
+ option :environment, :required => true
10
+ def bootstrap
11
+ apply
12
+ end
13
+
14
+ [:apply, :plan, :destroy, :pull, :refresh].each do |cmd|
15
+ desc cmd, "terraform #{cmd}"
16
+ option :environment, :required => true
17
+ option :debug, :required => false
18
+
19
+ define_method cmd do
20
+ send("_#{cmd}")
21
+ end
22
+
23
+ no_commands do
24
+ define_method "_#{cmd}" do |settings_overrides = {}|
25
+ enforce_version!
26
+ raise 'invalid environment' unless ['staging', 'production'].include?(options[:environment])
27
+
28
+ settings_overrides
29
+ .merge!({ app_environment: options[:environment] }
30
+ .merge(env_variable_keys)
31
+ .merge(settings)
32
+ .merge(shared_plan_variables))
33
+
34
+ decrypt_file(state_path)
35
+ decrypt_file(var_file_path)
36
+
37
+
38
+ begin
39
+ send("before_#{cmd}")
40
+
41
+ terraform_command = "terraform #{cmd} #{variables(settings_overrides)} -state #{state_path} #{var_file_param} #{config_directory}"
42
+
43
+ if (options[:debug])
44
+ puts "[DEBUG] running command: '#{terraform_command}"
45
+ end
46
+ if system terraform_command
47
+ send("after_#{cmd}")
48
+ end
49
+ rescue StandardError => e
50
+ puts e.message
51
+ puts e.backtrace
52
+ ensure
53
+ # need to always ensure the most recent tfstate is encrypted again.
54
+ encrypt_file(state_path)
55
+ delete_decrypted_var_file
56
+ end
57
+
58
+ end
59
+
60
+ define_method "before_#{cmd}" do
61
+ # no-op
62
+ end
63
+ end
64
+
65
+ no_commands do
66
+ define_method "after_#{cmd}" do
67
+ # no-op
68
+ end
69
+ end
70
+
71
+ desc cmd, "terraform #{cmd} for shared plan"
72
+ define_method "shared_#{cmd}" do
73
+ enforce_version!
74
+
75
+ decrypt_file(shared_state_path)
76
+
77
+ begin
78
+ system "terraform #{cmd} #{variables(env_variable_keys.merge(settings))} -state #{shared_state_path} #{shared_config_directory}"
79
+ rescue StandardError => e
80
+ puts e.message
81
+ puts e.backtrace
82
+ ensure
83
+ # need to always ensure the most recent tfstate is encrypted again.
84
+ encrypt_file(shared_state_path)
85
+ end
86
+ end
87
+ end
88
+
89
+ [:shared_apply, :shared_plan, :shared_destroy, :shared_pull, :shared_refresh].each do |cmd|
90
+
91
+ end
92
+
93
+ desc 'output', 'terraform output'
94
+ option :environment, :required => true
95
+ option :name, :required => true
96
+ def output
97
+ system output_cmd(state_path, options[:name])
98
+ end
99
+
100
+ desc 'taint', 'terraform taint'
101
+ option :environment, :required => true
102
+ option :name, :required => true
103
+ def taint
104
+ system "terraform taint -state #{state_path} #{options[:name]}"
105
+ end
106
+
107
+ desc 'show', 'terraform show'
108
+ option :environment, :required => true
109
+ def show
110
+ system "terraform show #{state_path}"
111
+ end
112
+
113
+ [ {commands: [:decrypt, :encrypt], file_path_method: :state_path, desc: 'upstream terraform changes'},
114
+ {commands: [:shared_decrypt, :shared_encrypt], file_path_method: :shared_state_path, desc: 'upstream shared terraform changes'},
115
+ {commands: [:var_file_decrypt, :var_file_encrypt], file_path_method: :var_file_path, desc: 'tfvars file'} ].each do |crypto_commands|
116
+
117
+ desc crypto_commands[:commands][0], "decrypt #{crypto_commands[:desc]}"
118
+ option :environment, :required => true
119
+ define_method crypto_commands[:commands][0] do
120
+ file_path = send(crypto_commands[:file_path_method])
121
+ decrypt_file(file_path)
122
+ end
123
+
124
+ desc crypto_commands[:commands][1], "encrypt #{crypto_commands[:desc]}"
125
+ option :environment, :required => true
126
+ define_method crypto_commands[:commands][1] do
127
+ file_path = send(crypto_commands[:file_path_method])
128
+ encrypt_file(file_path)
129
+ end
130
+ end
131
+
132
+ desc "console", "interactive session"
133
+ def console
134
+ require 'pry-byebug'
135
+ binding.pry
136
+ end
137
+
138
+ protected
139
+
140
+ def var_file_param
141
+ File.exist?(var_file_path) ?
142
+ "-var-file #{var_file_path}" :
143
+ ""
144
+ end
145
+
146
+ def delete_decrypted_var_file
147
+ return unless File.exist?(var_file_path)
148
+
149
+ enforce_cryptography_dependencies
150
+ if !File.exist?("#{var_file_path}.enc")
151
+ encrypt_file(var_file_path)
152
+ end
153
+
154
+ File.delete(var_file_path)
155
+ end
156
+
157
+ def encrypt_file(file_path)
158
+ enforce_cryptography_dependencies
159
+ if File.exist?(file_path)
160
+ system "openssl enc -aes-256-cbc -salt -in #{file_path} -out #{file_path}.enc -k #{ENV['TFSTATE_ENCRYPTION_PASSWORD']}"
161
+ end
162
+ end
163
+
164
+ def decrypt_file(file_path, enforce_file_existence=false)
165
+ enforce_cryptography_dependencies
166
+ if File.exist?("#{file_path}.enc")
167
+ system "openssl enc -aes-256-cbc -d -in #{file_path}.enc -out #{file_path} -k #{ENV['TFSTATE_ENCRYPTION_PASSWORD']}"
168
+ elsif enforce_file_existence
169
+ raise "Could not find #{file_path}.enc"
170
+ end
171
+ end
172
+
173
+ def state_path
174
+ "#{config_environment_path}/#{options[:environment]}.tfstate"
175
+ end
176
+
177
+ def shared_state_path
178
+ "#{shared_config_directory}/shared.tfstate"
179
+ end
180
+
181
+ def var_file_path
182
+ "#{config_environment_path}/variables.tfvars"
183
+ end
184
+
185
+ def config_directory
186
+ "config/infrastructure/#{infrastructure}"
187
+ end
188
+
189
+ def shared_config_directory
190
+ "config/infrastructure/#{infrastructure}/shared"
191
+ end
192
+
193
+ def config_environment_path
194
+ "#{config_directory}/environments/#{options[:environment]}"
195
+ end
196
+
197
+ def infrastructure
198
+ raise 'implement me'
199
+ end
200
+
201
+ def output_cmd(state_file_path, name=nil)
202
+ "terraform output -state=#{state_file_path} #{name}"
203
+ end
204
+
205
+ def output_variable(state_file_path, name)
206
+ `#{output_cmd(state_file_path, name)}`.chomp
207
+ end
208
+
209
+ def output_variables(state_file_path)
210
+ raw_plan_output = `#{output_cmd(state_file_path)}`
211
+ output_vars = {}
212
+ raw_plan_output.split("\n").each do |output_var|
213
+ key, value = output_var.split("=")
214
+ output_vars[key.strip] = value.strip
215
+ end
216
+
217
+ output_vars
218
+ end
219
+
220
+ def terraform_version
221
+ version_string = `terraform version`.chomp
222
+ version = /(\d+.\d+.\d+)/.match(version_string)
223
+ version[0]
224
+ end
225
+
226
+ def enforce_version!
227
+ if Gem::Version.new(terraform_version) < Gem::Version.new(TERRAFORM_VERSION)
228
+ raise "Terraform #{terraform_version} is out of date, please upgrade"
229
+ end
230
+ end
231
+
232
+ def enforce_cryptography_dependencies
233
+ raise "must supply TFSTATE_ENCRYPTION_PASSWORD environmental variable" if ENV['TFSTATE_ENCRYPTION_PASSWORD'].blank?
234
+ end
235
+
236
+ def settings
237
+ {} # override me to pass more variables into the terraform plan.
238
+ end
239
+
240
+ def asg_launch_config_name(asg_name)
241
+ asg_client = Aws::AutoScaling::Client.new(region: 'us-east-1')
242
+ group = asg_client.describe_auto_scaling_groups(auto_scaling_group_names: [asg_name]).auto_scaling_groups.first
243
+ group.try(:launch_configuration_name)
244
+ end
245
+
246
+ def env_variable_keys
247
+ {} # override me to pass environmental variables into the terraform plan
248
+ end
249
+
250
+ def shared_plan_variables
251
+ decrypt_file(shared_state_path, false)
252
+ if File.exist?(shared_state_path)
253
+ output_variables(shared_state_path)
254
+ else
255
+ {}
256
+ end
257
+ end
258
+
259
+ def fetch_terraform_modules
260
+ system "terraform get -update=true #{config_directory}"
261
+ end
262
+
263
+ def current_tfstate
264
+ return @current_tfstate if defined?(@current_tfstate)
265
+ raw_conf = File.read(state_path)
266
+ @current_tfstate = JSON.parse(raw_conf)
267
+ end
268
+
269
+ def read_config_file(path)
270
+ File.new('config/' + path).read
271
+ template = ERB.new File.new("config/#{path}").read, nil, "%"
272
+ template.result(OpenStruct.new(options).instance_eval { binding })
273
+ end
274
+
275
+ def dynect
276
+ @dynect ||= DynectRest.new("controlshiftlabs", ENV['DYNECT_USERNAME'], ENV['DYNECT_PASSWORD'], "controlshiftlabs.com")
277
+ end
278
+
279
+ def dns_record_exists?(parent_node_fqdn, record)
280
+ dynect.node_list(nil, parent_node_fqdn).include?(record.fqdn)
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,22 @@
1
+ module Hashicorptools
2
+ class UpdateLaunchConfiguration < Thor
3
+ desc 'deploy ASG_NAME', 'recycle instances in the ASG with no downtime'
4
+ def deploy(asg_name)
5
+ asg = AutoScalingGroup.new(name: asg_name)
6
+ if asg.group.nil?
7
+ raise "could not find asg #{asg_name}"
8
+ end
9
+ current_count = asg.group.instances.size || 1
10
+
11
+ if asg.group.max_size < (current_count * 2)
12
+ raise "max size must be more than twice current count to deploy a new AMI"
13
+ else
14
+ # first doulbe the instance count to get new launch config live.
15
+ asg.set_desired_instances(current_count * 2)
16
+
17
+ # then bring the instance count back down again.
18
+ asg.set_desired_instances(current_count)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ module Hashicorptools
2
+ module Variables
3
+ def aws_credentials_settings(settings_overrides = {})
4
+ {aws_access_key: ENV['AWS_ACCESS_KEY_ID'],
5
+ aws_secret_key: ENV['AWS_SECRET_ACCESS_KEY']}.merge(settings_overrides)
6
+ end
7
+
8
+ def variables(settings_overrides = {})
9
+ aws_credentials_settings(settings_overrides).collect{|key,value| "-var '#{key}=#{value}'" }.join(' ')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ require 'dotenv'
3
+ require 'thor'
4
+ require 'active_support/all'
5
+ require 'aws-sdk-v1'
6
+ require 'aws-sdk'
7
+ require 'dynect_rest'
8
+
9
+ module Hashicorptools
10
+ end
11
+
12
+ require_relative 'hashicorptools/variables'
13
+ require_relative 'hashicorptools/ec2_utilities'
14
+ require_relative 'hashicorptools/auto_scaling_group'
15
+ require_relative 'hashicorptools/packer'
16
+ require_relative 'hashicorptools/terraform'
17
+ require_relative 'hashicorptools/host'
18
+ require_relative 'hashicorptools/update_launch_configuration'
19
+ require_relative 'hashicorptools/code_deploy'
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Hashicorptools" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ require 'simplecov'
2
+
3
+ module SimpleCov::Configuration
4
+ def clean_filters
5
+ @filters = []
6
+ end
7
+ end
8
+
9
+ SimpleCov.configure do
10
+ clean_filters
11
+ load_adapter 'test_frameworks'
12
+ end
13
+
14
+ ENV["COVERAGE"] && SimpleCov.start do
15
+ add_filter "/.rvm/"
16
+ end
17
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
18
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
19
+
20
+ require 'rspec'
21
+ require 'hashicorptools'
22
+
23
+ # Requires supporting files with custom matchers and macros, etc,
24
+ # in ./support/ and its subdirectories.
25
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
26
+
27
+ RSpec.configure do |config|
28
+
29
+ end