dynect4r 0.2.1

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