capistrano-provisioning 0.0.3

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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Sam Phillips
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.mdown ADDED
@@ -0,0 +1,229 @@
1
+ # Capistrano Provisioning
2
+
3
+ Capistrano Provisioning is an extension to Capistrano that allows you to define clusters of servers and run provisioning tasks on them, such as installing users. It is a replacement for the fabric gem (http://rubygems.org/gems/fabric).
4
+
5
+ ## A word of caution
6
+
7
+ This software is in alpha - we're releasing early so we can start using this gem in anger.
8
+
9
+ ## Installation
10
+
11
+ ### With bundler
12
+
13
+ Add the following to your Gemfile:
14
+
15
+ gem 'capistrano-provisioning'
16
+
17
+ If your bundler environment isn't initialised yet, you'll need to add something like this to your deploy.rb/Capfile:
18
+
19
+ require 'rubygems'
20
+ require 'bundler'
21
+
22
+ Bundler.setup
23
+ require 'capistrano-provisioning/recipes'
24
+
25
+ ### Without bundler
26
+
27
+ gem install capistrano-provisioning
28
+
29
+ Either within your Capfile, or your deploy.rb, add:
30
+
31
+ require 'capistrano-provisioning/recipes'
32
+
33
+ ## Usage
34
+
35
+ Once you have required the gem, the `cluster` command is available to you within capistrano recipes.
36
+
37
+ The simplest usage of this would be something like:
38
+
39
+ cluster :web, 'web1.example.com'
40
+
41
+ In fact, this is shorthand for the following:
42
+
43
+ cluster :web do
44
+ servers 'web1.example.com'
45
+ end
46
+
47
+ Both of these would add a cap task called 'web'; calling this will mean any task specified later in the chain will run on the 'web' cluster. Eg:
48
+
49
+ cap web invoke COMMAND='uptime'
50
+
51
+ Which will run the 'uptime' command on 'web1.example.com'.
52
+
53
+ You can specify multiple remote hosts like so:
54
+
55
+ cluster :web, 'web1.example.com', 'web2.example.com'
56
+
57
+ So... this far it's quite similar to cap's own 'role' syntax. After this, however, life gets interesting...
58
+
59
+ ### Chaining
60
+
61
+ If you want to run a task on multiple clusters, simply chain them as so:
62
+
63
+ cap web db invoke COMMAND='uptime'
64
+
65
+ ... Assuming that you've defined a web and a db cluster!
66
+
67
+ This will also work with namespaced clusters.
68
+
69
+ ### Installing users
70
+
71
+ Specify the users that belong on a cluster as so:
72
+
73
+ cluster :web do
74
+ servers 'web1.example.com'
75
+ users 'bob', 'joe'
76
+ end
77
+
78
+ And put these users' public ssh keys in config/keys directory as so `config/keys/bob.pub` and `config/keys/joe.pub`
79
+
80
+ Running:
81
+
82
+ cap web install_users
83
+
84
+ ... will now create these users on the servers if they don't exist, and add the keys to their authorized_keys file.
85
+
86
+ You can specify groups as so:
87
+
88
+ cluster :web do
89
+ servers 'web1.example.com'
90
+ users 'bob', 'joe', :groups => ['some_group']
91
+ users 'rupert'
92
+ end
93
+
94
+ This will add Bob and Joe to some_group (:groups is an array, so add as many as you like), but won't add Rupert.
95
+
96
+ ### Bootstrapping
97
+
98
+ To specify a block of code that is to be run to 'bootstrap' a server, do this:
99
+
100
+ cluster :web do
101
+ servers 'web1.example.com'
102
+ bootstrap do
103
+ run 'some_command'
104
+ end
105
+ end
106
+
107
+ Run the following:
108
+
109
+ cap web run_bootstrap
110
+
111
+ And `some_command` will be run on the servers.
112
+
113
+ ### Namespaces and default users
114
+
115
+ Namespaces work as you would expect:
116
+
117
+ namespace :system1 do
118
+ cluster :web do
119
+ # ...
120
+ end
121
+
122
+ cluster :db do
123
+ # ...
124
+ end
125
+ end
126
+
127
+ This block will create the following tasks:
128
+
129
+ cap system1:web
130
+ cap system1:db
131
+
132
+ As well as
133
+
134
+ cap system1:all
135
+
136
+ Which will load all of the clusters defined in `system1`.
137
+
138
+ It is also possible to define a default group of users for a namespace:
139
+
140
+ namespace :system1 do
141
+ default_users 'bob', 'joe', :groups => ['some_group']
142
+ default_users 'rupert'
143
+ cluster :web do
144
+ # ...
145
+ end
146
+
147
+ cluster :db do
148
+ # ...
149
+ end
150
+ end
151
+
152
+ Which would mean that running:
153
+
154
+ cap system1:web install_users
155
+
156
+ ... would add Bob, Joe and Rupert to the web cluster, with the appropriate groups.
157
+
158
+ #### Inheriting default users
159
+
160
+ By default, namespaces do _not_ inherit default users from the namespace above. If you want this inheritance, it's easy:
161
+
162
+ namespace :nested_namespace do
163
+ inherit_default_users
164
+ end
165
+
166
+ If you want those users to have an additional group within this namespace only, use the following:
167
+
168
+ namespace :nested_namespace do
169
+ inherit_default_users :additional_groups => 'additional_group'
170
+ end
171
+
172
+ You can also pass an array of additional groups:
173
+
174
+ namespace :nested_namespace do
175
+ inherit_default_users :additional_groups => ['additional_group_1', 'additional_group_2']
176
+ end
177
+
178
+ #### Inheriting specific default users
179
+
180
+ These groups of default users can also be named, so you can atomically specify inheritance:
181
+
182
+ namespace :system1 do
183
+ default_users 'bob', 'joe', :groups => ['some_group']
184
+ default_users 'rupert'
185
+ cluster :web do
186
+ # ...
187
+ end
188
+
189
+ cluster :db do
190
+ # ...
191
+ end
192
+ end
193
+
194
+ Clusters can also inherit these named groups:
195
+
196
+ namespace :system1 do
197
+ default_users :admins, 'bob', 'joe'
198
+ cluster :web do
199
+ user :admins
200
+ end
201
+ end
202
+
203
+ These can be mixed in with normal user names:
204
+
205
+ namespace :system1 do
206
+ default_users :admins, 'bob', 'joe'
207
+ cluster :web do
208
+ user :admins, 'sam'
209
+ end
210
+ end
211
+
212
+ ## Features we guess we're probably going to need
213
+
214
+ * Ability to output business-friendly documentation of how the clusters are set up (and what user access exists)
215
+ * A way to remove user accounts that don't belong
216
+
217
+ ## Note on Patches/Pull Requests
218
+
219
+ * Fork the project.
220
+ * Make your feature addition or bug fix.
221
+ * Add tests for it. This is important so I don't break it in a
222
+ future version unintentionally.
223
+ * Commit, do not mess with rakefile, version, or history.
224
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
225
+ * Send me a pull request. Bonus points for topic branches.
226
+
227
+ ## Copyright
228
+
229
+ Copyright (c) 2010 Sam Phillips. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "capistrano-provisioning"
8
+ gem.summary = %Q{Capistrano provisioning}
9
+ gem.description = %Q{Capistrano provisioning}
10
+ gem.email = "sam@samdanavia.com"
11
+ gem.homepage = "http://github.com/samdanavia/capistrano-provisioning"
12
+ gem.authors = ["Sam Phillips"]
13
+ gem.add_dependency "capistrano", ">= 2.5.18"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "capistrano-provisioning #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.3
@@ -0,0 +1,70 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{capistrano-provisioning}
8
+ s.version = "0.0.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Sam Phillips"]
12
+ s.date = %q{2010-06-16}
13
+ s.description = %q{Capistrano provisioning}
14
+ s.email = %q{sam@samdanavia.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.mdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "LICENSE",
22
+ "README.mdown",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "capistrano-provisioning.gemspec",
26
+ "lib/capistrano-provisioning.rb",
27
+ "lib/capistrano-provisioning/cluster.rb",
28
+ "lib/capistrano-provisioning/namespaces.rb",
29
+ "lib/capistrano-provisioning/recipes.rb",
30
+ "lib/capistrano-provisioning/user.rb",
31
+ "pkg/capistrano-provisioning-0.0.0.gem",
32
+ "pkg/capistrano-provisioning-0.0.1.gem",
33
+ "pkg/capistrano-provisioning-0.0.3.gem",
34
+ "spec/cluster_spec.rb",
35
+ "spec/namespaces_spec.rb",
36
+ "spec/recipes_spec.rb",
37
+ "spec/spec.opts",
38
+ "spec/spec_helper.rb",
39
+ "spec/user_spec.rb"
40
+ ]
41
+ s.homepage = %q{http://github.com/samdanavia/capistrano-provisioning}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.6}
45
+ s.summary = %q{Capistrano provisioning}
46
+ s.test_files = [
47
+ "spec/cluster_spec.rb",
48
+ "spec/namespaces_spec.rb",
49
+ "spec/recipes_spec.rb",
50
+ "spec/spec_helper.rb",
51
+ "spec/user_spec.rb"
52
+ ]
53
+
54
+ if s.respond_to? :specification_version then
55
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
56
+ s.specification_version = 3
57
+
58
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
59
+ s.add_runtime_dependency(%q<capistrano>, [">= 2.5.18"])
60
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
61
+ else
62
+ s.add_dependency(%q<capistrano>, [">= 2.5.18"])
63
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
64
+ end
65
+ else
66
+ s.add_dependency(%q<capistrano>, [">= 2.5.18"])
67
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
68
+ end
69
+ end
70
+
@@ -0,0 +1,79 @@
1
+ module CapistranoProvisioning
2
+ class Cluster
3
+ attr_accessor :name, :servers, :bootstrap, :config
4
+
5
+ def initialize(name, opts = {})
6
+ self.name = name
7
+ self.servers = opts[:servers]
8
+ self.bootstrap = opts[:bootstrap]
9
+ self.config = opts[:config]
10
+
11
+ @users = []
12
+
13
+ add_cluster_cap_task
14
+ end
15
+
16
+ def install_users
17
+ ensure_users
18
+
19
+ self.servers.each do |server|
20
+ self.users.each do |user|
21
+ user.install(:server => server)
22
+ end
23
+ end
24
+ end
25
+
26
+ def preview_users
27
+ ensure_users
28
+
29
+ self.servers.each do |server|
30
+ puts "#{server}: "
31
+ self.users.each do |user|
32
+ groups = user.groups.empty? ? '' : "(#{user.groups.join(', ')})"
33
+ puts "\t#{user.name} #{groups}"
34
+ end
35
+ end
36
+ end
37
+
38
+ def ensure_users
39
+ if @users.empty? and config.respond_to?(:default_users)
40
+ @users = config.default_users
41
+ end
42
+
43
+ abort "No users found" unless @users
44
+ end
45
+
46
+ def add_users(users, opts = {})
47
+ @users += users.collect do |user|
48
+ if user.is_a? CapistranoProvisioning::User
49
+ user.config = self.config
50
+ user
51
+ else
52
+ opts.merge!(:name => user, :config => self.config)
53
+ CapistranoProvisioning::User.new(opts) # This dependency should be injected, really.
54
+ end
55
+ end
56
+ end
57
+
58
+ def unique_name
59
+ self.config.send(:unique_name) + ":" + self.name.to_s
60
+ end
61
+
62
+ def users
63
+ ensure_users
64
+ @users
65
+ end
66
+
67
+ protected
68
+
69
+ def add_cluster_cap_task
70
+ cluster = self
71
+
72
+ self.config.task(name.to_sym, :desc => "Set the current cluster to '#{name}'") do
73
+ logger.info "Setting servers to #{cluster.servers.join(', ')}"
74
+ current_cluster = fetch(:clusters, [])
75
+ set(:clusters, current_cluster.push(cluster))
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,152 @@
1
+ module CapistranoProvisioning
2
+ module NamespaceExtension
3
+
4
+ attr_accessor :clusters
5
+ def cluster(name, *servers, &block)
6
+ # A note on initialising. At the moment I can't figure out how to initialize stuff from this module,
7
+ # hence the untidy defaulting here.
8
+ self.clusters ||= {}
9
+ self.create_namespace_all_task
10
+
11
+ cluster = CapistranoProvisioning::Cluster.new(name, :config => self)
12
+
13
+ self.clusters[cluster.unique_name] = cluster
14
+ self.clusters[cluster.unique_name].servers = servers
15
+
16
+ return unless block_given?
17
+
18
+ @current_cluster = cluster
19
+ yield block
20
+ @current_cluster = nil
21
+ end
22
+
23
+ def servers(*servers)
24
+ @current_cluster.servers = servers
25
+ end
26
+ alias :server :servers
27
+
28
+ def default_users(*args)
29
+ return @users || [] if args.empty?
30
+
31
+ name, users, options = parse_name_collection_and_options_args(args)
32
+ new_users = users.collect do |user|
33
+ CapistranoProvisioning::User.new(options.merge!(:name => user, :config => self))
34
+ end
35
+
36
+ if name
37
+ @user_groups ||= {}
38
+ @user_groups[name] = new_users
39
+ end
40
+
41
+ @users ||= []
42
+ @users += new_users
43
+ end
44
+ alias :default_user :default_users
45
+
46
+ def default_user_group(group)
47
+ @user_groups[group]
48
+ end
49
+
50
+ def inherit_default_users(*args)
51
+ groups, options = parse_collection_and_options_args(args)
52
+
53
+ options[:additional_groups] = options[:additional_groups].to_a
54
+ parent_users = clone_parent_users(groups)
55
+
56
+ if options[:additional_groups]
57
+ parent_users.collect! do |user|
58
+ user.groups += options[:additional_groups]
59
+ user
60
+ end
61
+ end
62
+
63
+ @users ||= []
64
+ @users += parent_users
65
+ end
66
+ alias :inherit_default_user :inherit_default_users
67
+
68
+ def users(*args)
69
+ users, options = parse_collection_and_options_args(args)
70
+
71
+ users_to_add = users.collect do |user|
72
+ if user.is_a? Symbol
73
+ group_users = @current_cluster.config.default_user_group(user).flatten
74
+ abort "Can't find user group for #{user} (in #{self.name})" unless group_users
75
+ group_users
76
+ else
77
+ user
78
+ end
79
+ end
80
+
81
+ users_to_add.flatten!
82
+ @current_cluster.add_users(users_to_add, options)
83
+ end
84
+ alias :user :users
85
+
86
+ def bootstrap(&block)
87
+ @current_cluster.bootstrap = block
88
+ end
89
+
90
+ def unique_name
91
+ if self.parent.respond_to?(:unique_name)
92
+ self.parent.unique_name + ":" + self.name.to_s
93
+ else
94
+ self.name.to_s
95
+ end
96
+ end
97
+
98
+ protected
99
+ def create_namespace_all_task
100
+ return if self.tasks.keys.include?(:all)
101
+
102
+ task(:all, :desc => "Set the current clusters '#{name}'") do
103
+ clusters = self.clusters
104
+ clusters.merge!(self.offspring_clusters)
105
+
106
+ logger.info "Setting clusters to #{clusters.keys.collect(&:to_s).sort.join(', ')}"
107
+ set(:clusters, clusters.values)
108
+ end
109
+ end
110
+
111
+ def offspring_clusters
112
+ clusters = self.clusters
113
+
114
+ self.namespaces.each do |name, namespace|
115
+ clusters.merge!(namespace.offspring_clusters)
116
+ end
117
+
118
+ clusters
119
+ end
120
+
121
+ def clone_parent_users(groups = [])
122
+ unless groups.empty?
123
+ groups.collect { |group| self.parent.default_user_group(group).collect(&:dup) }.flatten
124
+ else
125
+ self.parent.default_users.collect(&:dup)
126
+ end
127
+ end
128
+
129
+ def parse_name_collection_and_options_args(args)
130
+ args = args.dup
131
+
132
+ name = (args.first.is_a? Symbol) ? args.shift : nil
133
+ collection, options = parse_collection_and_options_args(args)
134
+ return name, collection, options
135
+ end
136
+
137
+ def parse_collection_and_options_args(args)
138
+ args = args.dup
139
+
140
+ if args.last.is_a? Hash
141
+ options = args.pop
142
+ else
143
+ options = {}
144
+ end
145
+
146
+ collection = args
147
+ return collection, options
148
+ end
149
+ end
150
+ end
151
+
152
+ Capistrano::Configuration::Namespaces::Namespace.send(:include, CapistranoProvisioning::NamespaceExtension)
@@ -0,0 +1,40 @@
1
+ require 'capistrano-provisioning'
2
+
3
+ Capistrano::Configuration.instance(:must_exist).load do
4
+ on :before, "cluster_ensure", :only => ["run_bootstrap", "install_users", "preview_users"]
5
+ desc "[Internal] Ensures that a cluster has been specified"
6
+ task :cluster_ensure do
7
+ @clusters = fetch(:clusters, false)
8
+ unless @clusters
9
+ abort("No cluster specified - please use one of '#{self.clusters.keys.join(', ')}'")
10
+ end
11
+ end
12
+
13
+ # Will set up a 'cluster' role, that will be set by the current provision.
14
+ set :servers, []
15
+ role(:cluster) {
16
+ fetch(:clusters).collect(&:servers).flatten
17
+ }
18
+
19
+ desc "Runs the bootstrap comamnds on the cluster"
20
+ task :run_bootstrap do
21
+ @clusters.each do |cluster|
22
+ abort "No bootstrap block given for '#{cluster.name}' cluster" unless cluster.bootstrap
23
+ cluster.bootstrap.call
24
+ end
25
+ end
26
+
27
+ desc "Installs the specified users on the cluster"
28
+ task :install_users do
29
+ @clusters.each do |cluster|
30
+ cluster.install_users
31
+ end
32
+ end
33
+
34
+ task :preview_users do
35
+ puts "The following users will be added: "
36
+ @clusters.each do |cluster|
37
+ cluster.preview_users
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,114 @@
1
+ module CapistranoProvisioning
2
+ class User
3
+ attr_accessor :name, :config
4
+ attr_writer :groups, :key
5
+
6
+ def initialize(opts = {})
7
+ self.name = opts[:name]
8
+ self.config = opts[:config]
9
+ self.groups = opts[:groups].to_a || []
10
+ end
11
+
12
+ def install(opts = {})
13
+ abort "Aborting - Cannot install user #{self.name} as no server specified" unless opts[:server]
14
+ abort "Aborting - Could not find key for #{self.name} at #{local_key_file_path}" unless key
15
+
16
+ logger.debug "installing #{self.name} on #{opts[:server]}"
17
+
18
+ self.create_account_on_server(opts[:server]) unless self.account_exists?(opts[:server])
19
+ self.create_ssh_config_directory(opts[:server])
20
+ self.update_authorized_keys(opts[:server])
21
+ self.add_account_to_groups(opts[:server])
22
+ end
23
+
24
+ def groups
25
+ @groups.uniq
26
+ end
27
+
28
+ protected
29
+ def account_exists?(server)
30
+ begin
31
+ if capture("id #{self.name}", :hosts => server)
32
+ true
33
+ else
34
+ false
35
+ end
36
+ rescue
37
+ # Must figure out a better way to capture that the command has failed
38
+ false
39
+ end
40
+ end
41
+
42
+ def key
43
+ File.read(local_key_file_path) if File.exists?(local_key_file_path)
44
+ end
45
+
46
+ def create_account_on_server(server)
47
+ run "#{sudo} /usr/sbin/useradd -m #{name}", :pty => true, :hosts => server
48
+ end
49
+
50
+ def add_account_to_groups(server)
51
+ self.groups.each do |group|
52
+ run "#{sudo} /usr/sbin/usermod -a -G#{group} #{self.name}", :pty => true, :hosts => server
53
+ end
54
+ end
55
+
56
+ def create_ssh_config_directory(server)
57
+ # Actual dirt
58
+ commands = <<-COMMANDS
59
+ sudo su root -c 'if [ ! -d #{ssh_config_directory_path} ]; then
60
+ sudo mkdir #{ssh_config_directory_path};
61
+ fi';
62
+ #{sudo} chown #{name} #{ssh_config_directory_path} &&
63
+ #{sudo} chmod 700 #{ssh_config_directory_path}
64
+ COMMANDS
65
+
66
+ run commands, :pty => true, :hosts => server
67
+ end
68
+
69
+ def update_authorized_keys(server)
70
+ commands = <<-COMMANDS
71
+ #{sudo} touch #{authorized_keys_file_path} &&
72
+ echo '#{key}' | sudo tee #{authorized_keys_file_path}
73
+ COMMANDS
74
+ run commands, :pty => true, :hosts => server
75
+ end
76
+
77
+ def authorized_keys_file_path
78
+ "/home/#{self.name}/.ssh/authorized_keys"
79
+ end
80
+
81
+ def home_directory_path
82
+ "/home/#{self.name}/"
83
+ end
84
+
85
+ def ssh_config_directory_path
86
+ "/home/#{self.name}/.ssh/"
87
+ end
88
+
89
+ def local_key_file_path
90
+ "config/keys/#{self.name}.pub"
91
+ end
92
+
93
+
94
+ protected
95
+ # This isn't the best way to get to the run/logger stuff in this class -
96
+ # need to figure out a better way to do this, or a way to not need to.
97
+
98
+ def sudo(*parameters, &block)
99
+ self.config.sudo(*parameters, &block)
100
+ end
101
+
102
+ def logger
103
+ self.config.logger
104
+ end
105
+
106
+ def run(cmd, options={}, &block)
107
+ self.config.run(cmd, options, &block)
108
+ end
109
+
110
+ def capture(command, options={})
111
+ self.config.capture(command, options)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ require 'capistrano-provisioning/cluster'
2
+ require 'capistrano-provisioning/user'
3
+ require 'capistrano-provisioning/namespaces'
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe CapistranoProvisioning::Cluster do
4
+ let(:config) { Capistrano::Configuration.new }
5
+ let(:user) { CapistranoProvisioning::User.new }
6
+ let(:namespace) { Capistrano::Configuration::Namespaces::Namespace.new(:test_namespace, config) }
7
+ let(:cluster) { CapistranoProvisioning::Cluster.new(cluster_name, :servers => [], :config => config) }
8
+ let(:cluster_name) { '16nHElbcaf' }
9
+
10
+ it "should add a capistrano task" do
11
+ config_mock = mock(Capistrano::Configuration)
12
+ config_mock.should_receive(:task).with(cluster_name.to_sym, anything)
13
+ CapistranoProvisioning::Cluster.new(cluster_name, :servers => [], :config => config_mock)
14
+ end
15
+
16
+ it "should return an empty of array of users when it has none" do
17
+ cluster.users.should == []
18
+ end
19
+
20
+ it "should add users from their names" do
21
+ # For some reason `expect {}.to change` didn't work here - reverting to old style for now
22
+ cluster.add_users ['test1', 'test2']
23
+ cluster.users.length.should == 2
24
+ end
25
+
26
+ it "should add users from objects" do
27
+ cluster.add_users [user]
28
+ cluster.users.length.should == 1
29
+ end
30
+
31
+ it "should have a unique name" do
32
+ cluster = CapistranoProvisioning::Cluster.new(cluster_name, :servers => [], :config => namespace)
33
+ cluster.unique_name.should == "test_namespace:#{cluster_name}"
34
+ end
35
+
36
+ it "should take a bootstrap block" do
37
+ block = Proc.new {}
38
+ cluster.bootstrap = block
39
+ cluster.bootstrap.should == block
40
+ end
41
+
42
+ context "and users" do
43
+ it "should use the namespace's default users if no users are specified" do
44
+ namespace.default_users 'sam', 'david'
45
+ cluster = CapistranoProvisioning::Cluster.new(cluster_name, :config => namespace)
46
+
47
+ cluster.users.collect(&:name).should include('sam', 'david')
48
+ end
49
+
50
+ it "should not add users to the namespace" do
51
+ cluster = CapistranoProvisioning::Cluster.new(cluster_name, :config => namespace)
52
+ cluster.add_users ['bob', 'juan']
53
+
54
+ namespace.default_users.should == []
55
+ end
56
+
57
+ it "should use the clusters's users if users are specified" do
58
+ namespace.default_users 'sam', 'david'
59
+ cluster = CapistranoProvisioning::Cluster.new(cluster_name, :config => namespace)
60
+
61
+ cluster.add_users ['bob', 'juan']
62
+
63
+ user_names = cluster.users.collect(&:name)
64
+ user_names.should include('bob', 'juan')
65
+ user_names.should_not include('sam', 'david')
66
+ end
67
+
68
+ it "should install users" do
69
+ test_user, test_user_2 = user.dup, user.dup
70
+
71
+ test_user.stub!(:key => 'test key')
72
+ test_user.should_receive(:install)
73
+
74
+ test_user_2.stub!(:key => 'test key 2')
75
+ test_user_2.should_receive(:install)
76
+
77
+ cluster.servers = 'host1.example.com'
78
+ cluster.add_users [test_user, test_user_2]
79
+ cluster.install_users
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,227 @@
1
+ require 'spec_helper'
2
+
3
+ describe Capistrano::Configuration do
4
+ let(:config) { Capistrano::Configuration.new }
5
+ let(:namespace) { Capistrano::Configuration::Namespaces::Namespace.new(:test_namespace, config) }
6
+
7
+ context "default users" do
8
+ it "should take a list of default users" do
9
+ namespace.default_users 'sam', 'chris'
10
+ namespace.default_users.length.should == 2
11
+ end
12
+
13
+ it "should take a name for the list of users" do
14
+ namespace.default_users :admin, 'sam', 'chris'
15
+ namespace.default_users.length.should == 2
16
+ end
17
+
18
+ it "should return an empty array if there are no default users" do
19
+ namespace.default_users.length.should == 0
20
+ end
21
+ end
22
+
23
+ describe "default user inheritance" do
24
+ it "should inherit the users" do
25
+ namespace.default_users 'chris', 'sam'
26
+ namespace.namespace :nested do
27
+ inherit_default_users
28
+ end
29
+
30
+ namespace.namespaces[:nested].default_users.length.should == 2
31
+ end
32
+
33
+ it "should inherit a named group of users" do
34
+ namespace.default_users :admin, 'chris', 'sam'
35
+ namespace.namespace :nested do
36
+ inherit_default_users :admin
37
+ end
38
+
39
+ namespace.namespaces[:nested].default_users.length.should == 2
40
+ end
41
+
42
+ it "should inherit multiple named groups of users" do
43
+ namespace.default_users :admin, 'chris', 'sam'
44
+ namespace.default_users :developers, 'david', 'rob'
45
+
46
+ namespace.namespace :nested do
47
+ inherit_default_users :admin, :developers
48
+ end
49
+
50
+ namespace.namespaces[:nested].default_users.length.should == 4
51
+ end
52
+
53
+ it "should inherit the users' groups" do
54
+ namespace.default_users 'sam', 'chris', :groups => 'test_group'
55
+ namespace.namespace :nested do
56
+ inherit_default_users
57
+ end
58
+
59
+ namespace.namespaces[:nested].default_users.each do |user|
60
+ user.groups.should include('test_group')
61
+ end
62
+ end
63
+
64
+ describe "additional groups" do
65
+
66
+ it "should add one additional groups from a string" do
67
+ namespace.default_users 'sam', 'chris', :groups => 'test_group'
68
+ namespace.namespace :nested do
69
+ inherit_default_users :additional_groups => 'test_group_2'
70
+ end
71
+
72
+ namespace.namespaces[:nested].default_users.each do |user|
73
+ user.groups.should include('test_group')
74
+ user.groups.should include('test_group_2')
75
+ end
76
+ end
77
+
78
+ it "should add any additional groups from an array" do
79
+ namespace.default_users 'sam', 'chris', :groups => 'test_group'
80
+ namespace.namespace :nested do
81
+ inherit_default_users :additional_groups => ['test_group_2']
82
+ end
83
+
84
+ namespace.namespaces[:nested].default_users.each do |user|
85
+ user.groups.should include('test_group')
86
+ user.groups.should include('test_group_2')
87
+ end
88
+ end
89
+
90
+ it "should not add additional groups to the original users" do
91
+ config.namespace :test_namespace do
92
+ default_users 'sam', 'chris', :groups => 'test_group'
93
+ namespace :nested do
94
+ inherit_default_users :additional_groups => 'test_group_2'
95
+ end
96
+ end
97
+
98
+ config.namespaces[:test_namespace].default_users.each do |user|
99
+ user.groups.should_not include('test_group_2')
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ it "should create a cluster" do
106
+ namespace.cluster :test_cluster
107
+ namespace.clusters["test_namespace:test_cluster"].should be_a CapistranoProvisioning::Cluster
108
+ end
109
+
110
+ it "cluster should take a list of servers inline" do
111
+ namespace.cluster :test_cluster, 'server_1', 'server_2'
112
+ namespace.clusters["test_namespace:test_cluster"].servers.length.should == 2
113
+ end
114
+
115
+ context "within a cluster" do
116
+ it "should take a list of users" do
117
+ config.namespace :test_namespace do
118
+ cluster :test_cluster do
119
+ users 'sam', 'chris'
120
+ end
121
+ end
122
+
123
+ users = config.namespaces[:test_namespace].clusters["test_namespace:test_cluster"].users
124
+ user_names = users.collect(&:name)
125
+ user_names.should include('sam','chris')
126
+ end
127
+
128
+ it "should inherit named users" do
129
+ config.namespace :test_namespace do
130
+ default_users :chaps, 'joe', 'bob'
131
+ cluster :test_cluster do
132
+ users :chaps
133
+ end
134
+ end
135
+
136
+ users = config.namespaces[:test_namespace].clusters["test_namespace:test_cluster"].users
137
+ users.each do |user|
138
+ user.should be_a CapistranoProvisioning::User
139
+ end
140
+
141
+ user_names = users.collect(&:name)
142
+ user_names.should include('joe', 'bob')
143
+ user_names.should_not include(:chaps)
144
+
145
+ end
146
+
147
+ it "should raise an error if passed named users that do not exist" do
148
+ expect {
149
+ config.namespace :test_namespace do
150
+ default_users :chaps, 'joe', 'bob'
151
+ cluster :test_cluster do
152
+ users :non_existent_group
153
+ end
154
+ end
155
+ }.to raise_error
156
+ end
157
+
158
+ it "should inherit named users mixed in with user names" do
159
+ config.namespace :test_namespace do
160
+ default_users :chaps, 'joe', 'bob'
161
+ cluster :test_cluster do
162
+ users :chaps, 'sam'
163
+ end
164
+ end
165
+
166
+ user_names = config.namespaces[:test_namespace].clusters["test_namespace:test_cluster"].users.collect(&:name)
167
+ user_names.should include('joe', 'bob', 'sam')
168
+ user_names.should_not include(:chaps)
169
+ end
170
+ end
171
+
172
+ describe "unique name" do
173
+ it "should give a unique name on the top-tier namespace" do
174
+ namespace.send(:unique_name).should == 'test_namespace'
175
+ end
176
+
177
+ it "should give a unique name on a second-tier namespace" do
178
+ namespace.namespace :foo do; end
179
+ namespace.namespaces[:foo].send(:unique_name).should == "test_namespace:foo"
180
+ end
181
+
182
+ it "should give a unique name on a third-tier namespace" do
183
+ namespace.namespace :foo do
184
+ namespace :bar do; end
185
+ end
186
+ namespace.namespaces[:foo].namespaces[:bar].send(:unique_name).should == "test_namespace:foo:bar"
187
+ end
188
+ end
189
+
190
+ # Shouldn't necessarily be testing these protected methods,
191
+ # however it is complex and worthy of a spec, I feel.
192
+ context "argument parsing" do
193
+ it "should handle a named collection" do
194
+ args = [:test, 'sam', 'bob']
195
+ name, collection, options = namespace.send(:parse_name_collection_and_options_args, args)
196
+
197
+ name.should == :test
198
+ collection.should == ['sam', 'bob']
199
+ options.should be_empty
200
+ end
201
+
202
+ it "should handle a named collection with options" do
203
+ args = [:test, 'sam', 'bob', { :option => true } ]
204
+ name, collection, options = namespace.send(:parse_name_collection_and_options_args, args)
205
+
206
+ name.should == args.first
207
+ collection.should == args.slice(1,2)
208
+ options.should == args.last
209
+ end
210
+
211
+ it "should handle a collection with no options" do
212
+ args = ['sam', 'bob']
213
+ users, options = namespace.send(:parse_collection_and_options_args, args)
214
+
215
+ users.should == args
216
+ options.should be_empty
217
+ end
218
+
219
+ it "should handle a collection with options" do
220
+ args = ['sam', 'bob', { :option => true } ]
221
+ users, options = namespace.send(:parse_collection_and_options_args, args)
222
+
223
+ users.should == args.slice(0,2)
224
+ options.should == args.last
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ # TODO: Not sure how to test this yet
4
+
5
+ describe "Recipes" do
6
+ context "namespace" do
7
+ it "should create an :all task which loads all servers in that namespace"
8
+ end
9
+
10
+ context "cluster definition" do
11
+ it "should create a task to set the servers"
12
+
13
+ it "should create a task to bootstrap the servers"
14
+
15
+ it "should create a task to install users on the servers"
16
+ end
17
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format specdoc
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ # This might not be a good idea... http://gist.github.com/54177
5
+ require 'rubygems'
6
+
7
+ require 'spec'
8
+ require 'spec/autorun'
9
+
10
+ require 'capistrano'
11
+ require 'capistrano-provisioning'
12
+
13
+ Spec::Runner.configure do |config|
14
+
15
+ end
data/spec/user_spec.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe CapistranoProvisioning::User do
4
+ let(:username) { 'z6MeKbLOXPqi5P' }
5
+ let(:server) { 'test1.example.com' }
6
+ let(:config_mock) { mock(Capistrano::Configuration) }
7
+ let(:user) { CapistranoProvisioning::User.new(:name => username, :server => server, :config => config_mock) }
8
+
9
+ describe "install" do
10
+ it "should require a server" do
11
+ expect { user.install }.to raise_error(SystemExit)
12
+ end
13
+
14
+ it "should error if the user's ssh key cannot be loaded" do
15
+ expect { user.install(:server => server) }.to raise_error(SystemExit)
16
+ end
17
+ end
18
+
19
+ it "should create an account on the server" do
20
+ config_mock.should_receive(:run).with(/#{username}/, anything()).once
21
+ config_mock.should_receive(:sudo).with(no_args()).once
22
+
23
+ user.send(:create_account_on_server, server)
24
+ end
25
+
26
+ describe "and groups" do
27
+ it "should return an empty array if there are no groups" do
28
+ user.groups.should == []
29
+ end
30
+
31
+ it "should de-duplify groups" do
32
+ user.groups = ['test_group', 'test_group2', 'test_group']
33
+ user.groups.should == ['test_group','test_group2']
34
+ end
35
+
36
+ it "should add a user to one group" do
37
+ user = CapistranoProvisioning::User.new(:name => username, :server => server, :config => config_mock, :groups => 'test_group')
38
+ user.groups.should include("test_group")
39
+ end
40
+
41
+ it "should add a user to an array of groups" do
42
+ user = CapistranoProvisioning::User.new(:name => username, :server => server, :config => config_mock, :groups => ['test_group', 'test_group_2'])
43
+ user.groups.should include("test_group")
44
+ user.groups.should include("test_group_2")
45
+ end
46
+ end
47
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capistrano-provisioning
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 3
9
+ version: 0.0.3
10
+ platform: ruby
11
+ authors:
12
+ - Sam Phillips
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-06-16 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: capistrano
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 5
30
+ - 18
31
+ version: 2.5.18
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 2
44
+ - 9
45
+ version: 1.2.9
46
+ type: :development
47
+ version_requirements: *id002
48
+ description: Capistrano provisioning
49
+ email: sam@samdanavia.com
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - LICENSE
56
+ - README.mdown
57
+ files:
58
+ - .document
59
+ - LICENSE
60
+ - README.mdown
61
+ - Rakefile
62
+ - VERSION
63
+ - capistrano-provisioning.gemspec
64
+ - lib/capistrano-provisioning.rb
65
+ - lib/capistrano-provisioning/cluster.rb
66
+ - lib/capistrano-provisioning/namespaces.rb
67
+ - lib/capistrano-provisioning/recipes.rb
68
+ - lib/capistrano-provisioning/user.rb
69
+ - pkg/capistrano-provisioning-0.0.0.gem
70
+ - pkg/capistrano-provisioning-0.0.1.gem
71
+ - pkg/capistrano-provisioning-0.0.3.gem
72
+ - spec/cluster_spec.rb
73
+ - spec/namespaces_spec.rb
74
+ - spec/recipes_spec.rb
75
+ - spec/spec.opts
76
+ - spec/spec_helper.rb
77
+ - spec/user_spec.rb
78
+ has_rdoc: true
79
+ homepage: http://github.com/samdanavia/capistrano-provisioning
80
+ licenses: []
81
+
82
+ post_install_message:
83
+ rdoc_options:
84
+ - --charset=UTF-8
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ requirements: []
102
+
103
+ rubyforge_project:
104
+ rubygems_version: 1.3.6
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Capistrano provisioning
108
+ test_files:
109
+ - spec/cluster_spec.rb
110
+ - spec/namespaces_spec.rb
111
+ - spec/recipes_spec.rb
112
+ - spec/spec_helper.rb
113
+ - spec/user_spec.rb