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,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