asbestos 0.0.1
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.
- 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
|