asbestos 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +461 -0
- data/Rakefile +1 -0
- data/asbestos.gemspec +26 -0
- data/bin/asbestos +112 -0
- data/examples/0_simple.rb +5 -0
- data/examples/10_kitchen_sink.rb +72 -0
- data/examples/1_two_hosts.rb +18 -0
- data/examples/2_accept_from_many.rb +19 -0
- data/examples/3_groups.rb +39 -0
- data/examples/4_host_templates.rb +29 -0
- data/examples/5_static_addresses.rb +7 -0
- data/examples/6_interface_addresses.rb +19 -0
- data/examples/7_services.rb +9 -0
- data/examples/8_rule_sets.rb +37 -0
- data/examples/9_literal_commands.rb +8 -0
- data/lib/asbestos.rb +108 -0
- data/lib/asbestos/address.rb +8 -0
- data/lib/asbestos/dsl.rb +40 -0
- data/lib/asbestos/firewalls/iptables.rb +127 -0
- data/lib/asbestos/host.rb +244 -0
- data/lib/asbestos/host_template.rb +15 -0
- data/lib/asbestos/metadata.rb +4 -0
- data/lib/asbestos/rule_set.rb +131 -0
- data/lib/asbestos/rule_sets/accept_from_self.rb +19 -0
- data/lib/asbestos/rule_sets/allow_related_established.rb +5 -0
- data/lib/asbestos/rule_sets/icmp_protection.rb +28 -0
- data/lib/asbestos/rule_sets/sanity_check.rb +41 -0
- data/lib/asbestos/service.rb +86 -0
- data/lib/asbestos/services/chef.rb +4 -0
- data/lib/asbestos/services/cube.rb +14 -0
- data/lib/asbestos/services/http.rb +8 -0
- data/lib/asbestos/services/memcached.rb +4 -0
- data/lib/asbestos/services/mongodb.rb +28 -0
- data/lib/asbestos/services/monit.rb +4 -0
- data/lib/asbestos/services/mysql.rb +4 -0
- data/lib/asbestos/services/nfs.rb +5 -0
- data/lib/asbestos/services/redis.rb +4 -0
- data/lib/asbestos/services/ssh.rb +4 -0
- data/spec/asbestos/address_spec.rb +25 -0
- data/spec/asbestos/firewalls/iptables_spec.rb +179 -0
- data/spec/asbestos/host_spec.rb +173 -0
- data/spec/asbestos/host_template_spec.rb +32 -0
- data/spec/asbestos/rule_set_spec.rb +55 -0
- data/spec/asbestos/service_spec.rb +60 -0
- data/spec/spec_helper.rb +20 -0
- metadata +159 -0
data/lib/asbestos/dsl.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
def host_template(name, &block)
|
3
|
+
name = name.to_sym
|
4
|
+
Asbestos::HostTemplate.new(name, block).tap do |host_template|
|
5
|
+
|
6
|
+
#
|
7
|
+
# Calling define_method wont let you define block parameters,
|
8
|
+
# but doing it this way will
|
9
|
+
#
|
10
|
+
Object.send(:define_method, name) do |host_name, &block|
|
11
|
+
host(host_name, &host_template.template).tap do |h|
|
12
|
+
h.instance_eval &block if block
|
13
|
+
h.template = name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def host(name, &block)
|
21
|
+
Asbestos::Host.new(name.to_sym).tap do |h|
|
22
|
+
h.instance_eval &block if block_given?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def rule_set(name, &template)
|
27
|
+
Asbestos::RuleSet[name.to_sym] = template
|
28
|
+
end
|
29
|
+
|
30
|
+
def service(name, &template)
|
31
|
+
Asbestos::Service[name.to_sym] = template
|
32
|
+
end
|
33
|
+
|
34
|
+
def address(name, address)
|
35
|
+
Asbestos::Address[name] = [*address]
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# For referencing lazy hosts in the dsl without prepending "Asbestos::"
|
40
|
+
Host = Asbestos::Host
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Asbestos::Firewall
|
2
|
+
module IPTables
|
3
|
+
|
4
|
+
def self.preamble(host)
|
5
|
+
[ "# Generated by Asbestos at #{Time.now.utc} for #{host.name}",
|
6
|
+
"# #{Asbestos::HOMEPAGE}",
|
7
|
+
"*filter"
|
8
|
+
] +
|
9
|
+
host.chains.collect do |name, default_action|
|
10
|
+
chain name, default_action
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def self.chain(name, default_action)
|
16
|
+
default_action = '-' if default_action == :none
|
17
|
+
|
18
|
+
":#{name.upcase} #{default_action.upcase} [0:0]"
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def self.open_port(interfaces, port, protocol, comment, remote_address = nil)
|
23
|
+
if interfaces
|
24
|
+
interfaces.collect do |interface|
|
25
|
+
accept :state => :new,
|
26
|
+
:protocol => protocol,
|
27
|
+
:port => port,
|
28
|
+
:comment => comment,
|
29
|
+
:interface => interface,
|
30
|
+
:remote_address => remote_address
|
31
|
+
end
|
32
|
+
else
|
33
|
+
accept :state => :new,
|
34
|
+
:protocol => protocol,
|
35
|
+
:port => port,
|
36
|
+
:comment => comment,
|
37
|
+
:remote_address => remote_address
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# TODO: Use iptables' long options here for clarity?
|
43
|
+
#
|
44
|
+
def self.rule(args)
|
45
|
+
Array.new.tap do |r|
|
46
|
+
chain = \
|
47
|
+
if args[:chain]
|
48
|
+
args[:chain].to_s.upcase
|
49
|
+
else
|
50
|
+
'INPUT'
|
51
|
+
end
|
52
|
+
r << "-A #{chain}"
|
53
|
+
|
54
|
+
r << "-j #{args[:action].upcase}" if args[:action]
|
55
|
+
|
56
|
+
if args[:interface]
|
57
|
+
direction = \
|
58
|
+
if args[:direction]
|
59
|
+
case args[:direction]
|
60
|
+
when :incoming
|
61
|
+
'i'
|
62
|
+
when :outgoing
|
63
|
+
'o'
|
64
|
+
else
|
65
|
+
raise "you must provide a :direction flag of either :incoming or :outgoing"
|
66
|
+
end
|
67
|
+
elsif %w{INPUT PREROUTING}.include? chain
|
68
|
+
'i'
|
69
|
+
elsif %w{OUTPUT POSTROUTING}.include? chain
|
70
|
+
'o'
|
71
|
+
else
|
72
|
+
raise "you must provide a :direction flag of either :incoming or :outgoing to use :interface with chain #{chain}"
|
73
|
+
end
|
74
|
+
r << "-#{direction} #{args[:interface]}"
|
75
|
+
end
|
76
|
+
|
77
|
+
r << "-p #{args[:protocol]}" if args[:protocol]
|
78
|
+
r << "-d #{args[:local_address]}" if args[:local_address]
|
79
|
+
r << "-s #{args[:remote_address]}" if args[:remote_address]
|
80
|
+
|
81
|
+
r << "-m state --state #{args[:state].upcase}" if args[:state]
|
82
|
+
#r << "-m #{args[:protocol]} --dport #{args[:port]}" if args[:protocol] && args[:port]
|
83
|
+
r << "--dport #{args[:port]}" if args[:port]
|
84
|
+
r << "-m limit --limit #{args[:limit]}" if args[:limit]
|
85
|
+
|
86
|
+
r << %{--log-prefix "#{args[:log_prefix]}"} if args[:log_prefix]
|
87
|
+
r << "--log-level #{args[:log_level]}" if args[:log_level]
|
88
|
+
|
89
|
+
r << "--icmp-type #{args[:icmp_type]}" if args[:icmp_type]
|
90
|
+
|
91
|
+
if args[:comment]
|
92
|
+
if args[:interface]
|
93
|
+
r << %{-m comment --comment "#{args[:comment]} on #{args[:interface]}"}
|
94
|
+
else
|
95
|
+
r << %{-m comment --comment "#{args[:comment]}"}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end.join(' ')
|
99
|
+
end
|
100
|
+
|
101
|
+
class << self
|
102
|
+
[:accept, :reject, :drop, :log].each do |action|
|
103
|
+
define_method(action) do |args|
|
104
|
+
self.rule(args.merge(:action => action))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
def self.postamble(host)
|
112
|
+
Array.new.tap do |rules|
|
113
|
+
rules << log(:limit => '5/min',
|
114
|
+
:log_level => 7,
|
115
|
+
:log_prefix => "iptables dropped: ",
|
116
|
+
:comment => "log dropped packets") if host.log_denials?
|
117
|
+
|
118
|
+
rules << drop(:chain => :input,
|
119
|
+
:comment => "drop packets that haven't been explicitly accepted") if host.chains[:input].upcase == :ACCEPT
|
120
|
+
|
121
|
+
rules << 'COMMIT'
|
122
|
+
rules << "# Asbestos completed at #{Time.now.utc}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
|
2
|
+
class Asbestos::Host
|
3
|
+
include Asbestos::ClassCollection
|
4
|
+
|
5
|
+
class_collection :all
|
6
|
+
class_collection :groups
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# returns a lazily evaluated block, to allow hosts to be
|
10
|
+
# defined in the DSL without a lot of hoopla
|
11
|
+
def [](name)
|
12
|
+
lambda { @all[name] }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
attr_reader :groups
|
19
|
+
attr_reader :interfaces
|
20
|
+
attr_reader :addresses
|
21
|
+
attr_reader :rulesets
|
22
|
+
attr_reader :chains
|
23
|
+
|
24
|
+
# the HostTemplate that built this host
|
25
|
+
attr_accessor :template
|
26
|
+
|
27
|
+
|
28
|
+
def initialize(name)
|
29
|
+
@name = name
|
30
|
+
@groups = []
|
31
|
+
@rulesets = []
|
32
|
+
|
33
|
+
@chains = {}
|
34
|
+
|
35
|
+
@interfaces = {} # maps interface's tag to /dev name
|
36
|
+
@addresses = {} # maps interface's /dev name to an ip address
|
37
|
+
|
38
|
+
|
39
|
+
Asbestos.with_indifferent_access! @chains
|
40
|
+
Asbestos.with_indifferent_access! @interfaces
|
41
|
+
Asbestos.with_indifferent_access! @addresses
|
42
|
+
|
43
|
+
if Asbestos.hostname.to_sym == @name
|
44
|
+
Asbestos.interfaces.each do |if_name, info|
|
45
|
+
@addresses[if_name] = info[:inet_addr]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Define the necessary chains
|
51
|
+
# # FIXME do we need :forward too?
|
52
|
+
#
|
53
|
+
[:input, :output].each do |name|
|
54
|
+
chain(name, :accept)
|
55
|
+
end
|
56
|
+
|
57
|
+
self.class.all[name] = self
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def debug
|
62
|
+
[
|
63
|
+
"Hostname: #{@name}",
|
64
|
+
(@template ? " Template: #{@template}" : nil),
|
65
|
+
" Interfaces: #{@interfaces}",
|
66
|
+
" Addresses: #{@addresses}",
|
67
|
+
].tap do |a|
|
68
|
+
a << " Groups: #{@groups.sort.join(', ')}" unless @groups.empty?
|
69
|
+
unless @rulesets.empty?
|
70
|
+
a << " RuleSets/Services:"
|
71
|
+
@rulesets.each { |s| a << " #{s.inspect}" }
|
72
|
+
end
|
73
|
+
end.join("\n")
|
74
|
+
end
|
75
|
+
|
76
|
+
def inspect
|
77
|
+
"#<Host name:#{name}>"
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method :to_s, :inspect
|
81
|
+
|
82
|
+
|
83
|
+
#
|
84
|
+
# DSL ------------------------------------------------------------------------------
|
85
|
+
#
|
86
|
+
|
87
|
+
#
|
88
|
+
# Places this host in a named group
|
89
|
+
#
|
90
|
+
# host 'dax' do
|
91
|
+
# group :developers
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
def group(name = nil)
|
95
|
+
if name
|
96
|
+
@groups << name
|
97
|
+
self.class.groups[name] ||= []
|
98
|
+
self.class.groups[name] << self
|
99
|
+
else
|
100
|
+
@groups
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Defines an interface on this host with a given "tag". The interface's address can
|
106
|
+
# be defined explicitly, or at runtime via a block.
|
107
|
+
#
|
108
|
+
# host 'dax' do
|
109
|
+
# group :developers
|
110
|
+
#
|
111
|
+
# interface :external, :eth0 #=> address is "dax_external"
|
112
|
+
# interface :dmz, [:eth1, :eth2] #=> addresses are "dax_dmz_eth1" and "dax_dmz_eth2"
|
113
|
+
#
|
114
|
+
# interface :internal, :eth3 do |host|
|
115
|
+
# [host.group, host.name, 'foo'].join('_')
|
116
|
+
# end #=> address is "developers_dax_foo"
|
117
|
+
#
|
118
|
+
# interface :internal, :eth4, 'bar' #=> address is "bar"
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
def interface(tag, if_names, address = nil, &block)
|
122
|
+
if_names = [*if_names]
|
123
|
+
raise "single address, #{address}, given for multiple interfaces, #{if_names}, on host #{name}" if if_names.length > 1 && address
|
124
|
+
|
125
|
+
@interfaces[tag] = if_names
|
126
|
+
|
127
|
+
# determine the address for each interface
|
128
|
+
if_names.each do |if_name|
|
129
|
+
new_address = \
|
130
|
+
if !address
|
131
|
+
if block_given?
|
132
|
+
yield(self, if_name)
|
133
|
+
else
|
134
|
+
if if_names.length > 1
|
135
|
+
"#{name}_#{tag}_#{if_name}"
|
136
|
+
else
|
137
|
+
"#{name}_#{tag}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
else
|
141
|
+
address
|
142
|
+
end
|
143
|
+
@addresses[if_name] ||= new_address
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Indicates that this host should log denied firewall packets.
|
150
|
+
#
|
151
|
+
# host 'dax' do
|
152
|
+
# log_denials
|
153
|
+
# end
|
154
|
+
def log_denials
|
155
|
+
@log_denials = true
|
156
|
+
end
|
157
|
+
|
158
|
+
def log_denials?
|
159
|
+
!!@log_denials
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
#
|
164
|
+
# Defines a firewall chain on this host, this may be an IPTables-only concept.
|
165
|
+
#
|
166
|
+
# The default_action here is also called the chain's "policy" in IPTables parlance
|
167
|
+
#
|
168
|
+
def chain(name, default_action = :none)
|
169
|
+
@chains[name.downcase.to_sym] = default_action
|
170
|
+
end
|
171
|
+
|
172
|
+
#
|
173
|
+
# Indicates that this host should have rules to allow the corresponding
|
174
|
+
# service to run on it. The arguments provided after the service name
|
175
|
+
# should be valid DSL calls supported by the service. Certain DSL calls
|
176
|
+
# come standard with all services, see the Service class for more info.
|
177
|
+
#
|
178
|
+
# host 'dax' do
|
179
|
+
# runs :nginx, :on => :external
|
180
|
+
# runs :ssh, :on => :internal, :port => 22022
|
181
|
+
# runs :riak, :on => :internal, :from => {:riak_cluster => :internal}
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
def runs(service_name, args = {})
|
185
|
+
template = Asbestos::Service[service_name]
|
186
|
+
raise "Service not defined: #{service_name}" unless template
|
187
|
+
|
188
|
+
@rulesets <<
|
189
|
+
Asbestos::Service.new(service_name, self).tap do |s|
|
190
|
+
s.instance_eval &template
|
191
|
+
# override template defaults with provided options
|
192
|
+
args.each do |k, v|
|
193
|
+
s.send k, v
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
#
|
200
|
+
# Determine this host's firewall rules, according to the firewall type.
|
201
|
+
#
|
202
|
+
def rules
|
203
|
+
#
|
204
|
+
# This is called first in case any preable needs to be declared (chains, specifically)
|
205
|
+
#
|
206
|
+
_ruleset_rules = ruleset_rules
|
207
|
+
|
208
|
+
[
|
209
|
+
Asbestos.firewall.preamble(self),
|
210
|
+
_ruleset_rules,
|
211
|
+
Asbestos.firewall.postamble(self)
|
212
|
+
].flatten
|
213
|
+
end
|
214
|
+
|
215
|
+
#
|
216
|
+
# Ask each ruleset/service to generate its rules.
|
217
|
+
#
|
218
|
+
def ruleset_rules
|
219
|
+
@rulesets.collect do |r|
|
220
|
+
["# Begin [#{r.name}]",
|
221
|
+
r.firewall_rules,
|
222
|
+
"# End [#{r.name}]",
|
223
|
+
""]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
#
|
228
|
+
# Missing methods should be the name of RuleSets, if not, raise an error
|
229
|
+
#
|
230
|
+
# This is similar to the "runs" method above, but for RuleSets, rather than services.
|
231
|
+
#
|
232
|
+
def method_missing(rule_set_name, args = {})
|
233
|
+
template = Asbestos::RuleSet[rule_set_name]
|
234
|
+
raise %{Unknown host DSL call : "#{rule_set_name}" for host "#{name}"} unless template
|
235
|
+
|
236
|
+
@rulesets << \
|
237
|
+
Asbestos::RuleSet.new(rule_set_name, self, template).tap do |rs|
|
238
|
+
# override template defaults with provided options
|
239
|
+
args.each do |k, v|
|
240
|
+
rs.send k, v
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
class Asbestos::RuleSet
|
4
|
+
extend ::Forwardable
|
5
|
+
include Asbestos::ClassCollection
|
6
|
+
|
7
|
+
class_collection :all
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :host
|
11
|
+
attr_reader :attributes
|
12
|
+
attr_reader :commands
|
13
|
+
|
14
|
+
def initialize(name, host, template)
|
15
|
+
@name = name
|
16
|
+
@host = host
|
17
|
+
@attributes = {}
|
18
|
+
@commands = []
|
19
|
+
@template = template
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"#{name}:#{@attributes.inspect}"
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
#
|
28
|
+
# Asks this RuleSet to generate its firewall rules
|
29
|
+
#
|
30
|
+
def firewall_rules
|
31
|
+
instance_eval &@template
|
32
|
+
@commands
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# DSL ------------------------------------------------------------------------------
|
37
|
+
#
|
38
|
+
|
39
|
+
#
|
40
|
+
# These host functions are useful when building rules.
|
41
|
+
#
|
42
|
+
def_delegators :@host, :chain, :interfaces, :command, :addresses
|
43
|
+
|
44
|
+
#
|
45
|
+
# Records a literal firewall command for this host, ignoring firewall type (iptables, ipfw, etc)
|
46
|
+
#
|
47
|
+
def command(str)
|
48
|
+
@commands << str
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Requests a rule from this platform's firewall, with the given args.
|
53
|
+
#
|
54
|
+
[:rule, :accept, :reject, :drop, :log].each do |action|
|
55
|
+
define_method(action) do |args|
|
56
|
+
@commands << Asbestos.firewall.send(action, args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Given a list of "from" objects, resolve a list of hosts or addresses
|
62
|
+
#
|
63
|
+
def from_each(froms = @attributes[:from], &block)
|
64
|
+
case froms
|
65
|
+
when Array # a list of any of the other types
|
66
|
+
froms.each do |from|
|
67
|
+
from_each from, &block
|
68
|
+
end
|
69
|
+
when Hash # either a group or a specific host paired with an interface
|
70
|
+
froms.each do |host_or_group, their_interface_tag|
|
71
|
+
if [Symbol, String].include? host_or_group.class # it's a group name
|
72
|
+
Host.groups[host_or_group].uniq.each do |group_host|
|
73
|
+
next if group_host == @host
|
74
|
+
yield group_host, their_interface_tag
|
75
|
+
end
|
76
|
+
else # it's a Host or a lazly defined Host in a proc
|
77
|
+
host = host_or_group.is_a?(Proc) ? host_or_group.call : host_or_group
|
78
|
+
yield host, their_interface_tag
|
79
|
+
end
|
80
|
+
end
|
81
|
+
when String, Symbol # some kind of address(es)
|
82
|
+
if Asbestos::Address[froms]
|
83
|
+
Asbestos::Address[froms].each do |address|
|
84
|
+
yield address
|
85
|
+
end
|
86
|
+
else
|
87
|
+
yield froms
|
88
|
+
end
|
89
|
+
when nil # from everyone
|
90
|
+
yield nil
|
91
|
+
when Host, Proc
|
92
|
+
raise "#{@host.name}/#{name}: you specified a 'from' Host but no remote interface"
|
93
|
+
else
|
94
|
+
raise "#{@host.name}/#{name}: invalid 'from' object"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Resolves a set of "from" objects into addresses
|
100
|
+
#
|
101
|
+
def from_each_address(froms = @attributes[:from])
|
102
|
+
from_each(froms) do |host_or_address, remote_interface_tag|
|
103
|
+
case host_or_address
|
104
|
+
when Host # specific host, specific remote interface
|
105
|
+
host_or_address.interfaces[remote_interface_tag].each do |remote_interface|
|
106
|
+
yield host_or_address.addresses[remote_interface]
|
107
|
+
end
|
108
|
+
else
|
109
|
+
yield host_or_address
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Responsible for storing and retrieving unspecified DSL calls as service attributes.
|
116
|
+
#
|
117
|
+
def method_missing(attribute, *args)
|
118
|
+
if args.empty?
|
119
|
+
@attributes[attribute]
|
120
|
+
else
|
121
|
+
#
|
122
|
+
# Certain DSL properties should be stored as arrays
|
123
|
+
#
|
124
|
+
if [:ports, :protocols, :groups].include? attribute
|
125
|
+
@attributes[attribute] = [*args]
|
126
|
+
else
|
127
|
+
@attributes[attribute] = args.first
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|