wamupd 1.1.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.
- data/.gitignore +1 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +20 -0
- data/README.md +97 -0
- data/Rakefile +24 -0
- data/bin/wamupd +314 -0
- data/lib/wamupd/action.rb +40 -0
- data/lib/wamupd/avahi_model.rb +210 -0
- data/lib/wamupd/avahi_service.rb +112 -0
- data/lib/wamupd/avahi_service_file.rb +160 -0
- data/lib/wamupd/dns_avahi_controller.rb +230 -0
- data/lib/wamupd/dns_ip_controller.rb +95 -0
- data/lib/wamupd/dns_update.rb +154 -0
- data/lib/wamupd/lease_update.rb +29 -0
- data/lib/wamupd/main_settings.rb +202 -0
- data/lib/wamupd/signals.rb +52 -0
- data/lib/wamupd.rb +10 -0
- data/test/data/config.yaml +9 -0
- data/test/data/simple.service +16 -0
- data/test/data/ssh.service +12 -0
- data/test/test.rb +36 -0
- data/test/test_action.rb +28 -0
- data/test/test_avahi_model.rb +29 -0
- data/test/test_avahi_service.rb +50 -0
- data/test/test_avahi_service_file.rb +78 -0
- data/test/test_dns_avahi_controller.rb +69 -0
- data/test/test_dns_ip_controller.rb +30 -0
- data/test/test_main_settings.rb +43 -0
- data/test/test_signals.rb +58 -0
- data/wamupd.gemspec +29 -0
- metadata +165 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
# Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
|
2
|
+
#
|
3
|
+
# This file is part of wamupd.
|
4
|
+
#
|
5
|
+
# wamupd is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# wamupd is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with wamupd. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require "wamupd/action"
|
19
|
+
require "wamupd/avahi_service"
|
20
|
+
require "wamupd/dns_update"
|
21
|
+
require "wamupd/lease_update"
|
22
|
+
require "wamupd/main_settings"
|
23
|
+
require "wamupd/signals"
|
24
|
+
|
25
|
+
require "algorithms"
|
26
|
+
require "dnsruby"
|
27
|
+
require "thread"
|
28
|
+
|
29
|
+
# Wamupd is a module that is used to namespace all of the wamupd code.
|
30
|
+
module Wamupd
|
31
|
+
# Duplicate services were registered with the controller.
|
32
|
+
# Should almost certainly be treated as non-fatal
|
33
|
+
class DuplicateServiceError < StandardError
|
34
|
+
end
|
35
|
+
|
36
|
+
# Coordinate between a set of Avahi Services and DNS records
|
37
|
+
#
|
38
|
+
# == Signals
|
39
|
+
#
|
40
|
+
# [:added]
|
41
|
+
# Raised when a new service is added to the controller and
|
42
|
+
# successfully registered. has two parameters: the AvahiService
|
43
|
+
# added and the queue ID of the DNS request (or <tt>true</tt> if the
|
44
|
+
# request was synchronous)
|
45
|
+
#
|
46
|
+
# [:deleted]
|
47
|
+
# Raised when a service is deleted from the controller. Contains
|
48
|
+
# the deleted service as its parameter
|
49
|
+
#
|
50
|
+
# [:renewed]
|
51
|
+
# Raised when a service's lease is renewed. Contains the renewed
|
52
|
+
# service as its parameter
|
53
|
+
#
|
54
|
+
# [:quit]
|
55
|
+
# Raised when the controller is quitting
|
56
|
+
class DNSAvahiController
|
57
|
+
include Wamupd::Signals
|
58
|
+
|
59
|
+
# A queue to put actions into.
|
60
|
+
attr_reader :queue
|
61
|
+
|
62
|
+
# Initialize the controller.
|
63
|
+
def initialize()
|
64
|
+
@sa = MainSettings.instance
|
65
|
+
# Services stored as a hash from
|
66
|
+
@services = {}
|
67
|
+
@resolver = @sa.resolver
|
68
|
+
@added = []
|
69
|
+
@queue = Queue.new
|
70
|
+
# Make a min priority queue for leases
|
71
|
+
@lease_queue = Containers::PriorityQueue.new { |x,y| (x<=>y) == -1 }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add an array of services to the controller
|
75
|
+
def add_services(services)
|
76
|
+
services.each { |s| add_service(s) }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add a single service record to the controller
|
80
|
+
def add_service(service)
|
81
|
+
if service.kind_of?(AvahiService)
|
82
|
+
if (not @services.has_key?(service.identifier))
|
83
|
+
@services[service.identifier] = service
|
84
|
+
else
|
85
|
+
raise DuplicateServiceError.new("Got a duplicate")
|
86
|
+
end
|
87
|
+
elsif (service.kind_of?(AvahiServiceFile))
|
88
|
+
service.each { |service_entry|
|
89
|
+
add_service(service_entry)
|
90
|
+
}
|
91
|
+
else
|
92
|
+
raise ArgumentError.new("Not an AvahiService")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Delete a signle service record from the service
|
97
|
+
def delete_service(service)
|
98
|
+
if service.kind_of?(AvahiService)
|
99
|
+
@services.delete(service.identifier)
|
100
|
+
else
|
101
|
+
raise ArgumentError.new("Not an AvahiService")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return the number of elements in the controller
|
106
|
+
def size
|
107
|
+
return @services.size
|
108
|
+
end
|
109
|
+
|
110
|
+
# Keys
|
111
|
+
def keys
|
112
|
+
return @services.keys
|
113
|
+
end
|
114
|
+
|
115
|
+
# Publish all currently stored records
|
116
|
+
def publish_all
|
117
|
+
ids = []
|
118
|
+
@services.each { |key,service|
|
119
|
+
ids << publish(service)
|
120
|
+
}
|
121
|
+
return ids
|
122
|
+
end
|
123
|
+
|
124
|
+
# Unpublish all stored records
|
125
|
+
def unpublish_all
|
126
|
+
todo = []
|
127
|
+
@services.each { |key,service|
|
128
|
+
todo << { :target=>service.type_in_zone_with_name,
|
129
|
+
:type=>Dnsruby::Types.SRV,
|
130
|
+
:value=> "#{@sa.priority} #{@sa.weight} #{service.port} #{service.target}"}
|
131
|
+
todo << { :target=>service.type_in_zone,
|
132
|
+
:type=>Dnsruby::Types.PTR,
|
133
|
+
:value=>service.type_in_zone_with_name}
|
134
|
+
todo << { :target=>service.type_in_zone_with_name,
|
135
|
+
:type=>Dnsruby::Types.TXT}
|
136
|
+
}
|
137
|
+
DNSUpdate.unpublish_all(*todo)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Publish a single service
|
141
|
+
#
|
142
|
+
# Returns: the DNS request ID
|
143
|
+
def publish(service, ttl=@sa.ttl, lease_time=@sa.lease_time)
|
144
|
+
to_update = []
|
145
|
+
to_update << {:target=>service.type_in_zone,
|
146
|
+
:type=>Dnsruby::Types.PTR, :ttl=>ttl,
|
147
|
+
:value=>service.type_in_zone_with_name}
|
148
|
+
to_update << {:target=>service.type_in_zone_with_name,
|
149
|
+
:type=>Dnsruby::Types.SRV, :ttl=>ttl,
|
150
|
+
:value=> "#{@sa.priority} #{@sa.weight} #{service.port} #{service.target}"}
|
151
|
+
# why doesn't Ruby have !==
|
152
|
+
unless (service.txt === false)
|
153
|
+
to_update << {:target => service.type_in_zone_with_name,
|
154
|
+
:type=>Dnsruby::Types.TXT, :ttl=>ttl,
|
155
|
+
:value=>service.txt}
|
156
|
+
end
|
157
|
+
update_time = Time.now() + lease_time
|
158
|
+
@lease_queue.push(Wamupd::LeaseUpdate.new(update_time, service), update_time)
|
159
|
+
return DNSUpdate.publish_all(to_update)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Unpublish a single service
|
163
|
+
def unpublish(service, ttl=@sa.ttl)
|
164
|
+
todo = []
|
165
|
+
to_update << {:target=>service.type_in_zone,
|
166
|
+
:type=>Dnsruby::Types.PTR,
|
167
|
+
:value=>service.type_in_zone_with_name}
|
168
|
+
to_update << {:target=>service.type_in_zone_with_name,
|
169
|
+
:type=>DnsRuby::Types.SRV}
|
170
|
+
to_update << {:target=>service.type_in_zone_with_name,
|
171
|
+
:type=>Dnsruby::Types.TXT}
|
172
|
+
DNSUpdate.unpublish_all(todo)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Process a single Wamupd::Action out of the queue
|
176
|
+
def process_action(action)
|
177
|
+
case action.action
|
178
|
+
when Wamupd::ActionType::ADD
|
179
|
+
begin
|
180
|
+
add_service(action.record)
|
181
|
+
id = publish(action.record)
|
182
|
+
signal(:added, action.record, id)
|
183
|
+
rescue DuplicateServiceError
|
184
|
+
# Do nothing
|
185
|
+
end
|
186
|
+
when Wamupd::ActionType::DELETE
|
187
|
+
delete_service(action.record)
|
188
|
+
unpublish_service(action.record)
|
189
|
+
signal(:deleted, action.record)
|
190
|
+
when Wamupd::ActionType::QUIT
|
191
|
+
# Flush the queue, then signal quit
|
192
|
+
until @queue.empty?
|
193
|
+
process_action(@queue.pop(false))
|
194
|
+
end
|
195
|
+
unpublish_all
|
196
|
+
signal(:quit)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Exit out of the main loop
|
201
|
+
def exit
|
202
|
+
@queue << Wamupd::Action.new(Wamupd::ActionType::QUIT)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Wait for data to go into the queue, and handle it when it does
|
206
|
+
def run
|
207
|
+
while true
|
208
|
+
process_action(@queue.pop)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Takes care of updating leases. Run it in a separate thread from
|
213
|
+
# the main "run" function
|
214
|
+
def update_leases
|
215
|
+
while true
|
216
|
+
now = Time.now
|
217
|
+
while (not @lease_queue.empty?) and (@lease_queue.next.date < now)
|
218
|
+
item = @lease_queue.pop
|
219
|
+
if @services.has_key?(item.service.identifier)
|
220
|
+
signal(:renewed, item.service)
|
221
|
+
publish(item.service)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
sleep(@sa.sleep_time)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
private :process_action, :publish
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
|
2
|
+
#
|
3
|
+
# This file is part of wamupd.
|
4
|
+
#
|
5
|
+
# wamupd is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# wamupd is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with wamupd. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
require "wamupd/lease_update"
|
19
|
+
require "wamupd/main_settings"
|
20
|
+
|
21
|
+
require "algorithms"
|
22
|
+
require "dnsruby"
|
23
|
+
require "ipaddr"
|
24
|
+
require "socket"
|
25
|
+
|
26
|
+
# Wamupd is a module that is used to namespace all of the wamupd code.
|
27
|
+
module Wamupd
|
28
|
+
# Manage IP information in DNS
|
29
|
+
#
|
30
|
+
# == Signals
|
31
|
+
# [:added]
|
32
|
+
# A record was published. Parameters are the type (A/AAAA) and the
|
33
|
+
# address
|
34
|
+
# [:removed]
|
35
|
+
# A record was unpublished. Parameters are the type (A/AAAA) and the
|
36
|
+
# address
|
37
|
+
class DNSIpController
|
38
|
+
include Wamupd::Signals
|
39
|
+
|
40
|
+
# Constructor
|
41
|
+
def initialize()
|
42
|
+
@sa = MainSettings.instance
|
43
|
+
@sa.get_ip_addresses
|
44
|
+
@resolver = @sa.resolver
|
45
|
+
@lease_queue = Containers::PriorityQueue.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# Publish A and AAAA records
|
49
|
+
def publish
|
50
|
+
if (@sa.ipv4)
|
51
|
+
DNSUpdate.publish(@sa.target, Dnsruby::Types.A, @sa.ttl, @sa.ipv4)
|
52
|
+
signal(:added, Dnsruby::Types.A, @sa.ipv4)
|
53
|
+
end
|
54
|
+
if (@sa.ipv6)
|
55
|
+
DNSUpdate.publish(@sa.target, Dnsruby::Types.AAAA, @sa.ttl, @sa.ipv6)
|
56
|
+
signal(:added, Dnsruby::Types.AAAA, @sa.ipv6)
|
57
|
+
end
|
58
|
+
update_time = Time.now() + @sa.lease_time
|
59
|
+
@lease_queue.push(Wamupd::LeaseUpdate.new(update_time, nil), update_time)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Synonym for publish
|
63
|
+
def publish_all
|
64
|
+
publish
|
65
|
+
end
|
66
|
+
|
67
|
+
# Unpublish A and AAAA records
|
68
|
+
def unpublish
|
69
|
+
if (@sa.ipv4)
|
70
|
+
DNSUpdate.unpublish(@sa.target, Dnsruby::Types.A, @sa.ipv4)
|
71
|
+
signal(:removed, Dnsruby::Types.A, @sa.ipv4)
|
72
|
+
end
|
73
|
+
if (@sa.ipv6)
|
74
|
+
DNSUpdate.unpublish(@sa.target, Dnsruby::Types.AAAA, @sa.ipv6)
|
75
|
+
signal(:removed, Dnsruby::Types.A, @sa.ipv6)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Synonym for unpublish
|
80
|
+
def unpublish_all
|
81
|
+
unpublish
|
82
|
+
end
|
83
|
+
|
84
|
+
# Update leases when required. Please run in a separate thread.
|
85
|
+
def update_leases
|
86
|
+
while true
|
87
|
+
now = Time.now
|
88
|
+
while (not @lease_queue.empty?) and (@lease_queue.next.date < now)
|
89
|
+
publish
|
90
|
+
end
|
91
|
+
sleep(@sa.sleep_time)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# Copyright (C) 2009-2010 James Brown <roguelazer@roguelazer.com>.
|
3
|
+
#
|
4
|
+
# This file is part of wamupd.
|
5
|
+
#
|
6
|
+
# wamupd is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# wamupd is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with wamupd. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
require "dnsruby"
|
20
|
+
|
21
|
+
# Wamupd is a module that is used to namespace all of the wamupd code.
|
22
|
+
module Wamupd
|
23
|
+
# Class to help with constructing DNS UPDATEs. Probably not useful except to
|
24
|
+
# me.
|
25
|
+
class DNSUpdate
|
26
|
+
@@queue = nil
|
27
|
+
@@outstanding = []
|
28
|
+
|
29
|
+
# Set the queue
|
30
|
+
def self.queue=(v)
|
31
|
+
@@queue=v
|
32
|
+
end
|
33
|
+
|
34
|
+
# How many requests are outstanding?
|
35
|
+
def self.outstanding
|
36
|
+
return @@outstanding
|
37
|
+
end
|
38
|
+
|
39
|
+
# Is this update going to be asynchronous?
|
40
|
+
def self.async?
|
41
|
+
return (not @@queue.nil?)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Publish a batch of DNS records
|
45
|
+
#
|
46
|
+
# Arguments:
|
47
|
+
# An array of records to publish. Each record is either a Hash
|
48
|
+
# containing the keys :target, :type, :ttl, and :value, or an array of
|
49
|
+
# items which could be args to Dnsruby::Update::add
|
50
|
+
def self.publish_all(args)
|
51
|
+
sa = MainSettings.instance()
|
52
|
+
resolver = sa.resolver
|
53
|
+
update = Dnsruby::Update.new(sa.zone, "IN")
|
54
|
+
shortest_ttl=86400
|
55
|
+
args.each { |arg|
|
56
|
+
if (arg.kind_of?(Hash))
|
57
|
+
update.add(arg[:target], arg[:type], arg[:ttl], arg[:value])
|
58
|
+
if (arg[:ttl] < shortest_ttl)
|
59
|
+
shortest_ttl = arg[:ttl]
|
60
|
+
end
|
61
|
+
elsif (arg.kind_of?(Array))
|
62
|
+
if (arg[2] < shortest_ttl)
|
63
|
+
shortest_ttl = arg[2]
|
64
|
+
end
|
65
|
+
update.add(*arg)
|
66
|
+
else
|
67
|
+
raise ArgumentError.new("Could not parse arguments")
|
68
|
+
end
|
69
|
+
}
|
70
|
+
opt = Dnsruby::RR::OPT.new
|
71
|
+
lease_time = Dnsruby::RR::OPT::Option.new(2, [shortest_ttl].pack("N"))
|
72
|
+
opt.klass="IN"
|
73
|
+
opt.options=[lease_time]
|
74
|
+
opt.ttl = 0
|
75
|
+
update.add_additional(opt)
|
76
|
+
update.header.rd = false
|
77
|
+
queue_id = nil
|
78
|
+
begin
|
79
|
+
if (async?)
|
80
|
+
queue_id = resolver.send_async(update, @@queue)
|
81
|
+
@@outstanding << queue_id
|
82
|
+
else
|
83
|
+
resolver.send_message(update)
|
84
|
+
queue_id = true
|
85
|
+
end
|
86
|
+
rescue Dnsruby::TsigNotSignedResponseError => e
|
87
|
+
# Not really an error for UPDATE; we don't care if the reply is
|
88
|
+
# signed!
|
89
|
+
nil
|
90
|
+
rescue Exception => e
|
91
|
+
$stderr.puts "Registration failed: #{e.to_s}"
|
92
|
+
end
|
93
|
+
return queue_id
|
94
|
+
end
|
95
|
+
|
96
|
+
# Publish a single DNS record
|
97
|
+
#
|
98
|
+
# Arguments:
|
99
|
+
# Same as Dnsruby::Update::add
|
100
|
+
def self.publish(*args)
|
101
|
+
self.publish_all([args])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Unpublish a batch of DNS records
|
105
|
+
#
|
106
|
+
# Arguments:
|
107
|
+
# An array of records to unpublish. Each record is either a Hash
|
108
|
+
# containing keys :target, :type, and :value, or an array of items which
|
109
|
+
# could be args to Dnsruby::Update::delete
|
110
|
+
def self.unpublish_all(*args)
|
111
|
+
sa = MainSettings.instance()
|
112
|
+
resolver = sa.resolver
|
113
|
+
update = Dnsruby::Update.new(sa.zone, "IN")
|
114
|
+
args.each { |arg|
|
115
|
+
if (arg.kind_of?(Hash))
|
116
|
+
if (arg.has_key?(:value))
|
117
|
+
update.delete(arg[:target], arg[:type], arg[:value])
|
118
|
+
else
|
119
|
+
update.delete(arg[:target], arg[:type])
|
120
|
+
end
|
121
|
+
elsif (arg.kind_of?(Array))
|
122
|
+
update.delete(*arg)
|
123
|
+
else
|
124
|
+
raise ArgumentError.new("Could not parse arguments")
|
125
|
+
end
|
126
|
+
}
|
127
|
+
begin
|
128
|
+
if (async?)
|
129
|
+
queue_id = resolver.send_async(update, @@queue)
|
130
|
+
@@outstanding << queue_id
|
131
|
+
else
|
132
|
+
resolver.send_message(update)
|
133
|
+
end
|
134
|
+
rescue Dnsruby::NXRRSet => e
|
135
|
+
$stderr.puts "Could not remove record because it doesn't exist!"
|
136
|
+
rescue Dnsruby::TsigNotSignedResponseError => e
|
137
|
+
# Not really an error for UPDATE; we don't care if the reply is
|
138
|
+
# signed!
|
139
|
+
nil
|
140
|
+
rescue Exception => e
|
141
|
+
$stderr.puts "Unregistration failed: #{e.to_s}"
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
# Unpublish a single DNS record
|
147
|
+
#
|
148
|
+
# Arguments:
|
149
|
+
# Same as Dnsruby::Update::delete
|
150
|
+
def self.unpublish(*args)
|
151
|
+
self.unpublish_all(args)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright (C) 2010 James Brown <roguelazer@roguelazer.com>.
|
2
|
+
#
|
3
|
+
# This file is part of wamupd.
|
4
|
+
#
|
5
|
+
# wamupd is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# wamupd is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with wamupd. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
module Wamupd
|
19
|
+
# Struct for lease update records
|
20
|
+
class LeaseUpdate
|
21
|
+
attr_accessor :date
|
22
|
+
attr_accessor :service
|
23
|
+
|
24
|
+
def initialize(date, service)
|
25
|
+
@date = date
|
26
|
+
@service = service
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|