gcloud 0.3.1 → 0.4.0

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.
@@ -0,0 +1,152 @@
1
+ #--
2
+ # Copyright 2015 Google Inc. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require "gcloud/dns/record/list"
17
+
18
+ module Gcloud
19
+ module Dns
20
+ ##
21
+ # = DNS Record
22
+ #
23
+ # Represents a set of DNS resource records (RRs) for a given #name and #type
24
+ # in a Zone. Since it is a value object, a newly created Record instance
25
+ # is transient until it is added to a Zone with Zone#update. Note that
26
+ # Zone#add and the Zone#update block parameter can be used instead of
27
+ # Zone#record or +Record.new+ to create new records.
28
+ #
29
+ # require "gcloud"
30
+ #
31
+ # gcloud = Gcloud.new
32
+ # dns = gcloud.dns
33
+ # zone = dns.zone "example-com"
34
+ #
35
+ # zone.records.count #=> 2
36
+ # record = zone.record "example.com.", "A", 86400, "1.2.3.4"
37
+ # zone.records.count #=> 2
38
+ # change = zone.update record
39
+ # zone.records.count #=> 3
40
+ #
41
+ #
42
+ class Record
43
+ ##
44
+ # The owner of the record. For example: +example.com.+. (+String+)
45
+ attr_accessor :name
46
+
47
+ ##
48
+ # The identifier of a {supported record type
49
+ # }[https://cloud.google.com/dns/what-is-cloud-dns#supported_record_types]
50
+ # . For example: +A+, +AAAA+, +CNAME+, +MX+, or +TXT+. (+String+)
51
+ attr_accessor :type
52
+
53
+ ##
54
+ # The number of seconds that the record can be cached by resolvers.
55
+ # (+Integer+)
56
+ attr_accessor :ttl
57
+
58
+ ##
59
+ # The array of resource record data, as determined by +type+ and defined
60
+ # in {RFC 1035 (section 5)}[http://tools.ietf.org/html/rfc1035#section-5]
61
+ # and {RFC
62
+ # 1034 (section 3.6.1)}[http://tools.ietf.org/html/rfc1034#section-3.6.1].
63
+ # For example: ["10 mail.example.com.", "20 mail2.example.com."].
64
+ # (+Array+ of +String+)
65
+ attr_accessor :data
66
+
67
+ ##
68
+ # Creates a Record value object.
69
+ #
70
+ # === Parameters
71
+ #
72
+ # +name+::
73
+ # The owner of the record. For example: +example.com.+. (+String+)
74
+ # +type+::
75
+ # The identifier of a {supported record
76
+ # type}[https://cloud.google.com/dns/what-is-cloud-dns].
77
+ # For example: +A+, +AAAA+, +CNAME+, +MX+, or +TXT+. (+String+)
78
+ # +ttl+::
79
+ # The number of seconds that the record can be cached by resolvers.
80
+ # (+Integer+)
81
+ # +data+::
82
+ # The resource record data, as determined by +type+ and defined in {RFC
83
+ # 1035 (section 5)}[http://tools.ietf.org/html/rfc1035#section-5] and
84
+ # {RFC 1034
85
+ # (section 3.6.1)}[http://tools.ietf.org/html/rfc1034#section-3.6.1].
86
+ # For example: ["10 mail.example.com.", "20 mail2.example.com."].
87
+ # (+String+ or +Array+ of +String+)
88
+ #
89
+ def initialize name, type, ttl, data
90
+ fail ArgumentError, "name is required" unless name
91
+ fail ArgumentError, "type is required" unless type
92
+ fail ArgumentError, "ttl is required" unless ttl
93
+ fail ArgumentError, "data is required" unless data
94
+ @name = name.to_s
95
+ @type = type.to_s.upcase
96
+ @ttl = Integer(ttl)
97
+ @data = Array(data)
98
+ end
99
+
100
+ ##
101
+ # Returns an array of strings in the zone file format, one
102
+ # for each element in the record's data array.
103
+ def to_zonefile_records #:nodoc:
104
+ data.map do |rrdata|
105
+ "#{name} #{ttl} IN #{type} #{rrdata}"
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Returns a deep copy of the record. Useful for updating records, since
111
+ # the original, unmodified record must be passed for deletion when using
112
+ # Zone#update.
113
+ def dup
114
+ other = super
115
+ other.data = data.map(&:dup)
116
+ other
117
+ end
118
+
119
+ ##
120
+ # New Record from a Google API Client object.
121
+ def self.from_gapi gapi #:nodoc:
122
+ new gapi["name"], gapi["type"], gapi["ttl"], gapi["rrdatas"]
123
+ end
124
+
125
+ ##
126
+ # Convert the record object to a Google API hash.
127
+ def to_gapi #:nodoc:
128
+ { "name" => name, "type" => type, "ttl" => ttl, "rrdatas" => data }
129
+ end
130
+
131
+ def hash #:nodoc:
132
+ [name, type, ttl, data].hash
133
+ end
134
+
135
+ def eql? other #:nodoc:
136
+ return false unless other.is_a? self.class
137
+ name == other.name && type == other.type &&
138
+ ttl == other.ttl && data == other.data
139
+ end
140
+
141
+ def == other #:nodoc:
142
+ self.eql? other
143
+ end
144
+
145
+ def <=> other #:nodoc:
146
+ return nil unless other.is_a? self.class
147
+ [name, type, ttl, data] <=>
148
+ [other.name, other.type, other.ttl, other.data]
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,92 @@
1
+ #--
2
+ # Copyright 2015 Google Inc. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ module Gcloud
17
+ module Dns
18
+ class Record
19
+ ##
20
+ # Record::List is a special case Array with additional values.
21
+ class List < DelegateClass(::Array)
22
+ ##
23
+ # If not empty, indicates that there are more records that match
24
+ # the request and this value should be passed to continue.
25
+ attr_accessor :token
26
+
27
+ ##
28
+ # Create a new Record::List with an array of Record instances.
29
+ def initialize arr = []
30
+ super arr
31
+ end
32
+
33
+ ##
34
+ # Whether there a next page of records.
35
+ def next?
36
+ !token.nil?
37
+ end
38
+
39
+ ##
40
+ # Retrieve the next page of records.
41
+ def next
42
+ return nil unless next?
43
+ ensure_zone!
44
+ @zone.records token: token
45
+ end
46
+
47
+ ##
48
+ # Retrieves all records by repeatedly loading pages until #next? returns
49
+ # false. Returns the list instance for method chaining.
50
+ #
51
+ # === Example
52
+ #
53
+ # require "gcloud"
54
+ #
55
+ # gcloud = Gcloud.new
56
+ # dns = gcloud.dns
57
+ # zone = dns.zone "example-com"
58
+ # records = zone.records.all # Load all pages of records
59
+ #
60
+ def all
61
+ while next?
62
+ next_records = self.next
63
+ push(*next_records)
64
+ self.token = next_records.token
65
+ end
66
+ self
67
+ end
68
+
69
+ ##
70
+ # New Records::List from a response object.
71
+ def self.from_response resp, zone #:nodoc:
72
+ records = new(Array(resp.data["rrsets"]).map do |gapi_object|
73
+ Record.from_gapi gapi_object
74
+ end)
75
+ records.instance_eval do
76
+ @token = resp.data["nextPageToken"]
77
+ @zone = zone
78
+ end
79
+ records
80
+ end
81
+
82
+ protected
83
+
84
+ ##
85
+ # Raise an error unless an active connection is available.
86
+ def ensure_zone!
87
+ fail "Must have active connection" unless @zone
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,924 @@
1
+ #--
2
+ # Copyright 2015 Google Inc. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require "gcloud/dns/change"
17
+ require "gcloud/dns/zone/transaction"
18
+ require "gcloud/dns/zone/list"
19
+ require "gcloud/dns/record"
20
+ require "gcloud/dns/importer"
21
+ require "time"
22
+
23
+ module Gcloud
24
+ module Dns
25
+ ##
26
+ # = DNS Zone
27
+ #
28
+ # The managed zone is the container for DNS records for the same DNS name
29
+ # suffix and has a set of name servers that accept and responds to queries.
30
+ # A project can have multiple managed zones, but they must each have a
31
+ # unique name.
32
+ #
33
+ # require "gcloud"
34
+ #
35
+ # gcloud = Gcloud.new
36
+ # dns = gcloud.dns
37
+ # zone = dns.zone "example-com"
38
+ # zone.records.each do |record|
39
+ # puts record.name
40
+ # end
41
+ #
42
+ # For more information, see {Managing
43
+ # Zones}[https://cloud.google.com/dns/zones/].
44
+ #
45
+ class Zone
46
+ ##
47
+ # The Connection object.
48
+ attr_accessor :connection #:nodoc:
49
+
50
+ ##
51
+ # The Google API Client object.
52
+ attr_accessor :gapi #:nodoc:
53
+
54
+ ##
55
+ # Create an empty Zone object.
56
+ def initialize #:nodoc:
57
+ @connection = nil
58
+ @gapi = {}
59
+ end
60
+
61
+ ##
62
+ # Unique identifier for the resource; defined by the server.
63
+ #
64
+ def id
65
+ @gapi["id"]
66
+ end
67
+
68
+ ##
69
+ # User assigned name for this resource. Must be unique within the project.
70
+ # The name must be 1-32 characters long, must begin with a letter, end
71
+ # with a letter or digit, and only contain lowercase letters, digits or
72
+ # dashes.
73
+ #
74
+ def name
75
+ @gapi["name"]
76
+ end
77
+
78
+ ##
79
+ # The DNS name of this managed zone, for instance "example.com.".
80
+ #
81
+ def dns
82
+ @gapi["dnsName"]
83
+ end
84
+
85
+ ##
86
+ # A string of at most 1024 characters associated with this resource for
87
+ # the user's convenience. Has no effect on the managed zone's function.
88
+ #
89
+ def description
90
+ @gapi["description"]
91
+ end
92
+
93
+ ##
94
+ # Delegate your managed_zone to these virtual name servers; defined by the
95
+ # server.
96
+ #
97
+ def name_servers
98
+ Array(@gapi["nameServers"])
99
+ end
100
+
101
+ ##
102
+ # Optionally specifies the NameServerSet for this ManagedZone. A
103
+ # NameServerSet is a set of DNS name servers that all host the same
104
+ # ManagedZones. Most users will leave this field unset.
105
+ #
106
+ def name_server_set
107
+ @gapi["nameServerSet"]
108
+ end
109
+
110
+ ##
111
+ # The time that this resource was created on the server.
112
+ #
113
+ def created_at
114
+ Time.parse @gapi["creationTime"]
115
+ rescue
116
+ nil
117
+ end
118
+
119
+ ##
120
+ # Permanently deletes the zone.
121
+ #
122
+ # === Parameters
123
+ #
124
+ # +options+::
125
+ # An optional Hash for controlling additional behavior. (+Hash+)
126
+ # <code>options[:force]</code>::
127
+ # If +true+, ensures the deletion of the zone by first deleting all
128
+ # records. If +false+ and the zone contains non-essential records, the
129
+ # request will fail. Default is +false+. (+Boolean+)
130
+ #
131
+ # === Returns
132
+ #
133
+ # +true+ if the zone was deleted.
134
+ #
135
+ # === Examples
136
+ #
137
+ # require "gcloud"
138
+ #
139
+ # gcloud = Gcloud.new
140
+ # dns = gcloud.dns
141
+ # zone = dns.zone "example-com"
142
+ # zone.delete
143
+ #
144
+ # The zone can be forcefully deleted with the +force+ option:
145
+ #
146
+ # require "gcloud"
147
+ #
148
+ # gcloud = Gcloud.new
149
+ # dns = gcloud.dns
150
+ # zone = dns.zone "example-com"
151
+ # zone.delete force: true
152
+ #
153
+ def delete options = {}
154
+ clear! if options[:force]
155
+
156
+ ensure_connection!
157
+ resp = connection.delete_zone id
158
+ if resp.success?
159
+ true
160
+ else
161
+ fail ApiError.from_response(resp)
162
+ end
163
+ end
164
+
165
+ ##
166
+ # Removes non-essential records from the zone. Only NS and SOA records
167
+ # will be kept.
168
+ #
169
+ # === Examples
170
+ #
171
+ # require "gcloud"
172
+ #
173
+ # gcloud = Gcloud.new
174
+ # dns = gcloud.dns
175
+ # zone = dns.zone "example-com"
176
+ # zone.clear!
177
+ #
178
+ def clear!
179
+ non_essential = records.all.reject { |r| %w(SOA NS).include?(r.type) }
180
+ change = update [], non_essential
181
+ change.wait_until_done! unless change.nil?
182
+ end
183
+
184
+ ##
185
+ # Retrieves an existing change by id.
186
+ #
187
+ # === Parameters
188
+ #
189
+ # +change_id+::
190
+ # The id of a change. (+String+)
191
+ #
192
+ # === Returns
193
+ #
194
+ # Gcloud::Dns::Change or +nil+ if the change does not exist
195
+ #
196
+ # === Example
197
+ #
198
+ # require "gcloud"
199
+ #
200
+ # gcloud = Gcloud.new
201
+ # dns = gcloud.dns
202
+ # zone = dns.zone "example-com"
203
+ # change = zone.change "2"
204
+ # if change
205
+ # puts "#{change.id} - #{change.started_at} - #{change.status}"
206
+ # end
207
+ #
208
+ def change change_id
209
+ ensure_connection!
210
+ resp = connection.get_change id, change_id
211
+ if resp.success?
212
+ Change.from_gapi resp.data, self
213
+ else
214
+ nil
215
+ end
216
+ end
217
+ alias_method :find_change, :change
218
+ alias_method :get_change, :change
219
+
220
+ ##
221
+ # Retrieves the list of changes belonging to the zone.
222
+ #
223
+ # === Parameters
224
+ #
225
+ # +options+::
226
+ # An optional Hash for controlling additional behavior. (+Hash+)
227
+ # <code>options[:token]</code>::
228
+ # A previously-returned page token representing part of the larger set
229
+ # of results to view. (+String+)
230
+ # <code>options[:max]</code>::
231
+ # Maximum number of changes to return. (+Integer+)
232
+ # <code>options[:order]</code>::
233
+ # Sort the changes by change sequence. (+Symbol+ or +String+)
234
+ #
235
+ # Acceptable values are:
236
+ # * +asc+ - Sort by ascending change sequence
237
+ # * +desc+ - Sort by descending change sequence
238
+ #
239
+ # === Returns
240
+ #
241
+ # Array of Gcloud::Dns::Change (Gcloud::Dns::Change::List)
242
+ #
243
+ # === Examples
244
+ #
245
+ # require "gcloud"
246
+ #
247
+ # gcloud = Gcloud.new
248
+ # dns = gcloud.dns
249
+ # zone = dns.zone "example-com"
250
+ # changes = zone.changes
251
+ # changes.each do |change|
252
+ # puts "#{change.id} - #{change.started_at} - #{change.status}"
253
+ # end
254
+ #
255
+ # The changes can be sorted by change sequence:
256
+ #
257
+ # require "gcloud"
258
+ #
259
+ # gcloud = Gcloud.new
260
+ # dns = gcloud.dns
261
+ # zone = dns.zone "example-com"
262
+ # changes = zone.changes order: :desc
263
+ #
264
+ # If you have a significant number of changes, you may need to paginate
265
+ # through them: (See Gcloud::Dns::Change::List)
266
+ #
267
+ # require "gcloud"
268
+ #
269
+ # gcloud = Gcloud.new
270
+ # dns = gcloud.dns
271
+ # zone = dns.zone "example-com"
272
+ # changes = zone.changes
273
+ # loop do
274
+ # changes.each do |change|
275
+ # puts "#{change.name} - #{change.status}"
276
+ # end
277
+ # break unless changes.next?
278
+ # changes = changes.next
279
+ # end
280
+ #
281
+ def changes options = {}
282
+ ensure_connection!
283
+ # Fix the sort options
284
+ options[:order] = adjust_change_sort_order options[:order]
285
+ options[:sort] = "changeSequence" if options[:order]
286
+ # Continue with the API call
287
+ resp = connection.list_changes id, options
288
+ if resp.success?
289
+ Change::List.from_response resp, self
290
+ else
291
+ fail ApiError.from_response(resp)
292
+ end
293
+ end
294
+ alias_method :find_changes, :changes
295
+
296
+ ##
297
+ # Retrieves the list of records belonging to the zone.
298
+ #
299
+ # === Parameters
300
+ #
301
+ # +name+::
302
+ # Return only records with this domain or subdomain name. (+String+)
303
+ # +type+::
304
+ # Return only records with this {record
305
+ # type}[https://cloud.google.com/dns/what-is-cloud-dns].
306
+ # If present, the +name+ parameter must also be present. (+String+)
307
+ # +options+::
308
+ # An optional Hash for controlling additional behavior. (+Hash+)
309
+ # <code>options[:token]</code>::
310
+ # A previously-returned page token representing part of the larger set
311
+ # of results to view. (+String+)
312
+ # <code>options[:max]</code>::
313
+ # Maximum number of records to return. (+Integer+)
314
+ #
315
+ # === Returns
316
+ #
317
+ # Array of Gcloud::Dns::Record (Gcloud::Dns::Record::List)
318
+ #
319
+ # === Examples
320
+ #
321
+ # require "gcloud"
322
+ #
323
+ # gcloud = Gcloud.new
324
+ # dns = gcloud.dns
325
+ # zone = dns.zone "example-com"
326
+ # records = zone.records
327
+ # records.each do |record|
328
+ # puts record.name
329
+ # end
330
+ #
331
+ # Records can be filtered by name and type. The name argument can be a
332
+ # subdomain (e.g., +www+) fragment for convenience, but notice that the
333
+ # retrieved record's domain name is always fully-qualified.
334
+ #
335
+ # require "gcloud"
336
+ #
337
+ # gcloud = Gcloud.new
338
+ # dns = gcloud.dns
339
+ # zone = dns.zone "example-com"
340
+ # records = zone.records "www", "A"
341
+ # records.first.name #=> "www.example.com."
342
+ #
343
+ # If you have a significant number of records, you may need to paginate
344
+ # through them: (See Gcloud::Dns::Record::List)
345
+ #
346
+ # require "gcloud"
347
+ #
348
+ # gcloud = Gcloud.new
349
+ # dns = gcloud.dns
350
+ # zone = dns.zone "example-com"
351
+ # records = zone.records "example.com."
352
+ # loop do
353
+ # records.each do |record|
354
+ # puts record.name
355
+ # end
356
+ # break unless records.next?
357
+ # records = records.next
358
+ # end
359
+ #
360
+ # Or, instead of paging manually, you can retrieve all of the pages in a
361
+ # single call: (See Gcloud::Dns::Record::List#all)
362
+ #
363
+ # require "gcloud"
364
+ #
365
+ # gcloud = Gcloud.new
366
+ # dns = gcloud.dns
367
+ # zone = dns.zone "example-com"
368
+ # records = zone.records.all
369
+ #
370
+ def records name = nil, type = nil, options = {}
371
+ ensure_connection!
372
+
373
+ options = build_records_options name, type, options
374
+
375
+ resp = connection.list_records id, options
376
+ if resp.success?
377
+ Record::List.from_response resp, self
378
+ else
379
+ fail ApiError.from_response(resp)
380
+ end
381
+ end
382
+ alias_method :find_records, :records
383
+
384
+ ##
385
+ # Creates a new, unsaved Record that can be added to a Zone.
386
+ #
387
+ # === Returns
388
+ #
389
+ # Gcloud::Dns::Record
390
+ #
391
+ # === Example
392
+ #
393
+ # require "gcloud"
394
+ #
395
+ # gcloud = Gcloud.new
396
+ # dns = gcloud.dns
397
+ # zone = dns.zone "example-com"
398
+ # record = zone.record "example.com.", "A", 86400, ["1.2.3.4"]
399
+ # zone.add record
400
+ #
401
+ def record name, type, ttl, data
402
+ Gcloud::Dns::Record.new fqdn(name), type, ttl, data
403
+ end
404
+ alias_method :new_record, :record
405
+
406
+ ##
407
+ # Exports the zone to a local {DNS zone
408
+ # file}[https://en.wikipedia.org/wiki/Zone_file].
409
+ #
410
+ # === Parameters
411
+ #
412
+ # +path+::
413
+ # The path on the local file system to write the data to.
414
+ # The path provided must be writable. (+String+)
415
+ #
416
+ # === Returns
417
+ #
418
+ # +::File+ object on the local file system
419
+ #
420
+ # === Examples
421
+ #
422
+ # require "gcloud"
423
+ #
424
+ # gcloud = Gcloud.new
425
+ # dns = gcloud.dns
426
+ # zone = dns.zone "example-com"
427
+ #
428
+ # zone.export "path/to/db.example.com"
429
+ #
430
+ def export path
431
+ File.open path, "w" do |f|
432
+ f.write to_zonefile
433
+ end
434
+ end
435
+
436
+ ##
437
+ # Imports resource records from a {DNS zone
438
+ # file}[https://en.wikipedia.org/wiki/Zone_file], adding the new records
439
+ # to the zone, without removing any existing records from the zone.
440
+ #
441
+ # Because the Google Cloud DNS API only accepts a single resource record
442
+ # for each +name+ and +type+ combination (with multiple +data+ elements),
443
+ # the zone file's records are merged as necessary. During this merge, the
444
+ # lowest +ttl+ of the merged records is used. If none of the merged
445
+ # records have a +ttl+ value, the zone file's global TTL is used for the
446
+ # record.
447
+ #
448
+ # The zone file's SOA and NS records are not imported, because the zone
449
+ # was given SOA and NS records when it was created. These generated
450
+ # records point to Cloud DNS name servers.
451
+ #
452
+ # This operation automatically updates the SOA record serial number unless
453
+ # prevented with the +skip_soa+ option. See #update for details.
454
+ #
455
+ # The Google Cloud DNS service requires that record names and data use
456
+ # fully-qualified addresses. The @ symbol is not accepted, nor are
457
+ # unqualified subdomain addresses like www. If your zone file contains
458
+ # such values, you may need to pre-process it in order for the import
459
+ # operation to succeed.
460
+ #
461
+ # === Parameters
462
+ #
463
+ # +path_or_io+::
464
+ # The path to a zone file on the filesystem, or an IO instance from
465
+ # which zone file data can be read. (+String+ or +IO+)
466
+ # +options+::
467
+ # An optional Hash for controlling additional behavior. (+Hash+)
468
+ # <code>options[:only]</code>::
469
+ # Include only records of this type or types. (+String+ or +Array+)
470
+ # <code>options[:except]</code>::
471
+ # Exclude records of this type or types. (+String+ or +Array+)
472
+ # <code>options[:skip_soa]</code>::
473
+ # Do not automatically update the SOA record serial number. See #update
474
+ # for details. (+Boolean+)
475
+ # <code>options[:soa_serial]</code>::
476
+ # A value (or a lambda or Proc returning a value) for the new SOA record
477
+ # serial number. See #update for details. (+Integer+, lambda, or +Proc+)
478
+ #
479
+ # === Returns
480
+ #
481
+ # A new Change adding the imported Record instances.
482
+ #
483
+ # === Example
484
+ #
485
+ # require "gcloud"
486
+ #
487
+ # gcloud = Gcloud.new
488
+ # dns = gcloud.dns
489
+ # zone = dns.zone "example-com"
490
+ # change = zone.import "path/to/db.example.com"
491
+ #
492
+ def import path_or_io, options = {}
493
+ options[:except] = Array(options[:except]).map(&:to_s).map(&:upcase)
494
+ options[:except] = (options[:except] + %w(SOA NS)).uniq
495
+ additions = Gcloud::Dns::Importer.new(self, path_or_io).records(options)
496
+ update additions, []
497
+ end
498
+
499
+ # rubocop:disable all
500
+ # Disabled rubocop because this complexity cannot easily be avoided.
501
+
502
+ ##
503
+ # Adds and removes Records from the zone. All changes are made in a single
504
+ # API request.
505
+ #
506
+ # If the SOA record for the zone is not present in +additions+ or
507
+ # +deletions+ (and if present in one, it should be present in the other),
508
+ # it will be added to both, and its serial number will be incremented by
509
+ # adding +1+. This update to the SOA record can be prevented with the
510
+ # +skip_soa+ option. To provide your own value or behavior for the new
511
+ # serial number, use the +soa_serial+ option.
512
+ #
513
+ # === Parameters
514
+ #
515
+ # +additions+::
516
+ # The Record or array of records to add. (Record or +Array+)
517
+ # +deletions+::
518
+ # The Record or array of records to remove. (Record or +Array+)
519
+ # +options+::
520
+ # An optional Hash for controlling additional behavior. (+Hash+)
521
+ # <code>options[:skip_soa]</code>::
522
+ # Do not automatically update the SOA record serial number. (+Boolean+)
523
+ # <code>options[:soa_serial]</code>::
524
+ # A value (or a lambda or Proc returning a value) for the new SOA record
525
+ # serial number. (+Integer+, lambda, or +Proc+)
526
+ #
527
+ # === Returns
528
+ #
529
+ # Gcloud::Dns::Change
530
+ #
531
+ # === Examples
532
+ #
533
+ # The best way to add, remove, and update multiple records in a single
534
+ # {transaction}[https://cloud.google.com/dns/records] is with a block. See
535
+ # Zone::Transaction.
536
+ #
537
+ # require "gcloud"
538
+ #
539
+ # gcloud = Gcloud.new
540
+ # dns = gcloud.dns
541
+ # zone = dns.zone "example-com"
542
+ # change = zone.update do |tx|
543
+ # tx.add "example.com.", "A", 86400, "1.2.3.4"
544
+ # tx.remove "example.com.", "TXT"
545
+ # tx.replace "example.com.", "MX", 86400, ["10 mail1.example.com.",
546
+ # "20 mail2.example.com."]
547
+ # tx.modify "www.example.com.", "CNAME" do |cname|
548
+ # cname.ttl = 86400 # only change the TTL
549
+ # end
550
+ # end
551
+ #
552
+ # Or you can provide the record objects to add and remove.
553
+ #
554
+ # require "gcloud"
555
+ #
556
+ # gcloud = Gcloud.new
557
+ # dns = gcloud.dns
558
+ # zone = dns.zone "example-com"
559
+ # new_record = zone.record "example.com.", "A", 86400, ["1.2.3.4"]
560
+ # old_record = zone.record "example.com.", "A", 18600, ["1.2.3.4"]
561
+ # change = zone.update [new_record], [old_record]
562
+ #
563
+ # You can provide a lambda or Proc that receives the current SOA record
564
+ # serial number and returns a new serial number.
565
+ #
566
+ # require "gcloud"
567
+ #
568
+ # gcloud = Gcloud.new
569
+ # dns = gcloud.dns
570
+ # zone = dns.zone "example-com"
571
+ # new_record = zone.record "example.com.", "A", 86400, ["1.2.3.4"]
572
+ # change = zone.update new_record, soa_serial: lambda { |sn| sn + 10 }
573
+ #
574
+ def update additions = [], deletions = [], options = {}
575
+ # Handle only sending in options
576
+ if additions.is_a?(::Hash) && deletions.empty? && options.empty?
577
+ options = additions
578
+ additions = []
579
+ elsif deletions.is_a?(::Hash) && options.empty?
580
+ options = deletions
581
+ deletions = []
582
+ end
583
+
584
+ additions = Array additions
585
+ deletions = Array deletions
586
+
587
+ if block_given?
588
+ updater = Zone::Transaction.new self
589
+ yield updater
590
+ additions += updater.additions
591
+ deletions += updater.deletions
592
+ end
593
+
594
+ to_add = additions - deletions
595
+ to_remove = deletions - additions
596
+ return nil if to_add.empty? && to_remove.empty?
597
+ unless options[:skip_soa] || detect_soa(to_add) || detect_soa(to_remove)
598
+ increment_soa to_add, to_remove, options[:soa_serial]
599
+ end
600
+ create_change to_add, to_remove
601
+ end
602
+
603
+ # rubocop:enable all
604
+
605
+ ##
606
+ # Adds a record to the Zone. In order to update existing records, or add
607
+ # and delete records in the same transaction, use #update.
608
+ #
609
+ # This operation automatically updates the SOA record serial number unless
610
+ # prevented with the +skip_soa+ option. See #update for details.
611
+ #
612
+ # === Parameters
613
+ #
614
+ # +name+::
615
+ # The owner of the record. For example: +example.com.+. (+String+)
616
+ # +type+::
617
+ # The identifier of a {supported record
618
+ # type}[https://cloud.google.com/dns/what-is-cloud-dns].
619
+ # For example: +A+, +AAAA+, +CNAME+, +MX+, or +TXT+. (+String+)
620
+ # +ttl+::
621
+ # The number of seconds that the record can be cached by resolvers.
622
+ # (+Integer+)
623
+ # +data+::
624
+ # The resource record data, as determined by +type+ and defined in {RFC
625
+ # 1035 (section 5)}[http://tools.ietf.org/html/rfc1035#section-5] and
626
+ # {RFC 1034
627
+ # (section 3.6.1)}[http://tools.ietf.org/html/rfc1034#section-3.6.1].
628
+ # For example: +192.0.2.1+ or +example.com.+. (+String+ or +Array+ of
629
+ # +String+)
630
+ # +options+::
631
+ # An optional Hash for controlling additional behavior. (+Hash+)
632
+ # <code>options[:skip_soa]</code>::
633
+ # Do not automatically update the SOA record serial number. See #update
634
+ # for details. (+Boolean+)
635
+ # <code>options[:soa_serial]</code>::
636
+ # A value (or a lambda or Proc returning a value) for the new SOA record
637
+ # serial number. See #update for details. (+Integer+, lambda, or +Proc+)
638
+ #
639
+ # === Returns
640
+ #
641
+ # Gcloud::Dns::Change
642
+ #
643
+ # === Example
644
+ #
645
+ # require "gcloud"
646
+ #
647
+ # gcloud = Gcloud.new
648
+ # dns = gcloud.dns
649
+ # zone = dns.zone "example-com"
650
+ # change = zone.add "example.com.", "A", 86400, ["1.2.3.4"]
651
+ #
652
+ def add name, type, ttl, data, options = {}
653
+ update [record(name, type, ttl, data)], [], options
654
+ end
655
+
656
+ ##
657
+ # Removes records from the Zone. The records are looked up before they are
658
+ # removed. In order to update existing records, or add and remove records
659
+ # in the same transaction, use #update.
660
+ #
661
+ # This operation automatically updates the SOA record serial number unless
662
+ # prevented with the +skip_soa+ option. See #update for details.
663
+ #
664
+ # === Parameters
665
+ #
666
+ # +name+::
667
+ # The owner of the record. For example: +example.com.+. (+String+)
668
+ # +type+::
669
+ # The identifier of a {supported record
670
+ # type}[https://cloud.google.com/dns/what-is-cloud-dns].
671
+ # For example: +A+, +AAAA+, +CNAME+, +MX+, or +TXT+. (+String+)
672
+ # +options+::
673
+ # An optional Hash for controlling additional behavior. (+Hash+)
674
+ # <code>options[:skip_soa]</code>::
675
+ # Do not automatically update the SOA record serial number. See #update
676
+ # for details. (+Boolean+)
677
+ # <code>options[:soa_serial]</code>::
678
+ # A value (or a lambda or Proc returning a value) for the new SOA record
679
+ # serial number. See #update for details. (+Integer+, lambda, or +Proc+)
680
+ #
681
+ # === Returns
682
+ #
683
+ # Gcloud::Dns::Change
684
+ #
685
+ # === Example
686
+ #
687
+ # require "gcloud"
688
+ #
689
+ # gcloud = Gcloud.new
690
+ # dns = gcloud.dns
691
+ # zone = dns.zone "example-com"
692
+ # change = zone.remove "example.com.", "A"
693
+ #
694
+ def remove name, type, options = {}
695
+ update [], records(name: name, type: type).all.to_a, options
696
+ end
697
+
698
+ ##
699
+ # Replaces existing records on the Zone. Records matching the +name+ and
700
+ # +type+ are replaced. In order to update existing records, or add and
701
+ # delete records in the same transaction, use #update.
702
+ #
703
+ # This operation automatically updates the SOA record serial number unless
704
+ # prevented with the +skip_soa+ option. See #update for details.
705
+ #
706
+ # === Parameters
707
+ #
708
+ # +name+::
709
+ # The owner of the record. For example: +example.com.+. (+String+)
710
+ # +type+::
711
+ # The identifier of a {supported record
712
+ # type}[https://cloud.google.com/dns/what-is-cloud-dns].
713
+ # For example: +A+, +AAAA+, +CNAME+, +MX+, or +TXT+. (+String+)
714
+ # +ttl+::
715
+ # The number of seconds that the record can be cached by resolvers.
716
+ # (+Integer+)
717
+ # +data+::
718
+ # The resource record data, as determined by +type+ and defined in
719
+ # {RFC 1035 (section 5)}[http://tools.ietf.org/html/rfc1035#section-5]
720
+ # and {RFC 1034 (section
721
+ # 3.6.1)}[http://tools.ietf.org/html/rfc1034#section-3.6.1]. For
722
+ # example: +192.0.2.1+ or +example.com.+. (+String+ or +Array+ of
723
+ # +String+)
724
+ # +options+::
725
+ # An optional Hash for controlling additional behavior. (+Hash+)
726
+ # <code>options[:skip_soa]</code>::
727
+ # Do not automatically update the SOA record serial number. See #update
728
+ # for details. (+Boolean+)
729
+ # <code>options[:soa_serial]</code>::
730
+ # A value (or a lambda or Proc returning a value) for the new SOA record
731
+ # serial number. See #update for details. (+Integer+, lambda, or +Proc+)
732
+ #
733
+ # === Returns
734
+ #
735
+ # Gcloud::Dns::Change
736
+ #
737
+ # === Example
738
+ #
739
+ # require "gcloud"
740
+ #
741
+ # gcloud = Gcloud.new
742
+ # dns = gcloud.dns
743
+ # zone = dns.zone "example-com"
744
+ # change = zone.replace "example.com.", "A", 86400, ["5.6.7.8"]
745
+ #
746
+ def replace name, type, ttl, data, options = {}
747
+ update [record(name, type, ttl, data)],
748
+ records(name: name, type: type).all.to_a,
749
+ options
750
+ end
751
+
752
+ def to_zonefile #:nodoc:
753
+ records.all.map(&:to_zonefile_records).flatten.join("\n")
754
+ end
755
+
756
+ ##
757
+ # Modifies records on the Zone. Records matching the +name+ and +type+ are
758
+ # yielded to the block where they can be modified.
759
+ #
760
+ # This operation automatically updates the SOA record serial number unless
761
+ # prevented with the +skip_soa+ option. See #update for details.
762
+ #
763
+ # === Parameters
764
+ #
765
+ # +name+::
766
+ # The owner of the record. For example: +example.com.+. (+String+)
767
+ # +type+::
768
+ # The identifier of a {supported record
769
+ # type}[https://cloud.google.com/dns/what-is-cloud-dns].
770
+ # For example: +A+, +AAAA+, +CNAME+, +MX+, or +TXT+. (+String+)
771
+ # +options+::
772
+ # An optional Hash for controlling additional behavior. (+Hash+)
773
+ # <code>options[:skip_soa]</code>::
774
+ # Do not automatically update the SOA record serial number. See #update
775
+ # for details. (+Boolean+)
776
+ # <code>options[:soa_serial]</code>::
777
+ # A value (or a lambda or Proc returning a value) for the new SOA record
778
+ # serial number. See #update for details. (+Integer+, lambda, or +Proc+)
779
+ #
780
+ # === Returns
781
+ #
782
+ # Gcloud::Dns::Change
783
+ #
784
+ # === Example
785
+ #
786
+ # require "gcloud"
787
+ #
788
+ # gcloud = Gcloud.new
789
+ # dns = gcloud.dns
790
+ # zone = dns.zone "example-com"
791
+ # change = zone.modify "example.com.", "MX" do |mx|
792
+ # mx.ttl = 3600 # change only the TTL
793
+ # end
794
+ #
795
+ def modify name, type, options = {}
796
+ existing = records(name: name, type: type).all.to_a
797
+ updated = existing.map(&:dup)
798
+ updated.each { |r| yield r }
799
+ update updated, existing, options
800
+ end
801
+
802
+ ##
803
+ # This helper converts the given domain name or subdomain (e.g., +www+)
804
+ # fragment to a {fully qualified domain name
805
+ # (FQDN)}[https://en.wikipedia.org/wiki/Fully_qualified_domain_name] for
806
+ # the zone's #dns. If the argument is already a FQDN, it is returned
807
+ # unchanged.
808
+ #
809
+ # === Parameters
810
+ #
811
+ # +domain_name+::
812
+ # The name to convert to a fully qualified domain name. (+String+)
813
+ #
814
+ # === Returns
815
+ #
816
+ # A fully qualified domain name. (+String+)
817
+ #
818
+ # === Examples
819
+ #
820
+ # require "gcloud"
821
+ #
822
+ # gcloud = Gcloud.new
823
+ # dns = gcloud.dns
824
+ # zone = dns.zone "example-com"
825
+ # zone.fqdn "www" #=> "www.example.com."
826
+ # zone.fqdn "@" #=> "example.com."
827
+ # zone.fqdn "mail.example.com." #=> "mail.example.com."
828
+ #
829
+ def fqdn domain_name
830
+ Connection.fqdn domain_name, dns
831
+ end
832
+
833
+ ##
834
+ # New Zone from a Google API Client object.
835
+ def self.from_gapi gapi, conn #:nodoc:
836
+ new.tap do |f|
837
+ f.gapi = gapi
838
+ f.connection = conn
839
+ end
840
+ end
841
+
842
+ protected
843
+
844
+ ##
845
+ # Raise an error unless an active connection is available.
846
+ def ensure_connection!
847
+ fail "Must have active connection" unless connection
848
+ end
849
+
850
+ # rubocop:disable all
851
+ # Disabled rubocop because this complexity cannot easily be avoided.
852
+
853
+ def build_records_options name, type, options
854
+ # Handle only sending in options
855
+ if name.is_a?(::Hash) && type.nil? && options.empty?
856
+ options = name
857
+ name = nil
858
+ elsif type.is_a?(::Hash) && options.empty?
859
+ options = type
860
+ type = nil
861
+ end
862
+
863
+ # Set parameters as options, params have priority
864
+ options[:name] = name || options[:name]
865
+ options[:type] = type || options[:type]
866
+
867
+ # Ensure name is a FQDN
868
+ options[:name] = fqdn(options[:name]) if options[:name]
869
+
870
+ # return only the options
871
+ options
872
+ end
873
+
874
+ # rubocop:enable all
875
+
876
+ def create_change additions, deletions
877
+ ensure_connection!
878
+ resp = connection.create_change id,
879
+ additions.map(&:to_gapi),
880
+ deletions.map(&:to_gapi)
881
+ if resp.success?
882
+ Change.from_gapi resp.data, self
883
+ else
884
+ fail ApiError.from_response(resp)
885
+ end
886
+ end
887
+
888
+ def increment_soa to_add, to_remove, soa_serial
889
+ current_soa = detect_soa records(name: dns, type: "SOA").all
890
+ return false if current_soa.nil?
891
+ updated_soa = current_soa.dup
892
+ updated_soa.data[0] = replace_soa_serial updated_soa.data[0], soa_serial
893
+ to_add << updated_soa
894
+ to_remove << current_soa
895
+ end
896
+
897
+ def detect_soa records
898
+ records.detect { |r| r.type == "SOA" }
899
+ end
900
+
901
+ def replace_soa_serial soa_data, soa_serial
902
+ soa_data = soa_data.split " "
903
+ current_serial = soa_data[2].to_i
904
+ soa_data[2] = if soa_serial && soa_serial.respond_to?(:call)
905
+ soa_serial.call current_serial
906
+ elsif soa_serial
907
+ soa_serial.to_i
908
+ else
909
+ current_serial + 1
910
+ end
911
+ soa_data.join " "
912
+ end
913
+
914
+ def adjust_change_sort_order order
915
+ return nil if order.nil?
916
+ if order.to_s.downcase.start_with? "d"
917
+ "descending"
918
+ else
919
+ "ascending"
920
+ end
921
+ end
922
+ end
923
+ end
924
+ end