diff --git a/.gitignore b/.gitignore index 845127d..7004dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,11 @@ *.sw? .rbenv .rvmrc -.ruby-gemset -.ruby-version + +# https://github.com/fog/fog/wiki/Create-New-Provider-from-Scratch +#.ruby-gemset +#.ruby-version + .bundle .DS_Store .idea @@ -28,3 +31,4 @@ tags tests/digitalocean/fixtures/ providers/*/doc +.rakeTasks diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..b8388a4 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +fog-digitalocean diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..c1026d2 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.3.1 diff --git a/.travis.yml b/.travis.yml index 75c5032..000e1b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,20 @@ language: ruby sudo: false -script: bundle exec rake test +script: + | + bundle config disable_exec_load true + bundle exec rake test cache: bundler rvm: - - 2.0 - 2.1 - 2.2 - jruby-head + # Unable to make these work right now [2017-03-25 Christo] + - 2.3.1 + - 2.4.0 gemfile: - Gemfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0443b63..72e2f94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,16 +9,16 @@ New contributors are always welcome, when it doubt please ask questions. We stri ### Coding * Pick a task: - * Offer feedback on open [pull requests](https://github.com/fog/fog/pulls). - * Review open [issues](https://github.com/fog/fog/issues) for things to help on. - * [Create an issue](https://github.com/fog/fog/issues/new) to start a discussion on additions or features. + * Offer feedback on open [pull requests](https://github.com/fog/fog-digitalocean/pulls). + * Review open [issues](https://github.com/fog/fog-digitalocean/issues) for things to help on. + * [Create an issue](https://github.com/fog/fog-digitalocean/issues/new) to start a discussion on additions or features. * Fork the project, add your changes and tests to cover them in a topic branch. -* Commit your changes and rebase against `fog/fog` to ensure everything is up to date. -* [Submit a pull request](https://github.com/fog/fog/compare/) +* Commit your changes and rebase against `fog/fog-digitalocean` to ensure everything is up to date. +* [Submit a pull request](https://github.com/fog/fog-digitalocean/compare/) ### Non-Coding -* Offer feedback on open [issues](https://github.com/fog/fog/issues). +* Offer feedback on open [issues](https://github.com/fog/fog-digitalocean/issues). * Write and help edit [documentation](https://github.com/fog/fog.github.com). * Translate [documentation](https://github.com/fog/fog.github.com) in to other languages. * Organize or volunteer at events. diff --git a/Gemfile b/Gemfile index d3cca20..259d464 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,11 @@ -source "https://rubygems.org" +source "https://rubygems.org" do -gem "coveralls", require: false + group :development, :test, :integration do + gem 'coveralls', require: false + gem 'term-ansicolor', require: false + gem 'zonefile', '>= 1.0.4', require: false + end + +end gemspec diff --git a/fog-digitalocean.gemspec b/fog-digitalocean.gemspec index e664227..2bf7e41 100644 --- a/fog-digitalocean.gemspec +++ b/fog-digitalocean.gemspec @@ -55,6 +55,8 @@ Gem::Specification.new do |s| s.add_dependency 'fog-json', '>= 1.0' s.add_dependency 'fog-xml', '>= 0.1' s.add_dependency 'ipaddress', '>= 0.5' + s.add_dependency 'activesupport', '~> 4.2' + s.add_dependency 'zonefile', '>= 1.04' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {spec,tests}/*`.split("\n") diff --git a/gemfiles/Gemfile-edge b/gemfiles/Gemfile-edge index bd5433c..edbcade 100644 --- a/gemfiles/Gemfile-edge +++ b/gemfiles/Gemfile-edge @@ -6,7 +6,8 @@ gem "fog-json", :github => "fog/fog-json" group :development, :test do # This is here because gemspec doesn"t support require: false - gem "coveralls", :require => false + gem "coveralls", require: false + gem "term-ansicolor", require: false gem "netrc", :require => false gem "octokit", :require => false end diff --git a/lib/fog/digitalocean.rb b/lib/fog/digitalocean.rb index 54e609c..dcf7bbb 100644 --- a/lib/fog/digitalocean.rb +++ b/lib/fog/digitalocean.rb @@ -1 +1,2 @@ require 'fog/digitalocean/compute' +require 'fog/digitalocean/dns' diff --git a/lib/fog/digitalocean/compute.rb b/lib/fog/digitalocean/compute.rb index 494d9ff..541e2df 100644 --- a/lib/fog/digitalocean/compute.rb +++ b/lib/fog/digitalocean/compute.rb @@ -104,8 +104,13 @@ def request(params) response = @connection.request(params) rescue Excon::Errors::HTTPStatusError => error raise case error - when Excon::Errors::NotFound - NotFound.slurp(error) + when Excon::Errors::NotFound + klasa = self.class.name.split('::') + klasa[-1] = 'NotFound' + klass = klasa.inject(Object) { |mod, class_name| + mod.const_get(class_name) + } + klass.slurp(error) else error end diff --git a/lib/fog/digitalocean/core.rb b/lib/fog/digitalocean/core.rb index ae91538..92e0dce 100644 --- a/lib/fog/digitalocean/core.rb +++ b/lib/fog/digitalocean/core.rb @@ -5,5 +5,6 @@ module Fog module DigitalOcean extend Fog::Provider service(:compute, 'Compute') + service(:dns, 'DNS') end end diff --git a/lib/fog/digitalocean/dns.rb b/lib/fog/digitalocean/dns.rb new file mode 100644 index 0000000..758fc9b --- /dev/null +++ b/lib/fog/digitalocean/dns.rb @@ -0,0 +1,206 @@ +require 'fog/digitalocean/core' +require 'active_support/core_ext/hash/indifferent_access' + +module Fog + module DNS + class DigitalOcean < Fog::Service + requires :digitalocean_token + + model_path 'fog/digitalocean/models/dns' + model :domain + collection :domains + model :zone + collection :zones + model :record + collection :records + + request_path 'fog/digitalocean/requests/dns' + request :list_domains + request :create_domain + request :get_domain + request :delete_domain + + request :list_records + request :create_record + request :get_record + request :update_record + request :delete_record + + class Mock + def self.data + @data ||= Hash.new do |hash, key| + hash[key] = { + :domains => [ + { + "name" => "domain.com", + "ttl" => 1800, + "zone_file" => "$ORIGIN domain.com.\n$TTL 1800\ndomain.com. IN SOA ns1.digitalocean.com. hostmaster.domain.com. 1490145863 10800 3600 604800 1800\ndomain.com. 1800 IN NS ns1.digitalocean.com.\ndomain.com. 1800 IN NS ns2.digitalocean.com.\ndomain.com. 1800 IN NS ns3.digitalocean.com.\ndomain.com. 1800 IN A 127.0.0.3\n" + }, + { + "name" => "domain.net", + "ttl" => 1800, + "zone_file" => "$ORIGIN domain.net.\n$TTL 1800\ndomain.net. IN SOA ns1.digitalocean.com. hostmaster.domain.net. 1488909707 10800 3600 604800 1800\ndomain.net. 1800 IN NS ns1.digitalocean.com.\ndomain.net. 1800 IN NS ns2.digitalocean.com.\ndomain.net. 1800 IN NS ns3.digitalocean.com.\ndomain.net. 1800 IN A 64.99.64.37\n" + }, + { + "name" => "domain.org", + "ttl" => 1800, + "zone_file" => "$ORIGIN domain.org.\n$TTL 1800\ndomain.org. IN SOA ns1.digitalocean.com. hostmaster.domain.org. 1488395060 10800 3600 604800 1800\ndomain.org. 1800 IN NS ns1.digitalocean.com.\ndomain.org. 1800 IN NS ns2.digitalocean.com.\ndomain.org. 1800 IN NS ns3.digitalocean.com.\ndomain.org. 3600 IN A 208.38.128.210\ndomain.org. 1800 IN MX 1 aspmx.l.google.com.\ndomain.org. 1800 IN MX 5 alt1.aspmx.l.google.com.\ndomain.org. 1800 IN MX 5 alt2.aspmx.l.google.com.\ndomain.org. 1800 IN MX 10 alt3.aspmx.l.google.com.\ndomain.org. 1800 IN MX 10 alt4.aspmx.l.google.com.\n" + } + ], + :domain_records => { + 'domain.com' => + [ + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns1.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns2.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns3.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "A", + "name" => "@", + "data" => "127.0.0.1", + "priority" => nil, + "port" => nil, + "weight" => nil + } + ], + 'domain.net' => + [ + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns1.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns2.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns3.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "A", + "name" => "@", + "data" => "127.0.0.1", + "priority" => nil, + "port" => nil, + "weight" => nil + } + ], + 'domain.org' => + [ + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns1.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns2.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "NS", + "name" => "@", + "data" => "ns3.digitalocean.com", + "priority" => nil, + "port" => nil, + "weight" => nil + }, + { "id" => Fog::Mock.random_numbers(8).to_i, + "type" => "A", + "name" => "@", + "data" => "127.0.0.1", + "priority" => nil, + "port" => nil, + "weight" => nil + } + ], + } + } + end + end + + def initialize(options={}) + @digitalocean_token = options[:digitalocean_token] + end + + def data + self.class.data[@digitalocean_token] + end + + def reset_data + self.class.data.delete(@digitalocean_token) + end + end + + class Real < Fog::Compute::DigitalOcean::Real + # def initialize(options={}) + # digitalocean_token = options[:digitalocean_token] + # persistent = false + # options = { + # headers: { + # 'Authorization' => "Bearer #{digitalocean_token}", + # } + # } + # @connection = Fog::Core::Connection.new 'https://api.digitalocean.com', persistent, options + # end + # + # def request(params) + # params[:headers] ||= {} + # begin + # response = @connection.request(params) + # rescue Excon::Errors::HTTPStatusError => error + # raise case error + # when Excon::Errors::NotFound + # NotFound.slurp(error) + # else + # error + # end + # end + # unless response.body.empty? + # response.body = Fog::JSON.decode(response.body) + # end + # response + # end + end + end + end +end diff --git a/lib/fog/digitalocean/models/compute/images.rb b/lib/fog/digitalocean/models/compute/images.rb index 2b34000..9fae39d 100644 --- a/lib/fog/digitalocean/models/compute/images.rb +++ b/lib/fog/digitalocean/models/compute/images.rb @@ -15,7 +15,7 @@ class Images < Fog::Compute::DigitalOcean::PagingCollection # @see https://developers.digitalocean.com/documentation/v2/#list-all-images def all(filters = {}) data = service.list_images(filters) - links = data.body["links"] + links = data.body['meta']['links'] get_paged_links(links) images = data.body["images"] load(images) diff --git a/lib/fog/digitalocean/models/compute/servers.rb b/lib/fog/digitalocean/models/compute/servers.rb index e0e5fa5..6b61dc0 100644 --- a/lib/fog/digitalocean/models/compute/servers.rb +++ b/lib/fog/digitalocean/models/compute/servers.rb @@ -17,7 +17,7 @@ class Servers < Fog::Compute::DigitalOcean::PagingCollection # @see https://developers.digitalocean.com/documentation/v2/#droplets def all(filters = {}) data = service.list_servers(filters) - links = data.body["links"] + links = data.body['meta']['links'] get_paged_links(links) droplets = data.body["droplets"] load(droplets) diff --git a/lib/fog/digitalocean/models/compute/ssh_keys.rb b/lib/fog/digitalocean/models/compute/ssh_keys.rb index 85af5ba..bcd8b56 100644 --- a/lib/fog/digitalocean/models/compute/ssh_keys.rb +++ b/lib/fog/digitalocean/models/compute/ssh_keys.rb @@ -15,7 +15,7 @@ class SshKeys < Fog::Compute::DigitalOcean::PagingCollection # @see https://developers.digitalocean.com/documentation/v2/#list-all-keys def all(filters = {}) data = service.list_ssh_keys(filters) - links = data.body["links"] + links = data.body['meta']['links'] get_paged_links(links) keys = data.body["ssh_keys"] load(keys) diff --git a/lib/fog/digitalocean/models/compute/volumes.rb b/lib/fog/digitalocean/models/compute/volumes.rb index 9de4bc7..d1cbc94 100644 --- a/lib/fog/digitalocean/models/compute/volumes.rb +++ b/lib/fog/digitalocean/models/compute/volumes.rb @@ -15,7 +15,7 @@ class Volumes < Fog::Compute::DigitalOcean::PagingCollection # @see https://developers.digitalocean.com/documentation/v2/#list-all-images def all(filters = {}) data = service.list_volumes(filters) - links = data.body["links"] + links = data.body['meta']['links'] get_paged_links(links) volumes = data.body["volumes"] load(volumes) diff --git a/lib/fog/digitalocean/models/dns/domain.rb b/lib/fog/digitalocean/models/dns/domain.rb new file mode 100644 index 0000000..a7c5edc --- /dev/null +++ b/lib/fog/digitalocean/models/dns/domain.rb @@ -0,0 +1,91 @@ +module Fog + module DNS + class DigitalOcean + class Domain < Fog::Model + + # has_many :records, :records + + identity :name + attribute :ttl + attribute :zone_file + attribute :ip_address + + def initialize(attributes={}) + attributes = attributes.with_indifferent_access + attributes[:ip_address] ||= '127.0.0.1' + super + @attributes = @attributes.with_indifferent_access + self + end + + def create + requires :name, :ip_address + resp = service.create_domain(name, ip_address) + merge_attributes(resp.body['domain']) + true + end + alias :save :create + + def delete + requires :name + service.delete_domain name + end + alias :destroy :delete + + def get + requires :name + service.get_domain name + end + + def domain + requires :name + self.name + end + alias :zone :domain + + def records + @records ||= begin + Fog::DNS::DigitalOcean::Records.new( + :domain => self, + :service => service + ) + end + end + + def zonefile + @zonefile ||= begin + reload unless zone_file + require 'zonefile' + + ::Zonefile.new(zone_file) + end + end + + def nameservers + @nameservers ||= begin + zonefile.records.with_indifferent_access['ns'].map { |rec| rec[:host] } + # else + # self.records.all!.select { |rec| rec.type.eql?('NS') }.map { |rec| rec.data } + # end + end + end + + def soa + @soa ||= begin + zonefile.soa.with_indifferent_access + end + end + + def ttl + @ttl ||= begin + zonefile.ttl.to_i + end + end + + def to_h + self.attributes + end + end + end + end +end \ No newline at end of file diff --git a/lib/fog/digitalocean/models/dns/domains.rb b/lib/fog/digitalocean/models/dns/domains.rb new file mode 100644 index 0000000..9249c29 --- /dev/null +++ b/lib/fog/digitalocean/models/dns/domains.rb @@ -0,0 +1,61 @@ +require 'fog/digitalocean/models/paging_collection' + +module Fog + module DNS + class DigitalOcean + class Domains < Fog::Compute::DigitalOcean::PagingCollection + model Fog::DNS::DigitalOcean::Domain + + # Returns list of domains + # @return [Fog::DNS::DigitalOceanV2::Domains] Retrieves a list of ssh keys. + # @raise [Fog::DNS::DigitalOceanV2::NotFound] - HTTP 404 + # @raise [Fog::DNS::DigitalOceanV2::BadRequest] - HTTP 400 + # @raise [Fog::DNS::DigitalOceanV2::InternalServerError] - HTTP 500 + # @raise [Fog::DNS::DigitalOceanV2::ServiceError] + # @see https://developers.digitalocean.com/documentation/v2/#list-all-keys + def all(filters = {}) + data = service.list_domains(filters) + links = data.body['meta']['links'] + get_paged_links(links) + keys = data.body["domains"] + load(keys) + end + + # Returns domain + # @return [Fog::DNS::DigitalOceanV2::Domains] Retrieves a list of ssh keys. + # @raise [Fog::DNS::DigitalOceanV2::NotFound] - HTTP 404 + # @raise [Fog::DNS::DigitalOceanV2::BadRequest] - HTTP 400 + # @raise [Fog::DNS::DigitalOceanV2::InternalServerError] - HTTP 500 + # @raise [Fog::DNS::DigitalOceanV2::ServiceError] + # @see https://developers.digitalocean.com/documentation/v2/#retrieve-an-existing-key + def get(id) + key = service.get_domain(id).body['domain'] + new(key) if key + rescue Fog::Errors::NotFound + nil + end + + def delete(id) + id = id.name if id.is_a?(Fog::Model) + id = id.with_indifferent_access[:name] if id.is_a?(Hash) + return false unless id + response = service.delete_domain(id) + if response.status == 204 + self.replace(self.select{ |dom| !dom.name.eql?(id)}) + true + else + false + end + rescue Fog::Errors::NotFound + nil + end + + def create(attributes = {}) + object = super + self << object + object + end + end + end + end +end diff --git a/lib/fog/digitalocean/models/dns/record.rb b/lib/fog/digitalocean/models/dns/record.rb new file mode 100644 index 0000000..3143a77 --- /dev/null +++ b/lib/fog/digitalocean/models/dns/record.rb @@ -0,0 +1,115 @@ +require 'active_support/core_ext/hash/indifferent_access' + +module Fog + module DNS + class DigitalOcean + class Record < Fog::Model + + # provider_class :Record + # collection_name :records + + identity :id, type: 'Integer' # number A unique identifier for each domain record. + attribute :type # string The type of the DNS record (ex: A, CNAME, TXT, ...). + attribute :name # string The name to use for the DNS record. + attribute :data # string The value to use for the DNS record. + attribute :priority, type: 'Integer' # nullable number The priority for SRV and MX records. + attribute :port , type: 'Integer' # nullable number The port for SRV records. + attribute :weight , type: 'Integer' # nullable number The weight for SRV records. + + def initialize(new_attrs={}) + new_attrs = new_attrs.with_indifferent_access + new_attrs[:data] ||= new_attrs[:value] + new_attrs[:data] ||= new_attrs[:host] + new_attrs[:priority] ||= new_attrs[:pri] + super + @attributes = @attributes.with_indifferent_access + self + end + + def domain + @domain + end + + def create + requires :name, :type, :data + merge_attributes(service.create_domain_record(name, type, data).body['id']) + true + end + + def delete + requires :id + service.delete_record(domain.name, id) + end + alias :destroy :delete + + def update(delta={}) + requires :id + options = attributes_to_options('UPDATE') + delta = options.merge(delta.with_indifferent_access) + delta.delete('id') + data = service.create_record(domain.name, delta).body['domain_record'].with_indifferent_access + service.delete_record(domain.name, options) + merge_attributes(data) + true + end + alias :modify :update + + def save + options = attributes_to_options('CREATE') + data = (self.id ? service.update_record(domain.name, options) : service.create_record(domain.name, options)).body['domain_record'] + merge_attributes(data) + true + end + + def to_h + self.attributes + end + + def value + @attributes[:data] + end + + def value=(val) + @attributes[:data] = val + end + + def host + @attributes[:data] + end + + def host=(val) + @attributes[:data] = val + end + + def [](key) + @attributes[key] + end + + def []=(key, val) + @attributes[key] = val + end + + private + + def domain=(new_zone) + @domain = new_zone + end + + def attributes_to_options(action) + requires :name, :type, :data + # requires_one :value, :alias_target + options = { + id: id, + name: name, + type: type, + data: data, + priority: priority, + port: port, + weight: weight, + } + options.with_indifferent_access + end + end + end + end +end \ No newline at end of file diff --git a/lib/fog/digitalocean/models/dns/records.rb b/lib/fog/digitalocean/models/dns/records.rb new file mode 100644 index 0000000..4839287 --- /dev/null +++ b/lib/fog/digitalocean/models/dns/records.rb @@ -0,0 +1,87 @@ +# require 'fog/digitalocean/models/record' +require 'fog/digitalocean/models/paging_collection' + +module Fog + module DNS + class DigitalOcean + class Records < Fog::Compute::DigitalOcean::PagingCollection + attribute :domain + + model Fog::DNS::DigitalOcean::Record + + # Returns list of records + # @return [Fog::DNS::DigitalOceanV2::Records] Retrieves a list of domains. + # @raise [Fog::DNS::DigitalOceanV2::NotFound] - HTTP 404 + # @raise [Fog::DNS::DigitalOceanV2::BadRequest] - HTTP 400 + # @raise [Fog::DNS::DigitalOceanV2::InternalServerError] - HTTP 500 + # @raise [Fog::DNS::DigitalOceanV2::ServiceError] + # @see https://developers.digitalocean.com/documentation/v2/#list-all-keys + def all(filters = {}) + data = service.list_records(domain.name, filters) + links = data.body['links'] + get_paged_links(links) + keys = data.body["domain_records"] + load(keys) + end + + def all!(filters = {}) + list = all(filters) + begin + page = next_page(filters) + list += page if page + end while page + list + end + + # Returns record + # @return [Fog::DNS::DigitalOceanV2::Records] Retrieves a list of records + # @raise [Fog::DNS::DigitalOceanV2::NotFound] - HTTP 404 + # @raise [Fog::DNS::DigitalOceanV2::BadRequest] - HTTP 400 + # @raise [Fog::DNS::DigitalOceanV2::InternalServerError] - HTTP 500 + # @raise [Fog::DNS::DigitalOceanV2::ServiceError] + # @see https://developers.digitalocean.com/documentation/v2/#retrieve-an-existing-key + def get(id) + resp = service.get_record(domain.name, id) + key = resp.body['domain_record'] rescue nil + if key + new(key) + else + nil + end + rescue Fog::Errors::NotFound + nil + end + + def create(attributes = {}) + object = super + self.replace(self.all!) + object + end + + def delete(id) + id = id.id if id.is_a?(Fog::Model) + id = id.with_indifferent_access[:id] if id.is_a?(Hash) + return false unless id + response = service.delete_record(self.domain.name, id) + if response.status == 204 + self.replace(self.select{ |dom| dom.id != id }) + true + else + false + end + rescue Fog::Errors::NotFound + nil + end + + def new(attributes = {}) + requires :domain + super({ :domain => domain }.merge!(attributes)) + end + + def to_s + @domain + end + end + end + end +end diff --git a/lib/fog/digitalocean/models/dns/zone.rb b/lib/fog/digitalocean/models/dns/zone.rb new file mode 100644 index 0000000..79ca647 --- /dev/null +++ b/lib/fog/digitalocean/models/dns/zone.rb @@ -0,0 +1,13 @@ +module Fog + module DNS + class DigitalOcean + class Zone < Fog::DNS::DigitalOcean::Domain + + identity :name + attribute :ttl + attribute :zone_file + attribute :ip_address + end + end + end +end \ No newline at end of file diff --git a/lib/fog/digitalocean/models/dns/zones.rb b/lib/fog/digitalocean/models/dns/zones.rb new file mode 100644 index 0000000..f3c8fe8 --- /dev/null +++ b/lib/fog/digitalocean/models/dns/zones.rb @@ -0,0 +1,9 @@ +module Fog + module DNS + class DigitalOcean + class Zones < Fog::DNS::DigitalOcean::Domains + model Fog::DNS::DigitalOcean::Zone + end + end + end +end diff --git a/lib/fog/digitalocean/models/paging_collection.rb b/lib/fog/digitalocean/models/paging_collection.rb index 3ef77f5..4b4eaf8 100644 --- a/lib/fog/digitalocean/models/paging_collection.rb +++ b/lib/fog/digitalocean/models/paging_collection.rb @@ -1,18 +1,33 @@ module Fog module Compute class DigitalOcean +=begin + +The links object is returned as part of the response body when pagination is enabled. By default, 25 objects are returned per page. If the response contains 25 objects or fewer, no links object will be returned. If the response contains more than 25 objects, the first 25 will be returned along with the links object. + +You can request a different pagination limit or force pagination by appending ?per_page= to the request with the number of items you would like per page. For instance, to show only two results per page, you could add ?per_page=2 to the end of your query. The maximum number of results per page is 200. + +The links object contains a pages object. The pages object, in turn, contains keys indicating the relationship of additional pages. The values of these are the URLs of the associated pages. The keys will be one of the following: + +first: The URI of the first page of results. +prev: The URI of the previous sequential page of results. +next: The URI of the next sequential page of results. +last: The URI of the last page of results. +The pages object will only include the links that make sense. So for the first page of results, no first or prev links will ever be set. This convention holds true in other situations where a link would not make sense. + +=end class PagingCollection < Fog::Collection attribute :next attribute :last - def next_page - all(page: @next) if @next != @last + def next_page(filters={}) + all(filters.merge({page: @next})) if @next && @next > 0 && @next <= @last end - def previous_page - if @next.to_i > 2 - all(page: @next.to_i - 2) + def previous_page(filters={}) + if @next > 2 + all(filters.merge({page: @next - 2})) end end @@ -31,10 +46,15 @@ def get_page(link) end def get_paged_links(links) - next_link = deep_fetch(links, "pages", "next").to_s - last_link = deep_fetch(links, "pages", "last").to_s - @next = get_page(next_link) || @next - @last = get_page(last_link) || @last + if links && links.size > 0 + next_link = deep_fetch(links, "pages", "next").to_s + last_link = deep_fetch(links, "pages", "last").to_s + @next = get_page(next_link).to_i || @next + @last = get_page(last_link).to_i || @last + else + @next = 0 + @last = 0 + end end end end diff --git a/lib/fog/digitalocean/requests/dns/create_domain.rb b/lib/fog/digitalocean/requests/dns/create_domain.rb new file mode 100644 index 0000000..47ca975 --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/create_domain.rb @@ -0,0 +1,85 @@ +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + + def create_domain(name, ip_address) + create_options = { + :name => name, + :ip_address => ip_address, + } + + encoded_body = Fog::JSON.encode(create_options) + + request( + :expects => [201], + :headers => { + 'Content-Type' => "application/json; charset=UTF-8", + }, + :method => 'POST', + :path => '/v2/domains', + :body => encoded_body, + ) + end + alias :create_zone :create_domain + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def create_domain(name, ip_address) + response = Excon::Response.new + response.status = 200 + + data[:domains] << { + 'name' => name, + 'ttl' => 1800, + 'zone_file' => "$ORIGIN #{name}.\n$TTL 1800\n#{name}. IN SOA ns1.digitalocean.com. hostmaster.#{name}. 1490145863 10800 3600 604800 1800\n#{name}. 1800 IN NS ns1.digitalocean.com.\n#{name}. 1800 IN NS ns2.digitalocean.com.\n#{name}. 1800 IN NS ns3.digitalocean.com.\n#{name}. 1800 IN A #{ip_address}\n" + }.with_indifferent_access + data[:domain_records][name] ||= [] + require 'zonefile' + + zf = ::Zonefile.new(data[:domains].last[:zone_file]) + zf.records.each do |type, list| + list.each do |rec| + data[:domain_records][name] << self.send("#{type}_to_attributes", rec) + end + end + response.body ={ + 'domain' => data[:domains].last + } + + response + end + alias :create_zone :create_domain + + def rec_to_attributes(type, rec) + { + id: Fog::Mock.random_numbers(8).to_i, + name: rec[:name], + type: type, + data: rec[:host], + priority: nil, + port: nil, + weight: nil, + }.with_indifferent_access + end + + def ns_to_attributes(rec) + rec_to_attributes('NS', rec) + end + + def a_to_attributes(rec) + rec_to_attributes('A', rec) + end + + def mx_to_attributes(mx) + rec_to_attributes('MX', mx).merge({ + priority: mx[:pri], + }) + end + end + + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/create_record.rb b/lib/fog/digitalocean/requests/dns/create_record.rb new file mode 100644 index 0000000..051883e --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/create_record.rb @@ -0,0 +1,91 @@ +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + + def create_record(name, rec={}) + create_options = { + :type => rec[:type], + } + %w(name data priority port weight).each do |fld| + create_options[fld.to_sym] = rec[fld.to_sym] if rec[fld.to_sym] + end + + encoded_body = Fog::JSON.encode(create_options) + + request( + :expects => [201], + :headers => { + 'Content-Type' => "application/json; charset=UTF-8", + }, + :method => 'POST', + :path => "/v2/domains/#{name}/records", + :body => encoded_body, + ) + end + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def create_record(name, rec={}) + success_status = 201 + response = Excon::Response.new + if rec.with_indifferent_access[:type] =~ /^(A|SRV)/ + if rec.with_indifferent_access[:data] !~ /^[0-9]+/ + response.status = 422 + response.body = { + "id" => "unprocessable_entity", + "message" => "Data needs to in an IP address" + } + else + if rec.with_indifferent_access[:type] =~ /^AA/ + if rec.with_indifferent_access[:data] !~ /:/ + response.status = 422 + response.body = { + "id" => "unprocessable_entity", + "message" => "IP address did not match IPv6 format (e.g. 2001:db8::ff00:42:8329)." + } + else + response.status = success_status + end + else + response.status = success_status + end + end + elsif rec.with_indifferent_access[:type] =~ /TXT/ + if rec.with_indifferent_access[:data] =~ /[\r\n]/ + response.status = 422 + response.body = { + "id" => "unprocessable_entity", + "message" => "Data must not contain newlines" + } + else + response.status = success_status + end + elsif rec.with_indifferent_access[:data] !~ /\.$/ + response.status = 422 + response.body = { + "id" => "unprocessable_entity", + "message" => "Data needs to end with a dot (.)" + } + else + response.status = success_status + end + + if response.status == success_status + data[:domain_records][name] << rec.dup + last = data[:domain_records][name].last + #last['name'] = %(#{last['name']}.#{name}.) unless last['name'].match(%r{\.$}) unless last['name'].eql?('@') #|| last['name'].eql?('*') + last['id'] = Fog::Mock.random_numbers(8).to_i + response.body = { + "domain_record" => last + } + end + + response + end + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/delete_domain.rb b/lib/fog/digitalocean/requests/dns/delete_domain.rb new file mode 100644 index 0000000..f53b78f --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/delete_domain.rb @@ -0,0 +1,37 @@ +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + def delete_domain(id) + id = id.with_indifferent_access['domain'] if id.is_a?(Hash) + request( + :expects => [204], + :headers => { + 'Content-Type' => "application/json; charset=UTF-8", + }, + :method => 'DELETE', + :path => "/v2/domains/#{id}", + ) + end + alias :delete_zone :delete_domain + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def delete_domain(id) + id = id.with_indifferent_access['domain'] if id.is_a?(Hash) + data[:domain_records].delete(id) + data[:domains].select! do |key| + key['name'] != id + end + + response = Excon::Response.new + response.status = 204 + response + end + alias :delete_zone :delete_domain + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/delete_record.rb b/lib/fog/digitalocean/requests/dns/delete_record.rb new file mode 100644 index 0000000..62320f2 --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/delete_record.rb @@ -0,0 +1,33 @@ +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + + def delete_record(name, id) + id = id.with_indifferent_access['id'] if id.is_a?(Hash) + request( + :expects => [204], + :method => 'DELETE', + :path => "/v2/domains/#{name}/records/#{id}", + ) + end + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def delete_record(name, id) + id = id.with_indifferent_access['id'] if id.is_a?(Hash) + response = Excon::Response.new + response.status = 204 + + data[:domain_records][name].select!{ |rec| rec['id'] != id } + + response.body = {} + + response + end + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/get_domain.rb b/lib/fog/digitalocean/requests/dns/get_domain.rb new file mode 100644 index 0000000..19c0ba7 --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/get_domain.rb @@ -0,0 +1,43 @@ +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + + def get_domain(name) + request( + :expects => [200], + :method => 'GET', + :path => "/v2/domains/#{name}", + ) + end + alias :get_zone :get_domain + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def get_domain(name) + response = Excon::Response.new + + domains = data[:domains].select{ |dom| dom['name'].eql?(name) } + if domains.size > 0 + response.status = 200 + response.body = { + 'domain' => domains.last + } + else + response.status = 404 + response.body = { + 'id' => 'not_found', + 'message' => 'The resource you were accessing could not be found.' + } + raise Fog::Errors::NotFound.new(response.body['message']) + end + + response + end + alias :get_zone :get_domain + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/get_record.rb b/lib/fog/digitalocean/requests/dns/get_record.rb new file mode 100644 index 0000000..683f57e --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/get_record.rb @@ -0,0 +1,42 @@ +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + + def get_record(name, id) + request( + :expects => [200], + :method => 'GET', + :path => "/v2/domains/#{name}/records/#{id}", + ) + end + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def get_record(name, id) + id = id['id'] if id.is_a?(Hash) + response = Excon::Response.new + + recs = data[:domain_records][name].select { |rec| rec['id'] == id } + if recs.size > 0 + response.status = 200 + response.body = { + "domain_record" => recs.last + } + else + response.status = 404 + response.body = { + 'id' => 'not_found', + 'message' => 'The resource you were accessing could not be found.' + } + raise Fog::Errors::NotFound.new(response.body['message']) + end + + response + end + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/list_domains.rb b/lib/fog/digitalocean/requests/dns/list_domains.rb new file mode 100644 index 0000000..1ded856 --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/list_domains.rb @@ -0,0 +1,36 @@ +module Fog + module DNS + class DigitalOcean + class Real + def list_domains(filters = {}) + request( + :expects => [200], + :method => 'GET', + :path => "v2/domains", + :query => filters + ) + end + alias :list_zones :list_domains + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def list_domains(filters = {}) + response = Excon::Response.new + raise Fog::Errors::NotFound response.body['message'] unless data[:domains] && data[:domains].count > 0 + + response.status = 200 + response.body = { + "domains" => data[:domains], + "links" => {}, + "meta" => { + "total" => data[:domains].count + } + } + response + end + alias :list_zones :list_domains + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/list_records.rb b/lib/fog/digitalocean/requests/dns/list_records.rb new file mode 100644 index 0000000..513e73a --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/list_records.rb @@ -0,0 +1,63 @@ +module Fog + module DNS + class DigitalOcean + + class Real + def list_records(name, filters = {}) + request( + :expects => [200], + :method => 'GET', + :path => "v2/domains/#{name}/records", + :query => filters + ) + end + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def list_records(name, filters = {}) + + filters = filters.with_indifferent_access + filters[:per_page] ||= 25 + filters[:page] ||= 1 + raise Fog::Errors::Error.new("Invalid page size") if filters[:per_page] > 200 + + response = Excon::Response.new + if data[:domain_records][name] + response.status = 200 + records = data[:domain_records][name] + links = {} + if records.count > filters[:per_page] + per_page = filters[:per_page] != 25 ? "&per_page=#{filters[:per_page]}" : '' + pages = {} + + pages['first'] = "?page=1#{per_page}" unless filters[:page] == 1 + pages['prev'] = "?page=(filters[:page]-1).to_s#{per_page}" unless filters[:page] == 1 + pages['next'] = "?page=#{(filters[:page]+1).to_s}#{per_page}" if (records.size - filters[:page] * filters[:per_page]) > 0 + pages['last'] = "?page=#{(records.size / filters[:per_page] + 1).to_i}#{per_page}" if (records.size - filters[:page] * filters[:per_page]) > 0 + links = { + 'links' => { + 'pages' => pages + } + } + end + response.body = { + "domain_records" => records[(filters[:page]-1) * filters[:per_page]..filters[:page] * filters[:per_page] - 1], + "meta" => { + "total" => data[:domain_records][name].count + } + }.merge(links) + else + response.status = 404 + response.body = { + 'id' => 'not_found', + 'message' => 'The resource you were accessing could not be found.' + } + raise Fog::Errors::NotFound response.body['message'] + end + response + end + end + end + end +end diff --git a/lib/fog/digitalocean/requests/dns/update_record.rb b/lib/fog/digitalocean/requests/dns/update_record.rb new file mode 100644 index 0000000..6f0d2e7 --- /dev/null +++ b/lib/fog/digitalocean/requests/dns/update_record.rb @@ -0,0 +1,48 @@ +require 'active_support/core_ext/hash/indifferent_access' + +module Fog + module DNS + class DigitalOcean + # noinspection RubyStringKeysInHashInspection + class Real + + def update_record(name, rec={}) + update_options = { + } + %w(type name data priority port weight).each do |fld| + update_options[fld.to_sym] = rec[fld.to_sym] if rec[fld.to_sym] + end + + encoded_body = Fog::JSON.encode(update_options) + + request( + :expects => [200], + :headers => { + 'Content-Type' => "application/json; charset=UTF-8", + }, + :method => 'PUT', + :path => "/v2/domains/#{name}/records/#{rec[:id]}", + :body => encoded_body, + ) + end + end + + # noinspection RubyStringKeysInHashInspection + class Mock + def update_record(name, rec={}) + response = Excon::Response.new + response.status = 200 + + updated = data[:domain_records][name].select{ |rec| rec['id'] == rec[:id] }.last.with_indifferent_access + updated[:id] = Fog::Mock.random_numbers(8).to_i + updated.merge!(rec) + response.body = { + "domain_record" => updated + } + + response + end + end + end + end +end diff --git a/lib/fog/digitalocean/version.rb b/lib/fog/digitalocean/version.rb index 718f854..6533f22 100644 --- a/lib/fog/digitalocean/version.rb +++ b/lib/fog/digitalocean/version.rb @@ -1,5 +1,5 @@ module Fog module Digitalocean - VERSION = "0.3.0" + VERSION = '0.4.0' end end diff --git a/tests/digitalocean/models/dns/domain_tests.rb b/tests/digitalocean/models/dns/domain_tests.rb new file mode 100644 index 0000000..90223c1 --- /dev/null +++ b/tests/digitalocean/models/dns/domain_tests.rb @@ -0,0 +1,14 @@ +Shindo.tests("Fog::DNS[:digitalocean] | domain", ['digitalocean', 'dns']) do + tests("domains.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].domains.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + params = {:name => generate_unique_domain } + model_tests(Fog::DNS[:digitalocean].domains, params) +end diff --git a/tests/digitalocean/models/dns/domains_tests.rb b/tests/digitalocean/models/dns/domains_tests.rb new file mode 100644 index 0000000..b38c948 --- /dev/null +++ b/tests/digitalocean/models/dns/domains_tests.rb @@ -0,0 +1,14 @@ +Shindo.tests("Fog::DNS[:digitalocean] | domains", ['digitalocean', 'dns']) do + tests("domains.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].domains.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + params = {:name => generate_unique_domain } + collection_tests(Fog::DNS[:digitalocean].domains, params) +end diff --git a/tests/digitalocean/models/dns/record_tests.rb b/tests/digitalocean/models/dns/record_tests.rb new file mode 100644 index 0000000..56c3382 --- /dev/null +++ b/tests/digitalocean/models/dns/record_tests.rb @@ -0,0 +1,41 @@ +Shindo.tests("Fog::Dns[:digitalocean] | record", ['digitalocean', 'dns']) do + + tests("domains.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].domains.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + tests("domains#create").succeeds do + @domain = Fog::DNS[:digitalocean].domains.create(name: generate_unique_domain, ip_address: '5.5.5.5') + end + + [ + { :name => "#{@domain.name}.", :type => 'A', :data => '1.2.3.4' }, + { :name => '@', :type => 'A', :data => '1.2.3.4' }, + ].each do |params| + model_tests(@domain.records, params) do |instance| + + # Newly created records should have a change id + tests("#id") do + returns(true) { instance.id != nil } + end + + tests("#modify").succeeds do + new_value = '5.5.5.5' + returns(true) { instance.modify('data' => new_value) } + returns(new_value) { instance.data } + end + + end + end + + tests("domain.destroy").succeeds do + @domain.destroy + end + +end diff --git a/tests/digitalocean/models/dns/records_tests.rb b/tests/digitalocean/models/dns/records_tests.rb new file mode 100644 index 0000000..3a4c9d8 --- /dev/null +++ b/tests/digitalocean/models/dns/records_tests.rb @@ -0,0 +1,54 @@ +Shindo.tests("Fog::DNS[:digitalocean] | records", ['digitalocean', 'dns']) do + + tests("domains.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].domains.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + @domain = nil + tests("domains.create").succeeds do + @domain = Fog::DNS[:digitalocean].domains.create(name: generate_unique_domain) + end + + param_groups = [ + # A record + { :name => "#{@domain.name}.", :type => 'A', :ttl => 3600, :data => '1.2.3.4' }, + { :name => '@', :type => 'A', :ttl => 3600, :data => '5.6.7.8' }, + # CNAME record + { :name => "www", :type => "CNAME", :ttl => 300, :data => "#{@domain.name}." } + ] + + param_groups.each do |params| + collection_tests(@domain.records, params) + end + + records = [] + + 100.times do |i| + records << @domain.records.create(:name => "#{i}", :type => "A", :ttl => 3600, :data => '1.2.3.4') + end + + records << @domain.records.create(:name => "*", :type => "A", :ttl => 3600, :data => '1.2.3.4') + + tests("#all!").returns(105) do # We get an A record and 3 NS records "for free" ;) + @domain.records.all!.size + end + + tests("#all wildcard parsing").returns(true) do + set = @domain.records.all!.map(&:name) + set.include?('*') || set.include?("*.#{@domain.name}.") + end + + records.each do |record| + record.destroy + end + + tests("zones#destroy").succeeds do + @domain.destroy + end +end diff --git a/tests/digitalocean/models/dns/zone_tests.rb b/tests/digitalocean/models/dns/zone_tests.rb new file mode 100644 index 0000000..f53bb85 --- /dev/null +++ b/tests/digitalocean/models/dns/zone_tests.rb @@ -0,0 +1,14 @@ +Shindo.tests("Fog::DNS[:digitalocean] | zone", ['digitalocean', 'dns']) do + tests("zones.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].zones.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + params = {:name => generate_unique_domain } + model_tests(Fog::DNS[:digitalocean].zones, params) +end diff --git a/tests/digitalocean/models/dns/zones_tests.rb b/tests/digitalocean/models/dns/zones_tests.rb new file mode 100644 index 0000000..37a0c70 --- /dev/null +++ b/tests/digitalocean/models/dns/zones_tests.rb @@ -0,0 +1,14 @@ +Shindo.tests("Fog::DNS[:digitalocean] | zones", ['digitalocean', 'dns']) do + tests("zones.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].zones.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + params = {:name => generate_unique_domain } + collection_tests(Fog::DNS[:digitalocean].zones, params) +end diff --git a/tests/digitalocean/requests/dns/dns_tests.rb b/tests/digitalocean/requests/dns/dns_tests.rb new file mode 100644 index 0000000..5cb107a --- /dev/null +++ b/tests/digitalocean/requests/dns/dns_tests.rb @@ -0,0 +1,327 @@ +require 'active_support/core_ext/hash/indifferent_access' + +Shindo.tests('Fog::DNS[:digitalocean] | DNS requests', ['digitalocean', 'dns']) do + + tests("domains.cleanup").succeeds do + tests = Fog::DNS[:digitalocean].domains.all.select { |domain| + domain.name =~ /^test-[0-9]{12}\.com/ + } + tests.each do |domain| + domain.delete + end + true + end + + @domain_count = 0 + @new_records = [] + @domain_name = generate_unique_domain + # @domain = Fog::DNS[:digitalocean].domains.create(:name => generate_unique_domain) + + @service = Fog::DNS[:digitalocean] + + tests('success') do + + test('get current domain count') do + @domain_count= 0 + response = @service.list_domains + if response.status == 200 + @domains = response.body['domains'] + @domain_count = @domains.count + end + + response.status == 200 + end + + test('get current zone count') do + @zone_count= 0 + response = @service.list_zones + if response.status == 200 + @zones = response.body['domains'] + @zone_count = @domains.count + end + + response.status == 200 + end + + test('create simple domain') { + result = false + + response = @service.create_domain(@domain_name, '1.2.3.4') + if response.status.to_s =~ /^20[0-4]/ + tries = 0 + begin + domain = response.body['domain'] + name = domain['name'] + ttl = domain['ttl'] + zone_file = domain['zone_file'] + break unless zone_file.nil? + tries += 1 + break if tries > 3 + sleep 3 + response = @service.get_domain(@domain_name) + end until !zone_file.nil? + + if name and ttl and zone_file + + require 'zonefile' + + zf = ::Zonefile.new(zone_file) + + @zone_id = name + @origin = zf.soa.with_indifferent_access[:origin] + ns_srv_count = zf.records.with_indifferent_access[:ns].size + + if (@zone_id.length < @origin.length) and (ns_srv_count > 0) and (ttl.to_i == zf.ttl.to_i) + result = true + end + end + end + + result + } + + test("get info on domain #{@zone_id}") { + result = false + if @zone_id + response = @service.get_domain(@zone_id) + if response.status == 200 + domain = response.body['domain'] + name = domain['name'] + ttl = domain['ttl'] + zone_file = domain['zone_file'] + @zone_id = name + + if name and ttl and zone_file + + require 'zonefile' + + zf = ::Zonefile.new(zone_file) + + origin = zf.soa.with_indifferent_access[:origin] + ns_srv_count = zf.records.with_indifferent_access[:ns].size + + if (name.length < origin.length) and (ns_srv_count > 0) and (ttl.to_i == zf.ttl.to_i) + result = true + end + end + end + end + + result + } + + test('list domains') do + result = false + + response = @service.list_domains + if response.status == 200 + + zones = response.body['domains'] + if (zones.count > 0) + domain = zones.last + name = domain['name'] + ttl = domain['ttl'] + zone_file = domain['zone_file'] + + if name and ttl and zone_file + + require 'zonefile' + + zf = ::Zonefile.new(zone_file) + + origin = zf.soa.with_indifferent_access[:origin] + ns_srv_count = zf.records.with_indifferent_access[:ns].size + + if (name.length < origin.length) and (ns_srv_count > 0) and (ttl.to_i == zf.ttl.to_i) + result = true + end + end + end + end + + result + end + + test("get info on zone #{@zone_id}") { + result = false + if @zone_id + response = @service.get_zone(@zone_id) + if response.status == 200 + domain = response.body['domain'] + name = domain['name'] + ttl = domain['ttl'] + zone_file = domain['zone_file'] + @zone_id = name + + if name and ttl and zone_file + + require 'zonefile' + + zf = ::Zonefile.new(zone_file) + + origin = zf.soa.with_indifferent_access[:origin] + ns_srv_count = zf.records.with_indifferent_access[:ns].size + + if (name.length < origin.length) and (ns_srv_count > 0) and (ttl.to_i == zf.ttl.to_i) + result = true + end + end + end + end + + result + } + + test('list zones') do + result = false + + response = @service.list_zones + if response.status == 200 + + zones = response.body['domains'] + if (zones.count > 0) + domain = zones.last + name = domain['name'] + ttl = domain['ttl'] + zone_file = domain['zone_file'] + + if name and ttl and zone_file + + require 'zonefile' + + zf = ::Zonefile.new(zone_file) + + origin = zf.soa.with_indifferent_access[:origin] + ns_srv_count = zf.records.with_indifferent_access[:ns].size + + if (name.length < origin.length) and (ns_srv_count > 0) and (ttl.to_i == zf.ttl.to_i) + result = true + end + end + end + end + + result + end + + tests('add records') do + require 'base64' + # name: A, AAAA, CNAME, TXT, SRV, data: A, AAAA, CNAME, MX, TXT, SRV, NS + [ + { name: 'www', type: 'A', ttl: 3600, data: '1.2.3.4' }, + { name: 'www', type: 'AAAA', ttl: 3600, data: '2001:db8::ff00:42:8329' }, + { name: 'mail', type: 'CNAME', ttl: 3600, data: 'www.' + "#{@domain_name}." }, + { name: @domain_name, type: 'MX', ttl: 3600, data: 'mail.' + "#{@domain_name}.", priority: 6 }, + { name: @domain_name, type: 'TXT', ttl: 3600, data: Base64.encode64('txt.' + @domain_name).chomp }, + { name: '_smtp._tcp', type: 'SRV', ttl: 3600, data: '1.2.3.4', priority: 6, port: 666, weight: 6 }, + ].each do |resource_record| + rr = resource_record.with_indifferent_access + test("add a #{rr[:type]} resource record") { + # create an resource record + response = @service.create_record(@zone_id, rr) + + if response.status == 201 + dr = response.body['domain_record'].with_indifferent_access + if dr['name'].eql?(rr['name']) + @new_records << dr + @service.get_record(@zone_id, dr["id"]).body['domain_record']["name"].eql?(rr[:name]) + else + false + end + else + false + end + } + end + + tests('update NS records') do + ns_records = @service.list_records(@zone_id).body['domain_records'].select { |rec| rec['type'].eql?('NS') } + ns_records.each do |resource_record| + rr = resource_record.with_indifferent_access + test("update a #{rr[:type]} resource record") { + # update an resource record + update = {id: rr[:id], data: rr[:data].gsub(%r{^(ns[1-3]\.).*}, "\\1#{@zone_id}.") } + response = @service.update_record(@zone_id, update) + + if response.status == 200 + dr = response.body['domain_record'].with_indifferent_access + updated = @new_records.select { |rec| rec[:id] == rr[:id] } + if updated.size > 0 + updated.map { |rec| rec.merge!(dr) } + else + @new_records << dr + end + @service.get_record(@zone_id, dr["id"]).body['domain_record']["name"].eql?(rr[:name]) + else + false + end + } + end + + end + + tests("list resource records") { + # get resource records for zone + records = @service.list_records(@zone_id).body['domain_records'].map { |rec| rec.with_indifferent_access } + rec_ids = @new_records.map { |rec| rec[:id] } + test('all records found') do + records.select { |rec| + rec_ids.include?(rec[:id]) + }.size == @new_records.size + end + } + + test("delete #{@new_records.count} resource records") { + # change_batch = @new_records.map { |record| { id: record[:id] }.with_indifferent_access } + + results = @new_records.map do |rec| + response = @service.delete_record(@zone_id, rec) + if response.status == 204 + true + else + false + end + end + results.select { |res| res === false }.size == 0 + } + + tests("test remaining resource records") { + # get resource records for zone + records = @service.list_records(@zone_id).body['domain_records'].map { |rec| rec.with_indifferent_access } + # rec_ids = @new_records.map { |rec| rec[:id] } + test('only remaining records found') do + records.size == 1 + end + test('delete only remaining record') do + response = @service.delete_record(@zone_id, records.last) + response.status == 204 + end + # rec_ids = @new_records.map { |rec| rec[:id] } + test('no records found') do + @service.list_records(@zone_id).body['domain_records'].size == 0 + end + } + + test("delete hosted zone #{@zone_id}") { + @service.delete_domain(@zone_id).status == 204 + } + + end + + tests('failure') do + tests('create hosted zone using invalid domain name').raises(Excon::Error::UnprocessableEntity) do + pending if Fog.mocking? + @service.create_domain('invalid-domain', '0.0.0.0') + end + + tests('get hosted zone using invalid ID').raises(Fog::DNS::DigitalOcean::NotFound) do + pending if Fog.mocking? + zone_id = 'dummy-id' + @service.get_domain(zone_id) + end + + end + + end + +end diff --git a/tests/digitalocean/requests/dns/list_domains_tests.rb b/tests/digitalocean/requests/dns/list_domains_tests.rb new file mode 100644 index 0000000..583edfc --- /dev/null +++ b/tests/digitalocean/requests/dns/list_domains_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::DNS::DigitalOcean | list_domains request', ['digitalocean', 'dns']) do + service = Fog::DNS.new(:provider => 'DigitalOcean') + + domain_format = { + 'name' => String, + 'ttl' => Integer, + 'zone_file' => String, + } + + tests('success') do + tests('#list_domains') do + service.list_domains.body['domains'].each do |domain| + tests('format').data_matches_schema(domain_format) do + domain + end + end + end + end +end \ No newline at end of file diff --git a/tests/digitalocean/requests/dns/list_records_tests.rb b/tests/digitalocean/requests/dns/list_records_tests.rb new file mode 100644 index 0000000..1993d9e --- /dev/null +++ b/tests/digitalocean/requests/dns/list_records_tests.rb @@ -0,0 +1,40 @@ +Shindo.tests('Fog::DNS::DigitalOcean | list_records("domain.net") request', ['digitalocean', 'dns']) do + service = Fog::DNS.new(:provider => 'DigitalOcean') + + record_format = { + 'id' => Integer, + 'type' => String, + 'name' => String, + 'data' => String, + # 'priority' => Integer, + # 'port' => Integer, + # 'weight' => Integer, + } + + tests('success') do + tests('#list_records') do + body = service.list_domains.body + domain = body['domains'].last.with_indifferent_access + if domain + response = service.list_records(domain[:name]) + if response.status == 200 + domain_records = response.body['domain_records'] + + test('record count') do + domain_records.size >= 4 + end + + domain_records.each do |record| + tests('format').data_matches_schema(record_format) do + record + end + end + end + + response.status == 200 + else + false + end + end + end +end \ No newline at end of file diff --git a/tests/digitalocean/requests/dns/list_zones_tests.rb b/tests/digitalocean/requests/dns/list_zones_tests.rb new file mode 100644 index 0000000..1d8ce3d --- /dev/null +++ b/tests/digitalocean/requests/dns/list_zones_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::DNS::DigitalOcean | list_zones request', ['digitalocean', 'dns']) do + service = Fog::DNS.new(:provider => 'DigitalOcean') + + domain_format = { + 'name' => String, + 'ttl' => Integer, + 'zone_file' => String, + } + + tests('success') do + tests('#list_zones') do + service.list_zones.body['domains'].each do |domain| + tests('format').data_matches_schema(domain_format) do + domain + end + end + end + end +end \ No newline at end of file diff --git a/tests/helper.rb b/tests/helper.rb index 9eb22cb..ede5551 100644 --- a/tests/helper.rb +++ b/tests/helper.rb @@ -3,12 +3,19 @@ require File.expand_path('../../lib/fog/digitalocean', __FILE__) +def to_boolean(v) + v.nil? ? false : !!(v.to_s =~ /\A\s*(true|yes|1|y)\s*$/i) +end + Bundler.require(:test) Excon.defaults.merge!(:debug_request => true, :debug_response => true) +Excon.defaults[:ssl_verify_peer] = to_boolean(ENV['SSL_VERIFY_PEER']) if ENV['SSL_VERIFY_PEER'] -require File.expand_path(File.join(File.dirname(__FILE__), 'helpers', 'mock_helper')) -require File.expand_path(File.join(File.dirname(__FILE__), 'helpers', 'format_helper')) +Dir[File.expand_path(File.join(File.dirname(__FILE__), 'helpers', '*_helper.rb'))].each do |path| + file = path.gsub(%r{\.rb$}, '') + require file +end # This overrides the default 600 seconds timeout during live test runs unless Fog.mocking? diff --git a/tests/helpers/collection_helper.rb b/tests/helpers/collection_helper.rb new file mode 100644 index 0000000..dc623fd --- /dev/null +++ b/tests/helpers/collection_helper.rb @@ -0,0 +1,103 @@ +def collection_tests(collection, params = {}, mocks_implemented = true) + def fqdn(params) + params[:fqdn] ? params[:fqdn] : params[:name] + end + + tests('success') do + + tests("#new(#{params.inspect})").succeeds do + pending if Fog.mocking? && !mocks_implemented + coll = collection.new(params) + @collection_size = coll.collection.size + collection.size == coll.collection.size + end + + tests("#create(#{params.inspect})").returns(fqdn(params)) do + pending if Fog.mocking? && !mocks_implemented + @instance = collection.create(params) + @collection_size = collection.size + @instance.name.eql?('@') ? fqdn(params) : @instance.name + end + + tests("#{collection.class.name}#all").returns(@collection_size) do + pending if Fog.mocking? && !mocks_implemented + coll = collection.all + coll.size + end + + if !Fog.mocking? || mocks_implemented + @identity = @instance.identity + end + + tests("#get(#{@identity})").returns(fqdn(params)) do + pending if Fog.mocking? && !mocks_implemented + record = collection.get(@identity) + record.name.eql?('@') ? fqdn(params) : record.name + end + + tests('Enumerable') do + pending if Fog.mocking? && !mocks_implemented + + methods = [ + 'all?', 'any?', 'find', 'detect', 'collect', 'map', + 'find_index', 'flat_map', 'collect_concat', 'group_by', + 'none?', 'one?' + ] + + # JRuby 1.7.5+ issue causes a SystemStackError: stack level too deep + # https://github.com/jruby/jruby/issues/1265 + if RUBY_PLATFORM == "java" and JRUBY_VERSION =~ /1\.7\.[5-8]/ + methods.delete('all?') + end + + methods.each do |enum_method| + if collection.respond_to?(enum_method) + tests("##{enum_method}").succeeds do + block_called = false + collection.send(enum_method) {|x| block_called = true } + block_called + end + end + end + + [ + 'max_by','min_by' + ].each do |enum_method| + if collection.respond_to?(enum_method) + tests("##{enum_method}").succeeds do + block_called = false + collection.send(enum_method) {|x| block_called = true; 0 } + block_called + end + end + + end + + end + + if block_given? + yield(@instance) + end + + if !Fog.mocking? || mocks_implemented + collection.delete(@instance) + @collection_size = collection.size + end + end + + tests('failure') do + + if !Fog.mocking? || mocks_implemented + @identity = @identity.to_s + @identity = @identity.gsub(/[a-zA-Z]/) { Fog::Mock.random_letters(1) } + @identity = @identity.gsub(/\d/) { Fog::Mock.random_numbers(1) } + @identity + end + + tests("#get('#{@identity}')").returns(nil) do + pending if Fog.mocking? && !mocks_implemented + collection.get(@identity) + end + + end +end diff --git a/tests/helpers/dns_helper.rb b/tests/helpers/dns_helper.rb new file mode 100644 index 0000000..1c85cef --- /dev/null +++ b/tests/helpers/dns_helper.rb @@ -0,0 +1,55 @@ +def dns_providers + { + :aws => { + :mocked => false + }, + :bluebox => { + :mocked => false, + :zone_attributes => { + :ttl => 60 + } + }, + :dnsimple => { + :mocked => false + }, + :dnsmadeeasy => { + :mocked => false + }, + :dynect => { + :mocked => false, + :zone_attributes => { + :email => 'fog@example.com' + } + }, + :linode => { + :mocked => false, + :zone_attributes => { + :email => 'fog@example.com' + } + }, + :zerigo => { + :mocked => false + }, + :rackspace => { + :mocked => false, + :zone_attributes => { + :email => 'fog@example.com' + } + }, + :rage4 => { + :mocked => false + } + } +end + +def generate_unique_domain( with_trailing_dot = false) + #get time (with 1/100th of sec accuracy) + #want unique domain name and if provider is fast, this can be called more than once per second + time= (Time.now.to_f * 100).to_i + domain = 'test-' + time.to_s + '.com' + if with_trailing_dot + domain+= '.' + end + + domain +end diff --git a/tests/helpers/model_helper.rb b/tests/helpers/model_helper.rb new file mode 100644 index 0000000..e026a7b --- /dev/null +++ b/tests/helpers/model_helper.rb @@ -0,0 +1,31 @@ +def model_tests(collection, params = {}, mocks_implemented = true) + tests('success') do + + @instance = collection.new(params) + + tests("#save").succeeds do + pending if Fog.mocking? && !mocks_implemented + @instance.save + end + + if block_given? + yield(@instance) + end + + tests("#destroy").succeeds do + pending if Fog.mocking? && !mocks_implemented + @instance.destroy + end + + end +end + +# Generates a unique identifier with a random differentiator. +# Useful when rapidly re-running tests, so we don't have to wait +# serveral minutes for deleted objects to disappear from the API +# E.g. 'fog-test-1234' +def uniq_id(base_name = 'fog-test') + # random_differentiator + suffix = rand(65536).to_s(16).rjust(4, '0') + [base_name, suffix] * '-' +end diff --git a/tests/helpers/responds_to_helper.rb b/tests/helpers/responds_to_helper.rb new file mode 100644 index 0000000..5982700 --- /dev/null +++ b/tests/helpers/responds_to_helper.rb @@ -0,0 +1,11 @@ +module Shindo + class Tests + def responds_to(method_names) + for method_name in [*method_names] + tests("#respond_to?(:#{method_name})").returns(true) do + @instance.respond_to?(method_name) + end + end + end + end +end