dynect4r 0.2.1

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.
Files changed (5) hide show
  1. data/LICENSE +15 -0
  2. data/README.rdoc +56 -0
  3. data/bin/dynect4r-client +173 -0
  4. data/lib/dynect4r.rb +221 -0
  5. metadata +100 -0
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Copyright (c) 2010 Michael Conigliaro <mike [at] conigliaro [dot] org>
2
+
3
+ This program is free software; you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation; either version 2 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License
14
+ along with this program; if not, write to the Free Software
15
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
data/README.rdoc ADDED
@@ -0,0 +1,56 @@
1
+ = dynect4r
2
+
3
+ dynect4r is a Ruby library and command line client for the Dynect REST API (version 2).
4
+
5
+ == Installation
6
+
7
+ gem install dynect4r
8
+
9
+ == Using this library in your own project
10
+
11
+ require 'dynect4r'
12
+ client = Dynect::Client.new(:customer_name => 'example',
13
+ :user_name => 'example',
14
+ :password => 'example')
15
+ response = client.rest_call(:get, 'Zone/example.org')
16
+ pp response
17
+
18
+ == Using the built-in command line client
19
+
20
+ 1. Create a file called <b>dynect4r.secret</b> containing your Dynect customer name, username and password (i.e. on one line separated by whitespace). Note that this file is assumed to be in the current directory by default.
21
+ 2. See examples below:
22
+
23
+ === General usage
24
+
25
+ dynect4r-client [options] [rdata][, ...]
26
+
27
+ - Multiple sets of rdata can be specified by separating them with commas.
28
+ - Records can be deleted by not specifying rdata.
29
+
30
+ === Examples
31
+
32
+ ==== Create an A record
33
+
34
+ dynect4r-client -n test.example.org 1.1.1.1
35
+
36
+ ==== Create round-robin A records
37
+
38
+ dynect4r-client -n test.example.org 1.1.1.1,2.2.2.2,3.3.3.3
39
+
40
+ ==== Create a CNAME record
41
+
42
+ dynect4r-client -n test.example.org -t CNAME test.example.org.
43
+
44
+ ==== Create an SRV record
45
+
46
+ dynect4r-client -n srv.example.org -t SRV 0 10 20 target.example.org.
47
+
48
+ === Help and Troubleshooting
49
+
50
+ See the <b>--help</b> command line option:
51
+
52
+ dynect4r-client --help
53
+
54
+ Use debug logging:
55
+
56
+ dynect4r-client -v debug
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dynect4r'
4
+ require 'optparse'
5
+ require 'pp'
6
+ require 'set'
7
+
8
+ # set default command line options
9
+ options = {
10
+ :cred_file => './dynect4r.secret',
11
+ :customer => nil,
12
+ :username => nil,
13
+ :password => nil,
14
+ :zone => nil,
15
+ :node => Socket.gethostbyname(Socket.gethostname).first,
16
+ :ttl => 86400,
17
+ :type => 'A',
18
+ :rdata => nil,
19
+ :log_level => 'info',
20
+ :dry_run => false,
21
+ :cancel_on_error => false
22
+ }
23
+
24
+ # parse command line options
25
+ OptionParser.new do |opts|
26
+ opts.banner = "Usage: #{$0} [options] [rdata][, ...]\n" \
27
+ + "Example: #{$0} -n srv.example.org -t SRV 0 10 20 target.example.org"
28
+
29
+ opts.on('-c', '--credentials-file VALUE', 'Path to file containing API customer/username/password (default: %s)' % options[:cred_file]) do |opt|
30
+ options[:cred_file] = opt
31
+ end
32
+
33
+ opts.on('-z', '--zone VALUE', 'DNS Zone (default: Auto-detect)') do |opt|
34
+ options[:zone] = opt
35
+ end
36
+
37
+ opts.on('-n', '--node VALUE', 'Node name (default: %s)' % options[:node]) do |opt|
38
+ options[:node] = opt
39
+ end
40
+
41
+ opts.on('-s', '--ttl VALUE', 'Time to Live (default: %s)' % options[:ttl]) do |opt|
42
+ options[:ttl] = opt
43
+ end
44
+
45
+ opts.on('-t', '--type VALUE', 'Record type (default: %s)' % options[:type]) do |opt|
46
+ options[:type] = opt.upcase
47
+ end
48
+
49
+ opts.on('-v', '--verbosity VALUE', 'Log verbosity (default: %s)' % options[:log_level]) do |opt|
50
+ options[:log_level] = opt
51
+ end
52
+
53
+ opts.on('--dry-run', "Perform a trial run without making changes (default: %s)" % options[:dry_run]) do |opt|
54
+ options[:dry_run] = opt
55
+ end
56
+
57
+ opts.on('--cancel-on-error', "All changes will be canceled if any error occurs (default: %s)" % options[:cancel_on_error]) do |opt|
58
+ options[:cancel_on_error] = opt
59
+ end
60
+
61
+ end.parse!
62
+ options[:rdata] = ARGV.join(' ').split(',').collect { |obj| obj.strip() }
63
+
64
+ # instantiate logger
65
+ log = Dynect::Logger.new(STDOUT)
66
+ log.level = eval('Dynect::Logger::' + options[:log_level].upcase)
67
+ RestClient.log = log
68
+
69
+ # validate command line options
70
+ begin
71
+ (options[:customer_name], options[:user_name], options[:password]) = File.open(options[:cred_file]).readline().strip().split()
72
+ rescue Errno::ENOENT
73
+ log.error('Credentials file does not exist: %s' % options[:cred_file])
74
+ Process.exit(1)
75
+ end
76
+ if !options[:zone]
77
+ options[:zone] = options[:node][(options[:node].index('.') + 1)..-1]
78
+ end
79
+
80
+ # track number of changes and errors
81
+ changes = 0
82
+ errors = 0
83
+
84
+ # instantiate dynect client and log in
85
+ log.info('Starting session')
86
+ begin
87
+ c = Dynect::Client.new(:customer_name => options[:customer_name],
88
+ :user_name => options[:user_name],
89
+ :password => options[:password])
90
+ rescue Dynect::DynectError
91
+ log.error($!.message)
92
+ Process.exit(1)
93
+ end
94
+
95
+ # create set of existing records
96
+ begin
97
+ existing_rdata = {}
98
+ response = c.rest_call(:get, [Dynect::rtype_to_resource(options[:type]), options[:zone], options[:node]])
99
+ response[:data].each do |url|
100
+ rdata = c.rest_call(:get, url)[:data][:rdata].inject({}) { |memo,(k,v)| memo[k.to_s] = v.to_s; memo }
101
+ existing_rdata[rdata] = url
102
+ log.info('Found record (Zone="%s", Node="%s" TTL="%s", Type="%s", RData="%s")' %
103
+ [options[:zone], options[:node], options[:ttl], options[:type], rdata.to_json])
104
+ end
105
+ rescue Dynect::DynectError
106
+ log.error('Query for records failed - %s' % $!.message)
107
+ Process.exit(1)
108
+ end
109
+
110
+ # create set of new records
111
+ new_rdata = Set.new
112
+ options[:rdata].each do |rdata|
113
+ new_rdata << Dynect::args_for_rtype(options[:type], rdata)
114
+ end
115
+
116
+ # delete records
117
+ (existing_rdata.keys.to_set - new_rdata).each do |rdata|
118
+ log.warn('%sDeleting record (Zone="%s", Node="%s" TTL="%s", Type="%s", RData="%s")' %
119
+ [options[:dry_run] ? '(NOT) ' : '', options[:zone], options[:node], options[:ttl], options[:type], rdata.to_json])
120
+ begin
121
+ if not options[:dry_run]
122
+ c.rest_call(:delete, existing_rdata[rdata])
123
+ end
124
+ changes += 1
125
+ rescue Dynect::DynectError
126
+ errors += 1
127
+ log.error('Failed to delete record - %s' % $!.message)
128
+ end
129
+ end
130
+
131
+ # add new records
132
+ (new_rdata - existing_rdata.keys.to_set).each do |rdata|
133
+ log.warn('%sCreating record (Zone="%s", Node="%s" TTL="%s", Type="%s", RData="%s")' %
134
+ [options[:dry_run] ? '(NOT) ' : '', options[:zone], options[:node], options[:ttl], options[:type], rdata.to_json])
135
+ begin
136
+ if not options[:dry_run]
137
+ response = c.rest_call(:post, [Dynect::rtype_to_resource(options[:type]), options[:zone], options[:node]], { 'rdata' => rdata, 'ttl' => options[:ttl] })
138
+ end
139
+ changes += 1
140
+ rescue Dynect::DynectError
141
+ errors += 1
142
+ log.error('Failed to add record - %s' % $!.message)
143
+ end
144
+ end
145
+
146
+ # publish changes
147
+ if changes > 0
148
+ begin
149
+ if options[:cancel_on_error] and errors > 0
150
+ log.warn('%sCanceling changes' % [options[:dry_run] ? '(NOT) ' : ''])
151
+ if not options[:dry_run]
152
+ c.rest_call(:delete, [ 'ZoneChanges', options[:zone]])
153
+ end
154
+ else
155
+ log.info('%sPublishing changes' % [options[:dry_run] ? '(NOT) ' : ''])
156
+ if not options[:dry_run]
157
+ c.rest_call(:put, [ 'Zone', options[:zone]], { 'publish' => 'true' })
158
+ end
159
+ end
160
+ rescue Dynect::DynectError
161
+ log.error($!.message)
162
+ end
163
+ else
164
+ log.info('No changes made')
165
+ end
166
+
167
+ # terminate session
168
+ log.info('Terminating session')
169
+ begin
170
+ c.rest_call(:delete, 'Session')
171
+ rescue Dynect::DynectError
172
+ log.error($!.message)
173
+ end
data/lib/dynect4r.rb ADDED
@@ -0,0 +1,221 @@
1
+ require 'rubygems'
2
+ gem 'json', '>= 1.4.3'
3
+
4
+ require 'json'
5
+ require 'logger'
6
+ require 'rest_client'
7
+
8
+ module Dynect
9
+
10
+ class Client
11
+
12
+ # log in, set auth token
13
+ def initialize(params)
14
+ @base_url = 'https://api2.dynect.net'
15
+ @headers = { :content_type => :json, :accept => :json }
16
+ response = rest_call(:post, 'Session', params)
17
+ @headers['Auth-Token'] = response[:data][:token]
18
+ end
19
+
20
+ # do a rest call
21
+ def rest_call(action, resource, arguments = nil)
22
+
23
+ # set up retry loop
24
+ max_tries = 12
25
+ for try_counter in (1..max_tries)
26
+
27
+ # pause between retries
28
+ if try_counter > 1
29
+ sleep(5)
30
+ end
31
+
32
+ resource_url = resource_to_url(resource)
33
+
34
+ # do rest call
35
+ begin
36
+ response = case action
37
+ when :post, :put
38
+ RestClient.send(action, resource_url, arguments.to_json, @headers) do |res,req|
39
+ Dynect::Response.new(res)
40
+ end
41
+ else
42
+ RestClient.send(action, resource_url, @headers) do |res,req|
43
+ Dynect::Response.new(res)
44
+ end
45
+ end
46
+
47
+ # if we got this far, then it's safe to break out of the retry loop
48
+ break
49
+
50
+ # on redirect, rewrite rest call params and retry
51
+ rescue RedirectError
52
+ if try_counter < max_tries
53
+ action = :get
54
+ resource = $!.message
55
+ arguments = nil
56
+ else
57
+ raise OperationTimedOut, "Maximum number of tries (%d) exceeded on resource: %s" % [max_tries, resource]
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ # return a response object
64
+ response
65
+ end
66
+
67
+ private
68
+
69
+ # convert the given resource into a proper url
70
+ def resource_to_url(resource)
71
+
72
+ # convert into an array
73
+ if resource.is_a? String
74
+ resource = resource.split('/')
75
+ end
76
+
77
+ # remove empty elements
78
+ resource.delete('')
79
+
80
+ # make sure first element is 'REST'
81
+ if resource[0] != 'REST'
82
+ resource.unshift('REST')
83
+ end
84
+
85
+ # prepend base url and convert back to string
86
+ "%s/%s/" % [@base_url, resource.join('/')]
87
+ end
88
+
89
+ end
90
+
91
+ class Response
92
+
93
+ def initialize(response)
94
+
95
+ # parse response
96
+ begin
97
+ @hash = JSON.parse(response, :symbolize_names => true)
98
+ rescue JSON::ParserError
99
+ if response =~ /REST\/Job\/[0-9]+/
100
+ raise RedirectError, response
101
+ else
102
+ raise
103
+ end
104
+ end
105
+
106
+ # raise error based on error code
107
+ if @hash.has_key?(:msgs)
108
+ @hash[:msgs].each do |msg|
109
+ case msg[:ERR_CD]
110
+ when 'ILLEGAL_OPERATION'
111
+ raise IllegalOperationError, msg[:INFO]
112
+ when 'INTERNAL_ERROR'
113
+ raise InternalErrorError, msg[:INFO]
114
+ when 'INVALID_DATA'
115
+ raise InvalidDataError, msg[:INFO]
116
+ when 'INVALID_REQUEST'
117
+ raise InvalidRequestError, msg[:INFO]
118
+ when 'INVALID_VERSION'
119
+ raise InvalidVersionError, msg[:INFO]
120
+ when 'MISSING_DATA'
121
+ raise MissingDataError, msg[:INFO]
122
+ when 'NOT_FOUND'
123
+ raise NotFoundError, msg[:INFO]
124
+ when 'OPERATION_FAILED'
125
+ raise OperationFailedError, msg[:INFO]
126
+ when 'PERMISSION_DENIED'
127
+ raise PermissionDeniedError, msg[:INFO]
128
+ when 'SERVICE_UNAVAILABLE'
129
+ raise ServiceUnavailableError, msg[:INFO]
130
+ when 'TARGET_EXISTS'
131
+ raise TargetExistsError, msg[:INFO]
132
+ when 'UNKNOWN_ERROR'
133
+ raise UnknownErrorError, msg[:INFO]
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def [](key)
140
+ @hash[key]
141
+ end
142
+
143
+ end
144
+
145
+ # exceptions generated by class
146
+ class DynectError < StandardError; end
147
+ class RedirectError < DynectError; end
148
+ class OperationTimedOut < DynectError; end
149
+
150
+ # exceptions generated by api
151
+ class IllegalOperationError < DynectError; end
152
+ class InternalErrorError < DynectError; end
153
+ class InvalidDataError < DynectError; end
154
+ class InvalidRequestError < DynectError; end
155
+ class InvalidVersionError < DynectError; end
156
+ class MissingDataError < DynectError; end
157
+ class NotFoundError < DynectError; end
158
+ class OperationFailedError < DynectError; end
159
+ class PermissionDeniedError < DynectError; end
160
+ class ServiceUnavailableError < DynectError; end
161
+ class TargetExistsError < DynectError; end
162
+ class UnknownErrorError < DynectError; end
163
+
164
+ class Logger < Logger
165
+
166
+ # override << operator to control rest_client logging
167
+ # see http://github.com/archiloque/rest-client/issues/issue/34/
168
+ def << (msg)
169
+ debug(msg.strip)
170
+ end
171
+ end
172
+
173
+ class << self
174
+
175
+ # return the appropriate rest resource for the given rtype
176
+ def rtype_to_resource(rtype)
177
+ rtype.upcase + 'Record'
178
+ end
179
+
180
+ # return a hash of arguments for the specified rtype
181
+ def args_for_rtype(rtype, rdata)
182
+
183
+ arg_array = case rtype
184
+ when 'A', 'AAAA'
185
+ ['address']
186
+ when 'CNAME'
187
+ ['cname']
188
+ when 'DNSKEY', 'KEY'
189
+ ['flags', 'protocol', 'algorithm', 'public_key']
190
+ when 'DS'
191
+ ['keytag', 'algorithm', 'digtype', 'digest']
192
+ when 'LOC'
193
+ ['version', 'size', 'horiz_pre', 'vert_pre' 'latitude', 'longitude', 'altitude']
194
+ when 'MX'
195
+ ['preference', 'exchange']
196
+ when 'NS'
197
+ ['nsdname']
198
+ when 'PTR'
199
+ ['ptrdname']
200
+ when 'RP'
201
+ ['mbox', 'txtdname']
202
+ when 'SOA'
203
+ ['rname']
204
+ when 'SRV'
205
+ ['priority', 'weight', 'port', 'target']
206
+ when 'TXT'
207
+ ['txtdata']
208
+ else
209
+ []
210
+ end
211
+
212
+ if rtype == 'TXT'
213
+ rdata = { arg_array[0] => rdata }
214
+ else
215
+ rdata.split.inject({}) { |memo,obj| memo[arg_array[memo.length]] = obj; memo }
216
+ end
217
+ end
218
+
219
+ end
220
+
221
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynect4r
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Michael T. Conigliaro
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-08 00:00:00 -06:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 1
30
+ segments:
31
+ - 1
32
+ - 4
33
+ - 3
34
+ version: 1.4.3
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rest-client
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ description: dynect4r is a Ruby library and command line client for the Dynect REST API (version 2)
52
+ email:
53
+ - mike [at] conigliaro [dot] org
54
+ executables:
55
+ - dynect4r-client
56
+ extensions: []
57
+
58
+ extra_rdoc_files: []
59
+
60
+ files:
61
+ - LICENSE
62
+ - README.rdoc
63
+ - lib/dynect4r.rb
64
+ - bin/dynect4r-client
65
+ has_rdoc: true
66
+ homepage: http://github.com/mconigliaro/dynect4r
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options: []
71
+
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project: dynect4r
95
+ rubygems_version: 1.3.7
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Ruby library and command line client for the Dynect REST API (version 2)
99
+ test_files: []
100
+