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