kameleon-builder 2.0.0.dev
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.
- data/.editorconfig +23 -0
- data/.env +51 -0
- data/.gitignore +22 -0
- data/AUTHORS +19 -0
- data/CHANGELOG +36 -0
- data/COPYING +340 -0
- data/Gemfile +4 -0
- data/README.md +53 -0
- data/Rakefile +24 -0
- data/Vagrantfile +68 -0
- data/bin/kameleon +16 -0
- data/contrib/kameleon_bashrc.sh +138 -0
- data/contrib/scripts/VirtualBox_deploy.sh +12 -0
- data/contrib/scripts/chroot_env +9 -0
- data/contrib/scripts/create_passwd.py +17 -0
- data/contrib/scripts/umount-chroot.sh +290 -0
- data/contrib/steps/bootstrap/debian/bootstrap_if_needed.yaml +47 -0
- data/contrib/steps/bootstrap/debian/bootstrap_static.yaml +38 -0
- data/contrib/steps/setup/add_timestamp.yaml +6 -0
- data/contrib/steps/setup/autologin.yaml +16 -0
- data/contrib/steps/setup/copy_ssh_auth_file.yaml +10 -0
- data/contrib/steps/setup/debian/add_network_interface.yaml +7 -0
- data/contrib/steps/setup/debian/cluster_tools_install.yaml +16 -0
- data/contrib/steps/setup/debian/network_config_static.yaml +17 -0
- data/contrib/steps/setup/generate_user_ssh_key.yaml +15 -0
- data/contrib/steps/setup/install_my_ssh_key.yaml +26 -0
- data/contrib/steps/setup/make_swap_file.yaml +9 -0
- data/contrib/steps/setup/root_ssh_config.yaml +18 -0
- data/contrib/steps/setup/set_user_password.yaml +7 -0
- data/contrib/steps/setup/system_optimization.yaml +8 -0
- data/docs/.gitignore +1 -0
- data/docs/Makefile +177 -0
- data/docs/make.bat +242 -0
- data/docs/source/_static/.gitignore +0 -0
- data/docs/source/aliases.rst +29 -0
- data/docs/source/checkpoint.rst +28 -0
- data/docs/source/cli.rst +3 -0
- data/docs/source/commands.rst +62 -0
- data/docs/source/conf.py +254 -0
- data/docs/source/context.rst +42 -0
- data/docs/source/faq.rst +3 -0
- data/docs/source/getting_started.rst +3 -0
- data/docs/source/index.rst +38 -0
- data/docs/source/installation.rst +3 -0
- data/docs/source/recipe.rst +256 -0
- data/docs/source/why.rst +3 -0
- data/docs/source/workspace.rst +11 -0
- data/kameleon-builder.gemspec +37 -0
- data/lib/kameleon.rb +75 -0
- data/lib/kameleon/cli.rb +176 -0
- data/lib/kameleon/context.rb +83 -0
- data/lib/kameleon/engine.rb +357 -0
- data/lib/kameleon/environment.rb +38 -0
- data/lib/kameleon/error.rb +51 -0
- data/lib/kameleon/logger.rb +53 -0
- data/lib/kameleon/recipe.rb +474 -0
- data/lib/kameleon/shell.rb +290 -0
- data/lib/kameleon/step.rb +213 -0
- data/lib/kameleon/utils.rb +45 -0
- data/lib/kameleon/version.rb +3 -0
- data/templates/COPYRIGHT +21 -0
- data/templates/aliases/defaults.yaml +83 -0
- data/templates/checkpoints/docker.yaml +14 -0
- data/templates/checkpoints/qcow2.yaml +44 -0
- data/templates/debian-wheezy-chroot.yaml +98 -0
- data/templates/debian-wheezy-docker.yaml +97 -0
- data/templates/fedora-docker.yaml +96 -0
- data/templates/steps/bootstrap/debian/debootstrap.yaml +13 -0
- data/templates/steps/bootstrap/fedora/docker_bootstrap.yaml +25 -0
- data/templates/steps/bootstrap/fedora/yum_bootstrap.yaml +22 -0
- data/templates/steps/bootstrap/prepare_appliance_with_nbd.yaml +93 -0
- data/templates/steps/bootstrap/prepare_docker.yaml +38 -0
- data/templates/steps/bootstrap/start_chroot.yaml +53 -0
- data/templates/steps/bootstrap/start_docker.yaml +12 -0
- data/templates/steps/export/build_appliance_from_docker.yaml +105 -0
- data/templates/steps/export/clean_appliance.yaml +3 -0
- data/templates/steps/export/save_appliance_from_nbd.yaml +54 -0
- data/templates/steps/setup/create_user.yaml +12 -0
- data/templates/steps/setup/debian/kernel_install.yaml +20 -0
- data/templates/steps/setup/debian/keyboard_config.yaml +10 -0
- data/templates/steps/setup/debian/network_config.yaml +30 -0
- data/templates/steps/setup/debian/software_install.yaml +15 -0
- data/templates/steps/setup/debian/system_config.yaml +12 -0
- data/templates/steps/setup/fedora/kernel_install.yaml +27 -0
- data/templates/steps/setup/fedora/software_install.yaml +10 -0
- data/tests/helper.rb +22 -0
- data/tests/recipes/dummy_recipe.yaml +48 -0
- data/tests/recipes/steps/bootstrap/dummy_distro/dummy_bootstrap_static.yaml +4 -0
- data/tests/recipes/steps/export/dummy_save_appliance.yaml +9 -0
- data/tests/recipes/steps/setup/default/dummy_root_passwd.yaml +8 -0
- data/tests/recipes/steps/setup/dummy_distro/dummy_software_install.yaml +7 -0
- data/tests/test_context.rb +16 -0
- data/tests/test_recipe.rb +15 -0
- data/tests/test_version.rb +9 -0
- metadata +300 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Kameleon
|
2
|
+
|
3
|
+
# This class allows access to the recipes, CLI, etc. all in the scope of
|
4
|
+
# this environment
|
5
|
+
class Environment
|
6
|
+
|
7
|
+
attr_accessor :workspace
|
8
|
+
attr_accessor :templates_path
|
9
|
+
attr_accessor :recipes_path
|
10
|
+
attr_accessor :build_path
|
11
|
+
attr_accessor :log_file
|
12
|
+
attr_accessor :debug
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@logger = Log4r::Logger.new("kameleon::[env]")
|
17
|
+
# symbolify commandline options
|
18
|
+
options = options.inject({}) {|result,(key,value)| result.update({key.to_sym => value})}
|
19
|
+
workspace = File.expand_path(options[:workspace])
|
20
|
+
build_path = File.expand_path(options[:build_path] || File.join(workspace, "builds"))
|
21
|
+
defaults = {
|
22
|
+
:workspace => Pathname.new(workspace),
|
23
|
+
:templates_path => Kameleon.templates_path,
|
24
|
+
:templates_names => Kameleon.templates_names,
|
25
|
+
:recipes_path => Pathname.new(File.join(workspace, "recipes")),
|
26
|
+
:build_path => Pathname.new(build_path),
|
27
|
+
:log_file => Pathname.new(File.join(workspace, "kameleon.log"))
|
28
|
+
}
|
29
|
+
options = defaults.merge(options)
|
30
|
+
@logger.debug("Environment initialized (#{self})")
|
31
|
+
# Injecting all variables of the options and assign the variables
|
32
|
+
options.each do |key, value|
|
33
|
+
instance_variable_set("@#{key}".to_sym, options[key])
|
34
|
+
@logger.debug(" @#{key} : #{options[key]}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'thor/error'
|
2
|
+
|
3
|
+
module Kameleon
|
4
|
+
class KameleonError < ::StandardError
|
5
|
+
attr_accessor :object
|
6
|
+
|
7
|
+
def initialize(message=nil, object=nil)
|
8
|
+
super(message)
|
9
|
+
self.object = object
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.status_code(code)
|
13
|
+
define_method(:status_code) { code }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ExecError < KameleonError; status_code(2) ; end
|
18
|
+
class InternalError < KameleonError; status_code(3) ; end
|
19
|
+
class ContextError < KameleonError; status_code(4) ; end
|
20
|
+
class ShellError < KameleonError; status_code(5) ; end
|
21
|
+
class RecipeError < KameleonError; status_code(6) ; end
|
22
|
+
class BuildError < KameleonError; status_code(7) ; end
|
23
|
+
class AbortError < KameleonError; status_code(8) ; end
|
24
|
+
|
25
|
+
def self.with_friendly_errors
|
26
|
+
yield
|
27
|
+
rescue Kameleon::KameleonError => e
|
28
|
+
e.message.split( /\r?\n/ ).each {|m| Kameleon.logger.fatal m }
|
29
|
+
exit e.status_code
|
30
|
+
rescue Thor::UndefinedTaskError => e
|
31
|
+
$stderr << "#{e.message}\n"
|
32
|
+
e.backtrace.each {|m| Kameleon.logger.debug m }
|
33
|
+
exit 15
|
34
|
+
rescue Thor::Error => e
|
35
|
+
$stderr << "#{e.message}\n"
|
36
|
+
e.backtrace.each {|m| Kameleon.logger.debug m }
|
37
|
+
exit 15
|
38
|
+
rescue SystemExit, Interrupt => e
|
39
|
+
Kameleon.logger.fatal("Quitting...")
|
40
|
+
exit 1
|
41
|
+
rescue Exception => e
|
42
|
+
if ENV["KAMELEON_LOG"] != "debug"
|
43
|
+
$stderr << "Unfortunately, a fatal error has occurred : "\
|
44
|
+
"#{e.message}.\nUse --debug option for more details\n"
|
45
|
+
else
|
46
|
+
Kameleon.logger.debug "Error : #{e}"
|
47
|
+
e.backtrace.each {|m| puts "==> #{m}" }
|
48
|
+
end
|
49
|
+
exit 666
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'log4r-color'
|
2
|
+
|
3
|
+
module Kameleon
|
4
|
+
# Custom Log4r formatter for the console
|
5
|
+
class ConsoleFormatter < Log4r::BasicFormatter
|
6
|
+
@@basicformat = "%*s"
|
7
|
+
|
8
|
+
def initialize(hash={})
|
9
|
+
super(hash)
|
10
|
+
@max_level_length = 11
|
11
|
+
@on_progress = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def format(event)
|
15
|
+
buff = sprintf(@@basicformat, @max_level_length, event.name)
|
16
|
+
buff << (event.tracer.nil? ? "" : "(#{event.tracer[0]})") + ": "
|
17
|
+
unless Log4r::LNAMES[event.level].eql? "PROGRESS"
|
18
|
+
@on_progress = false
|
19
|
+
buff << format_object(event.data) + "\n"
|
20
|
+
else
|
21
|
+
if @on_progress
|
22
|
+
event.data
|
23
|
+
else
|
24
|
+
@on_progress = true
|
25
|
+
buff << format_object(event.data)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Custom Log4r formatter for files
|
32
|
+
class FileFormatter < Log4r::BasicFormatter
|
33
|
+
|
34
|
+
def initialize(hash={})
|
35
|
+
super(hash)
|
36
|
+
end
|
37
|
+
|
38
|
+
def format(logevent)
|
39
|
+
if Log4r::LNAMES[logevent.level].eql? "PROGRESS"
|
40
|
+
# Formats the data as is with no newline, to allow progress bars to be logged.
|
41
|
+
sprintf("%s", logevent.data.to_s)
|
42
|
+
else
|
43
|
+
if logevent.data.kind_of? String
|
44
|
+
# remove ^M characters
|
45
|
+
logevent.data.gsub!(/\r/, "")
|
46
|
+
# Prevent two newlines in the log file
|
47
|
+
logevent.data.chop! if logevent.data =~ /\n$/
|
48
|
+
end
|
49
|
+
sprintf("[%8s %s] %s\n", Log4r::LNAMES[logevent.level], Time.now.strftime("%m/%d/%Y %I:%M:%S %p"), format_object(logevent.data))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,474 @@
|
|
1
|
+
require 'kameleon/utils'
|
2
|
+
require 'kameleon/step'
|
3
|
+
|
4
|
+
module Kameleon
|
5
|
+
|
6
|
+
class Recipe
|
7
|
+
attr_accessor :path, :name, :global, :sections, :aliases, :aliases_path, \
|
8
|
+
:checkpoint, :checkpoint_path, :metainfo
|
9
|
+
|
10
|
+
def initialize(path)
|
11
|
+
@logger = Log4r::Logger.new("kameleon::[recipe]")
|
12
|
+
@path = Pathname.new(path)
|
13
|
+
@name = (@path.basename ".yaml").to_s
|
14
|
+
@recipe_content = File.open(@path, 'r') { |f| f.read }
|
15
|
+
@sections = {
|
16
|
+
"bootstrap" => Section.new("bootstrap"),
|
17
|
+
"setup" => Section.new("setup"),
|
18
|
+
"export" => Section.new("export"),
|
19
|
+
}
|
20
|
+
@required_global = %w(out_context in_context)
|
21
|
+
kameleon_id = SecureRandom.uuid
|
22
|
+
@global = {
|
23
|
+
"kameleon_recipe_name" => @name,
|
24
|
+
"kameleon_recipe_dir" => File.dirname(@path),
|
25
|
+
"kameleon_uuid" => kameleon_id,
|
26
|
+
"kameleon_short_uuid" => kameleon_id.split("-").last,
|
27
|
+
"kameleon_cwd" => File.join(Kameleon.env.build_path, @name),
|
28
|
+
}
|
29
|
+
@aliases = {}
|
30
|
+
@checkpoint = nil
|
31
|
+
@files = []
|
32
|
+
@logger.debug("Initialize new recipe (#{path})")
|
33
|
+
load!
|
34
|
+
end
|
35
|
+
|
36
|
+
def load!
|
37
|
+
# Find recipe path
|
38
|
+
@logger.notice("Loading #{@path}")
|
39
|
+
fail RecipeError, "Could not find this following recipe : #{@path}" \
|
40
|
+
unless File.file? @path
|
41
|
+
yaml_recipe = YAML.load File.open @path
|
42
|
+
unless yaml_recipe.kind_of? Hash
|
43
|
+
fail RecipeError, "Invalid yaml error"
|
44
|
+
end
|
45
|
+
unless yaml_recipe.key? "global"
|
46
|
+
fail RecipeError, "Recipe misses 'global' section"
|
47
|
+
end
|
48
|
+
|
49
|
+
#Load Global variables
|
50
|
+
@global.merge!(yaml_recipe.fetch("global"))
|
51
|
+
# Resolve dynamically-defined variables !!
|
52
|
+
resolved_global = Utils.resolve_vars(@global.to_yaml, @path, @global)
|
53
|
+
@global.merge! YAML.load(resolved_global)
|
54
|
+
|
55
|
+
# Loads aliases
|
56
|
+
load_aliases(yaml_recipe)
|
57
|
+
# Loads checkpoint configuration
|
58
|
+
load_checkpoint_config(yaml_recipe)
|
59
|
+
|
60
|
+
#Find and load steps
|
61
|
+
steps_dir = File.join(File.dirname(@path), 'steps')
|
62
|
+
@global['include_steps'] ||= []
|
63
|
+
@global['include_steps'] = [global['include_steps']].push ''
|
64
|
+
@global['include_steps'].flatten!
|
65
|
+
@global['include_steps'].compact!
|
66
|
+
@sections.values.each do |section|
|
67
|
+
dir_to_search = @global['include_steps'].map do |path|
|
68
|
+
[File.join(steps_dir, section.name, path),
|
69
|
+
File.join(steps_dir, path)]
|
70
|
+
end
|
71
|
+
dir_to_search.flatten!
|
72
|
+
if yaml_recipe.key? section.name
|
73
|
+
yaml_section = yaml_recipe.fetch(section.name)
|
74
|
+
next unless yaml_section.kind_of? Array
|
75
|
+
yaml_section.each do |raw_macrostep|
|
76
|
+
|
77
|
+
# Get macrostep name and arguments if available
|
78
|
+
if raw_macrostep.kind_of? String
|
79
|
+
name = raw_macrostep
|
80
|
+
args = nil
|
81
|
+
elsif raw_macrostep.kind_of? Hash
|
82
|
+
name = raw_macrostep.keys[0]
|
83
|
+
args = raw_macrostep.values[0]
|
84
|
+
else
|
85
|
+
fail RecipeError, "Malformed yaml recipe in section: "\
|
86
|
+
"#{section.name}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Load macrostep yaml
|
90
|
+
loaded = false
|
91
|
+
dir_to_search.each do |dir|
|
92
|
+
macrostep_path = Pathname.new(File.join(dir, name + '.yaml'))
|
93
|
+
if File.file?(macrostep_path)
|
94
|
+
@logger.notice("Loading macrostep #{macrostep_path}")
|
95
|
+
macrostep = load_macrostep(macrostep_path, name, args)
|
96
|
+
section.macrosteps.push(macrostep)
|
97
|
+
@files.push(macrostep_path)
|
98
|
+
@logger.debug("Macrostep '#{name}' found in this path: " \
|
99
|
+
"#{macrostep_path}")
|
100
|
+
loaded = true
|
101
|
+
break
|
102
|
+
else
|
103
|
+
@logger.debug("Macrostep '#{name}' not found in this path: " \
|
104
|
+
"#{macrostep_path}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
fail RecipeError, "Step #{name} not found" unless loaded
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
@logger.notice("Loading recipe metadata")
|
112
|
+
@metainfo = {
|
113
|
+
"description" => Utils.extract_meta_var("description", @recipe_content),
|
114
|
+
"recipe" => Utils.extract_meta_var("recipe", @recipe_content),
|
115
|
+
"template" => Utils.extract_meta_var("template", @recipe_content),
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def load_aliases(yaml_recipe)
|
120
|
+
if yaml_recipe.keys.include? "aliases"
|
121
|
+
aliases = yaml_recipe.fetch("aliases")
|
122
|
+
if aliases.kind_of? Hash
|
123
|
+
@aliases = aliases
|
124
|
+
elsif aliases.kind_of? String
|
125
|
+
path = Pathname.new(File.join(File.dirname(@path), "aliases", aliases))
|
126
|
+
if File.file?(path)
|
127
|
+
@logger.notice("Loading aliases #{path}")
|
128
|
+
@aliases = YAML.load_file(path)
|
129
|
+
@files.push(path)
|
130
|
+
else
|
131
|
+
fail RecipeError, "Aliases file '#{path}' does not exists"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def load_checkpoint_config(yaml_recipe)
|
138
|
+
if yaml_recipe.keys.include? "checkpoint"
|
139
|
+
checkpoint = yaml_recipe.fetch("checkpoint")
|
140
|
+
if checkpoint.kind_of? Hash
|
141
|
+
@checkpoint = checkpoint
|
142
|
+
@checkpoint["path"] = @path
|
143
|
+
elsif checkpoint.kind_of? String
|
144
|
+
path = Pathname.new(File.join(File.dirname(@path),
|
145
|
+
"checkpoints",
|
146
|
+
checkpoint))
|
147
|
+
if File.file?(path)
|
148
|
+
@logger.notice("Loading checkpoint configuration #{path}")
|
149
|
+
@checkpoint = YAML.load_file(path)
|
150
|
+
@checkpoint["path"] = path.to_s
|
151
|
+
@files.push(path)
|
152
|
+
else
|
153
|
+
fail RecipeError, "Checkpoint configuraiton file '#{path}' " \
|
154
|
+
"does not exists"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def load_macrostep(step_path, name, args)
|
161
|
+
macrostep_yaml = YAML.load_file(step_path)
|
162
|
+
local_variables = {}
|
163
|
+
loaded_microsteps = []
|
164
|
+
# Basic macrostep syntax check
|
165
|
+
if not macrostep_yaml.kind_of? Array
|
166
|
+
fail RecipeError, "The macrostep #{step_path} is not valid "
|
167
|
+
"(should be a list of microsteps)"
|
168
|
+
end
|
169
|
+
# Load default local variables
|
170
|
+
macrostep_yaml.each do |yaml_microstep|
|
171
|
+
key = yaml_microstep.keys[0]
|
172
|
+
value = yaml_microstep[key]
|
173
|
+
# Set new variable if not defined yet
|
174
|
+
if value.kind_of? Array
|
175
|
+
loaded_microsteps.push Microstep.new(yaml_microstep)
|
176
|
+
else
|
177
|
+
local_variables[key] = @global.fetch(key, value)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
selected_microsteps = []
|
181
|
+
if args
|
182
|
+
args.each do |entry|
|
183
|
+
if entry.kind_of? Hash
|
184
|
+
# resolve variable before using it
|
185
|
+
entry.each do |key, value|
|
186
|
+
local_variables[key] = value
|
187
|
+
end
|
188
|
+
elsif entry.kind_of? String
|
189
|
+
selected_microsteps.push entry
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
unless selected_microsteps.empty?
|
194
|
+
# Some steps are selected so remove the others
|
195
|
+
# WARN: Allow the user to define this list not in the original order
|
196
|
+
strip_microsteps = []
|
197
|
+
selected_microsteps.each do |microstep_name|
|
198
|
+
macrostep = find_microstep(microstep_name, loaded_microsteps)
|
199
|
+
if macrostep.nil?
|
200
|
+
fail RecipeError, "Can't find microstep '#{microstep_name}' "\
|
201
|
+
"in macrostep file '#{step_path}'"
|
202
|
+
else
|
203
|
+
strip_microsteps.push(macrostep)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
loaded_microsteps = strip_microsteps
|
207
|
+
end
|
208
|
+
return Macrostep.new(name, loaded_microsteps, local_variables, step_path)
|
209
|
+
end
|
210
|
+
|
211
|
+
def find_microstep(microstep_name, loaded_microsteps)
|
212
|
+
@logger.debug("Looking for microstep #{microstep_name}")
|
213
|
+
loaded_microsteps.each do |microstep|
|
214
|
+
if microstep_name.eql? microstep.name
|
215
|
+
return microstep
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def resolve!
|
221
|
+
consistency_check
|
222
|
+
resolve_checkpoint unless @checkpoint.nil?
|
223
|
+
|
224
|
+
@logger.notice("Resolving variables")
|
225
|
+
@sections.values.each do |section|
|
226
|
+
section.macrosteps.each do |macrostep|
|
227
|
+
macrostep.resolve_variables!(@global)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
@sections.values.each do |section|
|
232
|
+
section.macrosteps.each do |macrostep|
|
233
|
+
# First pass : resolve aliases
|
234
|
+
@logger.debug("Resolving aliases for macrostep '#{macrostep.name}'")
|
235
|
+
macrostep.microsteps.each do |microstep|
|
236
|
+
microstep.commands.map! do |cmd|
|
237
|
+
# resolve alias
|
238
|
+
@aliases.keys.include?(cmd.key) ? resolve_alias(cmd) : cmd
|
239
|
+
end
|
240
|
+
end
|
241
|
+
# flatten for multiple-command alias + variables
|
242
|
+
@logger.debug("Resolving check statements for macrostep '#{macrostep.name}'")
|
243
|
+
macrostep.microsteps.each { |microstep| microstep.commands.flatten! }
|
244
|
+
# Second pass : resolve variables + clean/init hooks
|
245
|
+
macrostep.microsteps.each do |microstep|
|
246
|
+
microstep.commands.map! do |cmd|
|
247
|
+
resolve_hooks(cmd, macrostep, microstep)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
@logger.debug("Compacting macrostep '#{macrostep.name}'")
|
251
|
+
# remove empty steps
|
252
|
+
macrostep.microsteps.map! do |microstep|
|
253
|
+
microstep.commands.compact!
|
254
|
+
microstep.commands.empty? ? nil : microstep
|
255
|
+
end
|
256
|
+
# remove nil values
|
257
|
+
macrostep.microsteps.compact!
|
258
|
+
@logger.debug("Resolving commands for macrostep '#{macrostep.name}'")
|
259
|
+
macrostep.microsteps.each do |microstep|
|
260
|
+
microstep.resolve!
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
calculate_step_identifiers
|
265
|
+
end
|
266
|
+
|
267
|
+
def consistency_check()
|
268
|
+
# flatten list of hash to an a hash
|
269
|
+
%w(out_context in_context).each do |context_name|
|
270
|
+
if @global[context_name].kind_of? Array
|
271
|
+
old_context_args = @global[context_name].clone
|
272
|
+
@global[context_name] = {}
|
273
|
+
old_context_args.each do |arg|
|
274
|
+
@global[context_name].merge!(arg)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
@logger.notice("Starting recipe consistency check")
|
279
|
+
missings = []
|
280
|
+
@required_global.each do |key|
|
281
|
+
missings.push key unless @global.key? key
|
282
|
+
end
|
283
|
+
fail RecipeError, "Required parameters missing in global section :" \
|
284
|
+
" #{missings.join ' '}" unless missings.empty?
|
285
|
+
# check context args
|
286
|
+
required_args = %w(cmd)
|
287
|
+
missings = []
|
288
|
+
%w(out_context in_context).each do |context_name|
|
289
|
+
context = @global[context_name]
|
290
|
+
missings = required_args - (context.keys() & required_args)
|
291
|
+
fail RecipeError, "Required paramater missing for #{context_name}:" \
|
292
|
+
" #{ missings.join ' ' }" unless missings.empty?
|
293
|
+
end
|
294
|
+
unless @checkpoint.nil?
|
295
|
+
required_args = %w(create apply list clear)
|
296
|
+
missings = []
|
297
|
+
missings = required_args - (@checkpoint.keys() & required_args)
|
298
|
+
fail RecipeError, "Required paramater missing for checkpoint:" \
|
299
|
+
" #{ missings.join ' ' }" unless missings.empty?
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def resolve_checkpoint()
|
304
|
+
%w(create apply list clear).each do |key|
|
305
|
+
@checkpoint[key] = Utils.resolve_vars(@checkpoint[key],
|
306
|
+
@checkpoint["path"],
|
307
|
+
@global)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def resolve_alias(cmd)
|
312
|
+
name = cmd.key
|
313
|
+
aliases_cmd = @aliases.fetch(name).clone
|
314
|
+
aliases_cmd_str = aliases_cmd.to_yaml
|
315
|
+
args = YAML.load(cmd.string_cmd)[name]
|
316
|
+
args = [].push(args).flatten # convert args to array
|
317
|
+
expected_args_number = aliases_cmd_str.scan(/@\d+/).uniq.count
|
318
|
+
if expected_args_number != args.count
|
319
|
+
if args.length == 0
|
320
|
+
msg = "#{name} takes no arguments (#{args.count} given)"
|
321
|
+
else
|
322
|
+
msg = "#{name} takes exactly #{expected_args_number} arguments"
|
323
|
+
" (#{args.count} given)"
|
324
|
+
end
|
325
|
+
raise RecipeError, msg
|
326
|
+
end
|
327
|
+
microstep = Microstep.new({cmd.microstep_name => aliases_cmd})
|
328
|
+
args.each_with_index do |arg, i|
|
329
|
+
microstep.gsub!("@#{i+1}", arg)
|
330
|
+
end
|
331
|
+
microstep.commands.map do |escaped_cmd|
|
332
|
+
Command.new(YAML.load(escaped_cmd.string_cmd), cmd.microstep_name)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
#handle clean methods
|
337
|
+
def resolve_hooks(cmd, macrostep, microstep)
|
338
|
+
if (cmd.key =~ /on_(.*)clean/ || cmd.key =~ /on_(.*)init/)
|
339
|
+
cmds = []
|
340
|
+
if cmd.value.kind_of?(Array)
|
341
|
+
cmds = cmd.value.map do |c|
|
342
|
+
@aliases.keys.include?(c.key) ? resolve_alias(c) : c
|
343
|
+
end
|
344
|
+
cmds = cmds.flatten
|
345
|
+
else
|
346
|
+
fail RecipeError, "Invalid #{cmd.key} arguments"
|
347
|
+
end
|
348
|
+
if cmd.key.eql? "on_clean"
|
349
|
+
microstep_name = "_clean_#{macrostep.clean_microsteps.count}" \
|
350
|
+
"_#{microstep.name}"
|
351
|
+
new_clean_microstep = Microstep.new({microstep_name => []})
|
352
|
+
new_clean_microstep.on_checkpoint = microstep.on_checkpoint
|
353
|
+
new_clean_microstep.commands = cmds.clone
|
354
|
+
macrostep.clean_microsteps.unshift new_clean_microstep
|
355
|
+
return
|
356
|
+
elsif cmd.key.eql? "on_init"
|
357
|
+
microstep_name = "_init_#{macrostep.init_microsteps.count}"\
|
358
|
+
"_#{microstep.name}"
|
359
|
+
new_init_microstep = Microstep.new({microstep_name=> []},
|
360
|
+
microstep)
|
361
|
+
new_init_microstep.on_checkpoint = microstep.on_checkpoint
|
362
|
+
new_init_microstep.commands = cmds.clone
|
363
|
+
macrostep.init_microsteps.unshift new_init_microstep
|
364
|
+
return
|
365
|
+
else
|
366
|
+
@sections.values.each do |section|
|
367
|
+
section.clean_macrostep
|
368
|
+
if cmd.key.eql? "on_#{section.name}_clean"
|
369
|
+
microstep_name = "_clean_#{section.clean_macrostep.microsteps.count}" \
|
370
|
+
"_#{microstep.name}"
|
371
|
+
new_clean_microstep = Microstep.new({microstep_name=> []})
|
372
|
+
new_clean_microstep.commands = cmds.clone
|
373
|
+
new_clean_microstep.on_checkpoint = microstep.on_checkpoint
|
374
|
+
section.clean_macrostep.microsteps.unshift new_clean_microstep
|
375
|
+
return
|
376
|
+
elsif cmd.key.eql? "on_#{section.name}_init"
|
377
|
+
microstep_name = "_init_#{section.init_macrostep.microsteps.count}" \
|
378
|
+
"_#{microstep.name}"
|
379
|
+
new_init_microstep = Microstep.new({microstep_name=> []})
|
380
|
+
new_init_microstep.commands = cmds.clone
|
381
|
+
new_init_microstep.on_checkpoint = microstep.on_checkpoint
|
382
|
+
section.init_macrostep.microsteps.push new_init_microstep
|
383
|
+
return
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
fail RecipeError, "Invalid command : '#{cmd.key}'"
|
388
|
+
else
|
389
|
+
return cmd
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def microsteps
|
394
|
+
if @microsteps.nil?
|
395
|
+
microsteps = []
|
396
|
+
@sections.values.each do |section|
|
397
|
+
section.sequence do |macrostep|
|
398
|
+
macrostep.sequence do |microstep|
|
399
|
+
microsteps.push microstep
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
@microsteps = microsteps
|
404
|
+
end
|
405
|
+
return @microsteps
|
406
|
+
end
|
407
|
+
|
408
|
+
def calculate_step_identifiers
|
409
|
+
@logger.notice("Calculating microstep identifiers")
|
410
|
+
base_salt = ""
|
411
|
+
order = 0
|
412
|
+
@sections.values.each do |section|
|
413
|
+
section.sequence do |macrostep|
|
414
|
+
macrostep.sequence do |microstep|
|
415
|
+
base_salt = microstep.calculate_identifier base_salt
|
416
|
+
slug = "#{section.name}/#{macrostep.name}/#{microstep.name}"
|
417
|
+
microstep.slug = slug
|
418
|
+
microstep.order = (order += 1)
|
419
|
+
@logger.debug(" #{microstep.slug}: #{microstep.identifier}")
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def to_hash
|
426
|
+
recipe_hash = {
|
427
|
+
"name" => @name,
|
428
|
+
"path" => @path.to_s,
|
429
|
+
"files" => @files.map {|p| p.to_s },
|
430
|
+
"global" => @global,
|
431
|
+
"required_global" => @required_global,
|
432
|
+
"aliases" => @aliases,
|
433
|
+
}
|
434
|
+
recipe_hash["checkpoint"] = @checkpoint unless @checkpoint.nil?
|
435
|
+
recipe_hash["steps"] = to_array
|
436
|
+
return recipe_hash
|
437
|
+
end
|
438
|
+
|
439
|
+
def to_array
|
440
|
+
array = []
|
441
|
+
@sections.values.each do |section|
|
442
|
+
section.to_array.each { |m| array.push m }
|
443
|
+
end
|
444
|
+
return array
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
448
|
+
|
449
|
+
class RecipeTemplate < Recipe
|
450
|
+
|
451
|
+
def copy_template(dest_path, recipe_name, force)
|
452
|
+
Dir::mktmpdir do |tmp_dir|
|
453
|
+
recipe_path = File.join(tmp_dir, recipe_name + '.yaml')
|
454
|
+
FileUtils.cp(@path, recipe_path)
|
455
|
+
File.open(recipe_path, 'w+') do |file|
|
456
|
+
tpl = ERB.new(@recipe_content)
|
457
|
+
result = tpl.result(binding)
|
458
|
+
file.write(result)
|
459
|
+
end
|
460
|
+
|
461
|
+
@files.each do |path|
|
462
|
+
relative_path = path.relative_path_from(Kameleon.env.templates_path)
|
463
|
+
dst = File.join(tmp_dir, File.dirname(relative_path))
|
464
|
+
FileUtils.mkdir_p dst
|
465
|
+
FileUtils.cp(path, dst)
|
466
|
+
@logger.debug("Copying '#{path}' to '#{dst}'")
|
467
|
+
end
|
468
|
+
# Create recipe dir if not exists
|
469
|
+
FileUtils.mkdir_p Kameleon.env.recipes_path
|
470
|
+
FileUtils.cp_r(Dir[tmp_dir + '/*'], Kameleon.env.recipes_path)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|