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