aws-ec2 0.9.0 → 1.0.0

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.
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