kameleon-builder 2.0.0.dev

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/.editorconfig +23 -0
  2. data/.env +51 -0
  3. data/.gitignore +22 -0
  4. data/AUTHORS +19 -0
  5. data/CHANGELOG +36 -0
  6. data/COPYING +340 -0
  7. data/Gemfile +4 -0
  8. data/README.md +53 -0
  9. data/Rakefile +24 -0
  10. data/Vagrantfile +68 -0
  11. data/bin/kameleon +16 -0
  12. data/contrib/kameleon_bashrc.sh +138 -0
  13. data/contrib/scripts/VirtualBox_deploy.sh +12 -0
  14. data/contrib/scripts/chroot_env +9 -0
  15. data/contrib/scripts/create_passwd.py +17 -0
  16. data/contrib/scripts/umount-chroot.sh +290 -0
  17. data/contrib/steps/bootstrap/debian/bootstrap_if_needed.yaml +47 -0
  18. data/contrib/steps/bootstrap/debian/bootstrap_static.yaml +38 -0
  19. data/contrib/steps/setup/add_timestamp.yaml +6 -0
  20. data/contrib/steps/setup/autologin.yaml +16 -0
  21. data/contrib/steps/setup/copy_ssh_auth_file.yaml +10 -0
  22. data/contrib/steps/setup/debian/add_network_interface.yaml +7 -0
  23. data/contrib/steps/setup/debian/cluster_tools_install.yaml +16 -0
  24. data/contrib/steps/setup/debian/network_config_static.yaml +17 -0
  25. data/contrib/steps/setup/generate_user_ssh_key.yaml +15 -0
  26. data/contrib/steps/setup/install_my_ssh_key.yaml +26 -0
  27. data/contrib/steps/setup/make_swap_file.yaml +9 -0
  28. data/contrib/steps/setup/root_ssh_config.yaml +18 -0
  29. data/contrib/steps/setup/set_user_password.yaml +7 -0
  30. data/contrib/steps/setup/system_optimization.yaml +8 -0
  31. data/docs/.gitignore +1 -0
  32. data/docs/Makefile +177 -0
  33. data/docs/make.bat +242 -0
  34. data/docs/source/_static/.gitignore +0 -0
  35. data/docs/source/aliases.rst +29 -0
  36. data/docs/source/checkpoint.rst +28 -0
  37. data/docs/source/cli.rst +3 -0
  38. data/docs/source/commands.rst +62 -0
  39. data/docs/source/conf.py +254 -0
  40. data/docs/source/context.rst +42 -0
  41. data/docs/source/faq.rst +3 -0
  42. data/docs/source/getting_started.rst +3 -0
  43. data/docs/source/index.rst +38 -0
  44. data/docs/source/installation.rst +3 -0
  45. data/docs/source/recipe.rst +256 -0
  46. data/docs/source/why.rst +3 -0
  47. data/docs/source/workspace.rst +11 -0
  48. data/kameleon-builder.gemspec +37 -0
  49. data/lib/kameleon.rb +75 -0
  50. data/lib/kameleon/cli.rb +176 -0
  51. data/lib/kameleon/context.rb +83 -0
  52. data/lib/kameleon/engine.rb +357 -0
  53. data/lib/kameleon/environment.rb +38 -0
  54. data/lib/kameleon/error.rb +51 -0
  55. data/lib/kameleon/logger.rb +53 -0
  56. data/lib/kameleon/recipe.rb +474 -0
  57. data/lib/kameleon/shell.rb +290 -0
  58. data/lib/kameleon/step.rb +213 -0
  59. data/lib/kameleon/utils.rb +45 -0
  60. data/lib/kameleon/version.rb +3 -0
  61. data/templates/COPYRIGHT +21 -0
  62. data/templates/aliases/defaults.yaml +83 -0
  63. data/templates/checkpoints/docker.yaml +14 -0
  64. data/templates/checkpoints/qcow2.yaml +44 -0
  65. data/templates/debian-wheezy-chroot.yaml +98 -0
  66. data/templates/debian-wheezy-docker.yaml +97 -0
  67. data/templates/fedora-docker.yaml +96 -0
  68. data/templates/steps/bootstrap/debian/debootstrap.yaml +13 -0
  69. data/templates/steps/bootstrap/fedora/docker_bootstrap.yaml +25 -0
  70. data/templates/steps/bootstrap/fedora/yum_bootstrap.yaml +22 -0
  71. data/templates/steps/bootstrap/prepare_appliance_with_nbd.yaml +93 -0
  72. data/templates/steps/bootstrap/prepare_docker.yaml +38 -0
  73. data/templates/steps/bootstrap/start_chroot.yaml +53 -0
  74. data/templates/steps/bootstrap/start_docker.yaml +12 -0
  75. data/templates/steps/export/build_appliance_from_docker.yaml +105 -0
  76. data/templates/steps/export/clean_appliance.yaml +3 -0
  77. data/templates/steps/export/save_appliance_from_nbd.yaml +54 -0
  78. data/templates/steps/setup/create_user.yaml +12 -0
  79. data/templates/steps/setup/debian/kernel_install.yaml +20 -0
  80. data/templates/steps/setup/debian/keyboard_config.yaml +10 -0
  81. data/templates/steps/setup/debian/network_config.yaml +30 -0
  82. data/templates/steps/setup/debian/software_install.yaml +15 -0
  83. data/templates/steps/setup/debian/system_config.yaml +12 -0
  84. data/templates/steps/setup/fedora/kernel_install.yaml +27 -0
  85. data/templates/steps/setup/fedora/software_install.yaml +10 -0
  86. data/tests/helper.rb +22 -0
  87. data/tests/recipes/dummy_recipe.yaml +48 -0
  88. data/tests/recipes/steps/bootstrap/dummy_distro/dummy_bootstrap_static.yaml +4 -0
  89. data/tests/recipes/steps/export/dummy_save_appliance.yaml +9 -0
  90. data/tests/recipes/steps/setup/default/dummy_root_passwd.yaml +8 -0
  91. data/tests/recipes/steps/setup/dummy_distro/dummy_software_install.yaml +7 -0
  92. data/tests/test_context.rb +16 -0
  93. data/tests/test_recipe.rb +15 -0
  94. data/tests/test_version.rb +9 -0
  95. 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