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