ezdyn 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +30 -0
- data/README.md +139 -0
- data/bin/ezdyn +226 -0
- data/ezdyn.gemspec +24 -0
- data/lib/ezdyn.rb +11 -0
- data/lib/ezdyn/changes.rb +111 -0
- data/lib/ezdyn/client.rb +203 -0
- data/lib/ezdyn/consts.rb +10 -0
- data/lib/ezdyn/crud.rb +199 -0
- data/lib/ezdyn/log.rb +47 -0
- data/lib/ezdyn/record.rb +142 -0
- data/lib/ezdyn/record_type.rb +78 -0
- data/lib/ezdyn/response.rb +82 -0
- data/lib/ezdyn/version.rb +4 -0
- data/lib/ezdyn/zone.rb +113 -0
- metadata +73 -0
data/lib/ezdyn/record.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
module EZDyn
|
2
|
+
# Abstraction of Dyn REST API DNS records.
|
3
|
+
class Record
|
4
|
+
# Default TTL (time to live) for new records
|
5
|
+
DefaultTTL = 300
|
6
|
+
|
7
|
+
# @private
|
8
|
+
def initialize(client:, raw: nil, uri: nil, type: nil, fqdn: nil, value: nil, ttl: nil, record_id: nil)
|
9
|
+
@client = client
|
10
|
+
@exists = nil
|
11
|
+
@type = RecordType.find(type)
|
12
|
+
@fqdn = fqdn
|
13
|
+
@value = value
|
14
|
+
@ttl = ttl
|
15
|
+
@in_sync = false
|
16
|
+
|
17
|
+
if not raw.nil?
|
18
|
+
self.sync_raw(raw)
|
19
|
+
|
20
|
+
elsif not uri.nil?
|
21
|
+
@uri = uri.gsub(%r{^/?(REST/)?}, "")
|
22
|
+
if @uri !~ %r{^[A-Za-z]+Record/[^/]+/[^/]+/[0-9]+}
|
23
|
+
raise "Invalid Record URI: '#{uri}'"
|
24
|
+
end
|
25
|
+
|
26
|
+
type_uri_name, zone, fqdn, record_id = @uri.split('/')
|
27
|
+
|
28
|
+
@type = RecordType.find(type_uri_name)
|
29
|
+
@fqdn = fqdn
|
30
|
+
@record_id = record_id
|
31
|
+
@zone = Zone.new(client: @client, name: zone)
|
32
|
+
@exists = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the record type.
|
37
|
+
def type
|
38
|
+
self.sync! if @type.nil?
|
39
|
+
@type
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the TTL of this record.
|
43
|
+
def ttl
|
44
|
+
self.sync! if @ttl.nil?
|
45
|
+
@ttl
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the FQDN of this record.
|
49
|
+
def fqdn
|
50
|
+
self.sync! if @fqdn.nil?
|
51
|
+
@fqdn
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the record data value.
|
55
|
+
def value
|
56
|
+
self.sync! if @value.nil?
|
57
|
+
@value
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the Dyn REST API ID for this record.
|
61
|
+
def record_id
|
62
|
+
self.sync! if @record_id.nil?
|
63
|
+
@record_id
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the zone of this record.
|
67
|
+
def zone
|
68
|
+
@zone ||= @client.guess_zone(fqdn: self.fqdn)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns whether this record existed at its last sync.
|
72
|
+
def exists?
|
73
|
+
@exists
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns whether this record has been synced.
|
77
|
+
def in_sync?
|
78
|
+
@in_sync
|
79
|
+
end
|
80
|
+
|
81
|
+
# @private
|
82
|
+
def uri
|
83
|
+
if @uri.nil?
|
84
|
+
"#{self.type.uri_name}/#{self.zone.name}/#{self.fqdn}/#{self.record_id}"
|
85
|
+
else
|
86
|
+
@uri
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Attempts to sync the record to the API.
|
91
|
+
#
|
92
|
+
# @raise [RuntimeError] if the record could not be synced.
|
93
|
+
# @raise [RuntimeError] if more than one record matches this record.
|
94
|
+
# @raise [RuntimeError] if returned data was not in an expected format.
|
95
|
+
def sync!
|
96
|
+
return self if self.in_sync?
|
97
|
+
|
98
|
+
data = @client.fetch_uri_data(uri: self.uri)
|
99
|
+
if data.is_a? Array
|
100
|
+
if data.count == 0
|
101
|
+
@in_sync = true
|
102
|
+
@exists = false
|
103
|
+
return self
|
104
|
+
|
105
|
+
elsif data.count > 1
|
106
|
+
raise "More than one record was found"
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if data.is_a? Array and data.count == 1
|
112
|
+
data = data.first
|
113
|
+
end
|
114
|
+
|
115
|
+
if data.is_a? Hash
|
116
|
+
self.sync_raw(data)
|
117
|
+
|
118
|
+
else
|
119
|
+
raise "Unrecognized data: #{data.class} #{data}"
|
120
|
+
end
|
121
|
+
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# @private
|
126
|
+
def sync_raw(raw)
|
127
|
+
@zone = Zone.new(client: @client, name: raw["zone"])
|
128
|
+
@ttl = raw["ttl"]
|
129
|
+
@fqdn = raw["fqdn"]
|
130
|
+
@type = RecordType.find(raw["record_type"])
|
131
|
+
@record_id = raw["record_id"].to_s
|
132
|
+
@value = raw["rdata"][@type.value_key]
|
133
|
+
@in_sync = true
|
134
|
+
@exists = true
|
135
|
+
end
|
136
|
+
|
137
|
+
# Attempt to delete this record.
|
138
|
+
def delete!
|
139
|
+
@client.delete(record: self)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module EZDyn
|
2
|
+
# Encapsulates the properties of supported record types
|
3
|
+
class RecordType
|
4
|
+
# @private
|
5
|
+
@@types = []
|
6
|
+
|
7
|
+
# Check if a type is valid.
|
8
|
+
#
|
9
|
+
# @return [Boolean] Whether the type given is valid.
|
10
|
+
def self.valid_type?(t)
|
11
|
+
not RecordType.find(t).nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
# The URI path element that signals this record type.
|
15
|
+
attr_reader :uri_name
|
16
|
+
|
17
|
+
# The dict key for this record type's `rdata` value.
|
18
|
+
attr_reader :value_key
|
19
|
+
|
20
|
+
# RecordType constructor. In general there is no reason to call this method
|
21
|
+
# directly as all known record types are created at file load time. Instead
|
22
|
+
# the #find method is the preferred way to fetch a RecordType object.
|
23
|
+
#
|
24
|
+
# @param name [Symbol] The canonical type of the record.
|
25
|
+
# @param uri_name [String] URI path element for the record type.
|
26
|
+
# @param value_key [String] Record data value dict key.
|
27
|
+
def initialize(name:, uri_name:, value_key:)
|
28
|
+
EZDyn.debug { "RecordType.new( name: #{name}, uri_name: #{uri_name}, value_key: #{value_key} )" }
|
29
|
+
@name = name
|
30
|
+
@uri_name = uri_name
|
31
|
+
@value_key = value_key
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts several possible representations of a RecordType into an
|
35
|
+
# appropriate class instance. Pass in a RecordType instance, a String or
|
36
|
+
# Symbol of the type name or the String fragment of a REST API URI.
|
37
|
+
#
|
38
|
+
# @param to_find [EZDyn::RecordType, String, Symbol] Representation of the
|
39
|
+
# paramter to be found.
|
40
|
+
# @return [EZDyn::RecordType] The class instance requested or `nil` if none
|
41
|
+
# is found.
|
42
|
+
def self.find(to_find)
|
43
|
+
EZDyn.debug { "RecordType#find( [#{to_find.class}] #{to_find} )" }
|
44
|
+
if to_find.is_a? RecordType
|
45
|
+
return to_find
|
46
|
+
|
47
|
+
else
|
48
|
+
return @@types.find { |ctype| ctype.name == to_find.to_s.upcase } ||
|
49
|
+
@@types.find { |ctype| ctype.uri_name.downcase == to_find.to_s.downcase }
|
50
|
+
end
|
51
|
+
|
52
|
+
EZDyn.debug { "No such RecordType was found" }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Provides an all-caps String representation of the record type, eg 'A','CNAME'.
|
56
|
+
#
|
57
|
+
# @return [String] The name of the record type.
|
58
|
+
def name
|
59
|
+
@name.to_s.upcase
|
60
|
+
end
|
61
|
+
|
62
|
+
# Converts the RecordType to the all-caps String of the record type.
|
63
|
+
#
|
64
|
+
# @return [String] The displayable String representation.
|
65
|
+
def to_s
|
66
|
+
self.name
|
67
|
+
end
|
68
|
+
|
69
|
+
# initialize cache of supported types
|
70
|
+
[
|
71
|
+
{ name: :any, uri_name: "ANYRecord", value_key: nil },
|
72
|
+
{ name: :a, uri_name: "ARecord", value_key: "address" },
|
73
|
+
{ name: :cname, uri_name: "CNAMERecord", value_key: "cname" },
|
74
|
+
].each do |args|
|
75
|
+
@@types << EZDyn::RecordType.new(**args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module EZDyn
|
2
|
+
# Abstraction of a Dyn REST API response.
|
3
|
+
class Response
|
4
|
+
# @private
|
5
|
+
def initialize(response)
|
6
|
+
@response = response
|
7
|
+
EZDyn.debug { "API RESPONSE: #{@response.body}" }
|
8
|
+
if @response.body =~ %r{^/REST/Job/[0-9]+$}
|
9
|
+
EZDyn.debug { "Response delayed! Try again later!" }
|
10
|
+
@raw = {
|
11
|
+
"status" => "delayed",
|
12
|
+
"job_id" => @response.body.split('/').last,
|
13
|
+
"msgs" => [],
|
14
|
+
"data" => {},
|
15
|
+
}
|
16
|
+
else
|
17
|
+
@raw = JSON.parse(@response.body)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the status message of the response.
|
22
|
+
#
|
23
|
+
# @return [String] The status field of the response.
|
24
|
+
def status
|
25
|
+
@raw["status"]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Was the response successful?
|
29
|
+
#
|
30
|
+
# @return [Boolean] Returns true if the response was successful.
|
31
|
+
def success?
|
32
|
+
self.status == "success"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Do we have to wait for the response?
|
36
|
+
#
|
37
|
+
# @return [Boolean] Returns true if the response is delayed and you
|
38
|
+
# must wait and try again.
|
39
|
+
def delayed?
|
40
|
+
self.status == "delayed"
|
41
|
+
end
|
42
|
+
|
43
|
+
# The job ID returned by the response.
|
44
|
+
#
|
45
|
+
# @return [String] The job ID of the response.
|
46
|
+
def job_id
|
47
|
+
@raw["job_id"]
|
48
|
+
end
|
49
|
+
|
50
|
+
# The payload data from the response.
|
51
|
+
#
|
52
|
+
# @return [Hash] The payload data of the response.
|
53
|
+
def data
|
54
|
+
@raw["data"]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Full version of the descriptive response message with log levels and
|
58
|
+
# error codes.
|
59
|
+
#
|
60
|
+
# @return [String] Full descriptive message of the response with error
|
61
|
+
# codes and log levels.
|
62
|
+
def full_message
|
63
|
+
@raw["msgs"].collect do |msg|
|
64
|
+
"[#{msg["SOURCE"]}] #{msg["LVL"]} (#{msg["ERR_CD"]}): #{msg[msg["LVL"]]}"
|
65
|
+
end.join("\n")
|
66
|
+
end
|
67
|
+
|
68
|
+
# The simple descriptive message of the response from the API.
|
69
|
+
#
|
70
|
+
# @return [String] Descriptive message of the response.
|
71
|
+
def simple_message
|
72
|
+
@raw["msgs"].collect { |m| m[m["LVL"]] }.join("\n")
|
73
|
+
end
|
74
|
+
|
75
|
+
# All error codes returned by the response.
|
76
|
+
#
|
77
|
+
# @return [Array<String>] An array of error codes returned by the response.
|
78
|
+
def error_codes
|
79
|
+
@raw["msgs"].collect { |m| m["ERR_CD"] }.compact.uniq
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/ezdyn/zone.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
module EZDyn
|
2
|
+
# Abstraction of Dyn REST API DNS Zones
|
3
|
+
class Zone
|
4
|
+
attr_reader :name, :type, :serial, :serial_style
|
5
|
+
|
6
|
+
# @private
|
7
|
+
def initialize(client:, name: nil, type: nil, serial: nil, serial_style: nil, raw: nil, uri: nil)
|
8
|
+
EZDyn.debug { "Zone.new( client: Client{}, name: #{name}, type: #{type}, serial: #{serial}, serial_style: #{serial_style}, raw: #{raw.nil? ? nil : raw.to_json}, uri: #{uri} )" }
|
9
|
+
@client = client
|
10
|
+
@name = name
|
11
|
+
@type = type
|
12
|
+
@serial = serial
|
13
|
+
@serial_style = serial_style
|
14
|
+
@in_sync = false
|
15
|
+
|
16
|
+
if not raw.nil?
|
17
|
+
self.sync_raw(raw)
|
18
|
+
end
|
19
|
+
|
20
|
+
if not uri.nil?
|
21
|
+
uri.gsub!(%r{^/?(REST/)?}, '')
|
22
|
+
if uri =~ %r{^Zone/([^/]+)/?$}
|
23
|
+
@name = $1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the zone type.
|
29
|
+
def type
|
30
|
+
self.sync! if @type.nil?
|
31
|
+
@type
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the serial number of the zone.
|
35
|
+
def serial
|
36
|
+
self.sync! if @serial.nil?
|
37
|
+
@serial
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the style of serial number in use by this zone.
|
41
|
+
def serial_style
|
42
|
+
self.sync! if @serial_style.nil?
|
43
|
+
@serial_style
|
44
|
+
end
|
45
|
+
|
46
|
+
# @private
|
47
|
+
def uri
|
48
|
+
"/Zone/#{self.name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Indicates whether the object has been synced with the API.
|
52
|
+
def in_sync?
|
53
|
+
@in_sync
|
54
|
+
end
|
55
|
+
|
56
|
+
# Attempt to sync the object with the API.
|
57
|
+
#
|
58
|
+
# @raise [RuntimeError] if the object does not exist or could not be synced.
|
59
|
+
def sync!
|
60
|
+
EZDyn.debug { "Zone{#{self.name}}.sync!" }
|
61
|
+
return self if self.in_sync?
|
62
|
+
|
63
|
+
data = @client.fetch_uri_data(uri: self.uri)
|
64
|
+
if data.is_a? Hash
|
65
|
+
sync_raw(data)
|
66
|
+
else
|
67
|
+
raise "Failed to sync zone #{self.name}"
|
68
|
+
end
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# @private
|
74
|
+
def sync_raw(raw)
|
75
|
+
EZDyn.debug { "Zone{#{self.name}}.sync_raw( #{raw.nil? ? nil : raw.to_json} )" }
|
76
|
+
@name = raw["zone"]
|
77
|
+
@type = raw["zone_type"]
|
78
|
+
@serial = raw["serial"]
|
79
|
+
@serial_style = raw["serial_style"]
|
80
|
+
@in_sync = true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the name of the zone for display purposes.
|
84
|
+
def to_s
|
85
|
+
self.name
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Client
|
90
|
+
# @private
|
91
|
+
def fetch_zones
|
92
|
+
EZDyn.debug { "Client.fetch_zones()" }
|
93
|
+
@zones = self.fetch_uri_data(uri: '/Zone/').
|
94
|
+
collect { |uri| Zone.new(client: self, uri: uri) }
|
95
|
+
end
|
96
|
+
|
97
|
+
# List all DNS zones known to this client.
|
98
|
+
#
|
99
|
+
# @return [Array<Zone>] An array of Zone objects.
|
100
|
+
def zones
|
101
|
+
EZDyn.debug { "Client.zones()" }
|
102
|
+
@zones ||= self.fetch_zones
|
103
|
+
end
|
104
|
+
|
105
|
+
# Match the given FQDN to a zone known to this client.
|
106
|
+
#
|
107
|
+
# @return [Zone] The appropriate Zone object, or nil if nothing matched.
|
108
|
+
def guess_zone(fqdn:)
|
109
|
+
EZDyn.debug { "Client.guess_zone( fqdn: #{fqdn} )" }
|
110
|
+
self.zones.find { |z| fqdn.downcase =~ /#{z.name.downcase}$/ }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ezdyn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Adams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: yard
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
description: Library and CLI tool for easy Dynect DNS updates
|
28
|
+
email: dadams@instructure.com
|
29
|
+
executables:
|
30
|
+
- ezdyn
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- README.md
|
36
|
+
- bin/ezdyn
|
37
|
+
- ezdyn.gemspec
|
38
|
+
- lib/ezdyn.rb
|
39
|
+
- lib/ezdyn/changes.rb
|
40
|
+
- lib/ezdyn/client.rb
|
41
|
+
- lib/ezdyn/consts.rb
|
42
|
+
- lib/ezdyn/crud.rb
|
43
|
+
- lib/ezdyn/log.rb
|
44
|
+
- lib/ezdyn/record.rb
|
45
|
+
- lib/ezdyn/record_type.rb
|
46
|
+
- lib/ezdyn/response.rb
|
47
|
+
- lib/ezdyn/version.rb
|
48
|
+
- lib/ezdyn/zone.rb
|
49
|
+
homepage: https://github.com/instructure
|
50
|
+
licenses:
|
51
|
+
- Nonstandard
|
52
|
+
metadata: {}
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 2.5.2
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: Simple library for Dyn Managed DNS
|
73
|
+
test_files: []
|