stork 0.1.0.pre

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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +10 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +75 -0
  8. data/LICENSE +201 -0
  9. data/README.md +61 -0
  10. data/Rakefile +25 -0
  11. data/bin/stork +97 -0
  12. data/bin/storkctl +62 -0
  13. data/lib/stork/builder.rb +104 -0
  14. data/lib/stork/client/plugins/host_install.rb +19 -0
  15. data/lib/stork/client/plugins/host_list.rb +12 -0
  16. data/lib/stork/client/plugins/host_localboot.rb +19 -0
  17. data/lib/stork/client/plugins/host_show.rb +19 -0
  18. data/lib/stork/collection/base.rb +45 -0
  19. data/lib/stork/collection/chefs.rb +6 -0
  20. data/lib/stork/collection/distros.rb +6 -0
  21. data/lib/stork/collection/hosts.rb +6 -0
  22. data/lib/stork/collection/layouts.rb +6 -0
  23. data/lib/stork/collection/networks.rb +6 -0
  24. data/lib/stork/collection/snippets.rb +6 -0
  25. data/lib/stork/collection/templates.rb +6 -0
  26. data/lib/stork/collections.rb +38 -0
  27. data/lib/stork/configuration.rb +98 -0
  28. data/lib/stork/deploy/command.rb +59 -0
  29. data/lib/stork/deploy/install_script.rb +39 -0
  30. data/lib/stork/deploy/kickstart_binding.rb +222 -0
  31. data/lib/stork/deploy/section.rb +30 -0
  32. data/lib/stork/deploy/snippet_binding.rb +49 -0
  33. data/lib/stork/plugin.rb +54 -0
  34. data/lib/stork/pxe.rb +70 -0
  35. data/lib/stork/resource/base.rb +42 -0
  36. data/lib/stork/resource/chef.rb +81 -0
  37. data/lib/stork/resource/delegator.rb +50 -0
  38. data/lib/stork/resource/distro.rb +30 -0
  39. data/lib/stork/resource/firewall.rb +47 -0
  40. data/lib/stork/resource/host.rb +202 -0
  41. data/lib/stork/resource/interface.rb +125 -0
  42. data/lib/stork/resource/layout.rb +50 -0
  43. data/lib/stork/resource/logical_volume.rb +49 -0
  44. data/lib/stork/resource/network.rb +36 -0
  45. data/lib/stork/resource/partition.rb +47 -0
  46. data/lib/stork/resource/password.rb +26 -0
  47. data/lib/stork/resource/repo.rb +29 -0
  48. data/lib/stork/resource/snippet.rb +20 -0
  49. data/lib/stork/resource/template.rb +24 -0
  50. data/lib/stork/resource/timezone.rb +31 -0
  51. data/lib/stork/resource/volume_group.rb +31 -0
  52. data/lib/stork/resources.rb +17 -0
  53. data/lib/stork/server/application.rb +127 -0
  54. data/lib/stork/server/control.rb +38 -0
  55. data/lib/stork/version.rb +4 -0
  56. data/lib/stork.rb +30 -0
  57. data/specs/builder_spec.rb +14 -0
  58. data/specs/collections_spec.rb +72 -0
  59. data/specs/configuration_spec.rb +151 -0
  60. data/specs/keys/snakeoil-root.pem +27 -0
  61. data/specs/keys/snakeoil-validation.pem +27 -0
  62. data/specs/kickstart_spec.rb +36 -0
  63. data/specs/pxe_spec.rb +57 -0
  64. data/specs/resource_chef_spec.rb +122 -0
  65. data/specs/resource_distro_spec.rb +38 -0
  66. data/specs/resource_firewall_spec.rb +71 -0
  67. data/specs/resource_host_spec.rb +323 -0
  68. data/specs/resource_interface_spec.rb +158 -0
  69. data/specs/resource_layout_spec.rb +78 -0
  70. data/specs/resource_logical_volume_spec.rb +44 -0
  71. data/specs/resource_network_spec.rb +45 -0
  72. data/specs/resource_partition_spec.rb +51 -0
  73. data/specs/resource_password_spec.rb +10 -0
  74. data/specs/resource_repo_spec.rb +43 -0
  75. data/specs/resource_snippet_spec.rb +10 -0
  76. data/specs/resource_template_spec.rb +5 -0
  77. data/specs/resource_timezone_spec.rb +5 -0
  78. data/specs/resource_volume_group_spec.rb +23 -0
  79. data/specs/scripts/kssetup.sh +24 -0
  80. data/specs/scripts/ksvalidate.sh +21 -0
  81. data/specs/server_spec.rb +84 -0
  82. data/specs/spec_helper.rb +33 -0
  83. data/specs/stork/authorized_keys +1 -0
  84. data/specs/stork/bundles/chefs/default.rb +9 -0
  85. data/specs/stork/bundles/distros/centos.rb +5 -0
  86. data/specs/stork/bundles/hosts/example.org.rb +66 -0
  87. data/specs/stork/bundles/layouts/home.rb +33 -0
  88. data/specs/stork/bundles/networks/local.rb +6 -0
  89. data/specs/stork/bundles/networks/org.rb +7 -0
  90. data/specs/stork/bundles/snippets/authorized-keys.erb +10 -0
  91. data/specs/stork/bundles/snippets/chef-bootstrap.erb +104 -0
  92. data/specs/stork/bundles/snippets/chef-reconfigure.erb +22 -0
  93. data/specs/stork/bundles/snippets/etc-hosts.erb +6 -0
  94. data/specs/stork/bundles/snippets/network-config.erb +7 -0
  95. data/specs/stork/bundles/snippets/noop.erb +1 -0
  96. data/specs/stork/bundles/snippets/notify.erb +2 -0
  97. data/specs/stork/bundles/snippets/ntp.erb +2 -0
  98. data/specs/stork/bundles/snippets/resolv-conf.erb +6 -0
  99. data/specs/stork/bundles/snippets/setup.erb +1 -0
  100. data/specs/stork/bundles/templates/default.ks.erb +25 -0
  101. data/specs/stork/config.rb +11 -0
  102. data/stork.gemspec +35 -0
  103. metadata +329 -0
@@ -0,0 +1,19 @@
1
+ module HostInstallPlugin
2
+ class HostInstall < Stork::Plugin
3
+ banner "stork host install HOST (options)"
4
+
5
+ def run
6
+ host = args.shift
7
+ raise SyntaxError, "A host must be supplied" if host.nil?
8
+
9
+ response = RestClient.get "#{stork}/host/#{host}/install"
10
+ if response.code == 200
11
+ puts "OK"
12
+ else
13
+ data = JSON.parse(response)
14
+ puts "Error: #{data['message']}"
15
+ exit 1
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module HostListPlugin
2
+ class HostList < Stork::Plugin
3
+ banner "stork host list (options)"
4
+
5
+ def run
6
+ data = fetch('/hosts')
7
+ data['hosts'].sort.each do |host|
8
+ puts host
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module HostLocalbootPlugin
2
+ class HostLocalboot < Stork::Plugin
3
+ banner "stork host localboot HOST (options)"
4
+
5
+ def run
6
+ host = args.shift
7
+ raise SyntaxError, "A host must be supplied" if host.nil?
8
+
9
+ response = RestClient.get "#{stork}/host/#{host}/installed"
10
+ if response.code == 200
11
+ puts "OK"
12
+ else
13
+ data = JSON.parse(response)
14
+ puts "Error: #{data['message']}"
15
+ exit 1
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module HostShowPlugin
2
+ class HostShow < Stork::Plugin
3
+ banner "stork host show HOST (options)"
4
+
5
+ def run
6
+ host = args.shift
7
+ raise SyntaxError, "A host must be supplied" if host.nil?
8
+ data = fetch("/host/#{host}")
9
+ show('name', data)
10
+ show('distro', data)
11
+ show('template', data)
12
+ show('chef', data)
13
+ show('run_list', data)
14
+ show('pre_snippets', data)
15
+ show('post_snippets', data)
16
+ show('packages', data)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ module Stork
2
+ module Collection
3
+ class Base
4
+ include Enumerable
5
+
6
+ def initialize(*objs)
7
+ @objects = objs
8
+ end
9
+
10
+ def size
11
+ @objects.size
12
+ end
13
+
14
+ def add(*objs)
15
+ objs.each do |obj|
16
+ validate(obj)
17
+ @objects << obj
18
+ end
19
+ end
20
+
21
+ def each(&block)
22
+ @objects.each do |obj|
23
+ block.call(obj)
24
+ end
25
+ end
26
+
27
+ def validate(obj)
28
+ true
29
+ end
30
+
31
+ # Need to check for nil and raise
32
+ def get(id)
33
+ find { |obj| obj.id == id }
34
+ end
35
+
36
+ def hashify
37
+ objs = []
38
+ @objects.each do |obj|
39
+ objs << obj.name
40
+ end
41
+ objs
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Chefs < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Distros < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Hosts < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Layouts < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Networks < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Snippets < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Stork
2
+ module Collection
3
+ class Templates < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,38 @@
1
+ require 'stork/collection/base'
2
+ require 'stork/collection/chefs'
3
+ require 'stork/collection/distros'
4
+ require 'stork/collection/hosts'
5
+ require 'stork/collection/layouts'
6
+ require 'stork/collection/networks'
7
+ require 'stork/collection/snippets'
8
+ require 'stork/collection/templates'
9
+
10
+ module Stork
11
+ class Collections
12
+ attr_reader :hosts
13
+ attr_reader :layouts
14
+ attr_reader :networks
15
+ attr_reader :chefs
16
+ attr_reader :distros
17
+ attr_reader :snippets
18
+ attr_reader :templates
19
+
20
+ def initialize
21
+ @hosts = Stork::Collection::Hosts.new
22
+ @layouts = Stork::Collection::Layouts.new
23
+ @networks = Stork::Collection::Networks.new
24
+ @chefs = Stork::Collection::Chefs.new
25
+ @distros = Stork::Collection::Distros.new
26
+ @snippets = Stork::Collection::Snippets.new
27
+ @templates = Stork::Collection::Templates.new
28
+ end
29
+
30
+ alias_method :network, :networks
31
+ alias_method :layout, :layouts
32
+ alias_method :host, :hosts
33
+ alias_method :chef, :chefs
34
+ alias_method :distro, :distros
35
+ alias_method :snippet, :snippets
36
+ alias_method :template, :templates
37
+ end
38
+ end
@@ -0,0 +1,98 @@
1
+ module Stork
2
+ class Configuration
3
+ attr_accessor :path
4
+ attr_accessor :bundle_path
5
+ attr_accessor :authorized_keys_file
6
+ attr_accessor :pxe_path
7
+ attr_accessor :log_file
8
+ attr_accessor :pid_file
9
+ attr_accessor :server
10
+ attr_accessor :port
11
+ attr_accessor :bind
12
+ attr_accessor :timezone
13
+
14
+ def initialize
15
+ @path = '/etc/stork'
16
+ @authorized_keys_file = path + '/authorized_keys'
17
+ @bundle_path = path + '/bundles'
18
+
19
+ @pxe_path = '/var/lib/tftpboot/pxelinux.cfg'
20
+ @log_file = '/var/log/stork.log'
21
+ @pid_file = '/var/run/stork.pid'
22
+
23
+ @server = 'localhost'
24
+ @port = 9293
25
+ @bind = '0.0.0.0'
26
+ @timezone = 'America/Los_Angeles'
27
+ end
28
+
29
+ def hosts_path
30
+ bundle_path + '/hosts'
31
+ end
32
+
33
+ def snippets_path
34
+ bundle_path + '/snippets'
35
+ end
36
+
37
+ def layouts_path
38
+ bundle_path + '/layouts'
39
+ end
40
+
41
+ def networks_path
42
+ bundle_path + '/networks'
43
+ end
44
+
45
+ def distros_path
46
+ bundle_path + '/distros'
47
+ end
48
+
49
+ def templates_path
50
+ bundle_path + '/templates'
51
+ end
52
+
53
+ def chefs_path
54
+ bundle_path + '/chefs'
55
+ end
56
+
57
+ def to_file
58
+ <<-EOS
59
+ # Stork configuration file"
60
+ path "#{path}"
61
+ bundle_path "#{bundle_path}"
62
+ authorized_keys_file "#{authorized_keys_file}"
63
+ pxe_path "#{pxe_path}"
64
+ log_file "#{log_file}"
65
+ pid_file "#{pid_file}"
66
+ server "#{server}"
67
+ port #{port}
68
+ bind "#{bind}"
69
+ timezone "#{timezone}"
70
+ EOS
71
+ end
72
+
73
+ def self.from_file(filename)
74
+ find_or_create(filename)
75
+ end
76
+
77
+ def self.find_or_create(filename)
78
+ config = new
79
+ if File.exist?(filename)
80
+ delegator = ConfigDelegator.new(config)
81
+ delegator.instance_eval(File.read(filename), filename)
82
+ else
83
+ File.open(filename, 'w') { |file| file.write(config.to_file) }
84
+ end
85
+ config
86
+ end
87
+
88
+ class ConfigDelegator
89
+ def initialize(obj)
90
+ @delegated = obj
91
+ end
92
+
93
+ def method_missing(meth, *args)
94
+ @delegated.send("#{meth}=", *args)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,59 @@
1
+ module Stork
2
+ module Deploy
3
+ class Command
4
+ def initialize(name, value)
5
+ @name = name
6
+ @value = value
7
+ @options = Array.new
8
+ end
9
+
10
+ def to_s
11
+ command_str = @name
12
+ command_str << " #{@value}" if @value
13
+ if @options.empty?
14
+ command_str << "\n"
15
+ else
16
+ command_str << " #{@options.join(' ')}\n"
17
+ end
18
+ end
19
+
20
+ def boolean(opt, value)
21
+ @options << "--#{opt}" if value
22
+ end
23
+
24
+ def option(opt, value)
25
+ if value.is_a?(Array)
26
+ @options << "--#{opt}=#{value.join(',')}" unless value.empty?
27
+ else
28
+ @options << "--#{opt}=#{value}" if value
29
+ end
30
+ end
31
+
32
+ def yes_no(opt, value)
33
+ @options << "--#{opt}=#{value ? 'yes' : 'no'}" if value
34
+ end
35
+
36
+ def either_or(opt, alt, value)
37
+ @options << "--#{value ? opt : alt}"
38
+ end
39
+
40
+ def multi(opt, values)
41
+ values.each do |value|
42
+ @options << "--#{opt}=#{value}"
43
+ end
44
+ end
45
+
46
+ def value(value)
47
+ @value = value
48
+ end
49
+
50
+ def self.create(name, value=nil, &block)
51
+ command = new(name, value)
52
+ yield command if block_given?
53
+ # puts "COMM: #{command.inspect}"
54
+ # puts "COMS: #{command.to_s}"
55
+ command.to_s
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ module Stork
2
+ module Deploy
3
+ class InstallScript
4
+ attr_reader :type, :host, :configuration
5
+
6
+ def initialize(host, type=:kickstart)
7
+ @host = host
8
+ @type = type
9
+ @configuration = host.configuration
10
+ end
11
+
12
+ def render
13
+ renderer = ERB.new(host.template.content)
14
+ binding = InstallScriptBinding.new(type, host).get_binding
15
+ renderer.result(binding)
16
+ end
17
+
18
+ require 'forwardable'
19
+ class InstallScriptBinding
20
+ extend Forwardable
21
+ def_delegators :@builder, :url, :network, :password, :firewall,
22
+ :timezone, :selinux, :layout, :partitions, :repos, :volume_groups,
23
+ :packages, :pre_snippets, :post_snippets
24
+ attr_reader :host, :configuration
25
+
26
+ def initialize(type, host)
27
+ # puts Stork::Deploy::Commands.constants.inspect
28
+ @builder = Stork::Deploy.const_get("#{type.to_s.capitalize}Binding").new(host)
29
+ @configuration = configuration
30
+ @host = host.configuration
31
+ end
32
+
33
+ def get_binding
34
+ binding
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,222 @@
1
+ module Stork
2
+ module Deploy
3
+ class KickstartBinding
4
+ attr_reader :host, :configuration
5
+
6
+ def initialize(host)
7
+ @host = host
8
+ @configuration = host.configuration
9
+ end
10
+
11
+ def url
12
+ Command.create 'url' do |c|
13
+ c.option 'url', host.distro.url
14
+ end
15
+ end
16
+
17
+ def network
18
+ commands = []
19
+ host.interfaces.each do |i|
20
+ commands << interface(i)
21
+ end
22
+ commands.join
23
+ end
24
+
25
+ def interface(interface)
26
+ Command.create 'network' do |c|
27
+ c.option 'device', interface.device
28
+ c.option 'bootproto', interface.bootproto
29
+ if interface.static?
30
+ c.option 'ip', interface.ip
31
+ c.option 'netmask', interface.netmask
32
+ c.option 'gateway', interface.gateway
33
+ c.option 'nameserver', interface.nameservers
34
+ end
35
+ c.yes_no 'onboot', interface.onboot
36
+ c.boolean 'noipv4', !interface.ipv4
37
+ c.boolean 'noipv6', !interface.ipv6
38
+ c.boolean 'nodefroute', !interface.defroute
39
+ c.boolean 'nodns', !interface.dns
40
+ c.option 'ethtool', interface.ethtool
41
+ c.option 'mtu', interface.mtu
42
+ end
43
+ end
44
+
45
+ def password
46
+ Command.create 'rootpw' do |c|
47
+ c.value host.password.value
48
+
49
+ if host.password.locked?
50
+ c.boolean 'lock', true
51
+ else
52
+ c.either_or 'iscrypted', 'plaintext', host.password.encrypted
53
+ end
54
+ end
55
+ end
56
+
57
+ def firewall
58
+ fw = host.firewall
59
+ Command.create 'firewall' do |c|
60
+ c.either_or 'enabled', 'disabled', fw.enabled
61
+ if fw.enabled
62
+ c.boolean 'ssh', fw.ssh
63
+ c.boolean 'telnet', fw.telnet
64
+ c.boolean 'smtp', fw.smtp
65
+ c.boolean 'http', fw.http
66
+ c.boolean 'ftp', fw.ftp
67
+ c.option 'port', fw.allowed_ports
68
+ c.multi 'trust', fw.trusted_devices
69
+ end
70
+ end
71
+ end
72
+
73
+ def timezone
74
+ tz = host.timezone
75
+ Command.create 'timezone' do |c|
76
+ c.value tz.zone
77
+ # c.boolean 'utc', tz.utc
78
+ # c.boolean 'ntp', tz.ntp
79
+ # c.option 'ntpservers', tz.ntpservers
80
+ end
81
+ end
82
+
83
+ def selinux
84
+ Command.create 'selinux' do |c|
85
+ c.boolean host.selinux, true
86
+ end
87
+ end
88
+
89
+ def layout
90
+ commands = []
91
+ commands << bootloader
92
+ commands << zerombr
93
+ commands << clearpart
94
+ commands << partitions
95
+ commands << volume_groups
96
+ commands.join
97
+ end
98
+
99
+ def bootloader
100
+ Command.create 'bootloader' do |c|
101
+ c.option 'location', 'mbr'
102
+ end
103
+ end
104
+
105
+ def zerombr
106
+ Command.create 'zerombr' if host.layout.zerombr
107
+ end
108
+
109
+ def clearpart
110
+ Command.create 'clearpart' do |c|
111
+ c.boolean 'all', true
112
+ c.boolean 'initlabel', true
113
+ end if host.layout.clearpart
114
+ end
115
+
116
+ def repos
117
+ commands = []
118
+ host.repos.each do |repo|
119
+ command = Command.create 'repo' do |c|
120
+ c.option 'name', repo.name
121
+ c.option 'baseurl', repo.baseurl
122
+ c.option 'mirrorlist', repo.mirrorlist
123
+ end
124
+ commands << command
125
+ end
126
+ commands.join
127
+ end
128
+
129
+ def volume_groups
130
+ commands = []
131
+ host.layout.volume_groups.each do |vg|
132
+ a = volume_group(vg)
133
+ commands << a
134
+ commands << logical_volumes(vg)
135
+ end
136
+ commands.join
137
+ end
138
+
139
+ def volume_group(vg)
140
+ Command.create 'volgroup' do |c|
141
+ c.value "#{vg.name} #{vg.partition}"
142
+ end
143
+ end
144
+
145
+ def logical_volumes(volume_group)
146
+ commands = []
147
+ volume_group.logical_volumes.each do |lv|
148
+ commands << logical_volume(volume_group, lv)
149
+ end
150
+ commands.join
151
+ end
152
+
153
+ def logical_volume(vg, lv)
154
+ Command.create 'logvol' do |c|
155
+ c.value lv.path
156
+ c.option 'vgname', vg.name
157
+ c.option 'name', lv.name
158
+ filesystem_options(lv, c)
159
+ end
160
+ end
161
+
162
+ def partitions
163
+ commands = []
164
+ host.layout.partitions.each do |part|
165
+ commands << partition(part)
166
+ end
167
+ commands.join
168
+ end
169
+
170
+ def partition(part)
171
+ Command.create 'part' do |c|
172
+ c.value part.path
173
+ c.boolean 'asprimary', part.primary
174
+ filesystem_options(part, c)
175
+ end
176
+ end
177
+
178
+ def filesystem_options(part_or_lv, command)
179
+ command.option 'fstype', part_or_lv.type
180
+ if part_or_lv.recommended
181
+ command.boolean 'recommended', true
182
+ else
183
+ command.option 'size', part_or_lv.size
184
+ command.boolean 'grow', part_or_lv.grow
185
+ end
186
+ end
187
+
188
+ def packages
189
+ Section.create 'packages' do |s|
190
+ s.content host.packages.join("\n")
191
+ end
192
+ end
193
+
194
+ def pre_snippets
195
+ Section.create 'pre' do |s|
196
+ s.content render_snippets(host.pre_snippets)
197
+ end
198
+ end
199
+
200
+ def post_snippets
201
+ Section.create 'post', log: '/root/midwife-post.log' do |s|
202
+ s.content render_snippets(host.post_snippets)
203
+ end
204
+ end
205
+
206
+ private
207
+
208
+ def render_snippets(snippets)
209
+ lines = []
210
+ snippets.each do |snippet|
211
+ # Render me!!!
212
+ renderer = ERB.new(snippet.content, nil, '-')
213
+ lines << renderer.result(
214
+ Stork::Deploy::SnippetBinding.new(
215
+ @configuration, host).get_binding
216
+ )
217
+ end
218
+ lines.join("\n")
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,30 @@
1
+ module Stork
2
+ module Deploy
3
+ class Section
4
+ def initialize(name, options)
5
+ @name = name
6
+ @options = options
7
+ @contents = Array.new
8
+ end
9
+
10
+ def to_s
11
+ str = "%#{@name}"
12
+ @options.each { |key, value| str += " --#{key}=#{value}" }
13
+ str += "\n"
14
+ str += @contents.join("\n")
15
+ str += "\n%end"
16
+ str
17
+ end
18
+
19
+ def content(content)
20
+ @contents << content
21
+ end
22
+
23
+ def self.create(name, opts={}, &block)
24
+ section = new(name, opts)
25
+ yield section if block_given?
26
+ section.to_s
27
+ end
28
+ end
29
+ end
30
+ end