diff --git a/lib/fog.rb b/lib/fog.rb index e418c29bc..52d75bf2b 100644 --- a/lib/fog.rb +++ b/lib/fog.rb @@ -6,6 +6,7 @@ require 'fog/bluebox' require 'fog/brightbox' require 'fog/cloudstack' require 'fog/clodo' +require 'fog/digitalocean' require 'fog/dnsimple' require 'fog/dnsmadeeasy' require 'fog/dreamhost' diff --git a/lib/fog/bin.rb b/lib/fog/bin.rb index 423f5f6d6..c7a3650ef 100644 --- a/lib/fog/bin.rb +++ b/lib/fog/bin.rb @@ -63,6 +63,7 @@ require 'fog/bin/bluebox' require 'fog/bin/brightbox' require 'fog/bin/cloudstack' require 'fog/bin/clodo' +require 'fog/bin/digitalocean' require 'fog/bin/dnsimple' require 'fog/bin/dnsmadeeasy' require 'fog/bin/dreamhost' diff --git a/lib/fog/bin/digitalocean.rb b/lib/fog/bin/digitalocean.rb new file mode 100644 index 000000000..309ebe1ad --- /dev/null +++ b/lib/fog/bin/digitalocean.rb @@ -0,0 +1,31 @@ +class DigitalOcean < Fog::Bin + class << self + + def class_for(key) + case key + when :compute + Fog::Compute::DigitalOcean + else + raise ArgumentError, "Unsupported #{self} service: #{key}" + end + end + + def [](service) + @@connections ||= Hash.new do |hash, key| + hash[key] = case key + when :compute + Fog::Logger.warning("DigitalOcean[:compute] is not recommended, use Compute[:digitalocean] for portability") + Fog::Compute.new(:provider => 'DigitalOcean') + else + raise ArgumentError, "Unrecognized service: #{key.inspect}" + end + end + @@connections[service] + end + + def services + Fog::DigitalOcean.services + end + + end +end diff --git a/lib/fog/digitalocean.rb b/lib/fog/digitalocean.rb new file mode 100644 index 000000000..231b5c119 --- /dev/null +++ b/lib/fog/digitalocean.rb @@ -0,0 +1,9 @@ +require 'fog/core' + +module Fog + module DigitalOcean + extend Fog::Provider + service(:compute, 'digitalocean/compute', 'Compute') + end +end + diff --git a/lib/fog/digitalocean/compute.rb b/lib/fog/digitalocean/compute.rb new file mode 100644 index 000000000..60bc3b90a --- /dev/null +++ b/lib/fog/digitalocean/compute.rb @@ -0,0 +1,93 @@ +require 'fog/digitalocean' +require 'fog/compute' + +module Fog + module Compute + class DigitalOcean < Fog::Service + + requires :digitalocean_api_key + requires :digitalocean_client_id + + recognizes :digitalocean_api_url + + model_path 'fog/digitalocean/models/compute' + model :server + collection :servers + model :flavor + collection :flavors + model :image + collection :images + model :region + collection :regions + + request_path 'fog/digitalocean/requests/compute' + request :list_servers + request :list_images + request :list_regions + request :list_flavors + request :get_server_details + request :create_server + request :destroy_server + + # request :digitalocean_resize + + class Mock + + def self.data + @data ||= Hash.new do |hash, key| + hash[key] = {} + end + end + + def self.reset + @data = nil + end + + def initialize(options={}) + @digitalocean_api_key = options[:digitalocean_api_key] + end + + def data + self.class.data[@digitalocean_api_key] + end + + def reset_data + self.class.data.delete(@digitalocean_api_key) + end + + end + + class Real + + def initialize(options={}) + @digitalocean_api_key = options[:digitalocean_api_key] + @digitalocean_client_id = options[:digitalocean_client_id] + @digitalocean_api_url = options[:digitalocean_api_url] || \ + "https://api.digitalocean.com" + @connection = Fog::Connection.new(@digitalocean_api_url) + end + + def reload + @connection.reset + end + + def request(params) + params[:query] ||= {} + params[:query].merge!(:api_key => @digitalocean_api_key) + params[:query].merge!(:client_id => @digitalocean_client_id) + + response = @connection.request(params) + + unless response.body.empty? + response.body = Fog::JSON.decode(response.body) + if response.body['status'] != 'OK' + raise Fog::Errors::Error.new + end + end + response + end + + end + end + end +end diff --git a/lib/fog/digitalocean/models/compute/flavor.rb b/lib/fog/digitalocean/models/compute/flavor.rb new file mode 100644 index 000000000..2ed3dcc65 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/flavor.rb @@ -0,0 +1,14 @@ +require 'fog/core/model' + +module Fog + module Compute + class DigitalOcean + class Flavor < Fog::Model + + identity :id + attribute :name + + end + end + end +end diff --git a/lib/fog/digitalocean/models/compute/flavors.rb b/lib/fog/digitalocean/models/compute/flavors.rb new file mode 100644 index 000000000..333221594 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/flavors.rb @@ -0,0 +1,25 @@ +require 'fog/core/collection' +require 'fog/digitalocean/models/compute/flavor' + +module Fog + module Compute + class DigitalOcean + + class Flavors < Fog::Collection + model Fog::Compute::DigitalOcean::Flavor + + def all + load service.list_flavors.body['sizes'] + end + + def get(id) + all.find { |f| f.id == id } + rescue Fog::Errors::NotFound + nil + end + + end + + end + end +end diff --git a/lib/fog/digitalocean/models/compute/image.rb b/lib/fog/digitalocean/models/compute/image.rb new file mode 100644 index 000000000..45d536801 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/image.rb @@ -0,0 +1,15 @@ +require 'fog/core/model' + +module Fog + module Compute + class DigitalOcean + class Image < Fog::Model + + identity :id + attribute :name + attribute :distribution + + end + end + end +end diff --git a/lib/fog/digitalocean/models/compute/images.rb b/lib/fog/digitalocean/models/compute/images.rb new file mode 100644 index 000000000..1e08da3a9 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/images.rb @@ -0,0 +1,25 @@ +require 'fog/core/collection' +require 'fog/digitalocean/models/compute/image' + +module Fog + module Compute + class DigitalOcean + + class Images < Fog::Collection + model Fog::Compute::DigitalOcean::Image + + def all + load service.list_images.body['images'] + end + + def get(id) + all.find { |f| f.id == id } + rescue Fog::Errors::NotFound + nil + end + + end + + end + end +end diff --git a/lib/fog/digitalocean/models/compute/region.rb b/lib/fog/digitalocean/models/compute/region.rb new file mode 100644 index 000000000..448e42240 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/region.rb @@ -0,0 +1,14 @@ +require 'fog/core/model' + +module Fog + module Compute + class DigitalOcean + class Region < Fog::Model + + identity :id + attribute :name + + end + end + end +end diff --git a/lib/fog/digitalocean/models/compute/regions.rb b/lib/fog/digitalocean/models/compute/regions.rb new file mode 100644 index 000000000..d5348ac6a --- /dev/null +++ b/lib/fog/digitalocean/models/compute/regions.rb @@ -0,0 +1,25 @@ +require 'fog/core/collection' +require 'fog/digitalocean/models/compute/region' + +module Fog + module Compute + class DigitalOcean + + class Regions < Fog::Collection + model Fog::Compute::DigitalOcean::Region + + def all + load service.list_regions.body['regions'] + end + + def get(id) + all.find { |f| f.id == id } + rescue Fog::Errors::NotFound + nil + end + + end + + end + end +end diff --git a/lib/fog/digitalocean/models/compute/server.rb b/lib/fog/digitalocean/models/compute/server.rb new file mode 100644 index 000000000..2097be115 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/server.rb @@ -0,0 +1,50 @@ +require 'fog/compute/models/server' + +module Fog + module Compute + class DigitalOcean + # + # A DigitalOcean Droplet + # + class Server < Fog::Compute::Server + + identity :id + attribute :name + attribute :status + attribute :image_id + attribute :region_id + attribute :flavor_id, :aliases => :size_id + attribute :backups_active + + def reboot + end + + def shutdown + end + + def save + raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted? + requires :name, :flavor_id, :image_id, :region_id + meta_hash = {} + options = { + 'name' => name, + 'size_id' => flavor_id, + 'image_id' => image_id, + 'region_id' => region_id, + } + data = service.create_server name, flavor_id, image_id, region_id + merge_attributes(data.body['droplet']) + true + end + + def destroy + service.destroy_server id + end + + def ready? + status == 'active' + end + end + end + end +end diff --git a/lib/fog/digitalocean/models/compute/servers.rb b/lib/fog/digitalocean/models/compute/servers.rb new file mode 100644 index 000000000..70538a7d7 --- /dev/null +++ b/lib/fog/digitalocean/models/compute/servers.rb @@ -0,0 +1,27 @@ +require 'fog/core/collection' +require 'fog/digitalocean/models/compute/server' + +module Fog + module Compute + class DigitalOcean + + class Servers < Fog::Collection + model Fog::Compute::DigitalOcean::Server + + def all + load service.list_servers.body['droplets'] + end + + def get(id) + if server = service.get_server_details(id).body['droplet'] + new server + end + rescue Fog::Errors::NotFound + nil + end + + end + + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/create_server.rb b/lib/fog/digitalocean/requests/compute/create_server.rb new file mode 100644 index 000000000..620fd9705 --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/create_server.rb @@ -0,0 +1,47 @@ +module Fog + module Compute + class DigitalOcean + class Real + + # + # FIXME: missing ssh keys support + # + def create_server( name, + size_id, + image_id, + region_id, + options = {} ) + + query_args = [] + query_hash = { + :name => name, + :size_id => size_id, + :image_id => image_id, + :region_id => region_id + }.each { |k, v| query_args << "#{k}=#{v}" } + query_hash.each { |k, v| query_args << "#{k}=#{v}" } + + request( + :expects => [200], + :method => 'GET', + :path => 'droplets/new', + :query => query_hash + ) + end + + end + + class Mock + + def create_server( name, + size_id, + image_id, + region_id, + options = {} ) + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/destroy_server.rb b/lib/fog/digitalocean/requests/compute/destroy_server.rb new file mode 100644 index 000000000..cf73b0ab9 --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/destroy_server.rb @@ -0,0 +1,28 @@ +module Fog + module Compute + class DigitalOcean + class Real + + # + # FIXME: missing ssh keys support + # + def destroy_server( id ) + request( + :expects => [200], + :method => 'GET', + :path => "droplets/#{id}/destroy" + ) + end + + end + + class Mock + + def destroy_server( id ) + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/get_server_details.rb b/lib/fog/digitalocean/requests/compute/get_server_details.rb new file mode 100644 index 000000000..a14b61fa5 --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/get_server_details.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class DigitalOcean + class Real + + def get_server_details(server_id) + request( + :expects => [200], + :method => 'GET', + :path => "droplets/#{server_id}" + ) + end + + end + + class Mock + + def get_server_details(server_id) + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/list_flavors.rb b/lib/fog/digitalocean/requests/compute/list_flavors.rb new file mode 100644 index 000000000..cf0815646 --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/list_flavors.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class DigitalOcean + class Real + + def list_flavors(options = {}) + request( + :expects => [200], + :method => 'GET', + :path => 'sizes', + ) + end + + end + + class Mock + + def list_flavors + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/list_images.rb b/lib/fog/digitalocean/requests/compute/list_images.rb new file mode 100644 index 000000000..56c63fd9e --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/list_images.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class DigitalOcean + class Real + + def list_images(options = {}) + request( + :expects => [200], + :method => 'GET', + :path => 'images', + ) + end + + end + + class Mock + + def list_images + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/list_regions.rb b/lib/fog/digitalocean/requests/compute/list_regions.rb new file mode 100644 index 000000000..7992ac892 --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/list_regions.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class DigitalOcean + class Real + + def list_regions(options = {}) + request( + :expects => [200], + :method => 'GET', + :path => 'regions', + ) + end + + end + + class Mock + + def list_regions + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/lib/fog/digitalocean/requests/compute/list_servers.rb b/lib/fog/digitalocean/requests/compute/list_servers.rb new file mode 100644 index 000000000..a207873ba --- /dev/null +++ b/lib/fog/digitalocean/requests/compute/list_servers.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class DigitalOcean + class Real + + def list_servers(options = {}) + request( + :expects => [200], + :method => 'GET', + :path => 'droplets', + ) + end + + end + + class Mock + + def list_servers + Fog::Mock.not_implemented + end + + end + end + end +end diff --git a/tests/digitalocean/models/compute/flavor_tests.rb b/tests/digitalocean/models/compute/flavor_tests.rb new file mode 100644 index 000000000..f8993cda6 --- /dev/null +++ b/tests/digitalocean/models/compute/flavor_tests.rb @@ -0,0 +1,30 @@ +Shindo.tests("Fog::Compute[:digitalocean] | flavor model", ['digitalocean', 'compute']) do + + service = Fog::Compute[:digitalocean] + flavor = service.flavors.first + + tests('The flavor model should') do + tests('have the action') do + test('reload') { flavor.respond_to? 'reload' } + end + tests('have attributes') do + model_attribute_hash = flavor.attributes + attributes = [ + :id, + :name, + ] + tests("The flavor model should respond to") do + attributes.each do |attribute| + test("#{attribute}") { flavor.respond_to? attribute } + end + end + tests("The attributes hash should have key") do + attributes.each do |attribute| + test("#{attribute}") { model_attribute_hash.has_key? attribute } + end + end + end + end + +end + diff --git a/tests/digitalocean/models/compute/image_tests.rb b/tests/digitalocean/models/compute/image_tests.rb new file mode 100644 index 000000000..c189d230d --- /dev/null +++ b/tests/digitalocean/models/compute/image_tests.rb @@ -0,0 +1,31 @@ +Shindo.tests("Fog::Compute[:digitalocean] | image model", ['digitalocean', 'compute']) do + + service = Fog::Compute[:digitalocean] + image = service.images.first + + tests('The image model should') do + tests('have the action') do + test('reload') { image.respond_to? 'reload' } + end + tests('have attributes') do + model_attribute_hash = image.attributes + attributes = [ + :id, + :name, + :distribution + ] + tests("The image model should respond to") do + attributes.each do |attribute| + test("#{attribute}") { image.respond_to? attribute } + end + end + tests("The attributes hash should have key") do + attributes.each do |attribute| + test("#{attribute}") { model_attribute_hash.has_key? attribute } + end + end + end + end + +end + diff --git a/tests/digitalocean/models/compute/region_tests.rb b/tests/digitalocean/models/compute/region_tests.rb new file mode 100644 index 000000000..03345ac67 --- /dev/null +++ b/tests/digitalocean/models/compute/region_tests.rb @@ -0,0 +1,30 @@ +Shindo.tests("Fog::Compute[:digitalocean] | region model", ['digitalocean', 'compute']) do + + service = Fog::Compute[:digitalocean] + region = service.regions.first + + tests('The region model should') do + tests('have the action') do + test('reload') { region.respond_to? 'reload' } + end + tests('have attributes') do + model_attribute_hash = region.attributes + attributes = [ + :id, + :name, + ] + tests("The region model should respond to") do + attributes.each do |attribute| + test("#{attribute}") { region.respond_to? attribute } + end + end + tests("The attributes hash should have key") do + attributes.each do |attribute| + test("#{attribute}") { model_attribute_hash.has_key? attribute } + end + end + end + end + +end + diff --git a/tests/digitalocean/models/compute/server_tests.rb b/tests/digitalocean/models/compute/server_tests.rb new file mode 100644 index 000000000..d2f77d4c9 --- /dev/null +++ b/tests/digitalocean/models/compute/server_tests.rb @@ -0,0 +1,48 @@ +Shindo.tests("Fog::Compute[:digitalocean] | server model", ['digitalocean', 'compute']) do + + service = Fog::Compute[:digitalocean] + server = service.servers.create :name => 'fog-test', + :image_id => service.images.first.id, + :region_id => service.regions.first.id, + :flavor_id => service.flavors.first.id + + tests('The server model should') do + # Wait for the server to come up + begin + server.wait_for(120) { server.reload rescue nil; server.ready? } + rescue Fog::Errors::TimeoutError + # Server bootstrap took more than 120 secs! + end + + tests('have the action') do + test('reload') { server.respond_to? 'reload' } + %w{ + shutdown + reboot + }.each do |action| + test(action) { server.respond_to? action } + end + end + tests('have attributes') do + model_attribute_hash = server.attributes + attributes = [ + :id, + :name, + :status, + :backups_active, + :flavor_id, + :region_id, + :image_id + ] + tests("The server model should respond to") do + attributes.each do |attribute| + test("#{attribute}") { server.respond_to? attribute } + end + end + end + end + + server.destroy + +end + diff --git a/tests/digitalocean/requests/compute/create_server_tests.rb b/tests/digitalocean/requests/compute/create_server_tests.rb new file mode 100644 index 000000000..3643bbce8 --- /dev/null +++ b/tests/digitalocean/requests/compute/create_server_tests.rb @@ -0,0 +1,22 @@ +Shindo.tests('Fog::Compute[:digitalocean] | create_server request', ['digitalocean', 'compute']) do + + service = Fog::Compute[:digitalocean] + + tests('success') do + + test('#create_server') do + data = Fog::Compute[:digitalocean].create_server 'fog-test', + service.flavors.first.id, + service.images.first.id, + service.regions.first.id + # wait some time before destroying the server + # otherwise the request could be ignored, YMMV + sleep 120 + data.body['status'] == 'OK' and \ + (service.destroy_server(data.body['droplet']['id']).body['status'] == 'OK') + end + + end + + +end diff --git a/tests/digitalocean/requests/compute/get_server_details_tests.rb b/tests/digitalocean/requests/compute/get_server_details_tests.rb new file mode 100644 index 000000000..dbb1f5948 --- /dev/null +++ b/tests/digitalocean/requests/compute/get_server_details_tests.rb @@ -0,0 +1,11 @@ +Shindo.tests('Fog::Compute[:digitalocean] | get_server_details request', ['digitalocean', 'compute']) do + + tests('success') do + + test('#get_server_details') do + Fog::Compute[:digitalocean].get_server_details(nil).body.is_a? Hash + end + + end + +end diff --git a/tests/digitalocean/requests/compute/list_flavors_tests.rb b/tests/digitalocean/requests/compute/list_flavors_tests.rb new file mode 100644 index 000000000..45fd33cea --- /dev/null +++ b/tests/digitalocean/requests/compute/list_flavors_tests.rb @@ -0,0 +1,23 @@ +Shindo.tests('Fog::Compute[:digitalocean] | list_flavors request', ['digitalocean', 'compute']) do + + # {"id":2,"name":"Amsterdam 1"} + @flavor_format = { + 'id' => Integer, + 'name' => String, + } + + tests('success') do + + tests('#list_flavor') do + flavors = Fog::Compute[:digitalocean].list_flavors.body + test 'returns a Hash' do + flavors.is_a? Hash + end + tests('flavor').formats(@flavor_format, false) do + flavors['sizes'].first + end + end + + end + +end diff --git a/tests/digitalocean/requests/compute/list_images_tests.rb b/tests/digitalocean/requests/compute/list_images_tests.rb new file mode 100644 index 000000000..58a3f4916 --- /dev/null +++ b/tests/digitalocean/requests/compute/list_images_tests.rb @@ -0,0 +1,24 @@ +Shindo.tests('Fog::Compute[:digitalocean] | list_images request', ['digitalocean', 'compute']) do + + # {"id"=>1601, "name"=>"CentOS 5.8 x64", "distribution"=>"CentOS"} + @image_format = { + 'id' => Integer, + 'name' => String, + 'distribution' => String + } + + tests('success') do + + tests('#list_images') do + images = Fog::Compute[:digitalocean].list_images.body + test 'returns a Hash' do + images.is_a? Hash + end + tests('image').formats(@image_format, false) do + images['images'].first + end + end + + end + +end diff --git a/tests/digitalocean/requests/compute/list_regions_tests.rb b/tests/digitalocean/requests/compute/list_regions_tests.rb new file mode 100644 index 000000000..4883fe1e0 --- /dev/null +++ b/tests/digitalocean/requests/compute/list_regions_tests.rb @@ -0,0 +1,23 @@ +Shindo.tests('Fog::Compute[:digitalocean] | list_regions request', ['digitalocean', 'compute']) do + + # {"id":2,"name":"Amsterdam 1"} + @region_format = { + 'id' => Integer, + 'name' => String, + } + + tests('success') do + + tests('#list_regions') do + regions = Fog::Compute[:digitalocean].list_regions.body + test 'returns a Hash' do + regions.is_a? Hash + end + tests('region').formats(@region_format, false) do + regions['regions'].first + end + end + + end + +end diff --git a/tests/digitalocean/requests/compute/list_servers_tests.rb b/tests/digitalocean/requests/compute/list_servers_tests.rb new file mode 100644 index 000000000..319984467 --- /dev/null +++ b/tests/digitalocean/requests/compute/list_servers_tests.rb @@ -0,0 +1,11 @@ +Shindo.tests('Fog::Compute[:digitalocean] | list_servers request', ['digitalocean', 'compute']) do + + tests('success') do + + test('#list_servers') do + Fog::Compute[:digitalocean].list_servers.body.is_a? Hash + end + + end + +end