yyuu-capistrano-chef-solo 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -20,70 +20,190 @@ Or install it yourself as:
20
20
 
21
21
  This recipe will try to bootstrap your servers with `chef-solo`. Following processes will be invoked.
22
22
 
23
- 1. Install ruby for `chef-solo` with [capistrano-rbenv](https://github.com/yyuu/capistrano-rbenv)
24
- 2. Install `chef` with [bundler](http://gembundler.com)
25
- 3. Checkout your cookbooks and configurations
26
- 4. Invoke `chef-solo`
23
+ 1. Install ruby with using [capistrano-rbenv](https://github.com/yyuu/capistrano-rbenv)
24
+ 2. Install `chef` with using [bundler](http://gembundler.com)
25
+ 3. Checkout your cookbooks
26
+ 4. Generate `attributes` and `run_list` for your hosts
27
+ 5. Invoke `chef-solo`
27
28
 
28
29
  To setup your servers with `chef-solo`, add following in you `config/deploy.rb`.
29
30
 
30
- # in "config/deploy.rb"
31
- require "capistrano-chef-solo"
32
- set(:chef_solo_version, "10.16.4")
33
- set(:chef_solo_cookbooks_repository, "git@example.com:foo/bar.git")
31
+ ```ruby
32
+ # in "config/deploy.rb"
33
+ require "capistrano-chef-solo"
34
+ set(:chef_solo_version, "11.4.0")
35
+ ```
34
36
 
35
- And then, now you can start using `chef-solo` via capistrano.
37
+ And then, now you can start using `chef-solo` via Capistrano.
38
+ This task will deploy cookbooks from `./config/cookbooks`, and then invoke `chef-solo`.
36
39
 
37
40
  $ cap chef-solo
38
41
 
39
- Following options are available to manage your `chef-solo`.
42
+ Plus, there is special method `chef_solo.run_list`. You can use this to apply recipes during deployment.
43
+
44
+ ```ruby
45
+ after "deploy:finalize_update" do
46
+ chef_solo.run_list "recipe[foo]", "recipe[bar]", "recipe[baz]"
47
+ end
48
+ ```
49
+
50
+ ### Bootstrap mode
51
+
52
+ After the first time of boot up of servers, you might need to apply some recipes for your initial setup.
53
+ Let us say you want to have two users on servers.
54
+
55
+ * admin - the default user of system, created by system installer. use this for bootstrap. can invoke all commands via sudo.
56
+ * deploy - the user to use for application deployments. will be created during bootstrap. can invoke all commands via sudo.
57
+
58
+ There is _bootstrap_ mode for this kind of situations.
59
+ To setup these users, set them in your Capfile.
60
+
61
+ ```ruby
62
+ set(:user, "deploy")
63
+ set(:chef_solo_bootstrap_user, "admin")
64
+ ```
65
+
66
+ Then, apply recipes with _bootstrap_ mode.
67
+
68
+ % cap -S chef_solo_bootstrap=true chef-solo
69
+
70
+ After the bootstrap, you can deploy application normaly with `deploy` user.
71
+
72
+ % cap deploy:setup
40
73
 
41
- * `:chef_solo_version` - the version of chef.
42
- * `:chef_solo_user` - special user to invoke `chef-solo`. use `user` by default.
43
- * `:chef_solo_ssh_options` - special ssh options for `chef_solo_user`. use `ssh_options` by default.
44
- * `:chef_solo_ruby_version` - ruby version to launch `chef-solo`.
45
- * `:chef_solo_cookbooks_repository` - the URL of your cookbook repository. use `repository` by default.
46
- * `:chef_solo_cookbooks_revision` - the `branch` in the repository.
47
- * `:chef_solo_cookbooks_subdir` - the path to the `cookbooks` directory in the repository. use `config/cookbooks` by default.
48
- * `:chef_solo_cookbooks` - an alternative way to specify cookbooks repository. you can set multiple repositories from here.
49
- * `:chef_solo_attributes` - the `attributes` of chef-solo. must be a `Hash<String,String>`. will be converted into JSON.
50
- * `:chef_solo_run_list` - the `run_list` of chef-solo. must be an `Array<String>`. will be merged into `:chef_solo_attributes`.
51
- * `:chef_solo_host_attributes` - per-host `attributes` of chef-solo. must be a `Hash<String,Hash<String,String>>`.
52
- * `:chef_solo_host_run_list` - per-host `run_list` of chef-solo. must be a `Hash<String,Array<String>>`.
53
74
 
54
75
  ## Examples
55
76
 
56
- ### Setting `attributes` and `run_list`
77
+ ### Using cookbooks
78
+
79
+ #### Using cookbooks from local path
80
+
81
+ By default, `capistrano-chef-solo` searches cookbooks from local path of `config/cookbooks`.
82
+ You can specify the cookbooks directory with using `chef_solo_cookbooks_subdir`.
83
+
84
+ ```ruby
85
+ set(:chef_solo_cookbooks_scm, :none)
86
+ set(:chef_solo_cookbooks_subdir, "config/cookbooks")
87
+ ```
88
+
89
+ #### Using cookbooks from remote repository
90
+
91
+ You can use cookbooks in remote repository.
92
+
93
+ ```ruby
94
+ set(:chef_solo_cookbooks_scm, :git)
95
+ set(:chef_solo_cookbooks_repository, "git://example.com/example.git")
96
+ set(:chef_solo_cookbooks_revision, "master")
97
+ set(:chef_solo_cookbooks_subdir, "/")
98
+ ```
99
+
100
+ #### Using mixed configuration
57
101
 
58
- To apply some `attributes` to your hosts, set something like following in your `config/deploy.rb` or so.
102
+ You can use multiple cookbooks repositories at once.
59
103
 
60
- set(:chef_solo_attributes) {{
61
- "locales" => { "language" => "ja" },
62
- "tzdata" => { "timezone" => "Asia/Tokyo" },
63
- }}
64
- set(:chef_solo_run_list, ["recipe[build-essential]", "recipe[locales]", "recipe[tzdata]"])
104
+ ```ruby
105
+ set(:chef_solo_cookbooks) {{
106
+ # use cookbooks in ./config/cookbooks.
107
+ "local" => {
108
+ :scm => :none,
109
+ :cookbooks => "config/cookbooks",
110
+ },
111
+ # use cookbooks in git repository.
112
+ "repository" => {
113
+ :scm => :git,
114
+ :repository => "git://example.com/example.git",
115
+ :revision => "master",
116
+ :cookbooks => "/",
117
+ }
118
+ }}
119
+ ```
65
120
 
66
- ### Setting individual `attributes` and `run_list` per host
67
121
 
68
- In some cases, you may want to apply individual `attributes` per host.
122
+ ### Attributes configuration
123
+
124
+ By default, the Chef attributes will be generated by following order.
125
+
126
+ 1. Use _non-lazy_ variables of Capistrano.
127
+ 2. Use attributes defined in `:chef_solo_attributes`.
128
+ 3. Use attributes defined in `:chef_solo_role_attributes` for target role.
129
+ 4. Use attributes defined in `:chef_solo_host_attributes` for target host.
130
+
131
+ #### Setting common attributes
132
+
133
+ To apply same attributes to all hosts.
134
+
135
+ ```ruby
136
+ set(:chef_solo_attributes) {{
137
+ "locales" => { "language" => "ja" },
138
+ "tzdata" => { "timezone" => "Asia/Tokyo" },
139
+ }}
140
+ set(:chef_solo_run_list, ["recipe[locales]", "recipe[tzdata]"])
141
+ ```
142
+
143
+ #### Setting individual attributes per roles
144
+
145
+ In some cases, you may want to apply individual `attributes` per roles.
146
+
147
+ ```ruby
148
+ set(:chef_solo_role_attributes) {
149
+ :app => {
150
+ "foo" => "foo",
151
+ },
152
+ :web => {
153
+ "bar" => "bar",
154
+ },
155
+ }
156
+ set(:chef_solo_role_run_list) {
157
+ :app => ["recipe[build-essential]"],
158
+ }
159
+ ```
160
+
161
+ #### Setting individual attributes per host
162
+
163
+ In some cases, you may want to apply individual `attributes` per hosts.
69
164
  (Something like `server_id` of mysqld or VRRP priority of keepalived)
70
165
 
71
- set(:chef_solo_host_attributes) {
72
- "foo1.example.com" => {
73
- "keepalived" => {
74
- "virtual_router_id" => 1,
75
- "priority" => 100, #=> MASTER
76
- "virtual_ipaddress" => "192.168.0.1/24",
77
- },
78
- },
79
- "foo2.example.com" => {
80
- "keepalived" => {
81
- "virtual_router_id" => 1,
82
- "priority" => 50, #=> BACKUP
83
- "virtual_ipaddress" => "192.168.0.1/24",
84
- },
85
- },
86
- }
166
+ ```ruby
167
+ set(:chef_solo_host_attributes) {
168
+ "foo1.example.com" => {
169
+ "keepalived" => {
170
+ "virtual_router_id" => 1,
171
+ "priority" => 100, #=> MASTER
172
+ "virtual_ipaddress" => "192.168.0.1/24",
173
+ },
174
+ },
175
+ "foo2.example.com" => {
176
+ "keepalived" => {
177
+ "virtual_router_id" => 1,
178
+ "priority" => 50, #=> BACKUP
179
+ "virtual_ipaddress" => "192.168.0.1/24",
180
+ },
181
+ },
182
+ }
183
+ ```
184
+
185
+ #### Testing attributes
186
+
187
+ You can check generated attributes with using `chef-solo:attributes` task.
188
+
189
+ % cap HOST=foo.example.com chef-solo:attributes
190
+
191
+
192
+ ## Reference
193
+
194
+ Following options are available to manage your `chef-solo`.
195
+
196
+ * `:chef_solo_version` - the version of chef.
197
+ * `:chef_solo_cookbooks` - the definition of cookbooks. by default, copy cookbooks from `./config/cookbooks`.
198
+ * `:chef_solo_attributes` - the `attributes` of chef-solo. must be a `Hash<String,String>`. will be converted into JSON.
199
+ * `:chef_solo_run_list` - the `run_list` of chef-solo. must be an `Array<String>`. will be merged into `:chef_solo_attributes`.
200
+ * `:chef_solo_role_attributes` - the per-roles `attributes` of chef-solo. must be a `Hash<Symbol,Hash<String,String>>`.
201
+ * `:chef_solo_role_run_list` - the per-roles `run_list` of chef-solo. must be a `Hash<Symbol,Array<String>>`.
202
+ * `:chef_solo_host_attributes` - the per-hosts `attributes` of chef-solo. must be a `Hash<String,Hash<String,String>>`.
203
+ * `:chef_solo_host_run_list` - the per-hosts `run_list` of chef-solo. must be a `Hash<String,Array<String>>`.
204
+ * `:chef_solo_capistrano_attributes` - the Capistrano variables to use as Chef attributes.
205
+ * `:chef_solo_capistrano_attributes_exclude` - the black list for `:chef_solo_capistrano_attributes`
206
+ * `:chef_solo_capistrano_attributes_include` - the white list for `:chef_solo_capistrano_attributes`
87
207
 
88
208
 
89
209
  ## Contributing
@@ -17,5 +17,8 @@ Gem::Specification.new do |gem|
17
17
  gem.version = Capistrano::ChefSolo::VERSION
18
18
 
19
19
  gem.add_dependency("capistrano")
20
- gem.add_dependency("capistrano-rbenv", "~> 0.0.3")
20
+ gem.add_dependency("capistrano-rbenv", "~> 1.0.0")
21
+ gem.add_development_dependency("net-scp", "~> 1.0.4")
22
+ gem.add_development_dependency("net-ssh", "~> 2.2.2")
23
+ gem.add_development_dependency("vagrant", "~> 1.0.6")
21
24
  end
@@ -10,245 +10,404 @@ module Capistrano
10
10
  def self.extended(configuration)
11
11
  configuration.load {
12
12
  namespace(:"chef-solo") {
13
- _cset(:chef_solo_home) {
14
- capture('echo $HOME').strip
13
+ desc("Setup chef-solo. (an alias of chef_solo:setup)")
14
+ task(:setup, :except => { :no_release => true }) {
15
+ find_and_execute_task("chef_solo:setup")
15
16
  }
16
- _cset(:chef_solo_version, "10.16.4")
17
- _cset(:chef_solo_path) { File.join(chef_solo_home, 'chef') }
17
+
18
+ desc("Run chef-solo. (an alias of chef_solo)")
19
+ task(:default, :except => { :no_release => true }) {
20
+ find_and_execute_task("chef_solo:default")
21
+ }
22
+
23
+ desc("Show chef-solo version. (an alias of chef_solo:version)")
24
+ task(:version, :except => { :no_release => true }) {
25
+ find_and_execute_task("chef_solo:version")
26
+ }
27
+
28
+ desc("Show chef-solo attributes. (an alias of chef_solo:attributes)")
29
+ task(:attributes, :except => { :no_release => true }) {
30
+ find_and_execute_task("chef_solo:attributes")
31
+ }
32
+ }
33
+
34
+ namespace(:chef_solo) {
35
+ _cset(:chef_solo_version, "11.4.0")
36
+ _cset(:chef_solo_path) { capture("echo $HOME/chef").strip }
18
37
  _cset(:chef_solo_path_children, %w(bundle cache config cookbooks))
38
+ _cset(:chef_solo_config_file) { File.join(chef_solo_path, "config", "solo.rb") }
39
+ _cset(:chef_solo_attributes_file) { File.join(chef_solo_path, "config", "solo.json") }
19
40
 
20
- def connect_with_settings()
21
- # preserve original :user and :ssh_options
22
- set(:_chef_solo_user, user)
23
- set(:_chef_solo_ssh_options, ssh_options)
24
- set(:_chef_solo_rbenv_ruby_version, rbenv_ruby_version)
25
- begin
26
- # login as chef user if specified
27
- set(:user, fetch(:chef_solo_user, user))
28
- set(:ssh_options, fetch(:chef_solo_ssh_options, ssh_options))
29
- set(:rbenv_ruby_version, fetch(:chef_solo_ruby_version, rbenv_ruby_version))
41
+ _cset(:chef_solo_bootstrap_user) {
42
+ if variables.key?(:chef_solo_user)
43
+ logger.info(":chef_solo_user has been deprecated. use :chef_solo_bootstrap_user instead.")
44
+ fetch(:chef_solo_user, user)
45
+ else
46
+ user
47
+ end
48
+ }
49
+ _cset(:chef_solo_bootstrap_password) { password }
50
+ _cset(:chef_solo_bootstrap_ssh_options) {
51
+ if variables.key?(:chef_solo_ssh_options)
52
+ logger.info(":chef_solo_ssh_options has been deprecated. use :chef_solo_bootstrap_ssh_options instead.")
53
+ fetch(:chef_solo_ssh_options, ssh_options)
54
+ else
55
+ ssh_options
56
+ end
57
+ }
58
+ _cset(:chef_solo_use_password) {
59
+ auth_methods = ssh_options.fetch(:auth_methods, []).map { |m| m.to_sym }
60
+ auth_methods.include?(:password) or auth_methods.empty?
61
+ }
62
+ def _bootstrap_settings(&block)
63
+ if fetch(:_chef_solo_bootstrapped, false)
64
+ yield
65
+ else
66
+ # preserve original :user and :ssh_options
67
+ set(:_chef_solo_bootstrap_user, fetch(:user))
68
+ set(:_chef_solo_bootstrap_password, fetch(:password)) if chef_solo_use_password
69
+ set(:_chef_solo_bootstrap_ssh_options, fetch(:ssh_options))
70
+ servers = find_servers
71
+ begin
72
+ # we have to establish connections before teardown.
73
+ # https://github.com/capistrano/capistrano/pull/416
74
+ establish_connections_to(servers)
75
+ logger.info("entering chef-solo bootstrap mode. reconnect to servers as `#{chef_solo_bootstrap_user}'.")
76
+ # drop connection which is connected as standard :user.
77
+ teardown_connections_to(servers)
78
+ set(:user, chef_solo_bootstrap_user)
79
+ set(:password, chef_solo_bootstrap_password) if chef_solo_use_password
80
+ set(:ssh_options, chef_solo_bootstrap_ssh_options)
81
+ set(:_chef_solo_bootstrapped, true)
82
+ yield
83
+ ensure
84
+ set(:user, _chef_solo_bootstrap_user)
85
+ set(:password, _chef_solo_bootstrap_password) if chef_solo_use_password
86
+ set(:ssh_options, _chef_solo_bootstrap_ssh_options)
87
+ set(:_chef_solo_bootstrapped, false)
88
+ # we have to establish connections before teardown.
89
+ # https://github.com/capistrano/capistrano/pull/416
90
+ establish_connections_to(servers)
91
+ logger.info("leaving chef-solo bootstrap mode. reconnect to servers as `#{user}'.")
92
+ # drop connection which is connected as bootstrap :user.
93
+ teardown_connections_to(servers)
94
+ end
95
+ end
96
+ end
97
+ _cset(:chef_solo_bootstrap, false)
98
+ def connect_with_settings(&block)
99
+ if chef_solo_bootstrap
100
+ _bootstrap_settings do
101
+ yield
102
+ end
103
+ else
30
104
  yield
31
- ensure
32
- # restore original :user and :ssh_options
33
- set(:user, _chef_solo_user)
34
- set(:ssh_options, _chef_solo_ssh_options)
35
- set(:rbenv_ruby_version, _chef_solo_rbenv_ruby_version)
36
105
  end
37
106
  end
38
107
 
39
108
  desc("Setup chef-solo.")
40
- task(:setup) {
41
- connect_with_settings {
42
- transaction {
43
- bootstrap
44
- }
45
- }
109
+ task(:setup, :except => { :no_release => true }) {
110
+ connect_with_settings do
111
+ transaction do
112
+ install
113
+ end
114
+ end
46
115
  }
47
116
 
48
117
  desc("Run chef-solo.")
49
- task(:default) {
50
- connect_with_settings {
51
- transaction {
52
- bootstrap
118
+ task(:default, :except => { :no_release => true }) {
119
+ connect_with_settings do
120
+ setup
121
+ transaction do
53
122
  update
54
- }
55
- }
123
+ invoke
124
+ end
125
+ end
56
126
  }
57
127
 
58
- desc("Show version.")
59
- task(:version) {
60
- connect_with_settings {
61
- run("cd #{chef_solo_path} && #{bundle_cmd} exec chef-solo --version")
62
- }
128
+ # Acts like `default`, but will apply specified recipes only.
129
+ def run_list(*recipes)
130
+ connect_with_settings do
131
+ setup
132
+ transaction do
133
+ update(:run_list => recipes)
134
+ invoke
135
+ end
136
+ end
137
+ end
138
+
139
+ desc("Show chef-solo version.")
140
+ task(:version, :except => { :no_release => true }) {
141
+ connect_with_settings do
142
+ run("cd #{chef_solo_path.dump} && #{bundle_cmd} exec chef-solo --version")
143
+ end
144
+ }
145
+
146
+ desc("Show chef-solo attributes.")
147
+ task(:attributes, :except => { :no_release => true }) {
148
+ hosts = ENV.fetch("HOST", "").split(/\s*,\s*/)
149
+ roles = ENV.fetch("ROLE", "").split(/\s*,\s*/).map { |role| role.to_sym }
150
+ roles += hosts.map { |host| role_names_for_host(ServerDefinition.new(host)) }
151
+ attributes = _generate_attributes(:hosts => hosts, :roles => roles)
152
+ STDOUT.puts(_json_attributes(attributes))
63
153
  }
64
154
 
65
- task(:bootstrap) {
155
+ task(:install, :except => { :no_release => true }) {
66
156
  install_ruby
67
157
  install_chef
68
158
  }
69
159
 
70
- task(:install_ruby) {
71
- set(:rbenv_use_bundler, true)
72
- find_and_execute_task('rbenv:setup')
160
+ task(:install_ruby, :except => { :no_release => true }) {
161
+ set(:rbenv_install_bundler, true)
162
+ find_and_execute_task("rbenv:setup")
73
163
  }
74
164
 
75
165
  _cset(:chef_solo_gemfile) {
76
- (<<-EOS).gsub(/^\s*/, '')
166
+ (<<-EOS).gsub(/^\s*/, "")
77
167
  source "https://rubygems.org"
78
168
  gem "chef", #{chef_solo_version.to_s.dump}
79
169
  EOS
80
170
  }
81
- task(:install_chef) {
82
- dirs = chef_solo_path_children.map { |dir| File.join(chef_solo_path, dir) }
83
- run("mkdir -p #{dirs.join(' ')}")
84
- put(chef_solo_gemfile, "#{File.join(chef_solo_path, 'Gemfile')}")
85
- run("cd #{chef_solo_path} && #{bundle_cmd} install --path=#{chef_solo_path}/bundle --quiet")
171
+ task(:install_chef, :except => { :no_release => true }) {
172
+ begin
173
+ version = capture("cd #{chef_solo_path.dump} && #{bundle_cmd} exec chef-solo --version")
174
+ installed = Regexp.new(Regexp.escape(chef_solo_version)) =~ version
175
+ rescue
176
+ installed = false
177
+ end
178
+ unless installed
179
+ dirs = chef_solo_path_children.map { |dir| File.join(chef_solo_path, dir) }
180
+ run("mkdir -p #{dirs.map { |x| x.dump }.join(" ")}")
181
+ top.put(chef_solo_gemfile, File.join(chef_solo_path, "Gemfile"))
182
+ args = fetch(:chef_solo_bundle_options, [])
183
+ args << "--path=#{File.join(chef_solo_path, "bundle").dump}"
184
+ args << "--quiet"
185
+ run("cd #{chef_solo_path.dump} && #{bundle_cmd} install #{args.join(" ")}")
186
+ end
86
187
  }
87
188
 
88
- task(:update) {
89
- update_cookbooks
90
- update_config
91
- update_attributes
92
- invoke
93
- }
189
+ def update(options={})
190
+ update_cookbooks(options)
191
+ update_config(options)
192
+ update_attributes(options)
193
+ end
94
194
 
95
- task(:update_cookbooks) {
96
- tmpdir = `mktemp -d /tmp/capistrano-chef-solo.XXXXXXXXXX`.chomp
97
- remote_tmpdir = capture("mktemp -d /tmp/capistrano-chef-solo.XXXXXXXXXX").chomp
98
- destination = File.join(tmpdir, 'cookbooks')
99
- remote_destination = File.join(chef_solo_path, 'cookbooks')
100
- filename = File.join(tmpdir, 'cookbooks.tar.gz')
101
- remote_filename = File.join(remote_tmpdir, 'cookbooks.tar.gz')
195
+ def update_cookbooks(options={})
196
+ tmpdir = run_locally("mktemp -d /tmp/chef-solo.XXXXXXXXXX").strip
197
+ remote_tmpdir = capture("mktemp -d /tmp/chef-solo.XXXXXXXXXX").strip
198
+ destination = File.join(tmpdir, "cookbooks")
199
+ remote_destination = File.join(chef_solo_path, "cookbooks")
200
+ filename = File.join(tmpdir, "cookbooks.tar.gz")
201
+ remote_filename = File.join(remote_tmpdir, "cookbooks.tar.gz")
102
202
  begin
103
203
  bundle_cookbooks(filename, destination)
104
- run("mkdir -p #{remote_tmpdir}")
204
+ run("mkdir -p #{remote_tmpdir.dump}")
105
205
  distribute_cookbooks(filename, remote_filename, remote_destination)
106
206
  ensure
107
- run("rm -rf #{remote_tmpdir}") rescue nil
108
- run_locally("rm -rf #{tmpdir}") rescue nil
207
+ run("rm -rf #{remote_tmpdir.dump}") rescue nil
208
+ run_locally("rm -rf #{tmpdir.dump}") rescue nil
109
209
  end
110
- }
111
-
112
- # s/cookbook/&s/g for backward compatibility with releases older than 0.0.2.
113
- # they will be removed in future releases.
114
- _cset(:chef_solo_cookbook_repository) {
115
- logger.info("WARNING: `chef_solo_cookbook_repository' has been deprecated. use `chef_solo_cookbooks_repository' instead.")
116
- abort("chef_solo_cookbook_repository not set")
117
- }
118
- _cset(:chef_solo_cookbook_revision) {
119
- logger.info("WARNING: `chef_solo_cookbook_revision' has been deprecated. use `chef_solo_cookbooks_revision' instead.")
120
- "HEAD"
121
- }
122
- _cset(:chef_solo_cookbook_subdir) {
123
- logger.info("WARNING: `chef_solo_cookbook_subdir' has been deprecated. use `chef_solo_cookbooks_subdir' instead.")
124
- "/"
125
- }
126
- _cset(:chef_solo_cookbooks_exclude, %w(.hg .git .svn))
210
+ end
127
211
 
128
- # special variable to set multiple cookbooks repositories.
129
- # by default, it will build from :chef_solo_cookbooks_* variables.
212
+ #
213
+ # The definition of cookbooks.
214
+ # By default, load cookbooks from local path of "config/cookbooks".
215
+ #
216
+ _cset(:chef_solo_cookbooks_name) { application }
217
+ _cset(:chef_solo_cookbooks_scm, :none)
130
218
  _cset(:chef_solo_cookbooks) {
131
- repository = fetch(:chef_solo_cookbooks_repository, nil)
132
- repository = fetch(:chef_solo_cookbook_repository, nil) unless repository # for backward compatibility
133
- name = File.basename(repository, File.extname(repository))
134
- options = { :repository => repository, :cookbooks_exclude => chef_solo_cookbooks_exclude }
135
- options[:revision] = fetch(:chef_solo_cookbooks_revision, nil)
136
- options[:revision] = fetch(:chef_solo_cookbook_revision, nil) unless options[:revision] # for backward compatibility
137
- options[:cookbooks] = fetch(:chef_solo_cookbooks_subdir, nil)
138
- options[:cookbooks] = fetch(:chef_solo_cookbook_subdir, nil) unless options[:cookbooks] # for backward compatibility
139
- { name => options }
219
+ cookbooks = {}
220
+ cookbooks[chef_solo_cookbooks_name] = {}
221
+ cookbooks[chef_solo_cookbooks_name][:cookbooks] = fetch(:chef_solo_cookbooks_subdir, "config/cookbooks")
222
+ cookbooks[chef_solo_cookbooks_name][:repository] = fetch(:chef_solo_cookbooks_repository) if exists?(:chef_solo_cookbooks_repository)
223
+ cookbooks[chef_solo_cookbooks_name][:revision] = fetch(:chef_solo_cookbooks_revision) if exists?(:chef_solo_cookbooks_revision)
224
+ cookbooks
140
225
  }
141
226
 
142
- _cset(:chef_solo_repository_cache) { File.expand_path('./tmp/cookbooks-cache') }
227
+ _cset(:chef_solo_cookbooks_exclude, %w(.hg .git .svn))
228
+ def _normalize_cookbooks(cookbooks)
229
+ xs = cookbooks.map { |name, options|
230
+ options[:scm] ||= chef_solo_cookbooks_scm
231
+ options[:cookbooks_exclude] ||= chef_solo_cookbooks_exclude
232
+ [name, options]
233
+ }
234
+ Hash[xs]
235
+ end
236
+
237
+ _cset(:chef_solo_repository_cache) { File.expand_path("./tmp/cookbooks-cache") }
143
238
  def bundle_cookbooks(filename, destination)
144
239
  dirs = [ File.dirname(filename), destination ].uniq
145
- run_locally("mkdir -p #{dirs.join(' ')}")
146
- chef_solo_cookbooks.each do |name, options|
147
- configuration = Capistrano::Configuration.new()
148
- # refreshing just :source, :revision and :real_revision is enough?
149
- options = {
150
- :source => proc { Capistrano::Deploy::SCM.new(configuration[:scm], configuration) },
151
- :revision => proc { configuration[:source].head },
152
- :real_revision => proc {
153
- configuration[:source].local.query_revision(configuration[:revision]) { |cmd| with_env("LC_ALL", "C") { run_locally(cmd) } }
154
- },
155
- }.merge(options)
156
- variables.merge(options).each do |key, val|
157
- configuration.set(key, val)
158
- end
159
- repository_cache = File.join(chef_solo_repository_cache, name)
160
- if File.exist?(repository_cache)
161
- run_locally(configuration[:source].sync(configuration[:real_revision], repository_cache))
240
+ run_locally("mkdir -p #{dirs.map { |x| x.dump }.join(" ")}")
241
+ cookbooks = _normalize_cookbooks(chef_solo_cookbooks)
242
+ cookbooks.each do |name, options|
243
+ case options[:scm].to_sym
244
+ when :none
245
+ fetch_cookbooks_none(name, destination, options)
162
246
  else
163
- run_locally(configuration[:source].checkout(configuration[:real_revision], repository_cache))
247
+ fetch_cookbooks_repository(name, destination, options)
164
248
  end
249
+ end
250
+ run_locally("cd #{File.dirname(destination).dump} && tar chzf #{filename.dump} #{File.basename(destination).dump}")
251
+ end
165
252
 
166
- cookbooks = [ options.fetch(:cookbooks, '/') ].flatten.compact
167
- execute = cookbooks.map { |c|
168
- repository_cache_subdir = File.join(repository_cache, c)
169
- exclusions = options.fetch(:cookbooks_exclude, []).map { |e| "--exclude=\"#{e}\"" }.join(' ')
170
- "rsync -lrpt #{exclusions} #{repository_cache_subdir}/ #{destination}"
171
- }
172
- run_locally(execute.join(' && '))
253
+ def _fetch_cookbook(source, destination, options)
254
+ exclusions = options.fetch(:cookbooks_exclude, []).map { |e| "--exclude=#{e.dump}" }.join(" ")
255
+ run_locally("rsync -lrpt #{exclusions} #{source}/ #{destination}")
256
+ end
257
+
258
+ def _fetch_cookbooks(source, destination, options)
259
+ cookbooks = [ options.fetch(:cookbooks, "/") ].flatten.compact
260
+ cookbooks.each do |cookbook|
261
+ _fetch_cookbook(File.join(source, cookbook), destination, options)
262
+ end
263
+ end
264
+
265
+ def fetch_cookbooks_none(name, destination, options={})
266
+ _fetch_cookbooks(options.fetch(:repository, "."), destination, options)
267
+ end
268
+
269
+ def fetch_cookbooks_repository(name, destination, options={})
270
+ configuration = Capistrano::Configuration.new
271
+ # refreshing just :source, :revision and :real_revision is enough?
272
+ options = {
273
+ :source => lambda { Capistrano::Deploy::SCM.new(configuration[:scm], configuration) },
274
+ :revision => lambda { configuration[:source].head },
275
+ :real_revision => lambda {
276
+ configuration[:source].local.query_revision(configuration[:revision]) { |cmd| with_env("LC_ALL", "C") { run_locally(cmd) } }
277
+ },
278
+ }.merge(options)
279
+ variables.merge(options).each do |key, val|
280
+ configuration.set(key, val)
281
+ end
282
+ repository_cache = File.join(chef_solo_repository_cache, name)
283
+ if File.exist?(repository_cache)
284
+ run_locally(configuration[:source].sync(configuration[:real_revision], repository_cache))
285
+ else
286
+ run_locally(configuration[:source].checkout(configuration[:real_revision], repository_cache))
173
287
  end
174
- run_locally("cd #{File.dirname(destination)} && tar chzf #{filename} #{File.basename(destination)}")
288
+ _fetch_cookbooks(repository_cache, destination, options)
175
289
  end
176
290
 
177
291
  def distribute_cookbooks(filename, remote_filename, remote_destination)
178
292
  upload(filename, remote_filename)
179
- run("rm -rf #{remote_destination}")
180
- run("cd #{File.dirname(remote_destination)} && tar xzf #{remote_filename}")
293
+ run("rm -rf #{remote_destination.dump}")
294
+ run("cd #{File.dirname(remote_destination).dump} && tar xzf #{remote_filename.dump}")
181
295
  end
182
296
 
183
297
  _cset(:chef_solo_config) {
184
- (<<-EOS).gsub(/^\s*/, '')
185
- file_cache_path #{File.join(chef_solo_path, 'cache').dump}
186
- cookbook_path #{File.join(chef_solo_path, 'cookbooks').dump}
298
+ (<<-EOS).gsub(/^\s*/, "")
299
+ file_cache_path #{File.join(chef_solo_path, "cache").dump}
300
+ cookbook_path #{File.join(chef_solo_path, "cookbooks").dump}
187
301
  EOS
188
302
  }
189
- task(:update_config) {
190
- put(chef_solo_config, File.join(chef_solo_path, 'config', 'solo.rb'))
191
- }
303
+ def update_config(options={})
304
+ top.put(chef_solo_config, chef_solo_config_file)
305
+ end
192
306
 
193
307
  # merge nested hashes
194
- def deep_merge(a, b)
195
- f = lambda { |key, val1, val2| Hash === val1 && Hash === val2 ? val1.merge(val2, &f) : val2 }
196
- a.merge(b, &f)
308
+ def _merge_attributes!(a, b)
309
+ f = lambda { |key, val1, val2|
310
+ case val1
311
+ when Array
312
+ val1 + val2
313
+ when Hash
314
+ val1.merge(val2, &f)
315
+ else
316
+ val2
317
+ end
318
+ }
319
+ a.merge!(b, &f)
197
320
  end
198
321
 
199
- def json(x)
200
- if fetch(:chef_solo_pretty_json, true)
201
- JSON.pretty_generate(x)
202
- else
203
- JSON.generate(x)
204
- end
322
+ def _json_attributes(x)
323
+ JSON.send(fetch(:chef_solo_pretty_json, true) ? :pretty_generate : :generate, x)
205
324
  end
206
325
 
207
326
  _cset(:chef_solo_capistrano_attributes) {
208
- # reject lazy variables since they might have side-effects.
209
- Hash[variables.reject { |key, value| value.respond_to?(:call) }]
327
+ #
328
+ # The rule of generating chef attributes from Capistrano variables
329
+ #
330
+ # 1. Reject variables if it is in exclude list.
331
+ # 2. Reject variables if it is lazy and not in include list.
332
+ # (lazy variables might have any side-effects)
333
+ #
334
+ attributes = variables.reject { |key, value|
335
+ excluded = chef_solo_capistrano_attributes_exclude.include?(key)
336
+ included = chef_solo_capistrano_attributes_include.include?(key)
337
+ excluded or (not included and value.respond_to?(:call))
338
+ }
339
+ Hash[attributes.map { |key, value| [key, fetch(key, nil)] }]
210
340
  }
341
+ _cset(:chef_solo_capistrano_attributes_include, [
342
+ :application, :deploy_to, :rails_env, :latest_release,
343
+ :releases_path, :shared_path, :current_path, :release_path,
344
+ ])
345
+ _cset(:chef_solo_capistrano_attributes_exclude, [:logger, :password])
211
346
  _cset(:chef_solo_attributes, {})
212
347
  _cset(:chef_solo_host_attributes, {})
348
+ _cset(:chef_solo_role_attributes, {})
213
349
  _cset(:chef_solo_run_list, [])
214
350
  _cset(:chef_solo_host_run_list, {})
351
+ _cset(:chef_solo_role_run_list, {})
215
352
 
216
- def generate_attributes(options={})
217
- attributes = deep_merge(chef_solo_capistrano_attributes, chef_solo_attributes)
218
- attributes = deep_merge(attributes, {"run_list" => chef_solo_run_list})
219
- if options.has_key?(:host)
220
- attributes = deep_merge(attributes, chef_solo_host_attributes.fetch(options[:host], {}))
221
- attributes = deep_merge(attributes, {"run_list" => chef_solo_host_run_list.fetch(options[:host], [])})
353
+ def _generate_attributes(options={})
354
+ hosts = [ options.delete(:hosts) ].flatten.compact.uniq
355
+ roles = [ options.delete(:roles) ].flatten.compact.uniq
356
+ run_list = [ options.delete(:run_list) ].flatten.compact.uniq
357
+ #
358
+ # By default, the Chef attributes will be generated by following order.
359
+ #
360
+ # 1. Use _non-lazy_ variables of Capistrano.
361
+ # 2. Use attributes defined in `:chef_solo_attributes`.
362
+ # 3. Use attributes defined in `:chef_solo_role_attributes` for target role.
363
+ # 4. Use attributes defined in `:chef_solo_host_attributes` for target host.
364
+ #
365
+ attributes = chef_solo_capistrano_attributes.dup
366
+ _merge_attributes!(attributes, chef_solo_attributes)
367
+ roles.each do |role|
368
+ _merge_attributes!(attributes, chef_solo_role_attributes.fetch(role, {}))
369
+ end
370
+ hosts.each do |host|
371
+ _merge_attributes!(attributes, chef_solo_host_attributes.fetch(host, {}))
372
+ end
373
+ #
374
+ # The Chef `run_list` will be generated by following rules.
375
+ #
376
+ # * If `:run_list` was given as argument, just use it.
377
+ # * Otherwise, generate it from `:chef_solo_role_run_list`, `:chef_solo_role_run_list`
378
+ # and `:chef_solo_host_run_list`.
379
+ #
380
+ if run_list.empty?
381
+ _merge_attributes!(attributes, {"run_list" => chef_solo_run_list})
382
+ roles.each do |role|
383
+ _merge_attributes!(attributes, {"run_list" => chef_solo_role_run_list.fetch(role, [])})
384
+ end
385
+ hosts.each do |host|
386
+ _merge_attributes!(attributes, {"run_list" => chef_solo_host_run_list.fetch(host, [])})
387
+ end
388
+ else
389
+ attributes["run_list"] = [] # ignore run_list not from argument
390
+ _merge_attributes!(attributes, {"run_list" => run_list})
222
391
  end
223
392
  attributes
224
393
  end
225
394
 
226
- desc("Show chef-solo attributes.")
227
- task(:show_attributes) {
228
- STDOUT.puts(json(generate_attributes))
229
- }
230
-
231
- task(:update_attributes) {
232
- to = File.join(chef_solo_path, "config", "solo.json")
233
- if chef_solo_host_attributes.empty? and chef_solo_host_run_list.empty?
234
- put(json(generate_attributes), to)
235
- else
236
- execute_on_servers { |servers|
237
- servers.each { |server|
238
- put(json(generate_attributes(:host => server.host), to, :hosts => server.host))
239
- }
240
- }
395
+ def update_attributes(options={})
396
+ run_list = options.delete(:run_list)
397
+ servers = find_servers_for_task(current_task)
398
+ servers.each do |server|
399
+ attributes = _generate_attributes(:hosts => server.host, :roles => role_names_for_host(server), :run_list => run_list)
400
+ top.put(_json_attributes(attributes), chef_solo_attributes_file, options.merge(:hosts => server.host))
241
401
  end
242
- }
402
+ end
243
403
 
244
- task(:invoke) {
245
- execute = []
246
- execute << "cd #{chef_solo_path}"
247
- execute << "#{sudo} #{bundle_cmd} exec chef-solo " + \
248
- "-c #{File.join(chef_solo_path, 'config', 'solo.rb')} " + \
249
- "-j #{File.join(chef_solo_path, 'config', 'solo.json')}"
250
- run(execute.join(' && '))
251
- }
404
+ def invoke(options={})
405
+ bin = fetch(:chef_solo_executable, "chef-solo")
406
+ args = fetch(:chef_solo_options, [])
407
+ args << "-c #{chef_solo_config_file.dump}"
408
+ args << "-j #{chef_solo_attributes_file.dump}"
409
+ run("cd #{chef_solo_path.dump} && #{sudo} #{bundle_cmd} exec #{bin.dump} #{args.join(" ")}", options)
410
+ end
252
411
  }
253
412
  }
254
413
  end