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,176 @@
1
+ require 'kameleon/engine'
2
+ require 'kameleon/recipe'
3
+ require 'kameleon/utils'
4
+
5
+ module Kameleon
6
+ class CLI < Thor
7
+
8
+ class_option :no_color, :type => :boolean, :default => false,
9
+ :desc => "Disable colorization in output"
10
+ class_option :debug, :type => :boolean, :default => false,
11
+ :desc => "Enable debug output"
12
+ class_option :workspace, :aliases => '-w', :type => :string,
13
+ :desc => 'Change the kameleon current work directory. ' \
14
+ '(The folder containing your recipes folder).' \
15
+ ' Default : ./'
16
+ no_commands do
17
+ def logger
18
+ @logger ||= Log4r::Logger.new("kameleon::[cli]")
19
+ end
20
+ end
21
+
22
+ method_option :template, :aliases => "-t",
23
+ :desc => "Starting from a template", :required => true,
24
+ :enum => Kameleon.templates_names
25
+ method_option :force,:type => :boolean,
26
+ :default => false, :aliases => "-f",
27
+ :desc => "overwrite the recipe"
28
+ desc "new [RECIPE_NAME]", "Creates a new recipe"
29
+ def new(recipe_name)
30
+ logger.notice("Cloning template '#{options[:template]}'")
31
+ templates_path = Kameleon.env.templates_path
32
+ recipes_path = Kameleon.env.recipes_path
33
+
34
+ template_path = File.join(templates_path, options[:template]) + '.yaml'
35
+ template_recipe = RecipeTemplate.new(template_path)
36
+ template_recipe.copy_template(recipes_path,
37
+ recipe_name,
38
+ options[:force])
39
+ recipe_path = File.join(recipes_path, recipe_name + ".yaml")
40
+ logger.notice("New recipe \"#{recipe_name}\" "\
41
+ "as been created in #{recipe_path}")
42
+ end
43
+
44
+ desc "templates", "Lists all defined templates"
45
+ def templates
46
+ Log4r::Outputter['console'].level = Log4r::ERROR unless Kameleon.env.debug
47
+ puts "The following templates are available in " \
48
+ "#{ Kameleon.templates_path }:"
49
+ templates_hash = []
50
+ Kameleon.templates_files.each do |f|
51
+ begin
52
+ recipe = RecipeTemplate.new(f)
53
+ templates_hash.push({
54
+ "name" => recipe.name,
55
+ "description" => recipe.metainfo['description'],
56
+ })
57
+ rescue => e
58
+ raise e if Kameleon.env.debug
59
+ end
60
+ end
61
+ tp templates_hash, {"name" => {:width => 30}}, { "description" => {:width => 60}}
62
+ end
63
+
64
+ desc "version", "Prints the Kameleon's version information"
65
+ def version
66
+ Log4r::Outputter['console'].level = Log4r::OFF unless Kameleon.env.debug
67
+ puts "Kameleon version #{Kameleon::VERSION}"
68
+ end
69
+ map %w(-v --version) => :version
70
+
71
+
72
+ desc "build [RECIPE_NAME]", "Builds the appliance from the recipe"
73
+ method_option :build_path, :type => :string ,
74
+ :default => nil, :aliases => "-b",
75
+ :desc => "Set the build directory path"
76
+ method_option :from_checkpoint, :type => :string ,
77
+ :default => nil,
78
+ :desc => "Using specific checkpoint to build the image. " \
79
+ "Default value is the last checkpoint."
80
+ method_option :no_checkpoint, :type => :boolean ,
81
+ :default => false,
82
+ :desc => "Do not use checkpoints"
83
+ def build(recipe_name)
84
+ logger.notice("Starting build recipe '#{recipe_name}'")
85
+ start_time = Time.now.to_i
86
+ recipe_path = File.join(Kameleon.env.recipes_path, recipe_name) + '.yaml'
87
+ engine = Kameleon::Engine.new(Recipe.new(recipe_path), options)
88
+ engine.build
89
+ total_time = Time.now.to_i - start_time
90
+ logger.notice("")
91
+ logger.notice("Build recipe '#{recipe_name}' is completed !")
92
+ logger.notice("Build total duration : #{total_time} secs")
93
+ logger.notice("Build directory : #{engine.cwd}")
94
+ logger.notice("Build recipe file : #{engine.build_recipe_path}")
95
+ logger.notice("Log file : #{Kameleon.env.log_file}")
96
+ end
97
+
98
+ desc "checkpoints [RECIPE_NAME]", "Lists all availables checkpoints"
99
+ method_option :build_path, :type => :string ,
100
+ :default => nil, :aliases => "-b",
101
+ :desc => "Set the build directory path"
102
+ def checkpoints(recipe_name)
103
+ Log4r::Outputter['console'].level = Log4r::ERROR unless Kameleon.env.debug
104
+ recipe_path = File.join(Kameleon.env.recipes_path, recipe_name) + '.yaml'
105
+ engine = Kameleon::Engine.new(Recipe.new(recipe_path), options)
106
+ engine.pretty_checkpoints_list
107
+ end
108
+
109
+ desc "clear [RECIPE_NAME]", "Cleaning out context and removing all checkpoints"
110
+ method_option :build_path, :type => :string ,
111
+ :default => nil, :aliases => "-b",
112
+ :desc => "Set the build directory path"
113
+ def clear(recipe_name)
114
+ Log4r::Outputter['console'].level = Log4r::INFO
115
+ recipe_path = File.join(Kameleon.env.recipes_path, recipe_name) + '.yaml'
116
+ engine = Kameleon::Engine.new(Recipe.new(recipe_path), options)
117
+ engine.clear
118
+ end
119
+
120
+ # Hack Thor to init Kameleon env soon
121
+ def self.init(base_config)
122
+ options = base_config[:shell].base.options
123
+ workspace ||= options[:workspace] || ENV['KAMELEON_WORKSPACE'] || Dir.pwd
124
+ env_options = options.merge({:workspace => workspace})
125
+ FileUtils.mkdir_p workspace
126
+ # configure logger
127
+ env_options["debug"] = true if ENV["KAMELEON_LOG"] == "debug"
128
+ ENV["KAMELEON_LOG"] = "debug" if env_options["debug"]
129
+ if ENV["KAMELEON_LOG"] && ENV["KAMELEON_LOG"] != ""
130
+ level_name = ENV["KAMELEON_LOG"]
131
+ else
132
+ level_name = "info"
133
+ end
134
+ # Require Log4r and define the levels we'll be using
135
+ require 'log4r-color/config'
136
+ Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
137
+
138
+ begin
139
+ level = Log4r.const_get(level_name.upcase)
140
+ rescue NameError
141
+ fail KameleonError, "Invalid KAMELEON_LOG level is set: #{level_name}.\n" \
142
+ "Please use one of the standard log levels: debug," \
143
+ " info, warn, or error"
144
+ end
145
+ format = ConsoleFormatter.new
146
+ # format = Log4r::PatternFormatter.new(:pattern => '%11c: %M')
147
+ if !$stdout.tty? or env_options["no_color"]
148
+ console_output = Log4r::StdoutOutputter.new('console',
149
+ :formatter => format)
150
+ else
151
+ console_output = Log4r::ColorOutputter.new 'console', {
152
+ :colors => { :debug => :light_black,
153
+ :info => :green,
154
+ :notice => :light_blue,
155
+ :progress => :light_blue,
156
+ :warn => :yellow,
157
+ :error => :red,
158
+ :fatal => :red,
159
+ },
160
+ :formatter => format,
161
+ }
162
+ end
163
+ logger = Log4r::Logger.new('kameleon')
164
+ logger.outputters << console_output
165
+ logger.level = level
166
+ logger = nil
167
+ Kameleon.logger.debug("`kameleon` invoked: #{ARGV.inspect}")
168
+ Kameleon.env = Kameleon::Environment.new(env_options)
169
+ end
170
+
171
+ def self.start(given_args=ARGV, config={})
172
+ config[:shell] ||= Thor::Base.shell.new
173
+ dispatch(nil, given_args.dup, nil, config) { init(config) }
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,83 @@
1
+ require 'kameleon/shell'
2
+
3
+ module Kameleon
4
+ class Context
5
+
6
+ attr_accessor :shell, :name
7
+
8
+ def initialize(name, cmd, workdir, exec_prefix, local_workdir)
9
+ @name = name.downcase
10
+ @logger = Log4r::Logger.new("kameleon::[#{@name}_ctx]")
11
+ @cmd = cmd
12
+ @workdir = workdir
13
+ @exec_prefix = exec_prefix
14
+ @local_workdir = local_workdir
15
+ @shell = Kameleon::Shell.new(@name, @cmd, @workdir, @local_workdir)
16
+ @logger.debug("Initialize new ctx (#{name})")
17
+
18
+ instance_variables.each do |v|
19
+ @logger.debug(" #{v} = #{instance_variable_get(v)}")
20
+ end
21
+ # Start the shell process
22
+ @shell.start
23
+ execute("echo The '#{name}_context' has been initialized", :log_level => "info")
24
+ end
25
+
26
+ def log(log_level, msg)
27
+ @logger.info msg if log_level == "info"
28
+ @logger.error msg if log_level == "error"
29
+ @logger.debug msg if log_level == "debug"
30
+ end
31
+
32
+ def execute(cmd, kwargs = {})
33
+ cmd_with_prefix = "#{@exec_prefix} #{cmd}"
34
+ cmd_with_prefix.split( /\r?\n/ ).each {|m| @logger.debug "+ #{m}" }
35
+ log_level = kwargs.fetch(:log_level, "info")
36
+ exit_status = @shell.execute(cmd_with_prefix, kwargs) do |out, err|
37
+ out.split( /\r?\n/ ).each {|m| log(log_level, m) } unless out.nil?
38
+ err.split( /\r?\n/ ).each {|m| log("error", m) } unless err.nil?
39
+ end
40
+ @logger.debug("Exit status : #{exit_status}")
41
+ fail ExecError unless exit_status.eql? 0
42
+ rescue ShellError => e
43
+ @logger.error(e.message)
44
+ fail ExecError
45
+ end
46
+
47
+ def pipe(cmd, other_cmd, other_ctx)
48
+ tmp = Tempfile.new("pipe-#{ Kameleon::Utils.generate_slug(cmd)[0..20] }")
49
+ @logger.info("Running piped commands")
50
+ @logger.info("Saving STDOUT from #{@name}_ctx to local file #{tmp.path}")
51
+ execute(cmd, :stdout => tmp)
52
+ tmp.close
53
+ @logger.info("Forwarding #{tmp.path} to STDIN of #{other_ctx.name}_ctx")
54
+ dest_pipe_path = "./pipe-#{ Kameleon::Utils.generate_slug(other_cmd)[0..20] }"
55
+ other_ctx.send_file(tmp.path, dest_pipe_path)
56
+ other_cmd_with_pipe = "cat #{dest_pipe_path} | #{other_cmd} && rm #{dest_pipe_path}"
57
+ other_ctx.execute(other_cmd_with_pipe)
58
+ end
59
+
60
+ def start_shell
61
+ #TODO: Load env and history
62
+ @logger.info("Starting interactive shell")
63
+ @shell.fork_and_wait
64
+ end
65
+
66
+ def closed?
67
+ @shell.exited?
68
+ end
69
+
70
+ def close!
71
+ @shell.stop
72
+ end
73
+
74
+ def reopen
75
+ @shell.restart
76
+ end
77
+
78
+ def send_file(source_path, dest_path)
79
+ @shell.send_file(source_path, dest_path)
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,357 @@
1
+ require 'kameleon/recipe'
2
+ require 'kameleon/context'
3
+
4
+
5
+ module Kameleon
6
+
7
+ class Engine
8
+ attr_accessor :recipe, :cwd, :build_recipe_path, :pretty_list_checkpoints
9
+
10
+ def initialize(recipe, options)
11
+ @options = options
12
+ @logger = Log4r::Logger.new("kameleon::[engine]")
13
+ @recipe = recipe
14
+ @cleaned_sections = []
15
+ @cwd = @recipe.global["kameleon_cwd"]
16
+ @build_recipe_path = File.join(@cwd, "kameleon_build_recipe.yaml")
17
+
18
+ build_recipe = load_build_recipe
19
+ # restore previous build uuid
20
+ unless build_recipe.nil?
21
+ # binding.pry
22
+ %w(kameleon_uuid kameleon_short_uuid).each do |key|
23
+ @recipe.global[key] = build_recipe["global"][key]
24
+ end
25
+ end
26
+
27
+ @enable_checkpoint = !@options[:no_checkpoint]
28
+ # Check if the recipe have checkpoint entry
29
+ @enable_checkpoint = !@recipe.checkpoint.nil? if @enable_checkpoint
30
+
31
+ @recipe.resolve!
32
+ @in_context = nil
33
+ begin
34
+ @logger.notice("Creating kameleon working directory : #{@cwd}")
35
+ FileUtils.mkdir_p @cwd
36
+ rescue
37
+ raise BuildError, "Failed to create working directory #{@cwd}"
38
+ end
39
+ @logger.notice("Building local context [local]")
40
+ @local_context = Context.new("local", "bash", @cwd, "", @cwd)
41
+ @logger.notice("Building external context [out]")
42
+ @out_context = Context.new("out",
43
+ @recipe.global["out_context"]["cmd"],
44
+ @recipe.global["out_context"]["workdir"],
45
+ @recipe.global["out_context"]["exec_prefix"],
46
+ @cwd)
47
+ end
48
+
49
+ def create_checkpoint(microstep_id)
50
+ cmd = @recipe.checkpoint["create"].gsub("@microstep_id", microstep_id)
51
+ create_cmd = Kameleon::Command.new({"exec_out" => cmd}, "checkpoint")
52
+ safe_exec_cmd(create_cmd, :log_level => "debug")
53
+ end
54
+
55
+ def apply_checkpoint(microstep_id)
56
+ cmd = @recipe.checkpoint["apply"].gsub("@microstep_id", microstep_id)
57
+ apply_cmd = Kameleon::Command.new({"exec_out" => cmd}, "checkpoint")
58
+ safe_exec_cmd(apply_cmd, :log_level => "debug")
59
+ end
60
+
61
+ def list_all_checkpoints
62
+ list = ""
63
+ cmd = Kameleon::Command.new({"exec_out" => @recipe.checkpoint['list']},
64
+ "checkpoint")
65
+ safe_exec_cmd(cmd, :stdout => list)
66
+ return list.split(/\r?\n/)
67
+ end
68
+
69
+ def list_checkpoints
70
+ if @list_checkpoints.nil?
71
+ checkpoints = list_all_checkpoints
72
+ all_microsteps_ids = @recipe.microsteps.map { |m| m.identifier }
73
+ # get sorted checkpoints by microsteps order
74
+ @list_checkpoints = []
75
+ all_microsteps_ids.each do |id|
76
+ @list_checkpoints.push(id) if checkpoints.include?(id)
77
+ end
78
+ end
79
+ return @list_checkpoints
80
+ end
81
+
82
+ def do_steps(section_name)
83
+ section = @recipe.sections.fetch(section_name)
84
+ section.sequence do |macrostep|
85
+ macrostep.sequence do |microstep|
86
+ @logger.notice("Step #{ microstep.order } : #{ microstep.slug }")
87
+ @logger.notice(" ---> #{ microstep.identifier }")
88
+ if @enable_checkpoint
89
+ if microstep.on_checkpoint == "skip"
90
+ @logger.notice(" ---> Skipped")
91
+ next
92
+ end
93
+ if microstep.in_cache && microstep.on_checkpoint == "use_cache"
94
+ @logger.notice(" ---> Using cache")
95
+ else
96
+ @logger.notice(" ---> Running step")
97
+ microstep.commands.each do |cmd|
98
+ safe_exec_cmd(cmd)
99
+ end
100
+ unless microstep.on_checkpoint == "redo"
101
+ @logger.notice(" ---> Creating checkpoint : #{ microstep.identifier }")
102
+ create_checkpoint(microstep.identifier)
103
+ end
104
+ end
105
+ else
106
+ @logger.notice(" ---> Running step")
107
+ microstep.commands.each do |cmd|
108
+ safe_exec_cmd(cmd)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ @cleaned_sections.push(section.name)
114
+ end
115
+
116
+ def safe_exec_cmd(cmd, kwargs = {})
117
+ finished = false
118
+ begin
119
+ exec_cmd(cmd, kwargs)
120
+ finished = true
121
+ rescue ExecError
122
+ finished = rescue_exec_error(cmd)
123
+ end until finished
124
+ end
125
+
126
+ def exec_cmd(cmd, kwargs = {})
127
+ def skip_alert(cmd)
128
+ @logger.warn("Skipping cmd '#{cmd.string_cmd}'. The in_context is" \
129
+ " not ready yet")
130
+ end
131
+ case cmd.key
132
+ when "breakpoint"
133
+ breakpoint(cmd.value, )
134
+ when "exec_in"
135
+ skip_alert(cmd) if @in_context.nil?
136
+ @in_context.execute(cmd.value, kwargs) unless @in_context.nil?
137
+ when "exec_out"
138
+ @out_context.execute(cmd.value, kwargs)
139
+ when "exec_local"
140
+ @local_context.execute(cmd.value, kwargs)
141
+ when "pipe"
142
+ first_cmd, second_cmd = cmd.value
143
+ if ((first_cmd.key == "exec_in" || second_cmd.key == "exec_in")\
144
+ && @in_context.nil?)
145
+ skip_alert(cmd)
146
+ else
147
+ expected_cmds = ["exec_in", "exec_out", "exec_local"]
148
+ [first_cmd.key, second_cmd.key].each do |key|
149
+ unless expected_cmds.include?(key)
150
+ @logger.error("Invalid pipe arguments. Expected "\
151
+ "#{expected_cmds} commands")
152
+ fail ExecError
153
+ end
154
+ end
155
+ map = {"exec_in" => @in_context,
156
+ "exec_out" => @out_context,
157
+ "exec_local" => @local_context,}
158
+ first_context = map[first_cmd.key]
159
+ second_context = map[second_cmd.key]
160
+ first_context.pipe(first_cmd.value, second_cmd.value, second_context)
161
+ end
162
+ when "rescue"
163
+ first_cmd, second_cmd = cmd.value
164
+ begin
165
+ exec_cmd(first_cmd)
166
+ rescue ExecError
167
+ exec_cmd(second_cmd)
168
+ end
169
+ else
170
+ @logger.warn("Unknown command : #{cmd.key}")
171
+ end
172
+ end
173
+
174
+
175
+ def breakpoint(message, kwargs = {})
176
+ message.split( /\r?\n/ ).each {|m| @logger.error "#{m}" }
177
+ enable_retry = kwargs[:enable_retry]
178
+ msg = ""
179
+ msg << "Press [r] to retry\n" if enable_retry
180
+ msg << "Press [c] to continue with execution"
181
+ msg << "\nPress [a] to abort execution"
182
+ msg << "\nPress [l] to switch to local_context shell" unless @local_context.nil?
183
+ msg << "\nPress [o] to switch to out_context shell" unless @out_context.nil?
184
+ msg << "\nPress [i] to switch to in_context shell" unless @in_context.nil?
185
+ responses = {"c" => "continue", "a" => "abort"}
186
+ responses["r"] = "retry" if enable_retry
187
+ responses.merge!({"l" => "launch local_context"}) unless @out_context.nil?
188
+ responses.merge!({"o" => "launch out_context"}) unless @out_context.nil?
189
+ responses.merge!({"i" => "launch in_context"}) unless @in_context.nil?
190
+ while true
191
+ msg.split( /\r?\n/ ).each {|m| @logger.notice "#{m}" }
192
+ @logger.progress "answer ? [" + responses.keys().join("/") + "]: "
193
+ answer = $stdin.gets
194
+ raise AbortError, "Execution aborted..." if answer.nil?
195
+ answer.chomp!
196
+ if responses.keys.include?(answer)
197
+ @logger.notice("User choice : [#{answer}] #{responses[answer]}")
198
+ if ["o", "i", "l"].include?(answer)
199
+ if answer.eql? "l"
200
+ @local_context.start_shell
201
+ elsif answer.eql? "o"
202
+ @out_context.start_shell
203
+ else
204
+ @in_context.start_shell
205
+ end
206
+ @logger.notice("Getting back to Kameleon ...")
207
+ elsif answer.eql? "a"
208
+ raise AbortError, "Execution aborted..."
209
+ elsif answer.eql? "c"
210
+ ## resetting the exit status
211
+ @in_context.execute("true") unless @in_context.nil?
212
+ @out_context.execute("true") unless @out_context.nil?
213
+ return true
214
+ elsif answer.eql? "r"
215
+ @logger.notice("Retrying the previous command...")
216
+ return false
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ def rescue_exec_error(cmd)
223
+ message = "Error occured when executing the following command :\n"
224
+ cmd.string_cmd.split( /\r?\n/ ).each {|m| message << "\n> #{m}" }
225
+ return breakpoint(message, :enable_retry => true)
226
+ end
227
+
228
+ def finish_clean()
229
+ @recipe.sections.values.each do |section|
230
+ next if @cleaned_sections.include?(section.name)
231
+ begin
232
+ @logger.notice("Cleaning #{section.name} section")
233
+ section.clean_macrostep.sequence do |microstep|
234
+ microstep.commands.each do |cmd|
235
+ begin
236
+ exec_cmd(cmd)
237
+ rescue
238
+ @logger.warn("An error occurred while executing : #{cmd.value}")
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ def clear
247
+ @recipe.sections.values.each do |section|
248
+ @logger.notice("Cleaning #{section.name} section")
249
+ section.clean_macrostep.sequence do |microstep|
250
+ microstep.commands.each do |cmd|
251
+ if (cmd.key == "exec_out" || cmd.key == "exec_local")
252
+ begin
253
+ exec_cmd(cmd)
254
+ rescue
255
+ @logger.warn("An error occurred while executing : #{cmd.value}")
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ unless @recipe.checkpoint.nil?
262
+ @logger.notice("Removing all old checkpoints")
263
+ cmd = @recipe.checkpoint["clear"]
264
+ clear_cmd = Kameleon::Command.new({"exec_out" => cmd}, "checkpoint")
265
+ safe_exec_cmd(clear_cmd, :log_level => "info")
266
+ end
267
+ end
268
+
269
+ def build
270
+ if @enable_checkpoint
271
+ @from_checkpoint = @options[:from_checkpoint]
272
+ if @from_checkpoint.nil?
273
+ @from_checkpoint = list_checkpoints.last
274
+ else
275
+ unless list_checkpoints.include?@from_checkpoint
276
+ fail BuildError, "Unknown checkpoint hash : #{@from_checkpoint}." \
277
+ " Use checkpoints command to find a valid" \
278
+ " checkpoint"
279
+ end
280
+ end
281
+ unless @from_checkpoint.nil?
282
+ @logger.notice("Restoring last build from step : #{@from_checkpoint}")
283
+ apply_checkpoint @from_checkpoint
284
+ @recipe.microsteps.each do |microstep|
285
+ microstep.in_cache = true
286
+ if microstep.identifier == @from_checkpoint
287
+ break
288
+ end
289
+ end
290
+ end
291
+ end
292
+ dump_build_recipe
293
+ begin
294
+ do_steps("bootstrap")
295
+ @logger.notice("Building internal context [in]")
296
+ @in_context = Context.new("in",
297
+ @recipe.global["in_context"]["cmd"],
298
+ @recipe.global["in_context"]["workdir"],
299
+ @recipe.global["in_context"]["exec_prefix"],
300
+ @cwd)
301
+ do_steps("setup")
302
+ do_steps("export")
303
+ rescue Exception => e
304
+ @logger.warn("Waiting for cleanup before exiting...")
305
+ @out_context.reopen if !@out_context.nil?
306
+ @in_context.reopen if !@in_context.nil?
307
+ @local_context.reopen if !@local_context.nil?
308
+ finish_clean
309
+ @out_context.close! unless @out_context.nil?
310
+ @in_context.close! unless @in_context.nil?
311
+ @local_context.close! unless @local_context.nil?
312
+ raise e
313
+ end
314
+ end
315
+
316
+ def dump_build_recipe
317
+ File.open(@build_recipe_path, 'w') do |f|
318
+ f.write @recipe.to_hash.to_yaml
319
+ end
320
+ end
321
+
322
+ def load_build_recipe
323
+ if File.file?(@build_recipe_path)
324
+ result = YAML.load_file(@build_recipe_path)
325
+ return result if result
326
+ end
327
+ return nil
328
+ end
329
+
330
+ def pretty_checkpoints_list
331
+ def find_microstep_slug_by_id(id)
332
+ @recipe.microsteps.each do |m|
333
+ return m.slug if m.identifier == id
334
+ end
335
+ end
336
+ dict_checkpoints = []
337
+ if @enable_checkpoint
338
+ list_checkpoints.each do |id|
339
+ slug = find_microstep_slug_by_id id
340
+ unless slug.nil?
341
+ dict_checkpoints.push({
342
+ "id" => id,
343
+ "step" => slug
344
+ })
345
+ end
346
+ end
347
+ end
348
+ if dict_checkpoints.empty?
349
+ puts "No checkpoint available for the recipe '#{recipe.name}'"
350
+ else
351
+ puts "The following checkpoints are available for " \
352
+ "the recipe '#{recipe.name}':"
353
+ tp dict_checkpoints, {"id" => {:width => 20}}, { "step" => {:width => 60}}
354
+ end
355
+ end
356
+ end
357
+ end