class EZDyn::Client
The main class for Dyn REST API interaction.
For more information about the API, see {help.dyn.com/dns-api-knowledge-base/ the official documentation}.
Public Class Methods
Initializes a new Dyn REST API client.
@param customer_name [String] API customer name. @param username [String] API username. @param password [String] API password. @return [Client] Dyn REST API client.
# File lib/ezdyn/client.rb, line 16 def initialize(customer_name: ENV['DYN_CUSTOMER_NAME'], username: ENV['DYN_USERNAME'], password: ENV['DYN_PASSWORD']) @customer_name = customer_name @username = username @password = password if @customer_name.nil? or @username.nil? or @password.nil? EZDyn.info { "Some credentials are missing" } raise "Missing credentials" end @base_url = URI('https://api2.dynect.net/') @headers = { 'Content-Type' => 'application/json', } @logged_in = false end
Public Instance Methods
@private
# File lib/ezdyn/changes.rb, line 4 def add_pending_change(chg) @pending_changes ||= [] @pending_changes << chg end
@private
# File lib/ezdyn/client.rb, line 139 def build_uri(type:, fqdn:, id: nil) EZDyn.debug { "Client.build_uri( type: #{type}, fqdn: #{fqdn}, id: #{id} )" } "#{RecordType.find(type).uri_name}/#{self.guess_zone(fqdn: fqdn)}/#{fqdn}/#{id}" end
@private
# File lib/ezdyn/client.rb, line 35 def build_url(path) path = path.to_s path = "/#{path}" unless path.start_with?('/') path = "/REST#{path}" unless path.start_with?('/REST') @base_url.merge(path) end
Performs an actual REST call.
@param method [String] HTTP method to use, eg “GET”, “POST”, “DELETE”, “PUT”. @param uri [String] Relative API URI to call. @param payload [Hash] JSON data payload to send with request. @return [Response] Response
object.
# File lib/ezdyn/client.rb, line 48 def call_api(method: "get", uri:, payload: {}, max_attempts: EZDyn::API_RETRY_MAX_ATTEMPTS) EZDyn.debug { "API CALL: #{method.to_s.upcase} #{uri} #{ ( payload || {} ).to_json }" } self.login if not self.logged_in? and uri != "Session" payload_str = payload.to_json if payload == {} payload_str = nil end response = nil begin EZDyn.debug { "About to make REST request to #{method.to_s.upcase} #{uri}" } request_url = build_url(uri) request = Net::HTTP.const_get(method.capitalize).new(request_url) @headers.each do |name, value| request[name] = value end request.body = payload_str if payload_str http = Net::HTTP.new(request_url.host, request_url.port) http.use_ssl = true if request_url.scheme == 'https' response = Response.new(http.start { |http| http.request(request) }) if response.delayed? wait = EZDyn::API_RETRY_DELAY_SECONDS max_attempts.times do |retry_count| EZDyn.debug { "Async API call response retrieval, attempt #{retry_count + 1}" } EZDyn.debug { "Waiting for #{wait} seconds" } sleep wait response = self.call_api(uri: "/REST/Job/#{response.job_id}") break if response.success? EZDyn.debug { "Async response status: #{response.status}" } wait += (retry_count * EZDyn::API_RETRY_BACKOFF) end end EZDyn.debug { "Call was successful" } rescue => e EZDyn.info { "REST request to #{uri} threw an exception: #{e}" } raise "Got an exception: #{e}" end response end
@private
# File lib/ezdyn/changes.rb, line 31 def clear_pending_changes(zone: nil) if zone.nil? @pending_changes = [] else @pending_changes.delete_if do |pc| pc.zone.name == zone.to_s end end end
Commits any pending changes to a zone or to all zones with an optional update message.
@raise [RuntimeError] if any commit was unsuccessful. @param zone [String] The zone name to commit. By default all pending
changes from any zone will be committed.
@param message [String] If supplied, this message will be used for the
zone update notes field.
# File lib/ezdyn/crud.rb, line 165 def commit(zone: nil, message: nil) EZDyn.debug { "Client{}.commit( zone: #{zone} )" } payload = { publish: true } payload[:notes] = message if not message.nil? zones = zone.nil? ? self.pending_change_zones : [Zone.find(zone)] zones.each do |zone| EZDyn.debug { " - committing Zone{#{zone.name}}" } response = self.call_api( method: "put", uri: "Zone/#{zone}", payload: payload ) if response.success? self.clear_pending_changes(zone: zone) else EZDyn.debug { " - failed to commit Zone{#{zone.name}}: #{response.simple_message}" } raise "Could not commit zone #{zone.name}: #{response.simple_message}" end end end
Calls the Dyn API to create a new record.
@note As a side effect upon success, this method creates a [CreateChange]
object in the `pending_changes` array.
@raise [RuntimeError] if the record could not be created. @param type [RecordType, String, Symbol] Type of record to create. @param fqdn [String] FQDN of the record to create. @param value [String, Array] Value(s) to submit for the record data. @param ttl [String, Integer] TTL value (optional). @return [Record] A Record
object filled with the values returned by the API.
# File lib/ezdyn/crud.rb, line 14 def create(type:, fqdn:, value:, ttl: nil) EZDyn.debug { "Client.create( type: #{type}, fqdn: #{fqdn}, value: #{value}, ttl: #{ttl} )" } ttl = ( ttl || Record::DefaultTTL ).to_i values = Array(value) return values.map do |val| value_key = Array(RecordType.find(type).value_key) split_val = val.split(' ', value_key.length) response = self.call_api( method: "post", uri: self.build_uri(type: type, fqdn: fqdn), payload: { rdata: value_key.zip(split_val).to_h, ttl: ttl } ) if not response.success? raise "Failed to create record: #{response.simple_message}" end record = Record.new(client: self, raw: response.data) self.add_pending_change(CreateChange.new(record: record)) record end end
Delete a record.
@note As a side effect upon success, this method creates a [DeleteChange]
object in the `pending_changes` array.
@raise [RuntimeError] if record does not exist. @raise [RuntimeError] if record cannot be deleted. @param record [Record] The Record
object of the record to be deleted.
# File lib/ezdyn/crud.rb, line 103 def delete(record:) EZDyn.debug { "Client{}.delete( record: Record{#{record.record_id}} )" } if not record.sync!.exists? raise "Nothing to delete" end response = self.call_api( method: "delete", uri: record.uri ) if not response.success? raise "Could not delete: #{response.simple_message}" end self.add_pending_change(DeleteChange.new(record: record)) end
Deletes all records of a specified type and FQDN. Specify type `:any` (or don't specify `type` at all) to delete all records for a FQDN.
@note As a side effect upon success, this method creates [DeleteChange]
objects in the `pending_changes` array for each record deleted.
@param type [RecordType,String,Symbol] The type of record(s) to delete.
Defaults to `:any`.
@param fqdn [String] The FQDN of the record(s) to delete.
# File lib/ezdyn/crud.rb, line 130 def delete_all(type: :any, fqdn:) EZDyn.debug { "Client{}.delete_all( type: #{type}, fqdn: #{fqdn} )" } self.records_for(type: type, fqdn: fqdn).each do |record| record.delete! end end
Signals if a particular record exists.
@param type [EZDyn::RecordType, String, Symbol] RecordType
of record to
check for. `:any` will match any record type.
@param fqdn [String] FQDN of records to check for. Required. @param id [String] API record ID of the record to check for. Optional. @return [Boolean] Returns true if any such record exists.
# File lib/ezdyn/client.rb, line 201 def exists?(type: :any, fqdn:, id: nil) EZDyn.debug { "Client.exists?( type: #{type}, fqdn: #{fqdn}, id: #{id} )" } if not id.nil? and type != :any EZDyn.debug { "Fetching a single record" } self.fetch_record(type: type, fqdn: fqdn, id: id) != [] else EZDyn.debug { "Fetching records_for #{type} #{fqdn}" } self.records_for(type: type, fqdn: fqdn).count > 0 end end
Fetches a record if you know the type, FQDN, and ID.
@param type [EZDyn::RecordType, String, Symbol] The record type to be fetched. @param fqdn [String] FQDN of the record to fetch. @param id [String] Dyn API record ID of the record to fetch. @return [EZDyn::Record] Newly created, synced Record
object.
# File lib/ezdyn/client.rb, line 163 def fetch_record(type:, fqdn:, id:) EZDyn.debug { "Client.fetch_record( type: #{type}, fqdn: #{fqdn}, id: #{id} )" } data = self.fetch_uri_data(uri: self.build_uri(type: type, fqdn: fqdn, id: id)) if data == [] nil else Record.new(client: self, raw: data) end end
@private
# File lib/ezdyn/client.rb, line 145 def fetch_uri_data(uri:) EZDyn.debug { "Client.fetch_uri_data( uri: #{uri} )" } response = call_api(uri: uri) if response.success? EZDyn.debug { "fetch_uri_data success!" } return response.data else EZDyn.debug { "fetch_uri_data failure!" } return [] end end
@private
# File lib/ezdyn/zone.rb, line 91 def fetch_zones EZDyn.debug { "Client.fetch_zones()" } @zones = self.fetch_uri_data(uri: '/Zone/'). collect { |uri| Zone.new(client: self, uri: uri) } end
Match the given FQDN to a zone known to this client.
@return [Zone] The appropriate Zone
object, or nil if nothing matched.
# File lib/ezdyn/zone.rb, line 108 def guess_zone(fqdn:) EZDyn.debug { "Client.guess_zone( fqdn: #{fqdn} )" } self.zones.find { |z| fqdn.downcase =~ /#{z.name.downcase}$/ } end
Signals whether the client has successfully logged in.
@return [Boolean] True if the client is logged in.
# File lib/ezdyn/client.rb, line 100 def logged_in? @logged_in end
Begin a Dyn REST API session. This method will be called implicitly when required.
# File lib/ezdyn/client.rb, line 105 def login EZDyn.debug { "Logging in..." } response = call_api( method: "post", uri: "Session", payload: { customer_name: @customer_name, user_name: @username, password: @password, } ) EZDyn.debug { "Response status: #{response.status}" } if response.success? @headers['Auth-Token'] = response.data["token"] @logged_in = true else raise "Login failed" end end
End the current Dyn REST API session.
# File lib/ezdyn/client.rb, line 128 def logout call_api( method: "delete", uri: "Session" ) @headers.delete('Auth-Token') @logged_in = false end
@private
# File lib/ezdyn/changes.rb, line 10 def pending_change_zones @pending_changes.collect(&:zone).uniq end
List currently pending changes (optionally per zone).
@param zone [String] (Optional) If specified, only return pending changes
for the named zone.
@return [Array] Array of [Change] objects awaiting commit.
# File lib/ezdyn/changes.rb, line 19 def pending_changes(zone: nil) if zone.nil? @pending_changes else zone = Zone.new(client: self, name: zone) @pending_changes.select do |pc| pc.zone.name == zone.name end end end
Fetches all records for a particular FQDN (and record type).
@param type [EZDyn::RecordType, String, Symbol] Desired record type. Use
`:any` to fetch all records.
@param fqdn [String] FQDN of the records to fetch. @return [Array<EZDyn::Record>] An array of synced Record
objects.
# File lib/ezdyn/client.rb, line 179 def records_for(type: :any, fqdn:) EZDyn.debug { "Client.records_for( type: #{type}, fqdn: #{fqdn} )" } self.fetch_uri_data(uri: self.build_uri(type: type, fqdn: fqdn)). collect { |uri| Record.new(client: self, uri: uri) } end
Fetches all records for a zone. NOTE: This can take very long, and probably isn't what you want.
@param zone [String] The Zone
to lookup.
# File lib/ezdyn/client.rb, line 188 def records_in_zone(zone:) EZDyn.debug { "Client.records_in_zone( zone: #{zone}" } self.fetch_uri_data(uri: "/AllRecord/#{zone}"). collect{ |uri| Record.new(client: self, uri: uri) } end
Rolls back any pending changes to a zone or to all zones.
@param zone [String] The zone name to roll back. By default all pending
changes from any zone will be rolled back.
# File lib/ezdyn/crud.rb, line 141 def rollback(zone: nil) EZDyn.debug { "Client{}.rollback( zone: #{zone} )" } zones = zone.nil? ? self.pending_change_zones : [Zone.new(client: self, name: zone)] zones.each do |zone| EZDyn.debug { " - rolling back Zone{#{zone.name}}" } response = self.call_api(method: "delete", uri: "ZoneChanges/#{zone}") if response.success? self.clear_pending_changes(zone: zone) else EZDyn.debug { " - failed to roll back Zone{#{zone.name}}: #{response.simple_message}" } raise "Failed to roll back zone #{zone.name}" end end end
Calls the Dyn API to update or create a record. Could also be called `upsert`.
@note As a side effect upon success, this method creates an [UpdateChange]
or a [CreateChange] object in the `pending_changes` array.
@raise [RuntimeError] if the record could not be created or updated. @param record [Record] A Record
object for the record to be updated.
Either this parameter or the `type` and `fqdn` parameters are required.
@param type [RecordType, String, Symbol] Type of record to update/create (not required if `record` is provided). @param fqdn [String] FQDN of the record to update/create (not requried if `record` is provided). @param value [String, Array] New value(s) to submit for the record data (optional if updating TTL and record already exists). @param ttl [String, Integer] New TTL value (optional). @return [Record] A Record
object filled with the values returned by the API.
# File lib/ezdyn/crud.rb, line 54 def update(record: nil, type: nil, fqdn: nil, value: nil, ttl: nil) EZDyn.debug { "Client.update( record: #{record.nil? ? nil : "Record{#{record.record_id}}"}, type: #{type}, fqdn: #{fqdn}, value: #{value}, ttl: #{ttl} )" } values = Array(value) if record.nil? if type.nil? or fqdn.nil? raise "Cannot update a record without a Record object or both record type and FQDN" end records = self.records_for(type: type, fqdn: fqdn) else records = [record] end response = nil if records.count.zero? && (fqdn.nil? || type.nil? || value.nil?) raise "Record doesn't exist, and insufficient information to create it was given" end response = self.call_api( method: "put", uri: self.build_uri(type: type, fqdn: fqdn), payload: { "#{type}Records" => values.map do |val| value_key = Array(RecordType.find(type).value_key) split_val = val.split(' ', value_key.length) { rdata: value_key.zip(split_val).to_h, ttl: ttl || records.map(&:ttl).min } end } ) if not response.success? raise "Could not update: #{response.simple_message}" end new_records = response.data.map { |res| Record.new(client: self, raw: res) } self.add_pending_change(UpdateChange.new(records: records, new_records: new_records)) return new_records end
List all DNS zones known to this client.
@return [Array<Zone>] An array of Zone
objects.
# File lib/ezdyn/zone.rb, line 100 def zones EZDyn.debug { "Client.zones()" } @zones ||= self.fetch_zones end