aws-ec2 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.gitmodules +0 -0
  4. data/CHANGELOG.md +11 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +18 -10
  7. data/LICENSE.txt +1 -1
  8. data/README.md +74 -7
  9. data/Rakefile +1 -1
  10. data/aws-ec2.gemspec +7 -5
  11. data/lib/aws-ec2.rb +5 -2
  12. data/lib/aws_ec2/ami.rb +1 -1
  13. data/lib/aws_ec2/base.rb +34 -1
  14. data/lib/aws_ec2/cli.rb +20 -1
  15. data/lib/aws_ec2/command.rb +34 -5
  16. data/lib/aws_ec2/completer.rb +161 -0
  17. data/lib/aws_ec2/completer/script.rb +6 -0
  18. data/lib/aws_ec2/completer/script.sh +10 -0
  19. data/lib/aws_ec2/config.rb +4 -2
  20. data/lib/aws_ec2/core.rb +5 -1
  21. data/lib/aws_ec2/create.rb +11 -8
  22. data/lib/aws_ec2/create/error_messages.rb +1 -1
  23. data/lib/aws_ec2/create/params.rb +2 -6
  24. data/lib/aws_ec2/help/completion.md +22 -0
  25. data/lib/aws_ec2/help/completion_script.md +3 -0
  26. data/lib/aws_ec2/profile.rb +26 -19
  27. data/lib/aws_ec2/script.rb +1 -0
  28. data/lib/aws_ec2/script/compile.rb +15 -6
  29. data/lib/aws_ec2/script/compress.rb +62 -0
  30. data/lib/aws_ec2/script/upload.rb +75 -9
  31. data/lib/aws_ec2/setting.rb +41 -0
  32. data/lib/aws_ec2/template.rb +13 -0
  33. data/lib/aws_ec2/template/context.rb +32 -0
  34. data/lib/aws_ec2/template/helper.rb +17 -0
  35. data/lib/aws_ec2/{template_helper → template/helper}/ami_helper.rb +8 -3
  36. data/lib/aws_ec2/template/helper/core_helper.rb +88 -0
  37. data/lib/aws_ec2/{template_helper → template/helper}/partial_helper.rb +2 -2
  38. data/lib/aws_ec2/template/helper/script_helper.rb +53 -0
  39. data/lib/aws_ec2/template/helper/ssh_key_helper.rb +21 -0
  40. data/lib/aws_ec2/version.rb +1 -1
  41. data/spec/lib/cli_spec.rb +14 -0
  42. data/spec/spec_helper.rb +16 -6
  43. metadata +54 -14
  44. data/lib/aws_ec2/template_helper.rb +0 -18
  45. data/lib/aws_ec2/template_helper/core_helper.rb +0 -98
@@ -3,20 +3,29 @@ require 'fileutils'
3
3
  # Class for aws-ec2 compile command
4
4
  class AwsEc2::Script
5
5
  class Compile < AwsEc2::Base
6
- include AwsEc2::TemplateHelper
7
- BUILD_ROOT = "tmp"
6
+ include AwsEc2::Template
8
7
 
9
- def compile
8
+ # used in upload
9
+ def compile_scripts
10
10
  clean
11
11
  compile_folder("scripts")
12
- compile_folder("user-data")
13
12
  end
14
13
 
15
- def compile_folder(folder)
14
+ # use in compile cli command
15
+ def compile_all
16
+ clean
17
+ compile_folder("scripts")
18
+ layout_path = context.layout_path(@options[:layout])
19
+ compile_folder("user-data", layout_path)
20
+ end
21
+
22
+ def compile_folder(folder, layout_path=false)
16
23
  puts "Compiling app/#{folder}:".colorize(:green)
17
24
  Dir.glob("#{AwsEc2.root}/app/#{folder}/**/*").each do |path|
18
25
  next if File.directory?(path)
19
- result = erb_result(path)
26
+ next if path.include?("layouts")
27
+
28
+ result = RenderMePretty.result(path, layout: layout_path, context: context)
20
29
  tmp_path = path.sub(%r{.*/app/}, "#{BUILD_ROOT}/app/")
21
30
  puts " #{tmp_path}"
22
31
  FileUtils.mkdir_p(File.dirname(tmp_path))
@@ -0,0 +1,62 @@
1
+ require 'fileutils'
2
+
3
+ class AwsEc2::Script
4
+ class Compress < AwsEc2::Base
5
+ def compress
6
+ reset
7
+ puts "Tarballing #{BUILD_ROOT}/app/scripts folder to scripts.tgz"
8
+ tarball_path = create_tarball
9
+ save_scripts_info(tarball_path)
10
+ puts "Tarball created at #{tarball_path}"
11
+ end
12
+
13
+ def create_tarball
14
+ # https://apple.stackexchange.com/questions/14980/why-are-dot-underscore-files-created-and-how-can-i-avoid-them
15
+ sh "cd #{BUILD_ROOT}/app && dot_clean ." if system("type dot_clean > /dev/null")
16
+
17
+ # https://serverfault.com/questions/110208/different-md5sums-for-same-tar-contents
18
+ # Using tar czf directly results in a new m5sum each time because the gzip
19
+ # timestamp is included. So using: tar -c ... | gzip -n
20
+ sh "cd #{BUILD_ROOT}/app && tar -c scripts | gzip -n > scripts.tgz" # temporary app/scripts.tgz file
21
+
22
+ rename_with_md5!
23
+ end
24
+
25
+ def clean
26
+ FileUtils.rm_f("#{BUILD_ROOT}/scripts/scripts-#{md5sum}.tgz")
27
+ end
28
+
29
+ # Apppend a md5 to file after it's been created and moves it to
30
+ # output/scripts/scripts-[MD5].tgz
31
+ def rename_with_md5!
32
+ md5_path = "#{BUILD_ROOT}/scripts/scripts-#{md5sum}.tgz"
33
+ FileUtils.mkdir_p(File.dirname(md5_path))
34
+ FileUtils.mv("#{BUILD_ROOT}/app/scripts.tgz", md5_path)
35
+ md5_path
36
+ end
37
+
38
+ def save_scripts_info(scripts_name)
39
+ FileUtils.mkdir_p(File.dirname(SCRIPTS_INFO_PATH))
40
+ IO.write(SCRIPTS_INFO_PATH, scripts_name)
41
+ end
42
+
43
+ # cache this because the file will get removed
44
+ def md5sum
45
+ @md5sum ||= Digest::MD5.file("#{BUILD_ROOT}/app/scripts.tgz").to_s[0..7]
46
+ end
47
+
48
+ def sh(command)
49
+ puts "=> #{command}"
50
+ system command
51
+ end
52
+
53
+ # Only avaialble after script has been built.
54
+ def scripts_name
55
+ IO.read(SCRIPTS_INFO_PATH).strip
56
+ end
57
+
58
+ def reset
59
+ FileUtils.rm_f(SCRIPTS_INFO_PATH)
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,5 @@
1
+ require 'filesize'
2
+ require 'aws-sdk-s3'
1
3
  require 'fileutils'
2
4
 
3
5
  # Class for aws-ec2 upload command
@@ -8,17 +10,77 @@ class AwsEc2::Script
8
10
  @compile = @options[:compile] ? @options[:compile] : true
9
11
  end
10
12
 
11
- def upload
12
- compiler.compile if @compile
13
- sync_scripts_to_s3
14
- compiler.clean if @compile and AwsEc2.config["compile_clean"]
13
+ def run
14
+ compiler.compile_scripts if @compile
15
+ compressor.compress
16
+ upload(tarball_path)
17
+ compressor.clean
18
+ compiler.clean if @compile and AwsEc2.settings["compile_clean"]
15
19
  end
16
20
 
17
- def sync_scripts_to_s3
18
- puts "Uploading tmp/app to s3...".colorize(:green)
19
- s3_bucket = AwsEc2.config["scripts_s3_bucket"]
20
- s3_path = AwsEc2.config["scripts_s3_path"] || "ec2/app"
21
- sh "aws s3 sync tmp/app s3://#{s3_bucket}/#{s3_path}"
21
+ def upload(tarball_path)
22
+ puts "Uploading scripts.tgz (#{filesize}) to #{s3_dest}"
23
+ obj = s3_resource.bucket(bucket_name).object(key)
24
+ start_time = Time.now
25
+ if @options[:noop]
26
+ puts "NOOP: Not uploading file to s3"
27
+ else
28
+ obj.upload_file(tarball_path)
29
+ end
30
+ time_took = pretty_time(Time.now-start_time).colorize(:green)
31
+ puts "Time to upload code to s3: #{time_took}"
32
+ end
33
+
34
+ def tarball_path
35
+ IO.read(SCRIPTS_INFO_PATH).strip
36
+ end
37
+
38
+ def filesize
39
+ Filesize.from(File.size(tarball_path).to_s + " B").pretty
40
+ end
41
+
42
+ def s3_dest
43
+ "s3://#{bucket_name}/#{key}"
44
+ end
45
+
46
+ def key
47
+ # Example key: ec2/development/scripts/scripts-md5
48
+ "#{dest_folder}/#{File.basename(tarball_path)}"
49
+ end
50
+
51
+ # Example:
52
+ # s3_folder: s3://infra-bucket/ec2
53
+ # bucket_name: infra-bucket
54
+ def bucket_name
55
+ s3_folder.sub('s3://','').split('/').first
56
+ end
57
+
58
+ # Removes s3://bucket-name and adds AwsEc2.env. Example:
59
+ # s3_folder: s3://infra-bucket/ec2
60
+ # bucket_name: ec2/development/scripts
61
+ def dest_folder
62
+ folder = s3_folder.sub('s3://','').split('/')[1..-1].join('/')
63
+ "#{folder}/#{AwsEc2.env}/scripts"
64
+ end
65
+
66
+ # s3_folder example:
67
+ def s3_folder
68
+ AwsEc2.settings["s3_folder"]
69
+ end
70
+
71
+ def s3_resource
72
+ @s3_resource ||= Aws::S3::Resource.new
73
+ end
74
+
75
+ # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby
76
+ def pretty_time(total_seconds)
77
+ minutes = (total_seconds / 60) % 60
78
+ seconds = total_seconds % 60
79
+ if total_seconds < 60
80
+ "#{seconds.to_i}s"
81
+ else
82
+ "#{minutes.to_i}m #{seconds.to_i}s"
83
+ end
22
84
  end
23
85
 
24
86
  def sh(command)
@@ -29,5 +91,9 @@ class AwsEc2::Script
29
91
  def compiler
30
92
  @compiler ||= Compile.new(@options)
31
93
  end
94
+
95
+ def compressor
96
+ @compressor ||= Compress.new(@options)
97
+ end
32
98
  end
33
99
  end
@@ -0,0 +1,41 @@
1
+ require 'yaml'
2
+
3
+ module AwsEc2
4
+ class Setting
5
+ def initialize(check_project=true)
6
+ @check_project = check_project
7
+ end
8
+
9
+ # data contains the settings.yml config. The order or precedence for settings
10
+ # is the project lono/settings.yml and then the ~/.lono/settings.yml.
11
+ @@data = nil
12
+ def data
13
+ return @@data if @@data
14
+
15
+ if @check_project && !File.exist?(project_settings_path)
16
+ puts "ERROR: No settings file at #{project_settings_path}. Are you sure you are in a aws-ec2 project?".colorize(:red)
17
+ exit 1
18
+ end
19
+
20
+ all_envs = load_file(project_settings_path)
21
+ @@data = all_envs[AwsEc2.env]
22
+ end
23
+
24
+ private
25
+ def load_file(path)
26
+ return Hash.new({}) unless File.exist?(path)
27
+
28
+ content = RenderMePretty.result(path)
29
+ data = YAML.load(content)
30
+ # ensure no nil values
31
+ data.each do |key, value|
32
+ data[key] = {} if value.nil?
33
+ end
34
+ data
35
+ end
36
+
37
+ def project_settings_path
38
+ "#{AwsEc2.root}/config/settings.yml"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ require "active_support" # for autoload
2
+ require "active_support/core_ext/string"
3
+
4
+ module AwsEc2
5
+ module Template
6
+ autoload :Context, "aws_ec2/template/context"
7
+ autoload :Helper, "aws_ec2/template/helper"
8
+
9
+ def context
10
+ @context ||= AwsEc2::Template::Context.new(@options)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ # Encapsulates helper methods and instance variables to be rendered in the ERB
4
+ # templates.
5
+ module AwsEc2::Template
6
+ class Context
7
+ include AwsEc2::Template::Helper
8
+
9
+ def initialize(options={})
10
+ @options = options
11
+ load_custom_helpers
12
+ end
13
+
14
+ private
15
+ # Load custom helper methods from project
16
+ def load_custom_helpers
17
+ Dir.glob("#{AwsEc2.root}/app/helpers/**/*_helper.rb").each do |path|
18
+ filename = path.sub(%r{.*/},'').sub('.rb','')
19
+ module_name = filename.classify
20
+
21
+ # Prepend a period so require works AWS_EC2_ROOT is set to a relative path
22
+ # without a period.
23
+ #
24
+ # Example: AWS_EC2_ROOT=tmp/project
25
+ first_char = path[0..0]
26
+ path = "./#{path}" unless %w[. /].include?(first_char)
27
+ require path
28
+ self.class.send :include, module_name.constantize
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ module AwsEc2::Template
4
+ module Helper
5
+ def autoinclude(klass)
6
+ autoload klass, "aws_ec2/template/helper/#{klass.to_s.underscore}"
7
+ include const_get(klass)
8
+ end
9
+ extend self
10
+
11
+ autoinclude :AmiHelper
12
+ autoinclude :CoreHelper
13
+ autoinclude :PartialHelper
14
+ autoinclude :ScriptHelper
15
+ autoinclude :SshKeyHelper
16
+ end
17
+ end
@@ -1,4 +1,4 @@
1
- module AwsEc2::TemplateHelper::AmiHelper
1
+ module AwsEc2::Template::Helper::AmiHelper
2
2
  include AwsEc2::AwsServices
3
3
 
4
4
  # Example:
@@ -13,11 +13,16 @@ module AwsEc2::TemplateHelper::AmiHelper
13
13
  def latest_ami(query, owners=["self"])
14
14
  images = search_ami(query, owners)
15
15
  image = images.sort_by(&:name).reverse.first
16
- image.image_id
16
+ if image
17
+ image.image_id
18
+ else
19
+ puts "latest_ami helper method could not find an AMI with the query of: #{query.inspect}".colorize(:red)
20
+ exit 1
21
+ end
17
22
  end
18
23
 
19
24
  def search_ami(query, owners=["self"])
20
- images = ec2.describe_images(
25
+ ec2.describe_images(
21
26
  owners: owners,
22
27
  filters: [
23
28
  {name: "name", values: [query]}
@@ -0,0 +1,88 @@
1
+ require "base64"
2
+ require "erb"
3
+
4
+ module AwsEc2::Template::Helper::CoreHelper
5
+ def user_data(name, base64:true, layout:"default")
6
+ # allow user to specify the path also
7
+ if File.exist?(name)
8
+ name = File.basename(name) # normalize name, change path to name
9
+ end
10
+ name = File.basename(name, '.sh')
11
+
12
+ layout_path = layout_path(layout)
13
+ path = "#{AwsEc2.root}/app/user-data/#{name}.sh"
14
+ result = RenderMePretty.result(path, context: self, layout: layout_path)
15
+ result = append_scripts(result)
16
+
17
+ # save the unencoded user-data script for easy debugging
18
+ temp_path = "#{AwsEc2.root}/tmp/user-data.txt"
19
+ FileUtils.mkdir_p(File.dirname(temp_path))
20
+ IO.write(temp_path, result)
21
+
22
+ base64 ? Base64.encode64(result).strip : result
23
+ end
24
+
25
+ # Get full path of layout from layout name
26
+ #
27
+ # layout_name=false - dont use layout at all
28
+ # layout_name=nil - default to default.sh layout if available
29
+ def layout_path(name="default")
30
+ return false if name == false # disable layout
31
+ name = "default" if name.nil? # in case user passes in nil
32
+
33
+ ext = File.extname(name)
34
+ name += ".sh" if ext.empty?
35
+ layout_path = "#{AwsEc2.root}/app/user-data/layouts/#{name}"
36
+
37
+ # special rule for default in case there's no default layout
38
+ if name.include?("default") and !File.exist?(layout_path)
39
+ return false
40
+ end
41
+
42
+ # other named layouts should error if it doesnt exit
43
+ unless File.exist?(layout_path)
44
+ puts "ERROR: Layout #{layout_path} does not exist. Are you sure it exists? Exiting".colorize(:red)
45
+ exit 1
46
+ end
47
+
48
+ layout_path
49
+ end
50
+
51
+ # provides access to config/* settings as variables
52
+ # AWS_EC2_ENV=development => config/development.yml
53
+ # AWS_EC2_ENV=production => config/production.yml
54
+ def config
55
+ AwsEc2.config
56
+ end
57
+
58
+ # provides access to config/settings.yml as variables
59
+ def settings
60
+ AwsEc2.settings
61
+ end
62
+
63
+ # pretty timestamp that is useful for ami ids.
64
+ # the timestamp is generated once and cached.
65
+ def timestamp
66
+ @timestamp ||= Time.now.strftime("%Y-%m-%d-%H-%M-%S")
67
+ end
68
+
69
+ private
70
+ def append_scripts(user_data)
71
+ # assuming user-data script is a bash script for simplicity
72
+ script = AwsEc2::Script.new(@options)
73
+ user_data += script.auto_terminate if @options[:auto_terminate]
74
+ user_data += script.create_ami if @options[:ami_name]
75
+ user_data
76
+ end
77
+
78
+ # Load custom helper methods from the project repo
79
+ def load_custom_helpers
80
+ Dir.glob("#{AwsEc2.root}/app/helpers/**/*_helper.rb").each do |path|
81
+ filename = path.sub(%r{.*/},'').sub('.rb','')
82
+ module_name = filename.classify
83
+
84
+ require path
85
+ self.class.send :include, module_name.constantize
86
+ end
87
+ end
88
+ end
@@ -1,4 +1,4 @@
1
- module AwsEc2::TemplateHelper::PartialHelper
1
+ module AwsEc2::Template::Helper::PartialHelper
2
2
  def partial_exist?(path)
3
3
  path = partial_path_for(path)
4
4
  path = auto_add_format(path)
@@ -17,7 +17,7 @@ module AwsEc2::TemplateHelper::PartialHelper
17
17
  path = partial_path_for(path)
18
18
  path = auto_add_format(path)
19
19
 
20
- result = erb_result(path)
20
+ result = RenderMePretty.result(path, context: self)
21
21
  result = indent(result, options[:indent]) if options[:indent]
22
22
  if options[:indent]
23
23
  # Add empty line at beginning because empty lines gets stripped during
@@ -0,0 +1,53 @@
1
+ module AwsEc2::Template::Helper::ScriptHelper
2
+ # Bash code that is meant to included in user-data
3
+ def extract_scripts(options={})
4
+ check_s3_folder_settings!
5
+
6
+ settings_options = settings["extract_scripts"] || {}
7
+ options = settings_options.merge(options)
8
+ # defaults also here in case they are removed from settings
9
+ to = options[:to] || "/opt"
10
+ user = options[:as] || "ec2-user"
11
+
12
+ if Dir.glob("#{AwsEc2.root}/app/scripts*").empty?
13
+ puts "WARN: you are using the extract_scripts helper method but you do not have any app/scripts.".colorize(:yellow)
14
+ calling_line = caller[0].split(':')[0..1].join(':')
15
+ puts "Called from: #{calling_line}"
16
+ return ""
17
+ end
18
+
19
+ <<-BASH_CODE
20
+ # Generated from the aws-ec2 extract_scripts helper.
21
+ # Downloads scripts from s3, extract them, and setup.
22
+ mkdir -p #{to}
23
+ aws s3 cp #{scripts_s3_path} #{to}/
24
+ (
25
+ cd #{to}
26
+ tar zxf #{to}/#{scripts_name}
27
+ chmod -R a+x #{to}/scripts
28
+ chown -R #{user}:#{user} #{to}/scripts
29
+ )
30
+ BASH_CODE
31
+ end
32
+
33
+ private
34
+ def check_s3_folder_settings!
35
+ return if settings["s3_folder"]
36
+
37
+ puts "Helper method called that requires the s3_folder to be set at:"
38
+ lines = caller.reject { |l| l =~ %r{lib/aws-ec2} } # hide internal aws-ec2 trace
39
+ puts " #{lines[0]}"
40
+
41
+ puts "Please configure your config/settings.yml with an s3_folder.".colorize(:red)
42
+ exit 1
43
+ end
44
+
45
+ def scripts_name
46
+ File.basename(scripts_s3_path)
47
+ end
48
+
49
+ def scripts_s3_path
50
+ upload = AwsEc2::Script::Upload.new
51
+ upload.s3_dest
52
+ end
53
+ end