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,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
@@ -0,0 +1,4 @@
1
+ module EZDyn
2
+ # The version number of the library.
3
+ VERSION = "0.2.2"
4
+ end
@@ -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: []