ezdyn 0.2.2

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