ezdyn 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d24c4fe0dc33f7c3811c278361eca07dc3238312
4
+ data.tar.gz: 77d401779004efe034f9e67a248798f289fe5042
5
+ SHA512:
6
+ metadata.gz: 605af431e632c95a448973f647fbc072cd8ef96765b47da25a089359f2a4bc96239f84f414945e503c561ef35ef2ca1f28e4e2eb4cc363482e7456c69b5cabd8
7
+ data.tar.gz: e08bc7239b87985c2a78dc77fd267c4c223886212fe5314f8009880b007d3853de83c93cf6242c0e1ee4abe9d1261e40e02a63c17f5767708b2920b564119a76
@@ -0,0 +1,30 @@
1
+ # ezdyn changelog
2
+
3
+ ## Version 0.2.0 - 2015-09-19
4
+
5
+ * ezdyn command line tool
6
+ * several bug fixes
7
+
8
+
9
+ ## Version 0.1.1 - 2015-09-19
10
+
11
+ * Added YARD docs
12
+ * Added Rakefile with gem and doc commands
13
+ * Renamed Type to RecordType for clarity
14
+
15
+
16
+ ## Version 0.1.0 - 2015-09-18
17
+
18
+ * Refactored client code to a new class
19
+ * Factored record type handling into its own class
20
+ * Added stderr verbose logger
21
+ * Keep track of pending changes globally, plus reporting
22
+ * Commit and Rollback do not require zone specification
23
+
24
+
25
+ ## Version 0.0.0 - 2015-09-18
26
+
27
+ * Session management: login/logout
28
+ * Basic CRUD: create, update, upsert, delete, delete_all, records_for
29
+ * Transactions: Commit, Rollback
30
+ * Zone, Record, Response objects
@@ -0,0 +1,139 @@
1
+ # ezdyn
2
+
3
+ Gem library and command line tool for Dyn Managed DNS API access.
4
+
5
+ See https://help.dyn.com/dns-api-knowledge-base/
6
+
7
+
8
+ ## Installation
9
+
10
+ $ rake gem:install
11
+
12
+
13
+ ## Command Line Usage
14
+
15
+ Use the `ezdyn` tool installed with the gem to make one-off or batched DNS changes from the command line.
16
+
17
+ See `ezdyn --help` for all options. Use command line flags or environment variables (see below) for authentication.
18
+
19
+ Important options include:
20
+
21
+ * `--file <filename>` provide a file of commands, one per line
22
+ * `--apply`: apply changes when done (default is dry-run mode)
23
+ * `--check`: check syntax only (does not connect to API or need credentials)
24
+
25
+ If `--file` is not specified, commands are read from the command line arguments. Each ezdyn command must be enclosed in quotes to distiguish it from other commands.
26
+
27
+ ### Command syntax
28
+
29
+ There are three possible commands:
30
+
31
+ * `create <type> <fqdn> <value> [<ttl>]`
32
+ * `upsert <type> <fqdn> <value> [<ttl>]`
33
+ * `delete <type> <fqdn>`
34
+
35
+ The `ttl` field is always optional. You may use `update` as well as `upsert`, but both commands will create a record if it does not exist, and update an existing record if it does exist.
36
+
37
+ ### Examples
38
+
39
+ You can issue commands directly from the command line.
40
+
41
+ To perform a dry-run of creating a new A record:
42
+
43
+ $ ezdyn "create A test.example.com 192.168.0.1"
44
+
45
+ To check the syntax (requiring no API interaction) on creating two CNAME records:
46
+
47
+ $ ezdyn --check "create cname test1.example.com other1.example.com" \
48
+ "create cname test2.example.com other2.example.com"
49
+
50
+ To actually update/upsert an A record:
51
+
52
+ $ ezdyn "update A test3.example.com 10.0.0.10" --apply
53
+
54
+ To process a list of commands from a file:
55
+
56
+ $ ezdyn --file dns-changes.txt
57
+
58
+ Where `dns-changes.txt` may look like:
59
+
60
+ create A web1.example.com 10.1.100.1 600
61
+ create A web2.example.com 10.1.200.1 1200
62
+ create A web3.example.com 192.168.0.2 30
63
+ delete ALL oldweb.example.com
64
+
65
+
66
+ ### Notes
67
+
68
+ * Dry runs do interact with the Dyn API. This is useful to check for the validity of your changes before making them.
69
+ * ezdyn does not yet deal correctly with multiple records on a single node. Delete operations will delete multiple records on a node, but create and update operations should bail and complain about multiple records.
70
+ * Exception handling is poor at the moment, so when things go wrong it might look ugly, but most dangerous situations should be prevented.
71
+ * Only A and CNAME records are supported at present.
72
+
73
+
74
+ ## Library Usage
75
+
76
+ require 'ezdyn'
77
+
78
+ # create new client object using explicit vars
79
+ dyn = EZDyn::Client.new(customer_name: customer_name,
80
+ username: username,
81
+ password: password)
82
+
83
+ # (or create one using environment variables (see below)
84
+ dyn2 = EZDyn::Client.new
85
+
86
+ # create a new A record
87
+ # (login step is implicit in first access)
88
+ dyn.create(type: "A",
89
+ fqdn: "website.example.com",
90
+ value: "10.0.0.1")
91
+
92
+ # no wait, take that back
93
+ dyn.rollback
94
+
95
+ # delete any CNAME records for alias.example.com
96
+ dyn.delete_all(type: "cname", fqdn: "alias.example.com")
97
+
98
+ # update the A record for mail.example.com, or, if it
99
+ # does not exist, create it with the given value and ttl
100
+ dyn.update(type: :a,
101
+ fqdn: "mail.example.com",
102
+ value: "10.0.0.2",
103
+ ttl: 600)
104
+
105
+ # print all pending changes
106
+ puts "Pending changes:"
107
+ dyn.pending_changes.each do |pc|
108
+ puts " #{pc}"
109
+ end
110
+
111
+ # commit all changes (with optional zone update message)
112
+ dyn.commit(message: "Just an example")
113
+
114
+ # close session (it will time out after 60 minutes)
115
+ dyn.logout
116
+
117
+
118
+ ## Environment Variables
119
+
120
+ You may specify the `customer_name`, `username`, and `password` parameters via environment variables:
121
+
122
+ * `DYN_CUSTOMER_NAME`
123
+ * `DYN_USERNAME`
124
+ * `DYN_PASSWORD`
125
+
126
+ You may also enable verbose debug logging by setting `EZDYN_DEBUG` to any value.
127
+
128
+
129
+ ## API Documentation
130
+
131
+ You can generate the YARD docs for the library by running `rake docs`. Then open `doc/index.html` in your web browser.
132
+
133
+
134
+ ## TODO
135
+
136
+ * Implement actual Exception classes
137
+ * Fewer exceptions should be raised overall
138
+ * Support additional record types (esp MX, AAAA)
139
+ * Mock API endpoint for testing, also actual tests
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'io/console'
5
+
6
+ def die(msg)
7
+ errsay "ERROR: #{msg}"
8
+ exit 1
9
+ end
10
+
11
+ def say(msg = nil)
12
+ puts msg unless $options[:quiet]
13
+ end
14
+
15
+ def errsay(msg = nil)
16
+ STDERR.puts msg unless $options[:quiet]
17
+ end
18
+
19
+ $options = {
20
+ dryrun: true,
21
+ ignore_syntax_errors: false,
22
+ check: false,
23
+ }
24
+ client_args = {}
25
+ OptionParser.new do |opts|
26
+ opts.banner = "Usage: #{File.basename($0)} [options]"
27
+
28
+ opts.on_head(
29
+ '-f', '--file FILENAME',
30
+ 'File path to a batch of ezdyn commands'
31
+ ) do |arg|
32
+
33
+ die("Input file '#{arg}' could not be found.") if not File.readable?(arg)
34
+
35
+ $options[:batchfile] = arg
36
+ $options[:batchmode] = true
37
+ end
38
+
39
+ opts.on_head('--apply', 'Actually apply changes, do not just perform a dry run.') do
40
+ $options[:dryrun] = false
41
+ end
42
+
43
+ opts.on_head('--check', 'Check all command syntax, but do not interact with the API.') do
44
+ $options[:check] = true
45
+ end
46
+
47
+ opts.on('-C', '--customer-name NAME', 'Dyn API Customer Name') do |arg|
48
+ client_args[:customer_name] = arg
49
+ end
50
+
51
+ opts.on('-u', '--user USERNAME', 'Dyn API Username') do |arg|
52
+ client_args[:username] = arg
53
+ end
54
+
55
+ opts.on('-p', '--password PASSWORD', 'Dyn API Password') do |arg|
56
+ client_args[:password] = arg
57
+ end
58
+
59
+ opts.on('--ignore-syntax-errors', 'Do not fail if a command has a syntax error. Just skip.') do
60
+ $options[:ignore_syntax_errors] = true
61
+ end
62
+
63
+ opts.on('--force', 'Do not ask for confirmation to commit changes.') do
64
+ $options[:force] = true
65
+ end
66
+
67
+ opts.on('-q', '--quiet', 'Quiet mode (implies --force: apply mode will not ask for confirmation!)') do
68
+ $options[:quiet] = true
69
+ $options[:force] = true
70
+ end
71
+
72
+ opts.on_tail('--debug', 'Enable debug logging') do
73
+ $options[:debug] = true
74
+ end
75
+
76
+ opts.on_tail('-v','--version', 'Display version') do
77
+ require 'ezdyn/version'
78
+ puts "ezdyn #{EZDyn::VERSION}"
79
+ exit
80
+ end
81
+ end.parse!
82
+
83
+ if $options[:debug]
84
+ ENV['EZDYN_DEBUG'] = "1"
85
+ end
86
+
87
+ if $options[:batchmode]
88
+ if ARGV.count > 0
89
+ die "Cannot handle commands both on the command line and in a file"
90
+ end
91
+ else
92
+ if ARGV.count < 1
93
+ die "Nothing to do"
94
+ end
95
+ end
96
+
97
+ commands =
98
+ if $options[:batchmode]
99
+ File.read($options[:batchfile]).split("\n")
100
+ else
101
+ ARGV.dup
102
+ end
103
+
104
+ # in check mode, we won't interact with the API
105
+ if not $options[:check]
106
+ require 'ezdyn'
107
+ $dyn = EZDyn::Client.new(**client_args)
108
+ end
109
+
110
+ $check_warnings = 0
111
+
112
+ def handle_syntax_error(msg)
113
+ if $options[:check]
114
+ $check_warnings += 1
115
+
116
+ elsif not $options[:ignore_syntax_errors]
117
+ die msg
118
+ end
119
+ errsay "WARNING: #{msg}"
120
+ end
121
+
122
+
123
+ commands.each do |line|
124
+ tokens = line.chomp.strip.split(/\s+/)
125
+ next if tokens.empty?
126
+
127
+ cmd = tokens.shift.downcase
128
+
129
+ case cmd
130
+ when 'create'
131
+ type, fqdn, value, ttl = tokens.shift(4)
132
+ if type.nil? or type.empty? or
133
+ fqdn.nil? or fqdn.empty? or
134
+ value.nil? or value.empty? or
135
+ tokens.count > 0
136
+ handle_syntax_error("'create' command expects 3-4 arguments")
137
+ next
138
+ end
139
+ next if $options[:check]
140
+
141
+ say "DynAPI: Creating #{fqdn} #{ttl||"(#{EZDyn::Record::DefaultTTL})"} #{type.upcase} #{value}"
142
+ $dyn.create(type: type, fqdn: fqdn, value: value, ttl: ttl)
143
+
144
+ when 'update','upsert'
145
+ type, fqdn, value, ttl = tokens.shift(4)
146
+ if type.nil? or type.empty? or
147
+ fqdn.nil? or fqdn.empty? or
148
+ value.nil? or value.empty? or
149
+ tokens.count > 0
150
+ handle_syntax_error("'#{cmd}' command expects 3-4 arguments")
151
+ next
152
+ end
153
+ next if $options[:check]
154
+
155
+ say "DynAPI: Upserting #{fqdn} #{ttl||"(#{EZDyn::Record::DefaultTTL})"} #{type.upcase} #{value}"
156
+ $dyn.update(type: type, fqdn: fqdn, value: value, ttl: ttl)
157
+
158
+ when 'delete'
159
+ type, fqdn = tokens.shift(2)
160
+ if type.nil? or type.empty? or
161
+ fqdn.nil? or fqdn.empty? or
162
+ tokens.count > 0
163
+ handle_syntax_error("'delete' command expects exactly two arguments")
164
+ next
165
+ end
166
+ next if $options[:check]
167
+
168
+ if type.downcase == "all" or type.downcase == "any"
169
+ say "DynAPI: Deleting ALL records for '#{fqdn}'"
170
+ $dyn.records_for(fqdn: fqdn).each(&:delete!)
171
+ else
172
+ say "DynAPI: Deleting #{type.upcase} records for '#{fqdn}'"
173
+ $dyn.records_for(type: type, fqdn: fqdn).each(&:delete!)
174
+ end
175
+
176
+ else
177
+ handle_syntax_error("Invalid or unknown command '#{cmd}'")
178
+ next
179
+ end
180
+ end
181
+
182
+ if $options[:check]
183
+ if $check_warnings > 0
184
+ die "Found #{$check_warnings} syntax errors"
185
+ else
186
+ say "Syntax OK"
187
+ end
188
+ exit
189
+ end
190
+
191
+ say
192
+ if $dyn.pending_changes.nil? or $dyn.pending_changes.count < 1
193
+ say "NOTICE: No changes were made."
194
+ exit
195
+ end
196
+
197
+ say "Pending changes:"
198
+ $dyn.pending_changes.each do |pc|
199
+ say " #{pc}"
200
+ end
201
+ say
202
+
203
+
204
+ if $options[:dryrun]
205
+ say "DynAPI: This is a dry run. Rolling back changes."
206
+ $dyn.rollback
207
+
208
+ else
209
+ if not $options[:force]
210
+ print "Would you like to apply these changes? (y/N) "
211
+ if STDIN.getch != 'y'
212
+ say
213
+ say "DynAPI: Changes rejected. Rolling back."
214
+ $dyn.rollback
215
+ else
216
+ say
217
+ say "DynAPI: Apply mode in effect and changes confirmed. Committing!"
218
+ $dyn.commit
219
+ end
220
+ else
221
+ say "DynAPI: Apply and force modes in effect. Committing!"
222
+ $dyn.commit
223
+ end
224
+ end
225
+
226
+ $dyn.logout
@@ -0,0 +1,24 @@
1
+ require_relative 'lib/ezdyn/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "ezdyn"
5
+ s.version = EZDyn::VERSION
6
+ s.author = "David Adams"
7
+ s.email = "dadams@instructure.com"
8
+ s.date = Time.now.strftime("%Y-%m-%d")
9
+ s.license = "Nonstandard"
10
+ s.homepage = "https://github.com/instructure"
11
+
12
+ s.summary = "Simple library for Dyn Managed DNS"
13
+ s.description = "Library and CLI tool for easy Dynect DNS updates"
14
+
15
+ s.require_paths = ["lib"]
16
+ s.files =
17
+ Dir["lib/**/*.rb"] +
18
+ Dir["*.md"] +
19
+ ["ezdyn.gemspec"]
20
+ s.bindir = "bin"
21
+ s.executables = ["ezdyn"]
22
+
23
+ s.add_development_dependency 'yard', '~>0.8'
24
+ end
@@ -0,0 +1,11 @@
1
+ require 'json'
2
+ require 'ezdyn/version'
3
+ require 'ezdyn/consts'
4
+ require 'ezdyn/log'
5
+ require 'ezdyn/client'
6
+ require 'ezdyn/zone'
7
+ require 'ezdyn/record_type'
8
+ require 'ezdyn/response'
9
+ require 'ezdyn/record'
10
+ require 'ezdyn/crud'
11
+ require 'ezdyn/changes'
@@ -0,0 +1,111 @@
1
+ module EZDyn
2
+ class Client
3
+ # @private
4
+ def add_pending_change(chg)
5
+ @pending_changes ||= []
6
+ @pending_changes << chg
7
+ end
8
+
9
+ # @private
10
+ def pending_change_zones
11
+ @pending_changes.collect(&:zone).uniq
12
+ end
13
+
14
+ # List currently pending changes (optionally per zone).
15
+ #
16
+ # @param zone [String] (Optional) If specified, only return pending changes
17
+ # for the named zone.
18
+ # @return [Array] Array of [Change] objects awaiting commit.
19
+ def pending_changes(zone: nil)
20
+ if zone.nil?
21
+ @pending_changes
22
+ else
23
+ zone = Zone.new(client: self, name: zone)
24
+ @pending_changes.select do |pc|
25
+ pc.zone.name == zone.name
26
+ end
27
+ end
28
+ end
29
+
30
+ # @private
31
+ def clear_pending_changes(zone: nil)
32
+ if zone.nil?
33
+ @pending_changes = []
34
+ else
35
+ @pending_changes.delete_if do |pc|
36
+ pc.zone.name == zone.to_s
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # Generic base class for any pending change.
43
+ class Change
44
+ # Returns the zone this change is part of.
45
+ #
46
+ # @return [Zone] The zone this change is part of.
47
+ def zone
48
+ @record.zone
49
+ end
50
+ end
51
+
52
+ # A pending record creation.
53
+ class CreateChange < Change
54
+ # @private
55
+ def initialize(record:)
56
+ @record = record
57
+ end
58
+
59
+ # Returns a string representation of the change.
60
+ #
61
+ # @return [String] A string representation of this change.
62
+ def to_s
63
+ "CREATE #{@record.fqdn}. #{@record.ttl} #{@record.type} #{@record.value}"
64
+ end
65
+ end
66
+
67
+ # A pending record update.
68
+ class UpdateChange < Change
69
+ # @private
70
+ def initialize(record:, new_record:)
71
+ @record = record.sync!
72
+ @new_record = new_record
73
+ end
74
+
75
+ # Returns a string representation of the change.
76
+ #
77
+ # @return [String] A string representation of this change.
78
+ def to_s
79
+ ttl_string =
80
+ if @record.ttl == @new_record.ttl
81
+ @record.ttl
82
+ else
83
+ "(( #{@record.ttl} -> #{@new_record.ttl} ))"
84
+ end
85
+
86
+ value_string =
87
+ if @record.value == @new_record.value
88
+ @record.value
89
+ else
90
+ "(( #{@record.value} -> #{@new_record.value} ))"
91
+ end
92
+
93
+ "UPDATE #{@record.fqdn}. #{ttl_string} #{@record.type} #{value_string}"
94
+ end
95
+ end
96
+
97
+ # A pending record deletion.
98
+ class DeleteChange < Change
99
+ # @private
100
+ def initialize(record:)
101
+ @record = record.sync!
102
+ end
103
+
104
+ # Returns a string representation of the change.
105
+ #
106
+ # @return [String] A string representation of this change.
107
+ def to_s
108
+ "DELETE #{@record.fqdn}. #{@record.ttl} #{@record.type} #{@record.value}"
109
+ end
110
+ end
111
+ end