circus-deployment 0.0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,7 @@
1
- require 'bundler'
2
-
3
1
  module Bundler
4
2
  class CircusUtil
5
3
  def self.fix_external_paths(dir)
4
+ require 'bundler'
6
5
  ENV['BUNDLE_GEMFILE'] = File.join(dir, 'Gemfile')
7
6
 
8
7
  # Correct any path based components in the Gemfile
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/cli'
5
+ require 'bundler'
6
+
7
+ Bundler.ui = Bundler::UI::Shell.new(Thor::Shell::Basic.new)
8
+
9
+ # TODO: Make RUBY_FRAMEWORK_VERSION resolvable instead of using 1.8
10
+ gem_install_root = "vendor/bundle/ruby/1.8/gems"
11
+ spec_install_root = "vendor/bundle/ruby/1.8/specifications"
12
+ FileUtils.mkdir_p(gem_install_root)
13
+ FileUtils.mkdir_p(spec_install_root)
14
+
15
+ Bundler.definition.specs.each do |spec|
16
+ install_path = "#{gem_install_root}/#{spec.name}-#{spec.version}"
17
+
18
+ unless install_path == spec.full_gem_path
19
+ FileUtils.mkdir_p(File.dirname(install_path))
20
+ FileUtils.rm_rf install_path
21
+ FileUtils.cp_r spec.full_gem_path, install_path
22
+ end
23
+
24
+ File.open(File.join(spec_install_root, "#{spec.name}-#{spec.version}.spec"), 'w') do |sf|
25
+ sf.write(spec.to_ruby)
26
+ end
27
+ in_dir_spec = "#{install_path}/#{spec.name}.gemspec"
28
+ unless File.exists? in_dir_spec
29
+ File.open(in_dir_spec, 'w') do |sf|
30
+ sf.write(spec.to_ruby)
31
+ end
32
+ end
33
+ end
34
+
35
+ File.open('Gemfile', 'w') do |f|
36
+ Bundler.definition.specs.each do |s|
37
+ f << "gem #{s.name.inspect}, #{s.version.to_s.inspect}, :path => '#{gem_install_root}/#{s.name}-#{s.version}'\n"
38
+ end
39
+ end
40
+ # FileUtils.mkdir_p('.bundle')
41
+ # File.open('.bundle/config', 'w') do |f|
42
+ # f.puts '---'
43
+ # f.puts 'BUNDLE_FROZEN: "1"'
44
+ # f.puts 'BUNDLE_DISABLE_SHARED_GEMS: "1"'
45
+ # f.puts 'BUNDLE_PATH: vendor/bundle'
46
+ # end
data/lib/circus.rb CHANGED
@@ -6,4 +6,5 @@ require File.expand_path('../circus/repos', __FILE__)
6
6
  require File.expand_path('../circus/booth_client', __FILE__)
7
7
  require File.expand_path('../circus/actstore_client', __FILE__)
8
8
  require File.expand_path('../circus/local_config', __FILE__)
9
- require File.expand_path('../circus/connection_builder', __FILE__)
9
+ require File.expand_path('../circus/connection_builder', __FILE__)
10
+ require File.expand_path('../circus/processes/daemontools', __FILE__)
data/lib/circus/act.rb CHANGED
@@ -3,7 +3,7 @@ require 'yaml'
3
3
 
4
4
  module Circus
5
5
  class Act
6
- attr_reader :name, :dir, :props, :profile
6
+ attr_reader :name, :dir, :props
7
7
 
8
8
  def initialize(name, dir, props = {})
9
9
  @name = name
@@ -14,9 +14,18 @@ module Circus
14
14
  if File.exists? act_file
15
15
  act_cfg = YAML.load(File.read(act_file))
16
16
  @props.merge! act_cfg
17
+
18
+ # Allow act name to be overriden
19
+ @name = @props['name'] if @props['name']
17
20
  end
18
21
  end
19
22
 
23
+ def profile
24
+ detect! unless @profile
25
+
26
+ @profile
27
+ end
28
+
20
29
  def should_package?
21
30
  return false if @props['no-package']
22
31
  true
@@ -47,14 +56,14 @@ module Circus
47
56
  return false unless profile.package_for_deploy(logger, overlay_dir)
48
57
 
49
58
  begin
50
- # Squash the output file
51
- include_dirs = ["#{overlay_dir}/*"]
52
- include_dirs << "#{@dir}/*" if profile.package_base_dir?
53
- include_dirs.concat(profile.extra_dirs)
59
+ # Package up the output file
60
+ include_dirs = ["-C #{overlay_dir} ."]
61
+ include_dirs << "-C #{@dir} ." if profile.package_base_dir?
62
+ include_dirs.concat(profile.extra_dirs.map { |d| "-C #{File.dirname(d)} #{File.basename(d)}"})
54
63
  output_name = File.join(output_root_dir, "#{name}.act")
55
64
 
56
65
  ExternalUtil.run_external(logger, 'Output packaging',
57
- "mksquashfs #{include_dirs.join(' ')} #{output_name} -noappend -noI -noD -noF 2>&1")
66
+ "tar -czf #{output_name} --exclude .circus #{include_dirs.join(' ')} 2>&1")
58
67
  ensure
59
68
  profile.cleanup_after_deploy(logger, overlay_dir)
60
69
  end
@@ -70,5 +79,19 @@ module Circus
70
79
  def act_file
71
80
  File.join(@dir, 'act.yaml')
72
81
  end
82
+
83
+ def pause(logger, run_root)
84
+ working_dir = File.join(run_root, name)
85
+ process_mgmt = Circus::Processes::Daemontools.new
86
+
87
+ process_mgmt.pause_service(name, working_dir, logger)
88
+ end
89
+
90
+ def resume(logger, run_root)
91
+ working_dir = File.join(run_root, name)
92
+ process_mgmt = Circus::Processes::Daemontools.new
93
+
94
+ process_mgmt.resume_service(name, working_dir, logger)
95
+ end
73
96
  end
74
97
  end
@@ -29,20 +29,61 @@ module Circus
29
29
  # Instructs the application to startup in place and activate all of its associated
30
30
  # acts.
31
31
  def go!(logger)
32
- load! unless @acts
33
-
34
- FileUtils.rm_rf(private_run_root)
35
- acts.each do |act|
36
- act.package_for_dev(logger, private_run_root)
32
+ # Ensure that we have svscan available. If we don't, die nice and early
33
+ `which svscan`
34
+ if $? != 0
35
+ logger.error 'The svscan utility (usually provided by daemontools) is not available. ' +
36
+ ' This utility must be installed before the circus go command can function.'
37
+ # TODO: Detect OS and suggest installation mechanism?
38
+
39
+ return
37
40
  end
38
- acts.each do |act|
41
+
42
+ load! unless @acts
43
+
44
+ run_acts = acts.select { |a| a.profile.supported_for_development? }
45
+ run_acts.each do |act|
39
46
  logger.info "Starting act #{act.name} at #{act.dir} using profile #{act.profile.name}"
40
47
  end
41
48
  logger.info "---------------------"
49
+ FileUtils.rm_rf(private_run_root)
50
+ run_acts.each do |act|
51
+ return unless act.package_for_dev(logger, private_run_root)
52
+ end
42
53
 
54
+ # If we've loaded bundler ourselves, then we need to remove its environment variables; otherwise,
55
+ # it will screw up child applications!
56
+ ENV['BUNDLE_GEMFILE'] = nil
57
+ ENV['BUNDLE_BIN_PATH'] = nil
43
58
  system("svscan #{private_run_root}")
44
59
  end
45
60
 
61
+ # Instructs the application to stop the given act that is running under development via go.
62
+ def pause(logger, act_name)
63
+ load! unless @acts
64
+
65
+ target_act = acts.find { |a| a.name == act_name }
66
+ unless target_act
67
+ logger.error "Act #{act_name} could not be found"
68
+ return
69
+ end
70
+
71
+ target_act.pause(logger, private_run_root)
72
+ end
73
+
74
+ # Instructs the application to resume the given act that is running under development via go.
75
+ def resume(logger, act_name)
76
+ load! unless @acts
77
+
78
+ target_act = acts.find { |a| a.name == act_name }
79
+ unless target_act
80
+ logger.error "Act #{act_name} could not be found"
81
+ return
82
+ end
83
+
84
+ target_act.resume(logger, private_run_root)
85
+ end
86
+
46
87
  # Instructs the application to assemble it's components for deployment and generate
47
88
  # act output files.
48
89
  def assemble!(output_dir, logger, dev = false, only_acts = nil)
@@ -58,6 +99,8 @@ module Circus
58
99
 
59
100
  assembly_acts = acts.select {|a| only_acts.nil? or only_acts.include? a.name }
60
101
 
102
+ FileUtils.rm_rf(output_dir)
103
+ FileUtils.rm_rf(private_overlay_root)
61
104
  FileUtils.mkdir_p(output_dir)
62
105
  assembly_acts.each do |act|
63
106
  act.detect!
@@ -25,6 +25,11 @@ module Circus
25
25
  repo_helper.repo_url
26
26
  end
27
27
 
28
+ unless repo_url
29
+ @logger.error("Could not detect repository source url")
30
+ return false
31
+ end
32
+
28
33
  app_name = options[:app_name] || File.basename(File.expand_path('.'))
29
34
 
30
35
  @logger.info("Creating booth connection #{name} on #{booth} for #{repo_url} as #{app_name}")
@@ -42,10 +47,15 @@ module Circus
42
47
 
43
48
  def admit(name, apps, options)
44
49
  booth = get_booth_or_default(name)
45
- return unless booth
50
+ return false unless booth
46
51
 
47
52
  repo_helper = Repos.find_repo_by_id(booth[:repo_type]).new(File.expand_path('.'))
48
53
  current_rev = repo_helper.current_revision
54
+
55
+ unless current_rev
56
+ @logger.error("Could not detect current repository version")
57
+ return false
58
+ end
49
59
 
50
60
  @logger.info("Starting admission into #{name} of version #{current_rev}")
51
61
  connection = ConnectionBuilder.new(booth).build(booth[:booth])
@@ -61,12 +71,15 @@ module Circus
61
71
  end
62
72
  admitted = Circus::Agents::Encoding.decode(client.admit(booth[:booth], booth[:booth_id], current_rev, apply_patch_fn, apps).result)
63
73
 
74
+ return false if booth[:target] == 'none'
64
75
  clown_connection = ConnectionBuilder.new(options).build(booth[:target])
65
76
  clown_client = ClownClient.new(clown_connection, @logger)
66
77
  admitted.each do |name, url|
67
78
  @logger.info("Executing deployment of #{name} from #{url} to #{booth[:target]}")
68
79
  clown_client.deploy(booth[:target], name, url).result
69
80
  end
81
+
82
+ true
70
83
  end
71
84
 
72
85
  private
data/lib/circus/cli.rb CHANGED
@@ -27,6 +27,18 @@ module Circus
27
27
  @app.go!(LOGGER)
28
28
  end
29
29
 
30
+ desc "pause ACT_NAME", "Temporarily halts an act that is running in development (via go)"
31
+ def pause(act_name)
32
+ load!
33
+ @app.pause(LOGGER, act_name)
34
+ end
35
+
36
+ desc "resume ACT_NAME", "Resumes a paused act that is running in development (via go)"
37
+ def resume(act_name)
38
+ load!
39
+ @app.resume(LOGGER, act_name)
40
+ end
41
+
30
42
  desc "assemble", "Assemble the application's acts for deployment"
31
43
  method_option :output, :default => '.circus/acts/', :desc => 'Destination directory for generated acts'
32
44
  method_option :actstore, :desc => 'URL of the act store to upload the built acts to'
@@ -82,7 +94,7 @@ module Circus
82
94
  method_option :uncommitted, :desc => 'Indicates that the current uncommitted changes should be sent to the booth and included in the app'
83
95
  def admit(name = nil, *apps)
84
96
  tool = BoothTool.new(LOGGER, LocalConfig.new)
85
- tool.admit(name, apps, options)
97
+ exit 1 unless tool.admit(name, apps, options)
86
98
  end
87
99
 
88
100
  desc "exec TARGET [ACT] [CMD]", "Executes the given command in the deployed context of the given act"
@@ -95,7 +107,14 @@ module Circus
95
107
 
96
108
  connection = ConnectionBuilder.new(options).build(target)
97
109
  client = ClownClient.new(connection, LOGGER)
98
- client.exec(target, act_name, cmd).result
110
+ exit 1 unless client.exec(target, act_name, cmd).result
111
+ end
112
+
113
+ desc "reset TARGET ACT", "Restarts the given act on the target host"
114
+ def reset(target, act_name)
115
+ connection = ConnectionBuilder.new(options).build(target)
116
+ client = ClownClient.new(connection, LOGGER)
117
+ exit 1 unless client.reset(target, act_name).result
99
118
  end
100
119
 
101
120
  desc "deploy TARGET NAME ACT", "Deploy the named object using the given act onto the given target server"
@@ -23,5 +23,9 @@ module Circus
23
23
  def exec(node, name, cmd)
24
24
  @connection.call(node, 'Clown', 'Clown', 'exec', {'name' => name, 'command' => cmd}, @logger)
25
25
  end
26
+
27
+ def reset(node, name)
28
+ @connection.call(node, 'Clown', 'Clown', 'reset', {'name' => name}, @logger)
29
+ end
26
30
  end
27
31
  end
@@ -10,5 +10,15 @@ module Circus
10
10
  true
11
11
  end
12
12
  end
13
+
14
+ def self.run_and_show_external(logger, desc, cmd)
15
+ IO.popen("#{cmd} 2>&1", 'r') do |pipe|
16
+ while (line = pipe.gets)
17
+ logger.info(line)
18
+ end
19
+ end
20
+
21
+ true
22
+ end
13
23
  end
14
24
  end
@@ -0,0 +1,65 @@
1
+ module Circus
2
+ module Processes
3
+ # Support for working with Daemontools
4
+ class Daemontools
5
+ def pause_service(name, working_dir, logger)
6
+ res = `svc -d #{working_dir}`
7
+ if $? != 0
8
+ logger.error("Failed to initiate service shutdown for #{name}")
9
+ return
10
+ end
11
+
12
+ unless await_service_pause(name, working_dir, logger)
13
+ logger.error("Service did not shutdown")
14
+ end
15
+ end
16
+
17
+ def resume_service(name, working_dir, logger)
18
+ res = `svc -u #{working_dir}`
19
+ if $? != 0
20
+ logger.error("Failed to initiate service startup for #{name}")
21
+ return
22
+ end
23
+
24
+ unless await_service_startup(name, working_dir, logger)
25
+ logger.error("Service did not resume")
26
+ end
27
+ end
28
+
29
+ def await_service_startup(name, working_dir, logger)
30
+ logger.info("Waiting for startup of #{name}")
31
+
32
+ (1..100).each do |i|
33
+ return true if is_service_running?(working_dir)
34
+ sleep 1
35
+ end
36
+
37
+ return false
38
+ end
39
+
40
+ def await_service_pause(name, working_dir, logger)
41
+ logger.info("Waiting for shutdown of #{name}")
42
+
43
+ (1..5).each do |i|
44
+ return true unless is_service_running?(working_dir)
45
+ sleep 1
46
+ end
47
+
48
+ (1..100).each do |i|
49
+ # Attempt a more brutal shutdown
50
+ `svc -k #{working_dir}`
51
+
52
+ return true unless is_service_running?(working_dir)
53
+ sleep 1
54
+ end
55
+
56
+ return false
57
+ end
58
+
59
+ def is_service_running?(working_dir)
60
+ res = `svstat #{working_dir}`
61
+ $? == 0 && res.include?('up (pid')
62
+ end
63
+ end
64
+ end
65
+ end
@@ -8,3 +8,5 @@ require File.expand_path('../profiles/make_base', __FILE__)
8
8
  require File.expand_path('../profiles/shell', __FILE__)
9
9
  require File.expand_path('../profiles/django', __FILE__)
10
10
  require File.expand_path('../profiles/jekyll', __FILE__)
11
+ require File.expand_path('../profiles/maven_webapp', __FILE__)
12
+ require File.expand_path('../profiles/chef_stack', __FILE__)
@@ -14,6 +14,16 @@ module Circus
14
14
  @props = props
15
15
  end
16
16
 
17
+ def mark_for_persistent_run?
18
+ true
19
+ end
20
+
21
+ # Whether acts using the given profile can be run during development. If this returns false,
22
+ # then local only commands (such as go) will ignore these items.
23
+ def supported_for_development?
24
+ true
25
+ end
26
+
17
27
  def package_base_dir?
18
28
  true
19
29
  end
@@ -34,8 +44,10 @@ module Circus
34
44
  write_run_script(run_dir) do |f|
35
45
  f.write(deploy_run_script_content.strip)
36
46
  end
47
+
48
+ reqs = build_default_requirements.merge(requirements)
37
49
  File.open(File.join(run_dir, 'requirements.yaml'), 'w') do |f|
38
- f.write(requirements.to_yaml)
50
+ f.write(reqs.to_yaml)
39
51
  end
40
52
  end
41
53
 
@@ -110,6 +122,13 @@ module Circus
110
122
  def run_external(logger, desc, cmd)
111
123
  ExternalUtil.run_external(logger, desc, cmd)
112
124
  end
125
+
126
+ private
127
+ def build_default_requirements
128
+ {
129
+ 'persistent-run' => mark_for_persistent_run?
130
+ }
131
+ end
113
132
  end
114
133
  end
115
134
  end
@@ -0,0 +1,75 @@
1
+ require 'json'
2
+
3
+ module Circus
4
+ module Profiles
5
+ # A Chef Stack provides the ability to trigger deployment of system packages and configuration.
6
+ class ChefStack < Base
7
+ CHEF_STACK_PROPERTY='chef-stack'
8
+
9
+ # Checks if this is a chef stack applcation. Will accept the application if it
10
+ # has a file named stack.yaml, or has a 'chef-stack' property describing the entry point.
11
+ def self.accepts?(name, dir, props)
12
+ return true if props.include? CHEF_STACK_PROPERTY
13
+ return File.exists?(File.join(dir, "stack.yaml"))
14
+ end
15
+
16
+ def initialize(name, dir, props)
17
+ super(name, dir, props)
18
+
19
+ @stack_script = props[CHEF_STACK_PROPERTY] || "stack.yaml"
20
+ end
21
+
22
+ # The name of this profile
23
+ def name
24
+ "chef-stack"
25
+ end
26
+
27
+ def requirements
28
+ reqs = super
29
+ reqs['execution-user'] = 'root'
30
+ reqs
31
+ end
32
+
33
+ def mark_for_persistent_run?
34
+ false
35
+ end
36
+
37
+ # Stacks are not useful in development
38
+ def supported_for_development?
39
+ false
40
+ end
41
+
42
+ def dev_run_script_content
43
+ shell_run_script do
44
+ <<-EOT
45
+ cd #{@dir}
46
+ exec echo "Stacks cannot be run in development"
47
+ EOT
48
+ end
49
+ end
50
+
51
+ def prepare_for_deploy(logger, overlay_dir)
52
+ # Generate a chef configuration file
53
+ stack_config = YAML::load(File.read(File.join(@dir, @stack_script)))
54
+ File.open(File.join(overlay_dir, 'stack.json'), 'w') do |f|
55
+ f.write(stack_config.to_json)
56
+ end
57
+ File.open(File.join(overlay_dir, 'solo-stack.rb'), 'w') do |f|
58
+ f << "cookbook_path File.expand_path('../cookbooks', __FILE__)"
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ def deploy_run_script_content
65
+ shell_run_script do
66
+ <<-EOT
67
+ exec chef-solo -c solo-stack.rb -j stack.json
68
+ EOT
69
+ end
70
+ end
71
+ end
72
+
73
+ PROFILES << ChefStack
74
+ end
75
+ end