sprinkle 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/CREDITS +14 -0
  2. data/History.txt +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/Manifest.txt +63 -0
  5. data/README.rdoc +218 -0
  6. data/Rakefile +4 -0
  7. data/TODO +56 -0
  8. data/bin/sprinkle +79 -0
  9. data/config/hoe.rb +70 -0
  10. data/config/requirements.rb +17 -0
  11. data/examples/merb/deploy.rb +5 -0
  12. data/examples/rails/README +15 -0
  13. data/examples/rails/deploy.rb +2 -0
  14. data/examples/rails/packages/database.rb +9 -0
  15. data/examples/rails/packages/essential.rb +6 -0
  16. data/examples/rails/packages/rails.rb +28 -0
  17. data/examples/rails/packages/search.rb +11 -0
  18. data/examples/rails/packages/server.rb +28 -0
  19. data/examples/rails/rails.rb +71 -0
  20. data/examples/sprinkle/sprinkle.rb +38 -0
  21. data/lib/sprinkle/actors/capistrano.rb +80 -0
  22. data/lib/sprinkle/actors/vlad.rb +30 -0
  23. data/lib/sprinkle/deployment.rb +33 -0
  24. data/lib/sprinkle/extensions/arbitrary_options.rb +10 -0
  25. data/lib/sprinkle/extensions/array.rb +7 -0
  26. data/lib/sprinkle/extensions/blank_slate.rb +5 -0
  27. data/lib/sprinkle/extensions/dsl_accessor.rb +15 -0
  28. data/lib/sprinkle/extensions/string.rb +10 -0
  29. data/lib/sprinkle/extensions/symbol.rb +7 -0
  30. data/lib/sprinkle/installers/apt.rb +20 -0
  31. data/lib/sprinkle/installers/gem.rb +33 -0
  32. data/lib/sprinkle/installers/installer.rb +85 -0
  33. data/lib/sprinkle/installers/rake.rb +17 -0
  34. data/lib/sprinkle/installers/rpm.rb +20 -0
  35. data/lib/sprinkle/installers/source.rb +120 -0
  36. data/lib/sprinkle/package.rb +94 -0
  37. data/lib/sprinkle/policy.rb +85 -0
  38. data/lib/sprinkle/script.rb +13 -0
  39. data/lib/sprinkle/sequence.rb +21 -0
  40. data/lib/sprinkle/version.rb +9 -0
  41. data/lib/sprinkle.rb +26 -0
  42. data/script/destroy +14 -0
  43. data/script/generate +14 -0
  44. data/spec/spec.opts +1 -0
  45. data/spec/spec_helper.rb +17 -0
  46. data/spec/sprinkle/actors/capistrano_spec.rb +150 -0
  47. data/spec/sprinkle/deployment_spec.rb +80 -0
  48. data/spec/sprinkle/extensions/array_spec.rb +19 -0
  49. data/spec/sprinkle/extensions/string_spec.rb +21 -0
  50. data/spec/sprinkle/installers/apt_spec.rb +53 -0
  51. data/spec/sprinkle/installers/gem_spec.rb +75 -0
  52. data/spec/sprinkle/installers/installer_spec.rb +125 -0
  53. data/spec/sprinkle/installers/rpm_spec.rb +50 -0
  54. data/spec/sprinkle/installers/source_spec.rb +315 -0
  55. data/spec/sprinkle/package_spec.rb +247 -0
  56. data/spec/sprinkle/policy_spec.rb +126 -0
  57. data/spec/sprinkle/script_spec.rb +51 -0
  58. data/spec/sprinkle/sequence_spec.rb +44 -0
  59. data/spec/sprinkle/sprinkle_spec.rb +25 -0
  60. data/sprinkle.gemspec +43 -0
  61. data/tasks/deployment.rake +34 -0
  62. data/tasks/environment.rake +7 -0
  63. data/tasks/rspec.rake +21 -0
  64. metadata +157 -0
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env sprinkle -s
2
+
3
+ # Annotated Example Sprinkle Rails deployment script
4
+ #
5
+ # This is an example Sprinkle script configured to install Rails from gems, Apache, Ruby and
6
+ # Sphinx from source, and mysql from apt on an Ubuntu system.
7
+ #
8
+ # Installation is configured to run via capistrano (and an accompanying deploy.rb recipe script).
9
+ # Source based packages are downloaded and built into /usr/local on the remote system.
10
+ #
11
+ # A sprinkle script is separated into 3 different sections. Packages, policies and deployment:
12
+
13
+
14
+ # Packages (separate files for brevity)
15
+ #
16
+ # Defines the world of packages as we know it. Each package has a name and
17
+ # set of metadata including its installer type (eg. apt, source, gem, etc). Packages can have
18
+ # relationships to each other via dependencies.
19
+
20
+ require 'packages/essential'
21
+ require 'packages/rails'
22
+ require 'packages/database'
23
+ require 'packages/server'
24
+ require 'packages/search'
25
+
26
+
27
+ # Policies
28
+ #
29
+ # Names a group of packages (optionally with versions) that apply to a particular set of roles:
30
+ #
31
+ # Associates the rails policy to the application servers. Contains rails, and surrounding
32
+ # packages. Note, appserver, database, webserver and search are all virtual packages defined above.
33
+ # If there's only one implementation of a virtual package, it's selected automatically, otherwise
34
+ # the user is requested to select which one to use.
35
+
36
+ policy :rails, :roles => :app do
37
+ requires :rails, :version => '2.1.0'
38
+ requires :appserver
39
+ requires :database
40
+ requires :webserver
41
+ requires :search
42
+ end
43
+
44
+
45
+ # Deployment
46
+ #
47
+ # Defines script wide settings such as a delivery mechanism for executing commands on the target
48
+ # system (eg. capistrano), and installer defaults (eg. build locations, etc):
49
+ #
50
+ # Configures spinkle to use capistrano for delivery of commands to the remote machines (via
51
+ # the named 'deploy' recipe). Also configures 'source' installer defaults to put package gear
52
+ # in /usr/local
53
+
54
+ deployment do
55
+
56
+ # mechanism for deployment
57
+ delivery :capistrano do
58
+ recipes 'deploy'
59
+ end
60
+
61
+ # source based package installer defaults
62
+ source do
63
+ prefix '/usr/local'
64
+ archives '/usr/local/sources'
65
+ builds '/usr/local/build'
66
+ end
67
+
68
+ end
69
+
70
+ # End of script, given the above information, Spinkle will apply the defined policy on all roles using the
71
+ # deployment settings specified.
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env sprinkle -c -s
2
+
3
+ # Example of the simplest Sprinkle script to install a single gem on a remote host. This
4
+ # particular script assumes that rubygems (and ruby, etc) are already installed on the remote
5
+ # host. To see a larger example of installing an entire ruby, rubygems, gem stack from source,
6
+ # please see the rails example.
7
+
8
+ # Packages, only sprinkle is defined in this world
9
+
10
+ package :sprinkle do
11
+ description 'Sprinkle Provisioning Tool'
12
+ gem 'crafterm-sprinkle' do
13
+ source 'http://gems.github.com' # use alternate gem server
14
+ #repository '/opt/local/gems' # specify an alternate local gem repository
15
+ end
16
+ end
17
+
18
+
19
+ # Policies, sprinkle policy requires only the sprinkle gem
20
+
21
+ policy :sprinkle, :roles => :app do
22
+ requires :sprinkle
23
+ end
24
+
25
+
26
+ # Deployment settings
27
+
28
+ deployment do
29
+
30
+ # use vlad for deployment
31
+ delivery :vlad do
32
+ role :app, 'yourhost.com'
33
+ end
34
+
35
+ end
36
+
37
+ # End of script, given the above information, Spinkle will apply the defined policy on all roles using the
38
+ # deployment settings specified.
@@ -0,0 +1,80 @@
1
+ require 'capistrano/cli'
2
+
3
+ module Sprinkle
4
+ module Actors
5
+ class Capistrano
6
+ attr_accessor :config, :loaded_recipes
7
+
8
+ def initialize(&block)
9
+ @config = ::Capistrano::Configuration.new
10
+ @config.logger.level = Sprinkle::OPTIONS[:verbose] ? ::Capistrano::Logger::INFO : ::Capistrano::Logger::IMPORTANT
11
+ @config.set(:password) { ::Capistrano::CLI.password_prompt }
12
+ if block
13
+ self.instance_eval &block
14
+ else
15
+ @config.load 'deploy' # normally in the config directory for rails
16
+ end
17
+ end
18
+
19
+ def recipes(script)
20
+ @loaded_recipes ||= []
21
+ @config.load script
22
+ @loaded_recipes << script
23
+ end
24
+
25
+ def process(name, commands, roles)
26
+ define_task(name, roles) do
27
+ via = fetch(:run_method, :sudo)
28
+ commands.each do |command|
29
+ invoke_command command, :via => via
30
+ end
31
+ end
32
+ run(name)
33
+ end
34
+
35
+ private
36
+
37
+ # REVISIT: can we set the description somehow?
38
+ def define_task(name, roles, &block)
39
+ @config.task task_sym(name), :roles => roles, &block
40
+ end
41
+
42
+ def run(task)
43
+ @config.send task_sym(task)
44
+ end
45
+
46
+ def task_sym(name)
47
+ "install_#{name.to_task_name}".to_sym
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+ =begin
55
+
56
+ # channel: the SSH channel object used for this response
57
+ # stream: either :err or :out, for stderr or stdout responses
58
+ # output: the text that the server is sending, might be in chunks
59
+ run "apt-get update" do |channel, stream, output|
60
+ if output =~ /Are you sure?/
61
+ answer = Capistrano::CLI.ui.ask("Are you sure: ")
62
+ channel.send_data(answer + "\n")
63
+ else
64
+ # allow the default callback to be processed
65
+ Capistrano::Configuration.default_io_proc.call[channel, stream, output]
66
+ end
67
+ end
68
+
69
+
70
+
71
+ You can tell subversion to use a different username+password by
72
+ setting a couple variables:
73
+ set :svn_username, "my svn username"
74
+ set :svn_password, "my svn password"
75
+ If you don't want to set the password explicitly in your recipe like
76
+ that, you can make capistrano prompt you for it like this:
77
+ set(:svn_password) { Capistrano::CLI.password_prompt("Subversion
78
+ password: ") }
79
+ - Jamis
80
+ =end
@@ -0,0 +1,30 @@
1
+ module Sprinkle
2
+ module Actors
3
+ class Vlad
4
+ require 'vlad'
5
+ attr_accessor :loaded_recipes
6
+
7
+ def initialize(&block)
8
+ self.instance_eval &block if block
9
+ end
10
+
11
+ def script(name)
12
+ @loaded_recipes ||= []
13
+ self.load name
14
+ @loaded_recipes << script
15
+ end
16
+
17
+ def process(name, commands, roles)
18
+ commands = commands.join ' && ' if commands.is_a? Array
19
+ t = remote_task(task_sym(name), :roles => roles) { run commands }
20
+ t.invoke
21
+ end
22
+
23
+ private
24
+
25
+ def task_sym(name)
26
+ "install_#{name.to_task_name}".to_sym
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ module Sprinkle
2
+ module Deployment
3
+ def deployment(&block)
4
+ @deployment = Deployment.new(&block)
5
+ end
6
+
7
+ class Deployment
8
+ attr_accessor :style, :defaults
9
+
10
+ def initialize(&block)
11
+ @defaults = {}
12
+ self.instance_eval(&block)
13
+ raise 'No delivery mechanism defined' unless @style
14
+ end
15
+
16
+ def delivery(type, &block)
17
+ @style = Actors.const_get(type.to_s.titleize).new &block
18
+ end
19
+
20
+ def method_missing(sym, *args, &block)
21
+ @defaults[sym] = block
22
+ end
23
+
24
+ def respond_to?(sym); !!@defaults[sym]; end
25
+
26
+ def process
27
+ POLICIES.each do |policy|
28
+ policy.process(self)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ module ArbitraryOptions
2
+ def self.included(base)
3
+ base.alias_method_chain :method_missing, :arbitrary_options
4
+ end
5
+
6
+ def method_missing_with_arbitrary_options(sym, *args, &block)
7
+ self.class.dsl_accessor sym
8
+ send(sym, *args, &block)
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ class Array
2
+
3
+ def to_task_name
4
+ collect(&:to_task_name).join('_')
5
+ end
6
+
7
+ end
@@ -0,0 +1,5 @@
1
+ class BlankSlate
2
+ instance_methods.each do |m|
3
+ undef_method(m) unless %w( __send__ __id__ send class inspect instance_eval instance_variables ).include?(m)
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ class Module
2
+ def dsl_accessor(*symbols)
3
+ symbols.each do |sym|
4
+ class_eval %{
5
+ def #{sym}(*val)
6
+ if val.empty?
7
+ @#{sym}
8
+ else
9
+ @#{sym} = val.size == 1 ? val[0] : val
10
+ end
11
+ end
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ class String
2
+
3
+ # REVISIT: what chars shall we allow in task names?
4
+ def to_task_name
5
+ s = downcase
6
+ s.gsub!(/-/, '_') # all - to _ chars
7
+ s
8
+ end
9
+
10
+ end
@@ -0,0 +1,7 @@
1
+ class Symbol
2
+
3
+ def to_task_name
4
+ to_s.to_task_name
5
+ end
6
+
7
+ end
@@ -0,0 +1,20 @@
1
+ module Sprinkle
2
+ module Installers
3
+ class Apt < Installer
4
+ attr_accessor :packages
5
+
6
+ def initialize(parent, packages, &block)
7
+ super parent, &block
8
+ packages = [packages] unless packages.is_a? Array
9
+ @packages = packages
10
+ end
11
+
12
+ protected
13
+
14
+ def install_commands
15
+ "DEBCONF_TERSE='yes' DEBIAN_PRIORITY='critical' DEBIAN_FRONTEND=noninteractive apt-get -qyu install #{@packages.join(' ')}"
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module Sprinkle
2
+ module Installers
3
+ class Gem < Installer
4
+ attr_accessor :gem
5
+
6
+ def initialize(parent, gem, options = {}, &block)
7
+ super parent, options, &block
8
+ @gem = gem
9
+ end
10
+
11
+ def source(location = nil)
12
+ # package defines an installer called source so here we specify a method directly
13
+ # rather than rely on the automatic options processing since packages' method missing
14
+ # won't be run
15
+ return @options[:source] unless location
16
+ @options[:source] = location
17
+ end
18
+
19
+ protected
20
+
21
+ # rubygems 0.9.5+ installs dependencies by default, and does platform selection
22
+
23
+ def install_commands
24
+ cmd = "gem install #{gem}"
25
+ cmd << " --version '#{version}'" if version
26
+ cmd << " --source #{source}" if source
27
+ cmd << " --install-dir #{repository}" if repository
28
+ cmd
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,85 @@
1
+ module Sprinkle
2
+ module Installers
3
+ class Installer
4
+ attr_accessor :delivery, :package, :options, :pre, :post
5
+
6
+ def initialize(package, options = {}, &block)
7
+ @package = package
8
+ @options = options
9
+ @pre = {}; @post = {}
10
+ self.instance_eval(&block) if block
11
+ end
12
+
13
+ def pre(stage, *commands)
14
+ @pre[stage] ||= []
15
+ @pre[stage] += commands
16
+ end
17
+
18
+ def post(stage, *commands)
19
+ @post[stage] ||= []
20
+ @post[stage] += commands
21
+ end
22
+
23
+ def defaults(deployment)
24
+ defaults = deployment.defaults[self.class.name.split(/::/).last.downcase.to_sym]
25
+ self.instance_eval(&defaults) if defaults
26
+ @delivery = deployment.style
27
+ end
28
+
29
+ def process(roles)
30
+ raise 'Unknown command delivery target' unless @delivery
31
+
32
+ if logger.debug?
33
+ sequence = install_sequence; sequence = sequence.join('; ') if sequence.is_a? Array
34
+ logger.debug "#{@package.name} install sequence: #{sequence} for roles: #{roles}\n"
35
+ end
36
+
37
+ unless Sprinkle::OPTIONS[:testing]
38
+ logger.info "--> Installing #{package.name} for roles: #{roles}"
39
+ @delivery.process(@package.name, install_sequence, roles)
40
+ end
41
+ end
42
+
43
+ def method_missing(sym, *args, &block)
44
+ unless args.empty? # mutate if not set
45
+ @options[sym] = *args unless @options[sym]
46
+ end
47
+
48
+ @options[sym] || @package.send(sym, *args, &block) # try the parents options if unknown
49
+ end
50
+
51
+ protected
52
+
53
+ # Installation is separated into two styles that concrete derivative installer classes
54
+ # can implement.
55
+ #
56
+ # Simple installers that issue a single or set of commands can overwride
57
+ # install_commands (eg. apt, gem, rpm). Pre/post install commands are included in this
58
+ # style for free.
59
+ #
60
+ # More complicated installers that have different stages, and require pre/post commands
61
+ # within stages can override install_sequence and take complete control of the install
62
+ # command sequence construction (eg. source based installer).
63
+
64
+ def install_sequence
65
+ commands = pre_commands(:install) + [ install_commands ] + post_commands(:install)
66
+ commands.flatten
67
+ end
68
+
69
+ def install_commands
70
+ raise 'Concrete installers implement this to specify commands to run to install their respective packages'
71
+ end
72
+
73
+ def pre_commands(stage)
74
+ dress @pre[stage] || [], :pre
75
+ end
76
+
77
+ def post_commands(stage)
78
+ dress @post[stage] || [], :post
79
+ end
80
+
81
+ def dress(commands, stage); commands; end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,17 @@
1
+ module Sprinkle
2
+ module Installers
3
+ class Rake < Installer
4
+ def initialize(parent, commands = [], &block)
5
+ super parent, &block
6
+ @commands = commands
7
+ end
8
+
9
+ protected
10
+
11
+ def install_commands
12
+ "rake #{@commands.join(' ')}"
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Sprinkle
2
+ module Installers
3
+ class Rpm < Installer
4
+ attr_accessor :packages
5
+
6
+ def initialize(parent, packages, &block)
7
+ super parent, &block
8
+ packages = [packages] unless packages.is_a? Array
9
+ @packages = packages
10
+ end
11
+
12
+ protected
13
+
14
+ def install_commands
15
+ "rpm -Uvh #{@packages.join(' ')}"
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,120 @@
1
+ module Sprinkle
2
+ module Installers
3
+ class Source < Installer
4
+ attr_accessor :source
5
+
6
+ def initialize(parent, source, options = {}, &block)
7
+ @source = source
8
+ super parent, options, &block
9
+ end
10
+
11
+ protected
12
+
13
+ def install_sequence
14
+ prepare + download + extract + configure + build + install
15
+ end
16
+
17
+ %w( prepare download extract configure build install ).each do |stage|
18
+ define_method stage do
19
+ pre_commands(stage.to_sym) + self.send("#{stage}_commands") + post_commands(stage.to_sym)
20
+ end
21
+ end
22
+
23
+ def prepare_commands
24
+ raise 'No installation area defined' unless @options[:prefix]
25
+ raise 'No build area defined' unless @options[:builds]
26
+ raise 'No source download area defined' unless @options[:archives]
27
+
28
+ [ "mkdir -p #{@options[:prefix]}",
29
+ "mkdir -p #{@options[:builds]}",
30
+ "mkdir -p #{@options[:archives]}" ]
31
+ end
32
+
33
+ def download_commands
34
+ [ "wget -cq --directory-prefix='#{@options[:archives]}' #{@source}" ]
35
+ end
36
+
37
+ def extract_commands
38
+ [ "bash -c 'cd #{@options[:builds]} && #{extract_command} #{@options[:archives]}/#{archive_name}'" ]
39
+ end
40
+
41
+ def configure_commands
42
+ return [] if custom_install?
43
+
44
+ command = "bash -c 'cd #{build_dir} && ./configure --prefix=#{@options[:prefix]} "
45
+
46
+ extras = {
47
+ :enable => '--enable', :disable => '--disable',
48
+ :with => '--with', :without => '--without'
49
+ }
50
+
51
+ extras.inject(command) { |m, (k, v)| m << create_options(k, v) if options[k]; m }
52
+
53
+ [ command << " > #{@package.name}-configure.log 2>&1'" ]
54
+ end
55
+
56
+ def build_commands
57
+ return [] if custom_install?
58
+ [ "bash -c 'cd #{build_dir} && make > #{@package.name}-build.log 2>&1'" ]
59
+ end
60
+
61
+ def install_commands
62
+ return custom_install_commands if custom_install?
63
+ [ "bash -c 'cd #{build_dir} && make install > #{@package.name}-install.log 2>&1'" ]
64
+ end
65
+
66
+ def custom_install?
67
+ !! @options[:custom_install]
68
+ end
69
+
70
+ # REVISIT: must be better processing of custom install commands somehow? use splat operator?
71
+ def custom_install_commands
72
+ dress @options[:custom_install], :install
73
+ end
74
+
75
+ protected
76
+
77
+ def dress(commands, stage)
78
+ commands.collect { |command| "bash -c 'cd #{build_dir} && #{command} >> #{@package.name}-#{stage}.log 2>&1'" }
79
+ end
80
+
81
+ private
82
+
83
+ def create_options(key, prefix)
84
+ @options[key].inject(' ') { |m, option| m << "#{prefix}-#{option} "; m }
85
+ end
86
+
87
+ def extract_command
88
+ case @source
89
+ when /(tar.gz)|(tgz)$/
90
+ 'tar xzf'
91
+ when /(tar.bz2)|(tb2)$/
92
+ 'tar xjf'
93
+ when /tar$/
94
+ 'tar xf'
95
+ when /zip$/
96
+ 'unzip'
97
+ else
98
+ raise "Unknown source archive format: #{archive_name}"
99
+ end
100
+ end
101
+
102
+ def archive_name
103
+ name = @source.split('/').last
104
+ raise "Unable to determine archive name for source: #{source}, please update code knowledge" unless name
105
+ name
106
+ end
107
+
108
+ def build_dir
109
+ "#{@options[:builds]}/#{base_dir}"
110
+ end
111
+
112
+ def base_dir
113
+ if @source.split('/').last =~ /(.*)\.(tar\.gz|tgz|tar\.bz2|tb2)/
114
+ return $1
115
+ end
116
+ raise "Unknown base path for source archive: #{@source}, please update code knowledge"
117
+ end
118
+ end
119
+ end
120
+ end