route53 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/Rakefile +5 -0
- data/lib/route53.rb +5 -411
- data/lib/route53/aws_response.rb +83 -0
- data/lib/route53/connection.rb +93 -0
- data/lib/route53/dns_record.rb +99 -0
- data/lib/route53/version.rb +1 -1
- data/lib/route53/zone.rb +134 -0
- data/route53.gemspec +9 -2
- data/spec/fixtures/vcr_cassettes/aws_zone.yml +198 -0
- data/spec/fixtures/vcr_cassettes/aws_zone_delete.yml +110 -0
- data/spec/fixtures/vcr_cassettes/aws_zone_records.yml +73 -0
- data/spec/fixtures/vcr_cassettes/aws_zones.yml +71 -0
- data/spec/lib/route53/aws_response_spec.rb +10 -0
- data/spec/lib/route53/connection_spec.rb +62 -0
- data/spec/lib/route53/dns_record_spec.rb +4 -0
- data/spec/lib/route53/zone_spec.rb +71 -0
- data/spec/spec_helper.rb +20 -0
- metadata +162 -80
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7245a232925c75451ca48b244e29e5050e201eb3
|
4
|
+
data.tar.gz: fcc78bc8b07928023c71c6f5c1e86180be9e70dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4aac19d233d5032337712448af778c6808188d69b7db542e8d4ae76dea6d4121fef4bb361827022ce0759fdf4d4364fbf55cd63d68d8448e0e6749a1b755059a
|
7
|
+
data.tar.gz: 2f1390fa5be1861a4418ea332e1f208ab2ebfa385e7cca404c89c8e90c81d0214129c0c6c5d21c04afe80ef7817e163560586ecb4f63f57b17245cd0533fca6a
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
route53
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.0.0-p353
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/lib/route53.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require 'rubygems'
|
3
2
|
require 'hmac'
|
4
3
|
require 'hmac-sha2'
|
@@ -7,418 +6,13 @@ require 'time'
|
|
7
6
|
require 'net/http'
|
8
7
|
require 'net/https'
|
9
8
|
require 'uri'
|
10
|
-
require '
|
9
|
+
require 'nokogiri'
|
11
10
|
require 'builder'
|
12
11
|
require 'digest/md5'
|
12
|
+
require 'route53/connection'
|
13
|
+
require 'route53/zone'
|
14
|
+
require 'route53/aws_response'
|
15
|
+
require 'route53/dns_record'
|
13
16
|
|
14
17
|
module Route53
|
15
|
-
|
16
|
-
class Connection
|
17
|
-
attr_reader :base_url
|
18
|
-
attr_reader :api
|
19
|
-
attr_reader :endpoint
|
20
|
-
attr_reader :verbose
|
21
|
-
|
22
|
-
def initialize(accesskey,secret,api='2012-12-12',endpoint='https://route53.amazonaws.com/',verbose=false,ssl_no_verify=false)
|
23
|
-
@accesskey = accesskey
|
24
|
-
@secret = secret
|
25
|
-
@api = api
|
26
|
-
@endpoint = endpoint
|
27
|
-
@base_url = endpoint+@api
|
28
|
-
@verbose = verbose
|
29
|
-
@ssl_no_verify = ssl_no_verify
|
30
|
-
end
|
31
|
-
|
32
|
-
def request(url,type = "GET",data = nil)
|
33
|
-
puts "URL: #{url}" if @verbose
|
34
|
-
puts "Type: #{type}" if @verbose
|
35
|
-
puts "Req: #{data}" if type != "GET" && @verbose
|
36
|
-
uri = URI(url)
|
37
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
38
|
-
http.use_ssl = true if uri.scheme == "https"
|
39
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if RUBY_VERSION.start_with?("1.8") or @ssl_no_verify
|
40
|
-
time = get_date
|
41
|
-
hmac = HMAC::SHA256.new(@secret)
|
42
|
-
hmac.update(time)
|
43
|
-
signature = Base64.encode64(hmac.digest).chomp
|
44
|
-
headers = {
|
45
|
-
'Date' => time,
|
46
|
-
'X-Amzn-Authorization' => "AWS3-HTTPS AWSAccessKeyId=#{@accesskey},Algorithm=HmacSHA256,Signature=#{signature}",
|
47
|
-
'Content-Type' => 'text/xml; charset=UTF-8'
|
48
|
-
}
|
49
|
-
resp = http.send_request(type,uri.path+"?"+(uri.query.nil? ? "" : uri.query),data,headers)
|
50
|
-
#puts "Resp:"+resp.to_s if @verbose
|
51
|
-
#puts "RespBody: #{resp.body}" if @verbose
|
52
|
-
return AWSResponse.new(resp.body,self)
|
53
|
-
end
|
54
|
-
|
55
|
-
def get_zones(name = nil)
|
56
|
-
truncated = true
|
57
|
-
query = []
|
58
|
-
zones = []
|
59
|
-
while truncated
|
60
|
-
if !name.nil? && name.start_with?("/hostedzone/")
|
61
|
-
resp = request("#{@base_url}#{name}")
|
62
|
-
truncated = false
|
63
|
-
else
|
64
|
-
resp = request("#{@base_url}/hostedzone?"+query.join("&"))
|
65
|
-
end
|
66
|
-
return nil if resp.error?
|
67
|
-
zone_list = Hpricot::XML(resp.raw_data)
|
68
|
-
elements = zone_list.search("HostedZone")
|
69
|
-
elements.each do |e|
|
70
|
-
zones.push(Zone.new(e.search("Name").first.innerText,
|
71
|
-
e.search("Id").first.innerText,
|
72
|
-
self))
|
73
|
-
end
|
74
|
-
truncated = (zone_list.search("IsTruncated").first.innerText == "true") if truncated
|
75
|
-
query = ["marker="+zone_list.search("NextMarker").first.innerText] if truncated
|
76
|
-
end
|
77
|
-
unless name.nil? || name.start_with?("/hostedzone/")
|
78
|
-
name_arr = name.split('.')
|
79
|
-
(0 ... name_arr.size).each do |i|
|
80
|
-
search_domain = name_arr.last(name_arr.size-i).join('.')+"."
|
81
|
-
zone_select = zones.select { |z| z.name == search_domain }
|
82
|
-
return zone_select
|
83
|
-
end
|
84
|
-
return nil
|
85
|
-
end
|
86
|
-
return zones
|
87
|
-
end
|
88
|
-
|
89
|
-
def get_date
|
90
|
-
#return Time.now.utc.rfc2822
|
91
|
-
#Cache date for 30 seconds to reduce extra calls
|
92
|
-
if @date_stale.nil? || @date_stale < Time.now - 30
|
93
|
-
uri = URI(@endpoint)
|
94
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
95
|
-
http.use_ssl = true if uri.scheme == "https"
|
96
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if RUBY_VERSION.start_with?("1.8") or @ssl_no_verify
|
97
|
-
resp = nil
|
98
|
-
puts "Making Date Request" if @verbose
|
99
|
-
http.start { |http| resp = http.head('/date') }
|
100
|
-
@date = resp['Date']
|
101
|
-
@date_stale = Time.now
|
102
|
-
puts "Received Date." if @verbose
|
103
|
-
end
|
104
|
-
return @date
|
105
|
-
end
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
|
110
|
-
class Zone
|
111
|
-
attr_reader :host_url
|
112
|
-
attr_reader :name
|
113
|
-
attr_reader :records
|
114
|
-
attr_reader :conn
|
115
|
-
|
116
|
-
def initialize(name,host_url,conn)
|
117
|
-
@name = name
|
118
|
-
unless @name.end_with?(".")
|
119
|
-
@name += "."
|
120
|
-
end
|
121
|
-
@host_url = host_url
|
122
|
-
@conn = conn
|
123
|
-
end
|
124
|
-
|
125
|
-
def nameservers
|
126
|
-
return @nameservers if @nameservers
|
127
|
-
response = Hpricot::XML(@conn.request(@conn.base_url + @host_url).to_s)
|
128
|
-
@nameservers = response.search("NameServer").map(&:innerText)
|
129
|
-
@nameservers
|
130
|
-
end
|
131
|
-
|
132
|
-
def delete_zone
|
133
|
-
@conn.request(@conn.base_url + @host_url,"DELETE")
|
134
|
-
end
|
135
|
-
|
136
|
-
def create_zone(comment = nil)
|
137
|
-
xml_str = ""
|
138
|
-
xml = Builder::XmlMarkup.new(:target=>xml_str, :indent=>2)
|
139
|
-
xml.instruct!
|
140
|
-
xml.CreateHostedZoneRequest(:xmlns => @conn.endpoint+'doc/'+@conn.api+'/') { |create|
|
141
|
-
create.Name(@name)
|
142
|
-
# AWS lists this as required
|
143
|
-
# "unique string that identifies the request and that
|
144
|
-
# allows failed CreateHostedZone requests to be retried without the risk of executing the operation twice."
|
145
|
-
# Just going to pass a random string instead.
|
146
|
-
create.CallerReference(rand(2**32).to_s(16))
|
147
|
-
create.HostedZoneConfig { |conf|
|
148
|
-
conf.Comment(comment)
|
149
|
-
}
|
150
|
-
}
|
151
|
-
#puts "XML:\n#{xml_str}" if @conn.verbose
|
152
|
-
resp = @conn.request(@conn.base_url + "/hostedzone","POST",xml_str)
|
153
|
-
resp_xml = Hpricot::XML(resp.raw_data)
|
154
|
-
@host_url = resp_xml.search("HostedZone").first.search("Id").first.innerText
|
155
|
-
return resp
|
156
|
-
end
|
157
|
-
|
158
|
-
def get_records(type="ANY")
|
159
|
-
return nil if host_url.nil?
|
160
|
-
|
161
|
-
truncated = true
|
162
|
-
query = []
|
163
|
-
dom_records = []
|
164
|
-
while truncated
|
165
|
-
resp = @conn.request(@conn.base_url+@host_url+"/rrset?"+query.join("&"))
|
166
|
-
if resp.error?
|
167
|
-
return nil
|
168
|
-
end
|
169
|
-
zone_file = Hpricot::XML(resp.raw_data)
|
170
|
-
records = zone_file.search("ResourceRecordSet")
|
171
|
-
|
172
|
-
records.each do |record|
|
173
|
-
#puts "Name:"+record.search("Name").first.innerText if @conn.verbose
|
174
|
-
#puts "Type:"+record.search("Type").first.innerText if @conn.verbose
|
175
|
-
#puts "TTL:"+record.search("TTL").first.innerText if @conn.verbose
|
176
|
-
#record.search("Value").each do |val|
|
177
|
-
# #puts "Val:"+val.innerText if @conn.verbose
|
178
|
-
#end
|
179
|
-
zone_apex_records = record.search("HostedZoneId")
|
180
|
-
values = record.search("Value").map { |val| val.innerText }
|
181
|
-
values << record.search("DNSName").first.innerText unless zone_apex_records.empty?
|
182
|
-
weight_records = record.search("Weight")
|
183
|
-
ident_records = record.search("SetIdentifier")
|
184
|
-
dom_records.push(DNSRecord.new(record.search("Name").first.innerText,
|
185
|
-
record.search("Type").first.innerText,
|
186
|
-
(record.search("TTL").first.innerText if zone_apex_records.empty?),
|
187
|
-
values,
|
188
|
-
self,
|
189
|
-
(zone_apex_records.first.innerText unless zone_apex_records.empty?),
|
190
|
-
(weight_records.first.innerText unless weight_records.empty?),
|
191
|
-
(ident_records.first.innerText unless ident_records.empty?)
|
192
|
-
))
|
193
|
-
end
|
194
|
-
|
195
|
-
truncated = (zone_file.search("IsTruncated").first.innerText == "true")
|
196
|
-
if truncated
|
197
|
-
next_name = zone_file.search("NextRecordName").first.innerText
|
198
|
-
next_type = zone_file.search("NextRecordType").first.innerText
|
199
|
-
query = ["name="+next_name,"type="+next_type]
|
200
|
-
end
|
201
|
-
end
|
202
|
-
@records = dom_records
|
203
|
-
if type != 'ANY'
|
204
|
-
return dom_records.select { |r| r.type == type }
|
205
|
-
end
|
206
|
-
return dom_records
|
207
|
-
end
|
208
|
-
|
209
|
-
#When deleting a record an optional value is available to specify just a single value within a recordset like an MX record
|
210
|
-
#Takes an array of [:action => , :record => ] where action is either CREATE or DELETE and record is a DNSRecord
|
211
|
-
def gen_change_xml(change_list,comment=nil)
|
212
|
-
#Get zone list and pick zone that matches most ending chars
|
213
|
-
|
214
|
-
xml_str = ""
|
215
|
-
xml = Builder::XmlMarkup.new(:target=>xml_str, :indent=>2)
|
216
|
-
xml.instruct!
|
217
|
-
xml.ChangeResourceRecordSetsRequest(:xmlns => @conn.endpoint+'doc/'+@conn.api+'/') { |req|
|
218
|
-
req.ChangeBatch { |batch|
|
219
|
-
batch.Comment(comment) unless comment.nil?
|
220
|
-
batch.Changes { |changes|
|
221
|
-
change_list.each { |change_item|
|
222
|
-
change_item[:record].gen_change_xml(changes,change_item[:action])
|
223
|
-
}
|
224
|
-
}
|
225
|
-
}
|
226
|
-
}
|
227
|
-
#puts "XML:\n#{xml_str}" if @conn.verbose
|
228
|
-
return xml_str
|
229
|
-
end
|
230
|
-
|
231
|
-
#For modifying multiple or single records within a single transaction
|
232
|
-
def perform_actions(change_list,comment=nil)
|
233
|
-
xml_str = gen_change_xml(change_list,comment)
|
234
|
-
@conn.request(@conn.base_url + @host_url+"/rrset","POST",xml_str)
|
235
|
-
end
|
236
|
-
|
237
|
-
|
238
|
-
def to_s
|
239
|
-
return "#{@name} #{@host_url}"
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
class AWSResponse
|
244
|
-
attr_reader :raw_data
|
245
|
-
|
246
|
-
#I wanted to put this in a seprate file but ruby's method of determinign the root of the gem is a pain in the butt and I was in a hurry. Sorry. -PC
|
247
|
-
|
248
|
-
|
249
|
-
def initialize(resp,conn)
|
250
|
-
@raw_data = unescape(resp)
|
251
|
-
if error?
|
252
|
-
$stderr.puts "ERROR: Amazon returned an error for the request."
|
253
|
-
$stderr.puts "ERROR: RAW_XML: "+@raw_data
|
254
|
-
$stderr.puts "ERROR: "+error_message
|
255
|
-
$stderr.puts ""
|
256
|
-
$stderr.puts "What now? "+helpful_message
|
257
|
-
#exit 1
|
258
|
-
end
|
259
|
-
@conn = conn
|
260
|
-
@created = Time.now
|
261
|
-
puts "Raw: #{@raw_data}" if @conn.verbose
|
262
|
-
end
|
263
|
-
|
264
|
-
def error?
|
265
|
-
return Hpricot::XML(@raw_data).search("ErrorResponse").size > 0
|
266
|
-
end
|
267
|
-
|
268
|
-
def error_message
|
269
|
-
xml = Hpricot::XML(@raw_data)
|
270
|
-
msg_code = xml.search("Code")
|
271
|
-
msg_text = xml.search("Message")
|
272
|
-
return (msg_code.size > 0 ? msg_code.first.inner_text : "") + (msg_text.size > 0 ? ': ' + msg_text.first.innerText : "")
|
273
|
-
end
|
274
|
-
|
275
|
-
def helpful_message
|
276
|
-
xml = Hpricot::XML(@raw_data)
|
277
|
-
msg_code = xml.search("Code").first.innerText
|
278
|
-
return $messages[msg_code] if $messages[msg_code]
|
279
|
-
return $messages["Other"]
|
280
|
-
end
|
281
|
-
|
282
|
-
def complete?
|
283
|
-
return true if error?
|
284
|
-
if @change_url.nil?
|
285
|
-
change = Hpricot::XML(@raw_data).search("ChangeInfo")
|
286
|
-
if change.size > 0
|
287
|
-
@change_url = change.first.search("Id").first.innerText
|
288
|
-
else
|
289
|
-
return false
|
290
|
-
end
|
291
|
-
end
|
292
|
-
if @complete.nil? || @complete == false
|
293
|
-
status = Hpricot::XML(@conn.request(@conn.base_url+@change_url).raw_data).search("Status")
|
294
|
-
@complete = status.size > 0 && status.first.innerText == "INSYNC" ? true : false
|
295
|
-
if !@complete && @created - Time.now > 60
|
296
|
-
$stderr.puts "WARNING: Amazon Route53 Change timed out on Sync. This may not be an issue as it may just be Amazon being assy. Then again your request may not have completed.'"
|
297
|
-
@complete = true
|
298
|
-
end
|
299
|
-
end
|
300
|
-
return @complete
|
301
|
-
end
|
302
|
-
|
303
|
-
def pending?
|
304
|
-
#Return opposite of complete via XOR
|
305
|
-
return complete? ^ true
|
306
|
-
end
|
307
|
-
|
308
|
-
def to_s
|
309
|
-
return @raw_data
|
310
|
-
end
|
311
|
-
|
312
|
-
def unescape(string)
|
313
|
-
string.gsub(/\\0(\d{2})/) { $1.oct.chr }
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
class DNSRecord
|
318
|
-
attr_reader :name
|
319
|
-
attr_reader :type
|
320
|
-
attr_reader :ttl
|
321
|
-
attr_reader :values
|
322
|
-
attr_reader :weight
|
323
|
-
attr_reader :ident
|
324
|
-
attr_reader :zone_apex
|
325
|
-
|
326
|
-
def initialize(name,type,ttl,values,zone,zone_apex=nil,weight=nil,ident=nil)
|
327
|
-
@name = name
|
328
|
-
unless @name.end_with?(".")
|
329
|
-
@name += "."
|
330
|
-
end
|
331
|
-
@type = type.upcase
|
332
|
-
@ttl = ttl
|
333
|
-
@values = values
|
334
|
-
@zone = zone
|
335
|
-
@zone_apex = zone_apex
|
336
|
-
@weight = weight
|
337
|
-
@ident = ident
|
338
|
-
end
|
339
|
-
|
340
|
-
def gen_change_xml(xml,action)
|
341
|
-
xml.Change { |change|
|
342
|
-
change.Action(action.upcase)
|
343
|
-
change.ResourceRecordSet { |record|
|
344
|
-
record.Name(@name)
|
345
|
-
record.Type(@type)
|
346
|
-
record.SetIdentifier(@ident) if @ident
|
347
|
-
record.Weight(@weight) if @weight
|
348
|
-
record.TTL(@ttl) unless @zone_apex
|
349
|
-
if @zone_apex
|
350
|
-
record.AliasTarget { |targets|
|
351
|
-
targets.HostedZoneId(@zone_apex)
|
352
|
-
targets.DNSName(@values.first)
|
353
|
-
}
|
354
|
-
else
|
355
|
-
record.ResourceRecords { |resources|
|
356
|
-
@values.each { |val|
|
357
|
-
resources.ResourceRecord { |record|
|
358
|
-
record.Value(val)
|
359
|
-
}
|
360
|
-
}
|
361
|
-
}
|
362
|
-
end
|
363
|
-
}
|
364
|
-
}
|
365
|
-
end
|
366
|
-
|
367
|
-
def delete(comment=nil)
|
368
|
-
@zone.perform_actions([{:action => "DELETE", :record => self}],comment)
|
369
|
-
end
|
370
|
-
|
371
|
-
def create(comment=nil)
|
372
|
-
@zone.perform_actions([{:action => "CREATE", :record => self}],comment)
|
373
|
-
end
|
374
|
-
|
375
|
-
#Need to modify to a param hash
|
376
|
-
def update(name,type,ttl,values,comment=nil, zone_apex = nil)
|
377
|
-
prev = self.clone
|
378
|
-
@name = name unless name.nil?
|
379
|
-
@type = type unless type.nil?
|
380
|
-
@ttl = ttl unless ttl.nil?
|
381
|
-
@values = values unless values.nil?
|
382
|
-
@zone_apex = zone_apex unless zone_apex.nil?
|
383
|
-
@zone.perform_actions([
|
384
|
-
{:action => "DELETE", :record => prev},
|
385
|
-
{:action => "CREATE", :record => self},
|
386
|
-
],comment)
|
387
|
-
end
|
388
|
-
|
389
|
-
#Returns the raw array so the developer can update large batches manually
|
390
|
-
#Need to modify to a param hash
|
391
|
-
def update_dirty(name,type,ttl,values,zone_apex = nil)
|
392
|
-
prev = self.clone
|
393
|
-
@name = name unless name.nil?
|
394
|
-
@type = type unless type.nil?
|
395
|
-
@ttl = ttl unless ttl.nil?
|
396
|
-
@values = values unless values.nil?
|
397
|
-
@zone_apex = zone_apex unless zone_apex.nil?
|
398
|
-
return [{:action => "DELETE", :record => prev},
|
399
|
-
{:action => "CREATE", :record => self}]
|
400
|
-
end
|
401
|
-
|
402
|
-
def to_s
|
403
|
-
if @weight
|
404
|
-
"#{@name} #{@type} #{@ttl} '#{@ident}' #{@weight} #{@values.join(",")}"
|
405
|
-
elsif @zone_apex
|
406
|
-
"#{@name} #{@type} #{@zone_apex} #{@values.join(",")}"
|
407
|
-
else
|
408
|
-
"#{@name} #{@type} #{@ttl} #{@values.join(",")}"
|
409
|
-
end
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
18
|
end
|
414
|
-
|
415
|
-
$messages = { "InvalidClientTokenId" => "You may have a missing or incorrect secret or access key. Please double check your configuration files and amazon account",
|
416
|
-
"MissingAuthenticationToken" => "You may have a missing or incorrect secret or access key. Please double check your configuration files and amazon account",
|
417
|
-
"OptInRequired" => "In order to use Amazon's Route 53 service you first need to signup for it. Please see http://aws.amazon.com/route53/ for your account information and use the associated access key and secret.",
|
418
|
-
"Other" => "It looks like you've run into an unhandled error. Please send a detailed bug report with the entire input and output from the program to support@50projects.com or to https://github.com/pcorliss/ruby_route_53/issues and we'll do out best to help you.",
|
419
|
-
"SignatureDoesNotMatch" => "It looks like your secret key is incorrect or no longer valid. Please check your amazon account information for the proper key.",
|
420
|
-
"HostedZoneNotEmpty" => "You'll need to first delete the contents of this zone. You can do so using the '--remove' option as part of the command line interface.",
|
421
|
-
"InvalidChangeBatch" => "You may have tried to delete a NS or SOA record. This error is safe to ignore if you're just trying to delete all records as part of a zone prior to deleting the zone. Or you may have tried to create a record that already exists. Otherwise please file a bug by sending a detailed bug report with the entire input and output from the program to support@50projects.com or to https://github.com/pcorliss/ruby_route_53/issues and we'll do out best to help you.",
|
422
|
-
"ValidationError" => "Check over your input again to make sure the record to be created is valid. The error message should give you some hints on what went wrong. If you're still having problems please file a bug by sending a detailed bug report with the entire input and output from the program to support@50projects.com or to https://github.com/pcorliss/ruby_route_53/issues and we'll do out best to help you."}
|
423
|
-
|
424
|
-
|