elzar 0.1.2 → 0.2.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 (42) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +46 -83
  5. data/README.md +19 -17
  6. data/USAGE.md +257 -0
  7. data/bin/elzar +94 -0
  8. data/chef/site-cookbooks/ruby/metadata.rb +3 -1
  9. data/chef/site-cookbooks/ruby/recipes/default.rb +1 -0
  10. data/chef/site-cookbooks/ruby/recipes/path.rb +17 -0
  11. data/elzar.gemspec +4 -0
  12. data/lib/elzar.rb +5 -1
  13. data/lib/elzar/assistant.rb +82 -70
  14. data/lib/elzar/aws_config.rb +46 -0
  15. data/lib/elzar/cli.rb +143 -0
  16. data/lib/elzar/compute.rb +52 -0
  17. data/lib/elzar/core_ext/hash.rb +18 -0
  18. data/lib/elzar/fog.rb +53 -0
  19. data/lib/elzar/ssh_key_locator.rb +37 -0
  20. data/lib/elzar/templates/Gemfile +4 -4
  21. data/lib/elzar/templates/Vagrantfile.erb +1 -1
  22. data/lib/elzar/templates/aws_config.private.yml +13 -0
  23. data/lib/elzar/templates/aws_config.yml +6 -0
  24. data/lib/elzar/templates/data_bags/deploy/authorized_keys.json +4 -5
  25. data/lib/elzar/templates/dna/rails.json +15 -0
  26. data/lib/elzar/templates/gitignore +1 -0
  27. data/lib/elzar/version.rb +1 -1
  28. data/script/ci_nightly +14 -0
  29. data/spec/fixtures/rails_integration_template/add_root_user.rb +9 -0
  30. data/spec/fixtures/rails_integration_template/database.yml +7 -0
  31. data/spec/fixtures/rails_integration_template/deploy.rb +11 -0
  32. data/spec/fixtures/rails_integration_template/template.rb +22 -0
  33. data/spec/integration/rails_spec.rb +190 -0
  34. data/spec/lib/elzar/assistant_spec.rb +30 -0
  35. data/spec/lib/elzar/aws_config_spec.rb +84 -0
  36. data/spec/lib/elzar/ssh_key_locator_spec.rb +51 -0
  37. data/spec/spec_helper.rb +11 -0
  38. data/spec/support/shell_interaction_helpers.rb +33 -0
  39. metadata +107 -7
  40. data/lib/elzar/chef_dna.rb +0 -48
  41. data/lib/elzar/templates/dna.json +0 -25
  42. data/spec/chef_dna_spec.rb +0 -58
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ require 'gli'
3
+ begin # XXX: Remove this begin/rescue before distributing your app
4
+ require 'elzar/cli'
5
+ rescue LoadError
6
+ STDERR.puts "In development, you need to use `bundle exec bin/elzar` to run your app"
7
+ STDERR.puts "At install-time, RubyGems will make sure lib, etc. are in the load path"
8
+ STDERR.puts "Feel free to remove this message from bin/elzar now"
9
+ exit 64
10
+ end
11
+
12
+ include GLI::App
13
+
14
+ program_desc 'Describe your application here'
15
+
16
+ version Elzar::VERSION
17
+
18
+ desc 'Sets up default provisioning skeleton'
19
+ command :init do |c|
20
+ c.flag :dna,
21
+ :default_value => 'rails',
22
+ :arg_name => 'dna',
23
+ :must_match => %w[rails], # TODO Dynamically determine the list of available DNAs (e.g., clojure, etc.)
24
+ :desc => 'The application stack you wish to deploy'
25
+
26
+ c.action do |global_options,options,args|
27
+ Elzar::Cli::Init.run(global_options.merge(options))
28
+ end
29
+ end
30
+
31
+ desc 'Spins up a new EC2 instance ready for cooking'
32
+ arg_name 'instance_name' # TODO raise error if arg not given
33
+ command :preheat do |c|
34
+ c.flag :aws_config_dir,
35
+ :default_value => Elzar::AwsConfig::DEFAULT_CONFIG_DIR,
36
+ :arg_name => 'aws_config_dir',
37
+ :desc => "The directory containing your AWS config files"
38
+
39
+ c.action do |global_options,options,args|
40
+ instance_name = args.first
41
+ Elzar::Cli::Preheat.run(instance_name, global_options.merge(options))
42
+ end
43
+ end
44
+
45
+ desc 'Converges and runs recipes on given instance'
46
+ arg_name 'instance_id' # TODO raise error if arg not given
47
+ command :cook do |c|
48
+ c.flag :aws_config_dir,
49
+ :default_value => Elzar::AwsConfig::DEFAULT_CONFIG_DIR,
50
+ :arg_name => 'aws_config_dir',
51
+ :desc => "The directory containing your AWS config files"
52
+
53
+ c.action do |global_options,options,args|
54
+ instance_id = args.first
55
+ Elzar::Cli::Cook.run(instance_id, global_options.merge(options))
56
+ end
57
+ end
58
+
59
+ desc 'Destroys the given EC2 instance'
60
+ arg_name 'instance_id' # TODO raise error if arg not given
61
+ command :destroy do |c|
62
+ c.flag :aws_config_dir,
63
+ :default_value => Elzar::AwsConfig::DEFAULT_CONFIG_DIR,
64
+ :arg_name => 'aws_config_dir',
65
+ :desc => "The directory containing your AWS config files"
66
+
67
+ c.action do |global_options,options,args|
68
+ instance_id = args.first
69
+ Elzar::Cli::Destroy.run(instance_id, global_options.merge(options))
70
+ end
71
+ end
72
+
73
+ pre do |global,command,options,args|
74
+ # Pre logic here
75
+ # Return true to proceed; false to abourt and not call the
76
+ # chosen command
77
+ # Use skips_pre before a command to skip this block
78
+ # on that command only
79
+ true
80
+ end
81
+
82
+ post do |global,command,options,args|
83
+ # Post logic here
84
+ # Use skips_post before a command to skip this
85
+ # block on that command only
86
+ end
87
+
88
+ on_error do |exception|
89
+ # Error logic here
90
+ # return false to skip default error handling
91
+ true
92
+ end
93
+
94
+ exit run(ARGV)
@@ -1,6 +1,8 @@
1
1
  maintainer "Relevance"
2
2
  maintainer_email "opfor@thinkrelevance.com"
3
3
  license "All rights reserved"
4
- description "Installs/Configures ruby"
4
+ description "Installs/Configures Ruby"
5
+ recipe "ruby", "Installs and configures Ruby"
6
+ recipe "ruby::path", "Adds Ruby to every user's PATH"
5
7
  long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
6
8
  version "0.0.1"
@@ -52,3 +52,4 @@ end
52
52
  execute "Updating rubygems" do
53
53
  command "#{node[:ruby][:install_path]}/bin/gem update --system && #{node[:ruby][:install_path]}/bin/gem update --system #{node[:ruby][:gems_version]}"
54
54
  end
55
+
@@ -0,0 +1,17 @@
1
+ #
2
+ # Cookbook Name:: ruby
3
+ # Recipe:: path
4
+ #
5
+ # Copyright 2012, Relevance
6
+ #
7
+ # All rights reserved - Do Not Redistribute
8
+
9
+ bash "Add ruby to each user's PATH" do
10
+ code <<-SH
11
+ for f in `ls /home/*/.bashrc`; do
12
+ if ! grep -q "#{node[:ruby][:install_path]}" "$f"; then
13
+ echo -e '\\nexport PATH="#{node[:ruby][:install_path]}/bin:$PATH"\\n' | sudo tee -a "$f"
14
+ fi
15
+ done
16
+ SH
17
+ end
@@ -15,8 +15,12 @@ Gem::Specification.new do |s|
15
15
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
 
18
+ s.add_dependency 'gli', '~> 2.0.0'
18
19
  s.add_dependency 'multi_json', '~> 1.3.0'
20
+ s.add_dependency 'fog', '~> 1.5.0'
21
+ s.add_dependency 'slushy', '~> 0.1.3'
19
22
  s.add_development_dependency 'rake', '~> 0.9.2.2'
20
23
  s.add_development_dependency 'rspec'
24
+ s.add_development_dependency 'bahia'
21
25
  s.add_development_dependency 'bundler'
22
26
  end
@@ -1,6 +1,10 @@
1
1
  require 'elzar/version'
2
2
  require 'elzar/template'
3
3
  require 'elzar/assistant'
4
+ require 'elzar/aws_config'
5
+ require 'elzar/compute'
6
+
7
+ require 'elzar/core_ext/hash'
4
8
 
5
9
  module Elzar
6
10
  ROOT_DIR = File.expand_path File.dirname(__FILE__) + '/../'
@@ -14,7 +18,7 @@ module Elzar
14
18
  end
15
19
 
16
20
  def self.create_provision_directory(destination, options={})
17
- Assistant.create_user_provision_dir destination.to_s, options[:local]
21
+ Assistant.create_user_provision_dir destination.to_s, options
18
22
  Assistant.generate_files destination.to_s, options
19
23
  end
20
24
 
@@ -1,96 +1,108 @@
1
1
  require 'fileutils'
2
2
  require 'tmpdir'
3
- require 'elzar/chef_dna'
4
3
 
5
4
  module Elzar
6
5
  module Assistant
7
6
  ELZAR_DIR = 'elzar'
8
7
  CHEF_SOLO_DIR = '/tmp/chef-solo'
9
8
 
10
- def self.generate_files(dest, options={})
11
- vm_host_name = options[:app_name] ?
12
- "#{options[:app_name].gsub('_','-')}.local" : "elzar.thinkrelevance.com"
13
- Template.generate 'Vagrantfile', dest, :vm_host_name => vm_host_name,
14
- :cookbooks_path => Elzar::COOKBOOK_DIRS, :local => options[:local]
15
- if options[:local]
16
- generate_local_files dest
17
- else
18
- require 'multi_json'
19
- generate_user_files dest, options
9
+ class InvalidDnaError < StandardError
10
+ def initialize(line, line_number)
11
+ super "Invalid configuration in dna.json:#{line_number} - #{line.strip}"
20
12
  end
21
13
  end
22
14
 
23
- def self.create_user_provision_dir(dest, local=false)
24
- FileUtils.mkdir_p dest
25
- cp "#{Elzar.templates_dir}/dna.json", dest
26
- cp "#{Elzar.templates_dir}/Gemfile", dest
27
- cp "#{Elzar.templates_dir}/upgrade-chef.sh", dest
28
- cp "#{Elzar.templates_dir}/.rvmrc", dest
29
- cp "#{Elzar.templates_dir}/README.md", dest
30
- cp_r "#{Elzar.templates_dir}/data_bags", dest
31
- cp_r "#{Elzar.templates_dir}/script", dest
32
- cp_r "#{Elzar.templates_dir}/.chef", dest
33
- end
15
+ class << self
16
+ def generate_files(dest, options={})
17
+ vm_host_name = options[:app_name] ?
18
+ "#{options[:app_name].gsub('_','-')}.local" : "elzar.thinkrelevance.com"
19
+ Template.generate 'Vagrantfile', dest, :vm_host_name => vm_host_name,
20
+ :cookbooks_path => Elzar::COOKBOOK_DIRS, :local => options[:local]
21
+ if options[:local]
22
+ generate_local_files dest
23
+ else
24
+ require 'multi_json'
25
+ generate_user_files dest, options
26
+ end
27
+ end
34
28
 
35
- def self.merge_and_create_temp_directory(user_dir)
36
- dest = Dir.mktmpdir
37
- elzar_dir = "#{dest}/#{ELZAR_DIR}"
38
- FileUtils.mkdir_p elzar_dir
39
-
40
- generate_solo_rb dest, Elzar::COOKBOOK_DIRS.map {|dir| "#{CHEF_SOLO_DIR}/#{ELZAR_DIR}/#{dir}" }
41
- cp_r Elzar::ROLES_DIR, dest
42
- cp_r "#{Elzar::CHEF_DIR}/cookbooks", elzar_dir
43
- cp_r "#{Elzar::CHEF_DIR}/site-cookbooks", elzar_dir
44
- # merges user provision with elzar's provision
45
- cp_r "#{user_dir}/.", dest
46
- dest
47
- end
29
+ def create_user_provision_dir(dest, options={})
30
+ dna = options[:dna] || 'rails' # TODO be better than this
48
31
 
49
- private
32
+ FileUtils.mkdir_p dest
33
+ cp "#{Elzar.templates_dir}/gitignore", "#{dest}/.gitignore"
34
+ cp "#{Elzar.templates_dir}/.rvmrc", dest
35
+ cp "#{Elzar.templates_dir}/aws_config.yml", dest
36
+ cp "#{Elzar.templates_dir}/aws_config.private.yml", dest
37
+ cp "#{Elzar.templates_dir}/dna/#{dna}.json", "#{dest}/dna.json"
38
+ cp "#{Elzar.templates_dir}/Gemfile", dest
39
+ cp "#{Elzar.templates_dir}/README.md", dest
40
+ cp "#{Elzar.templates_dir}/upgrade-chef.sh", dest
41
+ cp_r "#{Elzar.templates_dir}/data_bags", dest
42
+ cp_r "#{Elzar.templates_dir}/script", dest
43
+ cp_r "#{Elzar.templates_dir}/.chef", dest
44
+ end
50
45
 
51
- def self.generate_local_files(dest)
52
- generate_solo_rb dest
53
- cp_r Elzar::ROLES_DIR, dest
54
- cp_r "#{Elzar::CHEF_DIR}/cookbooks", dest
55
- cp_r "#{Elzar::CHEF_DIR}/site-cookbooks", dest
56
- end
46
+ def merge_and_create_temp_directory(user_dir)
47
+ validate_dna! "#{user_dir}/dna.json"
57
48
 
58
- def self.generate_user_files(dest, options={})
59
- if options[:authorized_keys]
60
- create_authorized_key_data_bag(options[:authorized_keys], dest)
49
+ dest = Dir.mktmpdir
50
+ elzar_dir = "#{dest}/#{ELZAR_DIR}"
51
+ FileUtils.mkdir_p elzar_dir
52
+
53
+ generate_solo_rb dest, Elzar::COOKBOOK_DIRS.map {|dir| "#{CHEF_SOLO_DIR}/#{ELZAR_DIR}/#{dir}" }
54
+ cp_r Elzar::ROLES_DIR, dest
55
+ cp_r "#{Elzar::CHEF_DIR}/cookbooks", elzar_dir
56
+ cp_r "#{Elzar::CHEF_DIR}/site-cookbooks", elzar_dir
57
+ # merges user provision with elzar's provision
58
+ cp_r "#{user_dir}/.", dest
59
+ dest
61
60
  end
62
- if options[:app_name] && options[:database] && options[:ruby_version]
63
- create_dna_json(dest, *options.values_at(:app_name, :database, :ruby_version))
61
+
62
+ def validate_dna!(dna_file_path)
63
+ lines = File.readlines(dna_file_path)
64
+ lines.each_with_index do |line, line_number|
65
+ raise InvalidDnaError.new(line, line_number + 1) if line.match(/TODO/)
66
+ end
64
67
  end
65
- end
66
68
 
67
- def self.generate_solo_rb(dest, additional=[])
68
- dirs = Elzar::COOKBOOK_DIRS.map {|dir| "#{CHEF_SOLO_DIR}/#{dir}" }
69
- Template.generate "solo.rb", dest, :cookbook_path => dirs + additional,
70
- :chef_solo_dir => CHEF_SOLO_DIR
71
- end
69
+ private
72
70
 
73
- def self.cp(*args)
74
- FileUtils.cp(*args)
75
- end
71
+ def generate_local_files(dest)
72
+ generate_solo_rb dest
73
+ cp_r Elzar::ROLES_DIR, dest
74
+ cp_r "#{Elzar::CHEF_DIR}/cookbooks", dest
75
+ cp_r "#{Elzar::CHEF_DIR}/site-cookbooks", dest
76
+ end
76
77
 
77
- def self.cp_r(*args)
78
- FileUtils.cp_r(*args)
79
- end
78
+ def generate_user_files(dest, options={})
79
+ if options[:authorized_keys]
80
+ create_authorized_key_data_bag(options[:authorized_keys], dest)
81
+ end
82
+ end
80
83
 
81
- def self.create_dna_json(dest, app_name, database, ruby_version)
82
- content = MultiJson.load(File.read("#{Elzar.templates_dir}/dna.json"))
83
- content['rails_app']['name'] = app_name
84
- ChefDNA.gene_splice(content, database, ruby_version)
85
- File.open("#{dest}/dna.json", 'w+') {|f| f.write MultiJson.dump(content) }
86
- end
84
+ def generate_solo_rb(dest, additional=[])
85
+ dirs = Elzar::COOKBOOK_DIRS.map {|dir| "#{CHEF_SOLO_DIR}/#{dir}" }
86
+ Template.generate "solo.rb", dest, :cookbook_path => dirs + additional,
87
+ :chef_solo_dir => CHEF_SOLO_DIR
88
+ end
87
89
 
88
- def self.create_authorized_key_data_bag(authorized_keys, dest)
89
- data_bag_dir = "#{dest}/data_bags/deploy"
90
- FileUtils.mkdir_p data_bag_dir
91
- File.open("#{data_bag_dir}/authorized_keys.json", 'w+') do |f|
92
- f.write MultiJson.dump("id" => "authorized_keys", "keys" => authorized_keys)
90
+ def cp(*args)
91
+ FileUtils.cp(*args)
92
+ end
93
+
94
+ def cp_r(*args)
95
+ FileUtils.cp_r(*args)
96
+ end
97
+
98
+ def create_authorized_key_data_bag(authorized_keys, dest)
99
+ data_bag_dir = "#{dest}/data_bags/deploy"
100
+ FileUtils.mkdir_p data_bag_dir
101
+ File.open("#{data_bag_dir}/authorized_keys.json", 'w+') do |f|
102
+ f.write MultiJson.dump("id" => "authorized_keys", "keys" => authorized_keys)
103
+ end
93
104
  end
94
105
  end
106
+
95
107
  end
96
108
  end
@@ -0,0 +1,46 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ module Elzar
5
+ module AwsConfig
6
+
7
+ DEFAULT_CONFIG_DIR = 'provision/'
8
+ CONFIG_FILE = 'aws_config.yml'
9
+ PRIVATE_CONFIG_FILE = 'aws_config.private.yml'
10
+
11
+ class ConfigFileNotFound < StandardError
12
+ def initialize(file)
13
+ super "Unable to locate config file: #{file.to_path}"
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def load_configs(config_directory = nil)
19
+ config_directory ||= DEFAULT_CONFIG_DIR
20
+
21
+ config_file, private_config_file = find_config_files(config_directory)
22
+ read_and_merge_config_files(config_file, private_config_file)
23
+ end
24
+
25
+ private
26
+
27
+ def find_config_files(config_directory)
28
+ dir = Pathname.new config_directory
29
+ config, private_config = dir.join(CONFIG_FILE), dir.join(PRIVATE_CONFIG_FILE)
30
+ raise_error_unless_files_exist! config, private_config
31
+
32
+ [config, private_config]
33
+ end
34
+
35
+ def read_and_merge_config_files(base_file, other_file)
36
+ base, other = YAML.load(base_file.read), YAML.load(other_file.read)
37
+ base.deep_merge(other)
38
+ end
39
+
40
+ def raise_error_unless_files_exist!(*files)
41
+ files.each { |f| raise ConfigFileNotFound.new(f) unless f.exist? }
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,143 @@
1
+ require 'elzar'
2
+ require 'elzar/ssh_key_locator'
3
+
4
+ module Elzar
5
+ module Cli
6
+ class MissingArgumentsError < StandardError
7
+ def initialize(cmd, arguments)
8
+ super "Required arguments missing (#{arguments.join(', ')})." \
9
+ " Run `elzar help #{cmd}` for more information."
10
+ end
11
+ end
12
+
13
+ class Runner
14
+ def self.run(*args)
15
+ runner = new(*args)
16
+ runner.require_arguments!
17
+ runner.run
18
+ end
19
+
20
+ def self.required_argument(*arg_names)
21
+ @required_arguments ||= []
22
+ @required_arguments += arg_names
23
+ end
24
+
25
+ def self.required_arguments
26
+ @required_arguments || []
27
+ end
28
+
29
+ def require_arguments!
30
+ missing_arguments = self.class.required_arguments.select do |arg|
31
+ arg_value = self.instance_variable_get(:"@#{arg}")
32
+ arg_value.to_s.strip.empty?
33
+ end
34
+
35
+ raise MissingArgumentsError.new(cmd, missing_arguments) unless missing_arguments.empty?
36
+ end
37
+
38
+ private
39
+
40
+ def notify(msg)
41
+ # TODO chop off only initial indentation level
42
+ puts msg.gsub(/^\s+/,'')
43
+ end
44
+
45
+ def aws_config
46
+ Elzar::AwsConfig.load_configs @aws_config_dir
47
+ end
48
+
49
+ def cmd
50
+ self.class.name.split('::').last.downcase
51
+ end
52
+
53
+
54
+ end
55
+
56
+ class Init < Runner
57
+ attr_reader :authorized_keys, :dna
58
+
59
+ def initialize(options = {})
60
+ @dna = options[:dna]
61
+ end
62
+
63
+ def run
64
+ Elzar.create_provision_directory 'provision', provisioning_options
65
+ notify <<-MSG
66
+ Created provision/ directory.
67
+ !!! You must go edit provision/dna.json to meet your app's needs !!!
68
+ MSG
69
+ end
70
+
71
+ private
72
+
73
+ def provisioning_options
74
+ {
75
+ :authorized_keys => find_ssh_keys,
76
+ :dna => dna
77
+ }
78
+ end
79
+
80
+ def find_ssh_keys
81
+ Elzar::SshKeyLocator.find_local_keys
82
+ end
83
+ end
84
+
85
+ class Preheat < Runner
86
+ attr_reader :instance_name
87
+
88
+ required_argument :instance_name
89
+
90
+ def initialize(instance_name, options = {})
91
+ @instance_name = instance_name
92
+ @aws_config_dir = options[:aws_config_dir]
93
+ end
94
+
95
+ def run
96
+ notify "Provisioning an instance..."
97
+ instance_id, instance_ip = Elzar::Compute.provision_and_bootstrap!(instance_name, aws_config)
98
+ notify <<-MSG
99
+ Finished provisioning server
100
+ Instance ID: #{instance_id}
101
+ Instance IP: #{instance_ip}
102
+ MSG
103
+ end
104
+ end
105
+
106
+ class Cook < Runner
107
+ attr_reader :instance_id
108
+
109
+ required_argument :instance_id
110
+
111
+ def initialize(instance_id, options = {})
112
+ @instance_id = instance_id
113
+ @aws_config_dir = options[:aws_config_dir]
114
+ end
115
+
116
+ def run
117
+ notify "Cooking..."
118
+ inst_id, inst_ip = Elzar::Compute.converge!(instance_id, aws_config)
119
+ notify <<-MSG
120
+ Finished cooking
121
+ Instance ID: #{inst_id}
122
+ Instance IP: #{inst_ip}
123
+ MSG
124
+ end
125
+ end
126
+
127
+ class Destroy < Runner
128
+ attr_reader :instance_id
129
+
130
+ required_argument :instance_id
131
+
132
+ def initialize(instance_id, options = {})
133
+ @instance_id = instance_id
134
+ @aws_config_dir = options[:aws_config_dir]
135
+ end
136
+
137
+ def run
138
+ Elzar::Compute.destroy!(instance_id, aws_config)
139
+ notify "Destroyed instance #{instance_id}"
140
+ end
141
+ end
142
+ end
143
+ end