circus-deployment 0.0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/bundler/circus_util.rb +1 -2
- data/lib/bundler/gem_cacher.rb +46 -0
- data/lib/circus.rb +2 -1
- data/lib/circus/act.rb +29 -6
- data/lib/circus/application.rb +49 -6
- data/lib/circus/booth_tool.rb +14 -1
- data/lib/circus/cli.rb +21 -2
- data/lib/circus/clown_client.rb +4 -0
- data/lib/circus/external_util.rb +10 -0
- data/lib/circus/processes/daemontools.rb +65 -0
- data/lib/circus/profiles.rb +2 -0
- data/lib/circus/profiles/base.rb +20 -1
- data/lib/circus/profiles/chef_stack.rb +75 -0
- data/lib/circus/profiles/make_base.rb +4 -1
- data/lib/circus/profiles/maven_webapp.rb +117 -0
- data/lib/circus/profiles/maven_webapp_jetty.xml.erb +74 -0
- data/lib/circus/profiles/maven_webapp_jetty_app.xml.erb +56 -0
- data/lib/circus/profiles/python_base.rb +5 -5
- data/lib/circus/profiles/rack.rb +11 -7
- data/lib/circus/profiles/ruby_base.rb +13 -4
- data/lib/circus/repos/git.rb +2 -1
- data/lib/circus/version.rb +1 -1
- metadata +106 -72
- data/lib/circus/agents/agent.rb +0 -59
- data/lib/circus/agents/logger.rb +0 -52
data/lib/bundler/circus_util.rb
CHANGED
@@ -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
|
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
|
-
#
|
51
|
-
include_dirs = ["#{overlay_dir}
|
52
|
-
include_dirs << "#{@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
|
-
"
|
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
|
data/lib/circus/application.rb
CHANGED
@@ -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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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!
|
data/lib/circus/booth_tool.rb
CHANGED
@@ -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"
|
data/lib/circus/clown_client.rb
CHANGED
@@ -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
|
data/lib/circus/external_util.rb
CHANGED
@@ -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
|
data/lib/circus/profiles.rb
CHANGED
@@ -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__)
|
data/lib/circus/profiles/base.rb
CHANGED
@@ -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(
|
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
|