ezdyn 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []