sprinkle 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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