gcloud 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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