elzar 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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