circus-deployment 0.0.1 → 0.2

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.
@@ -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