diff --git a/lib/fog/bin.rb b/lib/fog/bin.rb index 9f08745be..a4d411f0b 100644 --- a/lib/fog/bin.rb +++ b/lib/fog/bin.rb @@ -79,6 +79,7 @@ require 'fog/bin/ninefold' require 'fog/bin/rackspace' require 'fog/bin/openstack' require 'fog/bin/ovirt' +require 'fog/bin/serverlove' require 'fog/bin/stormondemand' require 'fog/bin/terremark' require 'fog/bin/vcloud' diff --git a/lib/fog/bin/serverlove.rb b/lib/fog/bin/serverlove.rb new file mode 100644 index 000000000..62b722d3c --- /dev/null +++ b/lib/fog/bin/serverlove.rb @@ -0,0 +1,31 @@ +class Serverlove < Fog::Bin + class << self + + def class_for(key) + case key + when :compute + Fog::Compute::Serverlove + else + raise ArgumentError, "Unrecognized service: #{key}" + end + end + + def [](service) + @@connections ||= Hash.new do |hash, key| + hash[key] = case key + when :compute + Fog::Logger.warning("Serverlove[:compute] is not recommended, use Compute[:serverlove] for portability") + Fog::Compute.new(:provider => 'Serverlove') + else + raise ArgumentError, "Unrecognized service: #{key.inspect}" + end + end + @@connections[service] + end + + def services + Fog::Serverlove.services + end + + end +end diff --git a/lib/fog/compute.rb b/lib/fog/compute.rb index f29676130..c5919e087 100644 --- a/lib/fog/compute.rb +++ b/lib/fog/compute.rb @@ -76,6 +76,9 @@ module Fog require 'fog/rackspace/compute' Fog::Compute::Rackspace.new(attributes) end + when :serverlove + require 'fog/serverlove/compute' + Fog::Compute::Serverlove.new(attributes) when :stormondemand require 'fog/storm_on_demand/compute' Fog::Compute::StormOnDemand.new(attributes) diff --git a/lib/fog/providers.rb b/lib/fog/providers.rb index b8867c452..880b95900 100644 --- a/lib/fog/providers.rb +++ b/lib/fog/providers.rb @@ -21,6 +21,7 @@ require 'fog/ninefold' require 'fog/rackspace' require 'fog/openstack' require 'fog/ovirt' +require 'fog/serverlove' require 'fog/storm_on_demand' require 'fog/vcloud' require 'fog/virtual_box' diff --git a/lib/fog/serverlove.rb b/lib/fog/serverlove.rb new file mode 100644 index 000000000..2bfb327d5 --- /dev/null +++ b/lib/fog/serverlove.rb @@ -0,0 +1,10 @@ +require(File.expand_path(File.join(File.dirname(__FILE__), 'core'))) + +module Fog + module Serverlove + extend Fog::Provider + + service(:compute, 'serverlove/compute', 'Compute') + + end +end diff --git a/lib/fog/serverlove/compute.rb b/lib/fog/serverlove/compute.rb new file mode 100644 index 000000000..5b7c97bb1 --- /dev/null +++ b/lib/fog/serverlove/compute.rb @@ -0,0 +1,97 @@ +module Fog + module Compute + class Serverlove < Fog::Service + + API_HOST = "api.z1-man.serverlove.com" + + requires :serverlove_uuid, :serverlove_api_key + + recognizes :serverlove_api_url + + request_path 'fog/serverlove/requests/compute' + + # Image + request :get_image + request :get_images + request :destroy_image + request :create_image + request :update_image + request :load_standard_image + + # Server + request :get_servers + request :get_server + request :destroy_server + request :create_server + request :update_server + request :start_server + request :stop_server + request :shutdown_server + request :reset_server + + model_path 'fog/serverlove/models/compute' + + model :image + collection :images + + model :server + collection :servers + + class Mock + + def initialize(options) + @serverlove_uuid = options[:serverlove_uuid] || Fog.credentials[:serverlove_uuid] + @serverlove_api_key = options[:serverlove_api_key] || Fog.credentials[:serverlove_api_key] + end + + def request(options) + raise "Not implemented" + end + + end + + class Real + + def initialize(options) + @api_uuid = options[:serverlove_uuid] || Fog.credentials[:serverlove_uuid] + @api_key = options[:serverlove_api_key] || Fog.credentials[:serverlove_api_key] + @api_host = options[:serverlove_api_url] || Fog.credentials[:serverlove_api_url] || API_HOST + + @connection = Fog::Connection.new("https://#{@api_uuid}:#{@api_key}@#{@api_host}") + end + + def request(params) + params = params.merge!( + :headers => { + "Accept" => "application/json" + } + ) + params[:body] = encode_pairs(params[:options]) unless params[:options].nil? + response = @connection.request(params) + + raise_if_error!(response) + + response.body = Fog::JSON.decode(response.body) if response.body && response.body.length > 0 + + response + end + + def encode_pairs(params) + params.keys.collect do |key| + "#{key} #{params[key]}" + end.join("\n") + end + + # TODO + def raise_if_error!(response) + case response.status + when 400 then + raise 'omg' + end + end + + end + + end + end +end diff --git a/lib/fog/serverlove/models/compute/image.rb b/lib/fog/serverlove/models/compute/image.rb new file mode 100644 index 000000000..557cbeaa8 --- /dev/null +++ b/lib/fog/serverlove/models/compute/image.rb @@ -0,0 +1,57 @@ +require 'fog/core/model' + +module Fog + module Compute + class Serverlove + + class Image < Fog::Model + + identity :id, :aliases => 'drive' + + attribute :name + attribute :user + attribute :size + attribute :claimed + attribute :status + attribute :imaging + attribute :encryption_cipher, :aliases => 'encryption:cipher' + + def save + attributes = {} + + if(identity) + attributes = connection.update_image(identity, allowed_attributes).body + else + requires :name + requires :size + attributes = connection.create_image(allowed_attributes).body + end + + merge_attributes(attributes) + self + end + + def load_standard_image(standard_image_uuid) + requires :identity + connection.load_standard_image(identity, standard_image_uuid) + end + + def ready? + status.upcase == 'ACTIVE' + end + + def destroy + requires :identity + connection.destroy_image(identity) + self + end + + def allowed_attributes + allowed = [:name, :size] + attributes.select {|k,v| allowed.include? k} + end + + end + end + end +end diff --git a/lib/fog/serverlove/models/compute/images.rb b/lib/fog/serverlove/models/compute/images.rb new file mode 100644 index 000000000..d6706603d --- /dev/null +++ b/lib/fog/serverlove/models/compute/images.rb @@ -0,0 +1,26 @@ +require 'fog/core/collection' +require 'fog/serverlove/models/compute/image' + +module Fog + module Compute + class Serverlove + + class Images < Fog::Collection + + model Fog::Compute::Serverlove::Image + + def all + data = connection.get_images.body + load(data) + end + + def get(image_id) + data = connection.get_image(image_id).body + new(data) + end + + end + + end + end +end diff --git a/lib/fog/serverlove/models/compute/server.rb b/lib/fog/serverlove/models/compute/server.rb new file mode 100644 index 000000000..ba501476a --- /dev/null +++ b/lib/fog/serverlove/models/compute/server.rb @@ -0,0 +1,72 @@ +require 'fog/core/model' +require 'fog/serverlove/util/compute/password_generator' + +module Fog + module Compute + class Serverlove + + class Server < Fog::Model + + identity :id, :aliases => 'server' + + attribute :name + attribute :cpu + attribute :mem + attribute :smp + attribute :ide_0_0, :aliases => 'ide:0:0' + attribute :ide_0_1, :aliases => 'ide:0:1' + attribute :ide_1_0, :aliases => 'ide:1:0' + attribute :ide_1_1, :aliases => 'ide:1:1' + attribute :boot + attribute :persistent + attribute :vnc + attribute :vnc_password, :aliases => 'vnc:password' + attribute :status + attribute :user + attribute :started + attribute :nic_0_model, :aliases => 'nic:0:model' + attribute :nic_0_dhcp, :aliases => 'nic:0:dhcp' + + def save + attributes = {} + + if(identity) + attributes = connection.update_server(identity, allowed_attributes).body + else + requires :name + requires :cpu + attributes = connection.create_server(self.defaults.merge(allowed_attributes)).body + end + + merge_attributes(attributes) + self + end + + def destroy + requires :identity + connection.destroy_server(identity) + self + end + + def allowed_attributes + allowed = [ + :name, :cpu, :smp, :mem, :persistent, + :vnc_password, :vnc, + :ide_0_0, :ide_0_1, :ide_1_0, :ide_1_1, + :boot, :nic_0_model, :nic_0_dhcp + ] + attributes.select {|k,v| allowed.include? k} + end + + def self.defaults + # TODO: Document default settings. + # Note that VNC password standards are strict (need explaining) + { 'nic:0:model' => 'e1000', 'nic:0:dhcp' => 'auto', + 'smp' => 'auto', 'vnc' => 'auto', + 'vnc:password' => Fog::Compute::Serverlove::PasswordGenerator.generate + } + end + end + end + end +end diff --git a/lib/fog/serverlove/models/compute/servers.rb b/lib/fog/serverlove/models/compute/servers.rb new file mode 100644 index 000000000..a64f5a0c0 --- /dev/null +++ b/lib/fog/serverlove/models/compute/servers.rb @@ -0,0 +1,26 @@ +require 'fog/core/collection' +require 'fog/serverlove/models/compute/server' + +module Fog + module Compute + class Serverlove + + class Servers < Fog::Collection + + model Fog::Compute::Serverlove::Server + + def all + data = connection.get_servers.body + load(data) + end + + def get(server_id) + data = connection.get_server(server_id).body + new(data) + end + + end + + end + end +end diff --git a/lib/fog/serverlove/requests/compute/create_image.rb b/lib/fog/serverlove/requests/compute/create_image.rb new file mode 100644 index 000000000..ab77e9a98 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/create_image.rb @@ -0,0 +1,32 @@ +module Fog + module Compute + class Serverlove + class Real + + def create_image(options) + return nil if options.empty? || options.nil? + request(:method => "post", :path => "/drives/create", :expects => 200, :options => options) + end + + end + + class Mock + + def create_image(options = {}) + response = Excon::Response.new + response.status = 200 + + data = { + 'drive' => Fog::Mock.random_numbers(1000000).to_s, + 'name' => options['name'] || 'Test', + 'size' => options['size'] || 12345 + } + + response.body = data + response + end + + end + end + end +end diff --git a/lib/fog/serverlove/requests/compute/create_server.rb b/lib/fog/serverlove/requests/compute/create_server.rb new file mode 100644 index 000000000..34bf6016e --- /dev/null +++ b/lib/fog/serverlove/requests/compute/create_server.rb @@ -0,0 +1,34 @@ +module Fog + module Compute + class Serverlove + class Real + + def create_server(options) + return nil if options.empty? || options.nil? + request(:method => "post", :path => "/servers/create/stopped", :expects => 200, :options => options) + end + + end + + class Mock + + def create_server(options = {}) + response = Excon::Response.new + response.status = 200 + + data = { + 'server' => Fog::Mock.random_numbers(1000000).to_s, + 'name' => options['name'] || 'Test', + 'cpu' => options['cpu'] || 1000, + 'persistent' => options['persistent'] || false, + 'vnc:password' => options['vnc:password'] || 'T35tServER!' + } + + response.body = data + response + end + + end + end + end +end diff --git a/lib/fog/serverlove/requests/compute/destroy_image.rb b/lib/fog/serverlove/requests/compute/destroy_image.rb new file mode 100644 index 000000000..745015084 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/destroy_image.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def destroy_image(drive_id) + request(:method => "post", :path => "/drives/#{drive_id}/destroy", :expects => 204) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/destroy_server.rb b/lib/fog/serverlove/requests/compute/destroy_server.rb new file mode 100644 index 000000000..692b8d22a --- /dev/null +++ b/lib/fog/serverlove/requests/compute/destroy_server.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def destroy_server(server_id) + request(:method => "post", :path => "/servers/#{server_id}/destroy", :expects => 204) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/get_image.rb b/lib/fog/serverlove/requests/compute/get_image.rb new file mode 100644 index 000000000..a13e9d1cc --- /dev/null +++ b/lib/fog/serverlove/requests/compute/get_image.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def get_image(image_id) + request(:method => "get", :path => "/drives/#{image_id}/info", :expects => 200) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/get_images.rb b/lib/fog/serverlove/requests/compute/get_images.rb new file mode 100644 index 000000000..c8e7c6282 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/get_images.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def get_images + request(:method => "get", :path => "/drives/info", :expects => 200) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/get_server.rb b/lib/fog/serverlove/requests/compute/get_server.rb new file mode 100644 index 000000000..219ff8bb8 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/get_server.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def get_server(server_id) + request(:method => "get", :path => "/servers/#{server_id}/info", :expects => 200) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/get_servers.rb b/lib/fog/serverlove/requests/compute/get_servers.rb new file mode 100644 index 000000000..19d9bf387 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/get_servers.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def get_servers + request(:method => "get", :path => "/servers/info", :expects => 200) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/load_standard_image.rb b/lib/fog/serverlove/requests/compute/load_standard_image.rb new file mode 100644 index 000000000..c4bcedc6d --- /dev/null +++ b/lib/fog/serverlove/requests/compute/load_standard_image.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def load_standard_image(destination_image, source_image) + request(:method => "post", :path => "/drives/#{destination_image}/image/#{source_image}/gunzip", :expects => 204) + end + + end + end + end +end diff --git a/lib/fog/serverlove/requests/compute/reset_server.rb b/lib/fog/serverlove/requests/compute/reset_server.rb new file mode 100644 index 000000000..99b0de64d --- /dev/null +++ b/lib/fog/serverlove/requests/compute/reset_server.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def reset_server(server_id) + request(:method => "post", :path => "/servers/#{server_id}/reset", :expects => 204) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/shutdown_server.rb b/lib/fog/serverlove/requests/compute/shutdown_server.rb new file mode 100644 index 000000000..ebc24893f --- /dev/null +++ b/lib/fog/serverlove/requests/compute/shutdown_server.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def shutdown_server(server_id) + request(:method => "post", :path => "/servers/#{server_id}/shutdown", :expects => 204) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/start_server.rb b/lib/fog/serverlove/requests/compute/start_server.rb new file mode 100644 index 000000000..a8e562ff4 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/start_server.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def start_server(server_id) + request(:method => "post", :path => "/servers/#{server_id}/start", :expects => 200) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/stop_server.rb b/lib/fog/serverlove/requests/compute/stop_server.rb new file mode 100644 index 000000000..2cc73b8e7 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/stop_server.rb @@ -0,0 +1,13 @@ +module Fog + module Compute + class Serverlove + class Real + + def stop_server(server_id) + request(:method => "post", :path => "/servers/#{server_id}/stop", :expects => 204) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/serverlove/requests/compute/update_image.rb b/lib/fog/serverlove/requests/compute/update_image.rb new file mode 100644 index 000000000..f7e532833 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/update_image.rb @@ -0,0 +1,15 @@ +module Fog + module Compute + class Serverlove + class Real + + def update_image(identifier, options) + return nil if identifier.nil? || identifier == "" + return nil if options.empty? || options.nil? + request(:method => "post", :path => "/drives/#{identifier}/set", :expects => 200, :options => options) + end + + end + end + end +end diff --git a/lib/fog/serverlove/requests/compute/update_server.rb b/lib/fog/serverlove/requests/compute/update_server.rb new file mode 100644 index 000000000..2994770e7 --- /dev/null +++ b/lib/fog/serverlove/requests/compute/update_server.rb @@ -0,0 +1,15 @@ +module Fog + module Compute + class Serverlove + class Real + + def update_server(identifier, options) + return nil if identifier.nil? || identifier == "" + return nil if options.empty? || options.nil? + request(:method => "post", :path => "/servers/#{identifier}/set", :expects => 200, :options => options) + end + + end + end + end +end diff --git a/lib/fog/serverlove/util/compute/password_generator.rb b/lib/fog/serverlove/util/compute/password_generator.rb new file mode 100644 index 000000000..168f247ab --- /dev/null +++ b/lib/fog/serverlove/util/compute/password_generator.rb @@ -0,0 +1,11 @@ +module Fog + module Compute + class Serverlove + class PasswordGenerator + def self.generate + ('a'...'z').to_a.concat(('A'...'Z').to_a).shuffle[0,8].join + end + end + end + end +end \ No newline at end of file diff --git a/tests/serverlove/requests/compute/image_tests.rb b/tests/serverlove/requests/compute/image_tests.rb new file mode 100644 index 000000000..159ce1bbe --- /dev/null +++ b/tests/serverlove/requests/compute/image_tests.rb @@ -0,0 +1,57 @@ +Shindo.tests('Fog::Compute[:serverlove] | drive requests', ['serverlove']) do + + @image_format = { + 'drive' => String, + 'name' => String, + 'user' => String, + 'size' => Integer, + 'claimed' => Fog::Nullable::String, + 'status' => String, + 'encryption:cipher' => String, + 'read:bytes' => String, + 'write:bytes' => String, + 'read:requests' => String, + 'write:requests' => String + } + + tests('success') do + + attributes = { 'name' => 'Test', 'size' => '24234567890' } + + tests("#create_image").formats(@image_format) do + @image = Fog::Compute[:serverlove].create_image(attributes).body + end + + tests("#list_images").succeeds do + Fog::Compute[:serverlove].images + end + + tests("#update_image").returns(true) do + @image['name'] = "Diff" + Fog::Compute[:serverlove].update_image(@image['drive'], { name: @image['name'], size: @image['size']}) + Fog::Compute[:serverlove].images.get(@image['drive']).name == "Diff" + end + + tests("#load_standard_image").returns(true) do + # Load centos + Fog::Compute[:serverlove].load_standard_image(@image['drive'], '88ed067f-d2b8-42ce-a25f-5297818a3b6f') + Fog::Compute[:serverlove].images.get(@image['drive']).imaging != "" # This will be "x%" when imaging + end + + tests("waits for imaging...").returns(true) do + while(percent_complete = Fog::Compute[:serverlove].images.get(@image['drive']).imaging) + sleep(1) + STDERR.print "#{percent_complete} " + break if percent_complete.include?("100") + end + STDERR.print "100% " + true + end + + tests("#destroy_image").succeeds do + Fog::Compute[:serverlove].destroy_image(@image['drive']) + end + + end + +end diff --git a/tests/serverlove/requests/compute/server_tests.rb b/tests/serverlove/requests/compute/server_tests.rb new file mode 100644 index 000000000..81f6bbd92 --- /dev/null +++ b/tests/serverlove/requests/compute/server_tests.rb @@ -0,0 +1,85 @@ +Shindo.tests('Fog::Compute[:serverlove] | server requests', ['serverlove']) do + + @server_format = { + 'server' => String, + 'name' => String, + 'user' => String, + 'status' => String, + 'started' => Fog::Nullable::String, + 'cpu' => Integer, + 'mem' => Integer, + 'smp' => Fog::Nullable::String, + 'persistent' => Fog::Nullable::String, + 'vnc' => Fog::Nullable::String, + 'vnc:password' => Fog::Nullable::String, + 'nic:0:dhcp' => String, + 'nic:0:model' => String + } + + tests('success') do + + attributes = { 'name' => 'Test', 'cpu' => '1000', 'mem' => '1000', 'persistent' => 'true' } + + tests("#create_server").formats(@server_format) do + @server = Fog::Compute[:serverlove].create_server(Fog::Compute::Serverlove::Server.defaults.merge(attributes)).body + end + + tests("#list_servers").succeeds do + Fog::Compute[:serverlove].servers + end + + tests("#update_server").returns(true) do + @server['name'] = "Diff" + Fog::Compute[:serverlove].update_server(@server['server'], { name: @server['name']}) + Fog::Compute[:serverlove].servers.get(@server['server']).name == "Diff" + end + + tests("assigns drive to server").succeeds do + @image = Fog::Compute[:serverlove].create_image('name' => 'Test', 'size' => '24234567890').body + # Load debian + Fog::Compute[:serverlove].load_standard_image(@image['drive'], 'aca2fa0b-40bc-4e06-ad99-f1467690d5de') + Fog::Compute[:serverlove].update_server(@server['server'], { 'ide:0:0' => @image['drive'], 'boot' => 'ide:0:0'}) + end + + tests("waits for imaging...").returns(true) do + while(percent_complete = Fog::Compute[:serverlove].images.get(@image['drive']).imaging) + sleep(1) + STDERR.print "#{percent_complete} " + break if percent_complete.include?("100") + end + STDERR.print "100% " + true + end + + tests("#start_server").returns(true) do + Fog::Compute[:serverlove].start_server(@server['server']) + Fog::Compute[:serverlove].servers.get(@server['server']).status == "active" + end + + tests("#reset_server").returns(true) do + Fog::Compute[:serverlove].reset_server(@server['server']) + Fog::Compute[:serverlove].servers.get(@server['server']).status == "active" + end + + tests("#shutdown_server").succeeds do + Fog::Compute[:serverlove].shutdown_server(@server['server']) + # Can't guarantee the OS will honour this command so don't test status + end + + tests("#stop_server").returns(true) do + Fog::Compute[:serverlove].start_server(@server['server']) + Fog::Compute[:serverlove].stop_server(@server['server']) + Fog::Compute[:serverlove].servers.get(@server['server']).status == "stopped" + end + + tests("#destroy_server").succeeds do + Fog::Compute[:serverlove].destroy_server(@server['server']) + end + + tests("destroy test drive").succeeds do + Fog::Compute[:serverlove].destroy_image(@image['drive']) + end + + end + +end diff --git a/tests/serverlove/util/compute/password_generator_tests.rb b/tests/serverlove/util/compute/password_generator_tests.rb new file mode 100644 index 000000000..3913b9a76 --- /dev/null +++ b/tests/serverlove/util/compute/password_generator_tests.rb @@ -0,0 +1,19 @@ +require 'fog/serverlove/util/compute/password_generator' + +Shindo.tests('Fog::Compute::Serverlove::PasswordGenerator | generate password', ['serverlove']) do + + @password = Fog::Compute::Serverlove::PasswordGenerator.generate + + tests("@password.length").returns(8) do + @password.length + end + + tests("@password contains one capital letter").returns(true) do + @password.match(/[A-Z]/) && true + end + + tests("@password contains one lower case letter").returns(true) do + @password.match(/[a-z]/) && true + end + +end \ No newline at end of file