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,290 @@
1
+ require 'kameleon/utils'
2
+ require 'shellwords'
3
+
4
+
5
+ module Kameleon
6
+ class Shell
7
+ ECHO_CMD = "echo"
8
+ READ_CHUNK_SIZE = 1048576
9
+ EXIT_TIMEOUT = 60
10
+
11
+ attr :exit_status, :process
12
+
13
+ def initialize(context_name, cmd, shell_workdir, local_workdir, kwargs = {})
14
+ @logger = Log4r::Logger.new("kameleon::[shell]")
15
+ @cmd = cmd.chomp
16
+ @context_name = context_name
17
+ @local_workdir = local_workdir
18
+ @shell_workdir = shell_workdir
19
+ @bashrc_file = "/tmp/kameleon_#{@context_name}_bash_rc"
20
+ @bash_history_file = "/tmp/kameleon_#{@context_name}_bash_history"
21
+ @bash_env_file = "/tmp/kameleon_#{@context_name}_bash_env"
22
+ change_dir_cmd = ""
23
+ if @shell_workdir
24
+ unless @shell_workdir.eql? "/"
25
+ change_dir_cmd = "mkdir -p #{@shell_workdir} &&"
26
+ end
27
+ change_dir_cmd = "#{change_dir_cmd} cd #{@shell_workdir} && "
28
+ end
29
+ @default_bashrc_file = File.join(Kameleon.source_root,
30
+ "contrib",
31
+ "kameleon_bashrc.sh")
32
+ bash_cmd = "bash --rcfile #{@bashrc_file}"
33
+ @shell_cmd = "source #{@default_bashrc_file} 2> /dev/null; "\
34
+ "#{@cmd} -c '#{change_dir_cmd}#{bash_cmd}'"
35
+ @logger.debug("Initialize shell (#{self})")
36
+ # Injecting all variables of the options and assign the variables
37
+ instance_variables.each do |v|
38
+ @logger.debug(" #{v} = #{instance_variable_get(v)}")
39
+ end
40
+ end
41
+
42
+ def start
43
+ @sent_first_cmd = false
44
+ @process, @stdout, @stderr = fork("pipe")
45
+ end
46
+
47
+ def stop
48
+ @process.stop
49
+ end
50
+
51
+ def exited?
52
+ @process.exited?
53
+ end
54
+
55
+ def restart
56
+ stop
57
+ start
58
+ end
59
+
60
+ def send_file(source_path, remote_dest_path, chunk_size=READ_CHUNK_SIZE)
61
+ copy_process, = fork("pipe")
62
+ copy_process.io.stdin << "cat > #{remote_dest_path}\n"
63
+ copy_process.io.stdin.flush
64
+ open(source_path, "rb") do |f|
65
+ begin
66
+ copy_process.io.stdin << f.read(chunk_size)
67
+ end until f.eof?
68
+ end
69
+ copy_process.io.stdin.flush
70
+ copy_process.io.stdin.close
71
+ copy_process.wait
72
+ copy_process.poll_for_exit(EXIT_TIMEOUT)
73
+ end
74
+
75
+ def init_shell_cmd
76
+ bashrc_content = ""
77
+ if File.file?(@default_bashrc_file)
78
+ tpl = ERB.new(File.read(@default_bashrc_file))
79
+ bashrc_content = tpl.result(binding)
80
+ end
81
+ bashrc = Shellwords.escape(bashrc_content)
82
+ shell_cmd = "mkdir -p $(dirname #{@bashrc_file})\n"
83
+ shell_cmd << "echo #{bashrc} > #{@bashrc_file}\n"
84
+ shell_cmd << "source #{@bashrc_file}\n"
85
+ shell_cmd
86
+ end
87
+
88
+ def send_command cmd
89
+ shell_cmd = "#{ ECHO_CMD } -n #{ cmd.begin_err } 1>&2\n"
90
+ shell_cmd << "#{ ECHO_CMD } -n #{ cmd.begin_out }\n"
91
+ unless @sent_first_cmd
92
+ shell_cmd << init_shell_cmd
93
+ @sent_first_cmd = true
94
+ end
95
+ shell_cmd << "KAMELEON_LAST_COMMAND=#{Shellwords.escape(cmd.value)}\n"
96
+ shell_cmd << "( set -o posix ; set ) > #{@bash_env_file}\n"
97
+ shell_cmd << "env | xargs -I {} echo export {} >> #{@bash_env_file}\n"
98
+ shell_cmd << "#{ cmd.value }\nexport __exit_status__=$?\n"
99
+ shell_cmd << "#{ ECHO_CMD } $KAMELEON_LAST_COMMAND >> \"$HISTFILE\"\n"
100
+ shell_cmd << "#{ ECHO_CMD } -n #{ cmd.end_err } 1>&2\n"
101
+ shell_cmd << "#{ ECHO_CMD } -n #{ cmd.end_out }\n"
102
+ @process.io.stdin.puts shell_cmd
103
+ @process.io.stdin.flush
104
+ end
105
+
106
+ def execute(cmd, kwargs = {})
107
+ cmd_obj = Command.new(cmd)
108
+ send_command cmd_obj = Command.new(cmd)
109
+ iodata = {:stderr => { :io => @stderr,
110
+ :name => 'stderr',
111
+ :begin => false,
112
+ :end => false,
113
+ :begin_pat => cmd_obj.begin_err_pat,
114
+ :end_pat => cmd_obj.end_err_pat,
115
+ :redirect => kwargs[:stderr],
116
+ :yield => lambda{|buf| yield(nil, buf)} },
117
+ :stdout => { :io => @stdout,
118
+ :name => 'stdout',
119
+ :begin => false,
120
+ :end => false,
121
+ :begin_pat => cmd_obj.begin_out_pat,
122
+ :end_pat => cmd_obj.end_out_pat,
123
+ :redirect => kwargs[:stdout],
124
+ :yield => lambda{|buf| yield(buf, nil)} }
125
+ }
126
+ while true
127
+ iodata.each do |_, iodat|
128
+ if iodat[:end] and not iodat[:begin]
129
+ raise ShellError, "Cannot read #{iodat[:begin]} from shell"
130
+ end
131
+ end
132
+ if iodata.all? { |k, iodat| iodat[:end] and iodat[:begin]}
133
+ break
134
+ end
135
+ readers = (iodata.map { |_, v| v[:io] unless v[:end] })
136
+ ready = IO.select(readers.compact, nil, nil, 0.1)
137
+ ready ||= [[]]
138
+ readers = ready[0]
139
+ # Check the readers to see if they're ready
140
+ if readers && !readers.empty?
141
+ readers.each do |r|
142
+ # Read from the IO object
143
+ iodat = r == @stdout ? iodata[:stdout] : iodata[:stderr]
144
+ data = read_io(r)
145
+ # We don't need to do anything if the data is empty
146
+ next if data.empty?
147
+ if !iodat[:begin] && (m = iodat[:begin_pat].match(data))
148
+ iodat[:begin] = true
149
+ data = m[1]
150
+ end
151
+ next unless iodat[:begin] and not iodat[:end] # ignore chaff
152
+ if !iodat[:end] && (m = iodat[:end_pat].match(data))
153
+ iodat[:end] = true
154
+ data = m[1]
155
+ end
156
+ next if data.empty?
157
+ if iodat[:redirect]
158
+ iodat[:redirect] << data
159
+ else
160
+ iodat[:yield].call data if block_given?
161
+ end
162
+ end
163
+ end
164
+ end
165
+ iodata = nil
166
+ return get_status
167
+ end
168
+
169
+ def fork_and_wait
170
+ process, = fork("inherit")
171
+ process.wait
172
+ end
173
+
174
+ protected
175
+
176
+ def get_status
177
+ var_name = "__exit_status__"
178
+ @process.io.stdin << "#{ ECHO_CMD } \"#{ var_name }=${#{ var_name }}\"\n"
179
+ @process.io.stdin.flush
180
+ while((line = @stdout.gets))
181
+ if (m = %r/#{ var_name }\s*=\s*(.*)/.match line)
182
+ exit_status = m[1]
183
+ unless exit_status =~ /^\s*\d+\s*$/o
184
+ raise ShellError, "could not determine exit status from <#{ exit_status.inspect }>"
185
+ end
186
+ @exit_status = Integer exit_status
187
+ return @exit_status
188
+ end
189
+ end
190
+ end
191
+
192
+ def read_io(io)
193
+ data = ""
194
+ while true
195
+ begin
196
+ # Do a simple non-blocking read on the IO object
197
+ data << io.read_nonblock(READ_CHUNK_SIZE)
198
+ rescue Exception => e
199
+ breakable = false
200
+ if e.is_a?(EOFError)
201
+ # An `EOFError` means this IO object is done!
202
+ breakable = true
203
+ elsif defined?(IO::WaitReadable) && e.is_a?(IO::WaitReadable)
204
+ breakable = true
205
+ elsif e.is_a?(Errno::EAGAIN)
206
+ breakable = true
207
+ end
208
+ break if breakable
209
+ raise
210
+ end
211
+ end
212
+ data
213
+ end
214
+
215
+ def fork(io)
216
+ command = ["bash", "-c", @shell_cmd]
217
+ @logger.notice("Starting process: #{@cmd.inspect}")
218
+ ChildProcess.posix_spawn = true
219
+ process = ChildProcess.build(*command)
220
+ # Create the pipes so we can read the output in real time as
221
+ # we execute the command.
222
+ if io.eql? "pipe"
223
+ stdout, stdout_writer = IO.pipe
224
+ stderr, stderr_writer = IO.pipe
225
+ process.io.stdout = stdout_writer
226
+ process.io.stderr = stderr_writer
227
+ # sets up pipe so process.io.stdin will be available after .start
228
+ process.duplex = true
229
+ elsif io.eql? "inherit"
230
+ process.io.inherit!
231
+ end
232
+
233
+ # Start the process
234
+ begin
235
+ process.cwd = @local_workdir
236
+ process.start
237
+ # Wait to child starting
238
+ sleep(0.2)
239
+ rescue ChildProcess::LaunchError => e
240
+ # Raise our own version of the error
241
+ raise ShellError, "Cannot launch #{command.inspect}: #{e.message}"
242
+ end
243
+ if io.eql? "pipe"
244
+ # Make sure the stdin does not buffer
245
+ process.io.stdin.sync = true
246
+ stdout_writer.close()
247
+ stderr_writer.close()
248
+ return process, stdout, stderr
249
+ else
250
+ return process, $stdout, $stderr
251
+ end
252
+ end
253
+
254
+ class Command
255
+ class << self
256
+ def counter; @counter ||= 0; end
257
+ def counter= n; @counter = n; end
258
+ end
259
+ attr :value
260
+ attr :number
261
+ attr :id
262
+ attr :slug
263
+ attr :begin_out
264
+ attr :begin_out_pat
265
+ attr :end_out
266
+ attr :end_out_pat
267
+ attr :begin_err
268
+ attr :begin_err_pat
269
+ attr :end_err
270
+ attr :end_err_pat
271
+
272
+ def initialize(raw)
273
+ @value = raw.to_s.strip
274
+ @number = self.class.counter
275
+ @slug = Kameleon::Utils.generate_slug(@value)[0...30]
276
+ @id = "%d_%d_%d" % [$$, @number, rand(Time.now.usec)]
277
+ @begin_out = "__CMD_OUT_%s_BEGIN__" % @id
278
+ @end_out = "__CMD_OUT_%s_END__" % @id
279
+ @begin_out_pat = %r/#{ Regexp.escape(@begin_out) }(.*)/m
280
+ @end_out_pat = %r/(.*)#{ Regexp.escape(@end_out) }/m
281
+ @begin_err = "__CMD_ERR_%s_BEGIN__" % @id
282
+ @end_err = "__CMD_ERR_%s_END__" % @id
283
+ @begin_err_pat = %r/#{ Regexp.escape(@begin_err) }(.*)/m
284
+ @end_err_pat = %r/(.*)#{ Regexp.escape(@end_err) }/m
285
+ self.class.counter += 1
286
+ end
287
+ end
288
+
289
+ end
290
+ end
@@ -0,0 +1,213 @@
1
+ module Kameleon
2
+
3
+ class Command
4
+ attr_accessor :string_cmd, :microstep_name
5
+
6
+ def initialize(yaml_cmd, microstep_name)
7
+ @string_cmd = YAML.dump(yaml_cmd).gsub("---", "").strip
8
+ @microstep_name = microstep_name
9
+ end
10
+
11
+ def resolve!
12
+ key
13
+ value
14
+ end
15
+
16
+ def key
17
+ if @key.nil?
18
+ @key = YAML.load(@string_cmd).keys.first
19
+ end
20
+ @key
21
+ rescue
22
+ lines = @string_cmd.split( /\r?\n/ ).map {|l| "> #{l}" }
23
+ fail RecipeError, "Syntax error for microstep #{@microstep_name} : \n"\
24
+ "#{ lines.join "\n"}"
25
+ end
26
+
27
+ def value
28
+ if @value.nil?
29
+ object = YAML.load(@string_cmd)
30
+ if object.kind_of? Command
31
+ @value = object
32
+ else
33
+ raise RecipeError unless object.kind_of? Hash
34
+ raise RecipeError unless object.keys.count == 1
35
+ _, val = object.first
36
+ unless val.kind_of?(Array)
37
+ val = val.to_s
38
+ end
39
+ # Nested commands
40
+ if val.kind_of? Array
41
+ val = val.map { |item| Command.new(item, @microstep_name) }
42
+ end
43
+ @value = val
44
+ end
45
+ end
46
+ @value
47
+ rescue
48
+ lines = YAML.dump(object).gsub("---", "").strip
49
+ lines = lines.split( /\r?\n/ ).map {|l| "> #{l}" }
50
+ fail RecipeError, "Syntax error for microstep #{@microstep_name} : \n"\
51
+ "#{ lines.join "\n"}"
52
+ end
53
+
54
+ def to_array
55
+ if value.kind_of? Array
56
+ map = value.map { |val| val.to_array }
57
+ return { key => map }
58
+ else
59
+ return { key => value }
60
+ end
61
+ end
62
+
63
+ def gsub!(arg1, arg2)
64
+ if value.kind_of? Array
65
+ value.each { |cmd| cmd.gsub!(arg1, arg2) }
66
+ else
67
+ @value.gsub!(arg1, arg2)
68
+ end
69
+ @string_cmd = YAML.dump(to_array).gsub("---", "").strip
70
+ end
71
+
72
+ end
73
+
74
+ class Microstep
75
+ attr_accessor :commands, :name, :identifier, :slug, :in_cache,
76
+ :on_checkpoint, :order
77
+
78
+ def initialize(string_or_hash)
79
+ @identifier = nil
80
+ @in_cache = false
81
+ @on_checkpoint = "use_cache"
82
+ @commands = []
83
+ @name, cmd_list = string_or_hash.first
84
+ cmd_list.each do |cmd_hash|
85
+ if cmd_hash.kind_of? Command
86
+ @commands.push cmd_hash
87
+ else
88
+ if cmd_hash.kind_of?(Hash) && cmd_hash.keys.first == "on_checkpoint"
89
+ @on_checkpoint = cmd_hash["on_checkpoint"]
90
+ else
91
+ @commands.push Command.new(cmd_hash, @name)
92
+ end
93
+ end
94
+ end
95
+ rescue
96
+ fail RecipeError, "Syntax error for microstep #{name}"
97
+ end
98
+
99
+ def resolve!
100
+ @commands.each {|cmd| cmd.resolve! }
101
+ end
102
+
103
+ def gsub!(arg1, arg2)
104
+ @commands.each {|cmd| cmd.gsub!(arg1, arg2) }
105
+ end
106
+
107
+ def unshift(cmd_list)
108
+ cmd_list.reverse.each {|cmd| @commands.unshift cmd}
109
+ end
110
+
111
+ def push(cmd)
112
+ @commands.push cmd
113
+ end
114
+
115
+ def calculate_identifier(salt)
116
+ commands_str = @commands.map { |cmd| cmd.string_cmd.to_s }
117
+ content_id = commands_str.join(' ') + salt
118
+ @identifier = "#{ Digest::SHA1.hexdigest content_id }"[0..11]
119
+ end
120
+
121
+ def to_array
122
+ microstep_array = @commands.map do |cmd|
123
+ cmd.to_array
124
+ end
125
+ return microstep_array
126
+ end
127
+
128
+ end
129
+
130
+ class Macrostep
131
+ attr_accessor :name, :clean_microsteps, :init_microsteps, :microsteps,
132
+ :path, :variables
133
+
134
+ def initialize(name, microsteps, variables, path)
135
+ @name = name
136
+ @variables = variables
137
+ @path = path
138
+ @microsteps = microsteps
139
+ @clean_microsteps = []
140
+ @init_microsteps = []
141
+ end
142
+
143
+ def resolve_variables!(global)
144
+ # Resolve dynamically-defined variables !!
145
+ tmp_resolved_vars = {}
146
+ @variables.clone.each do |key, value|
147
+ yaml_vars = { key => value }.to_yaml.chomp
148
+ yaml_resolved = Utils.resolve_vars(yaml_vars,
149
+ @path,
150
+ tmp_resolved_vars.merge(global))
151
+ tmp_resolved_vars.merge! YAML.load(yaml_resolved.chomp)
152
+ end
153
+ @variables.merge! tmp_resolved_vars
154
+ @microsteps.each do |m|
155
+ m.commands.each do |cmd|
156
+ cmd.string_cmd = Utils.resolve_vars(cmd.string_cmd,
157
+ @path,
158
+ global.merge(@variables))
159
+ end
160
+ end
161
+ end
162
+
163
+ def sequence
164
+ @init_microsteps.each { |m| yield m }
165
+ @microsteps.each { |m| yield m }
166
+ @clean_microsteps.each { |m| yield m }
167
+ end
168
+
169
+ def to_array
170
+ macrostep_array = []
171
+ @variables.each do |k, v|
172
+ macrostep_array.push({ k => v })
173
+ end
174
+ sequence do |microstep|
175
+ macrostep_array.push({ microstep.name => microstep.to_array })
176
+ end
177
+ return macrostep_array
178
+ end
179
+
180
+ end
181
+
182
+ class Section
183
+ attr_accessor :name, :clean_macrostep, :init_macrostep, :macrosteps
184
+
185
+ def initialize(name)
186
+ @name = name
187
+ @clean_macrostep = Macrostep.new("_clean_#{name}", [], {}, nil)
188
+ @init_macrostep = Macrostep.new("_init_#{name}", [], {}, nil)
189
+ @macrosteps = []
190
+ end
191
+
192
+ def sequence
193
+ yield @init_macrostep
194
+ @macrosteps.each { |m| yield m }
195
+ yield @clean_macrostep
196
+ end
197
+
198
+ def to_array
199
+ section_array = []
200
+ sequence do |macrostep|
201
+ macrostep.sequence do |microstep|
202
+ hash = {
203
+ "identifier" => microstep.identifier.to_s,
204
+ "cmds" => microstep.to_array
205
+ }
206
+ section_array.push({ microstep.slug => hash })
207
+ end
208
+ end
209
+ return section_array
210
+ end
211
+ end
212
+
213
+ end