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,195 @@
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 "zonefile"
17
+ require "gcloud/dns/record"
18
+
19
+ module Gcloud
20
+ module Dns
21
+ ##
22
+ # = DNS Importer
23
+ #
24
+ # Reads a {DNS zone
25
+ # file}[https://en.wikipedia.org/wiki/Zone_file] and parses it, creating a
26
+ # collection of Record instances. The returned records are unsaved,
27
+ # as they are not yet associated with a Zone. Use Zone#import to add zone
28
+ # file records to a Zone.
29
+ #
30
+ # Because the Google Cloud DNS API only accepts a single resource record for
31
+ # each +name+ and +type+ combination (with multiple +data+ elements), the
32
+ # zone file's records are merged as necessary. During this merge, the lowest
33
+ # +ttl+ of the merged records is used. If none of the merged records have a
34
+ # +ttl+ value, the zone file's global TTL is used for the record.
35
+ #
36
+ # The following record types are supported: A, AAAA, CNAME, MX, NAPTR, NS,
37
+ # PTR, SOA, SPF, SRV, and TXT.
38
+ class Importer #:nodoc:
39
+ ##
40
+ # Creates a new Importer that immediately parses the provided zone file
41
+ # data and creates Record instances.
42
+ #
43
+ # === Parameters
44
+ #
45
+ # +path_or_io+::
46
+ # The path to a zone file on the filesystem, or an IO instance from
47
+ # which zone file data can be read. (+String+ or +IO+)
48
+ #
49
+ def initialize zone, path_or_io
50
+ @zone = zone
51
+ @merged_zf_records = {}
52
+ @records = []
53
+ @zonefile = create_zonefile path_or_io
54
+ merge_zonefile_records
55
+ from_zonefile_records
56
+ @records.unshift soa_record
57
+ end
58
+
59
+ ##
60
+ # Returns the Record instances created from the zone file.
61
+ #
62
+ # === Parameters
63
+ #
64
+ # +options+::
65
+ # An optional Hash for controlling additional behavior. (+Hash+)
66
+ # <code>options[:only]</code>::
67
+ # Include only records of this type or types. (+String+ or +Array+)
68
+ # <code>options[:except]</code>::
69
+ # Exclude records of this type or types. (+String+ or +Array+)
70
+ #
71
+ # === Returns
72
+ #
73
+ # An array of unsaved Record instances.
74
+ #
75
+ def records options = {}
76
+ filtered_records options[:only], options[:except]
77
+ end
78
+
79
+ protected
80
+
81
+ def filtered_records only, except
82
+ ret = @records
83
+ ret = ret.select { |r| Array(only).include? r.type } if only
84
+ ret = ret.reject { |r| Array(except).include? r.type } if except
85
+ ret
86
+ end
87
+
88
+ ##
89
+ # The zonefile library returns a two-element array in which the first
90
+ # element is a symbol type (:a, :mx, and so on), and the second element
91
+ # is an array containing the records of that type. Group the records by
92
+ # name and type instead.
93
+ def merge_zonefile_records
94
+ @zonefile.records.map do |r|
95
+ type = r.first
96
+ type = :aaaa if type == :a4
97
+ r.last.each do |zf_record|
98
+ name = Connection.fqdn(zf_record[:name], @zonefile.origin)
99
+ key = [name, type]
100
+ (@merged_zf_records[key] ||= []) << zf_record
101
+ end
102
+ end
103
+ end
104
+
105
+ ##
106
+ # Convert the grouped records to single array of records, merging records
107
+ # of the same name and type into a single record with an array of rrdatas.
108
+ def from_zonefile_records
109
+ @records = @merged_zf_records.map do |key, zf_records|
110
+ ttl = ttl_from_zonefile_records zf_records
111
+ data = zf_records.map do |zf_record|
112
+ data_from_zonefile_record(key[1], zf_record)
113
+ end
114
+ @zone.record key[0], key[1], ttl, data
115
+ end
116
+ end
117
+
118
+ def soa_record
119
+ zf_soa = @zonefile.soa
120
+ ttl = ttl_to_i(zf_soa[:ttl]) || ttl_to_i(@zonefile.ttl)
121
+ data = data_from_zonefile_record :soa, zf_soa
122
+ @zone.record zf_soa[:origin], "SOA", ttl, data
123
+ end
124
+
125
+ ##
126
+ # From a collection of records, take the lowest ttl
127
+ def ttl_from_zonefile_records zf_records
128
+ ttls = zf_records.map do |zf_record|
129
+ ttl_to_i(zf_record[:ttl])
130
+ end
131
+ min_ttl = ttls.compact.sort.first
132
+ min_ttl || ttl_to_i(@zonefile.ttl)
133
+ end
134
+
135
+ # rubocop:disable all
136
+ # Rubocop's line-length and branch condition restrictions prevent
137
+ # the most straightforward approach to converting zonefile's records
138
+ # to our own. So disable rubocop for this operation.
139
+
140
+ def data_from_zonefile_record type, zf_record
141
+ case type.to_s.upcase
142
+ when "A"
143
+ "#{zf_record[:host]}"
144
+ when "AAAA"
145
+ "#{zf_record[:host]}"
146
+ when "CNAME"
147
+ "#{zf_record[:host]}"
148
+ when "MX"
149
+ "#{zf_record[:pri]} #{zf_record[:host]}"
150
+ when "NAPTR"
151
+ "#{zf_record[:order]} #{zf_record[:preference]} #{zf_record[:flags]} #{zf_record[:service]} #{zf_record[:regexp]} #{zf_record[:replacement]}"
152
+ when "NS"
153
+ "#{zf_record[:host]}"
154
+ when "PTR"
155
+ "#{zf_record[:host]}"
156
+ when "SOA"
157
+ "#{zf_record[:primary]} #{zf_record[:email]} #{zf_record[:serial]} #{zf_record[:refresh]} #{zf_record[:retry]} #{zf_record[:expire]} #{zf_record[:minimumTTL]}"
158
+ when "SPF"
159
+ "#{zf_record[:data]}"
160
+ when "SRV"
161
+ "#{zf_record[:pri]} #{zf_record[:weight]} #{zf_record[:port]} #{zf_record[:host]}"
162
+ when "TXT"
163
+ "#{zf_record[:text]}"
164
+ else
165
+ fail ArgumentError, "record type '#{type}' is not supported"
166
+ end
167
+ end
168
+
169
+ # rubocop:enable all
170
+
171
+ MULTIPLIER = { "s" => (1),
172
+ "m" => (60),
173
+ "h" => (60 * 60),
174
+ "d" => (60 * 60 * 24),
175
+ "w" => (60 * 60 * 24 * 7) } # :nodoc:
176
+
177
+ def ttl_to_i ttl
178
+ if ttl.respond_to?(:to_int) || ttl.to_s =~ /\A\d+\z/
179
+ return ttl.to_i
180
+ elsif (m = /\A(\d+)(w|d|h|m|s)\z/.match ttl)
181
+ return m[1].to_i * MULTIPLIER[m[2]].to_i
182
+ end
183
+ nil
184
+ end
185
+
186
+ def create_zonefile path_or_io # :nodoc:
187
+ if path_or_io.respond_to? :read
188
+ Zonefile.new path_or_io.read
189
+ else
190
+ Zonefile.from_file path_or_io
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,291 @@
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/gce"
17
+ require "gcloud/dns/connection"
18
+ require "gcloud/dns/credentials"
19
+ require "gcloud/dns/zone"
20
+ require "gcloud/dns/errors"
21
+
22
+ module Gcloud
23
+ module Dns
24
+ ##
25
+ # = Project
26
+ #
27
+ # The project is a top level container for resources including Cloud DNS
28
+ # ManagedZones. Projects can be created only in the {Google Developers
29
+ # Console}[https://console.developers.google.com].
30
+ #
31
+ # require "gcloud"
32
+ #
33
+ # gcloud = Gcloud.new
34
+ # dns = gcloud.dns
35
+ # zone = dns.zone "example-com"
36
+ # zone.records.each do |record|
37
+ # puts record.name
38
+ # end
39
+ #
40
+ # See Gcloud#dns
41
+ class Project
42
+ ##
43
+ # The Connection object.
44
+ attr_accessor :connection #:nodoc:
45
+
46
+ ##
47
+ # The Google API Client object.
48
+ attr_accessor :gapi #:nodoc:
49
+
50
+ ##
51
+ # Creates a new Connection instance.
52
+ #
53
+ # See Gcloud.dns
54
+ def initialize project, credentials #:nodoc:
55
+ project = project.to_s # Always cast to a string
56
+ fail ArgumentError, "project is missing" if project.empty?
57
+ @connection = Connection.new project, credentials
58
+ @gapi = nil
59
+ end
60
+
61
+ ##
62
+ # The unique ID string for the current project.
63
+ #
64
+ # === Example
65
+ #
66
+ # require "gcloud"
67
+ #
68
+ # gcloud = Gcloud.new "my-todo-project", "/path/to/keyfile.json"
69
+ # dns = gcloud.dns
70
+ #
71
+ # dns.project #=> "my-todo-project"
72
+ #
73
+ def project
74
+ connection.project
75
+ end
76
+ alias_method :id, :project
77
+
78
+ ##
79
+ # The project number.
80
+ def number
81
+ reload! if @gapi.nil?
82
+ @gapi["number"]
83
+ end
84
+
85
+ ##
86
+ # Maximum allowed number of zones in the project.
87
+ def zones_quota
88
+ reload! if @gapi.nil?
89
+ @gapi["quota"]["managedZones"] if @gapi["quota"]
90
+ end
91
+
92
+ ##
93
+ # Maximum allowed number of data entries per record.
94
+ def data_per_record
95
+ reload! if @gapi.nil?
96
+ @gapi["quota"]["resourceRecordsPerRrset"] if @gapi["quota"]
97
+ end
98
+
99
+ ##
100
+ # Maximum allowed number of records to add per change.
101
+ def additions_per_change
102
+ reload! if @gapi.nil?
103
+ @gapi["quota"]["rrsetAdditionsPerChange"] if @gapi["quota"]
104
+ end
105
+
106
+ ##
107
+ # Maximum allowed number of records to delete per change.
108
+ def deletions_per_change
109
+ reload! if @gapi.nil?
110
+ @gapi["quota"]["rrsetDeletionsPerChange"] if @gapi["quota"]
111
+ end
112
+
113
+ ##
114
+ # Maximum allowed number of records per zone in the project.
115
+ def records_per_zone
116
+ reload! if @gapi.nil?
117
+ @gapi["quota"]["rrsetsPerManagedZone"] if @gapi["quota"]
118
+ end
119
+
120
+ ##
121
+ # Maximum allowed total bytes size for all the data in one change.
122
+ def total_data_per_change
123
+ reload! if @gapi.nil?
124
+ @gapi["quota"]["totalRrdataSizePerChange"] if @gapi["quota"]
125
+ end
126
+
127
+ ##
128
+ # Default project.
129
+ def self.default_project #:nodoc:
130
+ ENV["DNS_PROJECT"] ||
131
+ ENV["GCLOUD_PROJECT"] ||
132
+ ENV["GOOGLE_CLOUD_PROJECT"] ||
133
+ Gcloud::GCE.project_id
134
+ end
135
+
136
+ ##
137
+ # Retrieves an existing zone by name or id.
138
+ #
139
+ # === Parameters
140
+ #
141
+ # +zone_id+::
142
+ # The name or id of a zone. (+String+ or +Integer+)
143
+ #
144
+ # === Returns
145
+ #
146
+ # Gcloud::Dns::Zone or +nil+ if the zone does not exist
147
+ #
148
+ # === Example
149
+ #
150
+ # require "gcloud"
151
+ #
152
+ # gcloud = Gcloud.new
153
+ # dns = gcloud.dns
154
+ # zone = dns.zone "example-com"
155
+ # puts zone.name
156
+ #
157
+ def zone zone_id
158
+ ensure_connection!
159
+ resp = connection.get_zone zone_id
160
+ if resp.success?
161
+ Zone.from_gapi resp.data, connection
162
+ else
163
+ nil
164
+ end
165
+ end
166
+ alias_method :find_zone, :zone
167
+ alias_method :get_zone, :zone
168
+
169
+ ##
170
+ # Retrieves the list of zones belonging to the project.
171
+ #
172
+ # === Parameters
173
+ #
174
+ # +options+::
175
+ # An optional Hash for controlling additional behavior. (+Hash+)
176
+ # <code>options[:token]</code>::
177
+ # A previously-returned page token representing part of the larger set
178
+ # of results to view. (+String+)
179
+ # <code>options[:max]</code>::
180
+ # Maximum number of zones to return. (+Integer+)
181
+ #
182
+ # === Returns
183
+ #
184
+ # Array of Gcloud::Dns::Zone (Gcloud::Dns::Zone::List)
185
+ #
186
+ # === Examples
187
+ #
188
+ # require "gcloud"
189
+ #
190
+ # gcloud = Gcloud.new
191
+ # dns = gcloud.dns
192
+ # zones = dns.zones
193
+ # zones.each do |zone|
194
+ # puts zone.name
195
+ # end
196
+ #
197
+ # If you have a significant number of zones, you may need to paginate
198
+ # through them: (See Gcloud::Dns::Zone::List)
199
+ #
200
+ # require "gcloud"
201
+ #
202
+ # gcloud = Gcloud.new
203
+ # dns = gcloud.dns
204
+ # zones = dns.zones
205
+ # loop do
206
+ # zones.each do |zone|
207
+ # puts zone.name
208
+ # end
209
+ # break unless zones.next?
210
+ # zones = zones.next
211
+ # end
212
+ #
213
+ def zones options = {}
214
+ ensure_connection!
215
+ resp = connection.list_zones options
216
+ if resp.success?
217
+ Zone::List.from_response resp, connection
218
+ else
219
+ fail ApiError.from_response(resp)
220
+ end
221
+ end
222
+ alias_method :find_zones, :zones
223
+
224
+ ##
225
+ # Creates a new zone.
226
+ #
227
+ # === Parameters
228
+ #
229
+ # +zone_name+::
230
+ # User assigned name for this resource. Must be unique within the
231
+ # project. The name must be 1-32 characters long, must begin with a
232
+ # letter, end with a letter or digit, and only contain lowercase
233
+ # letters, digits or dashes. (+String+)
234
+ # +zone_dns+::
235
+ # The DNS name of this managed zone, for instance "example.com.".
236
+ # (+String+)
237
+ # +options+::
238
+ # An optional Hash for controlling additional behavior. (+Hash+)
239
+ # <code>options[:description]</code>::
240
+ # A string of at most 1024 characters associated with this resource for
241
+ # the user's convenience. Has no effect on the managed zone's function.
242
+ # (+String+)
243
+ # <code>options[:name_server_set]</code>::
244
+ # A NameServerSet is a set of DNS name servers that all host the same
245
+ # ManagedZones. Most users will leave this field unset. (+String+)
246
+ #
247
+ # === Returns
248
+ #
249
+ # Gcloud::Dns::Zone
250
+ #
251
+ # === Examples
252
+ #
253
+ # require "gcloud"
254
+ #
255
+ # gcloud = Gcloud.new
256
+ # dns = gcloud.dns
257
+ # zone = dns.create_zone "example-com", "example.com."
258
+ #
259
+ def create_zone zone_name, zone_dns, options = {}
260
+ ensure_connection!
261
+ resp = connection.create_zone zone_name, zone_dns, options
262
+ if resp.success?
263
+ Zone.from_gapi resp.data, connection
264
+ else
265
+ fail ApiError.from_response(resp)
266
+ end
267
+ end
268
+
269
+ ##
270
+ # Reloads the change with updated status from the DNS service.
271
+ def reload!
272
+ ensure_connection!
273
+ resp = connection.get_project
274
+ if resp.success?
275
+ @gapi = resp.data
276
+ else
277
+ fail ApiError.from_response(resp)
278
+ end
279
+ end
280
+ alias_method :refresh!, :reload!
281
+
282
+ protected
283
+
284
+ ##
285
+ # Raise an error unless an active connection is available.
286
+ def ensure_connection!
287
+ fail "Must have active connection" unless connection
288
+ end
289
+ end
290
+ end
291
+ end