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.
- 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: []
|