From e470e6cc66a953d20f324c9337888f80dbedd20a Mon Sep 17 00:00:00 2001 From: Josh Lane & Jason Hansen Date: Tue, 8 May 2012 10:01:13 -0700 Subject: [PATCH] [cloudstack|compute] server abstraction and mocks --- lib/fog/cloudstack.rb | 14 +- lib/fog/cloudstack/compute.rb | 195 +++++++++++++++++- lib/fog/cloudstack/models/compute/server.rb | 62 ++++++ lib/fog/cloudstack/models/compute/servers.rb | 35 ++++ .../compute/deploy_virtual_machine.rb | 130 ++++++++++-- .../compute/destroy_virtual_machine.rb | 15 +- .../requests/compute/list_capabilities.rb | 2 +- .../requests/compute/list_virtual_machines.rb | 16 +- .../requests/virtual_machine_tests.rb | 20 +- tests/compute/helper.rb | 8 + tests/compute/models/servers_tests.rb | 2 + tests/helpers/mock_helper.rb | 3 +- 12 files changed, 456 insertions(+), 46 deletions(-) create mode 100644 lib/fog/cloudstack/models/compute/server.rb create mode 100644 lib/fog/cloudstack/models/compute/servers.rb diff --git a/lib/fog/cloudstack.rb b/lib/fog/cloudstack.rb index e1fc80931..b426fb246 100644 --- a/lib/fog/cloudstack.rb +++ b/lib/fog/cloudstack.rb @@ -17,12 +17,24 @@ module Fog end def self.signed_params(key,params) - query = params.to_a.sort.collect{|c| "#{c[0]}=#{escape(c[1].to_s)}"}.join('&').downcase + query = params.map{|k,v| [k.to_s, v]}.sort.collect{|c| "#{c[0]}=#{escape(c[1].to_s)}"}.join('&').downcase signed_string = Base64.encode64(OpenSSL::HMAC.digest(@@digest,key,query)).strip signed_string end + + def self.uuid + [8,4,4,4,12].map{|i| Fog::Mock.random_hex(i)}.join("-") + end + + def self.ip_address + 4.times.map{ Fog::Mock.random_numbers(3) }.join(".") + end + + def self.mac_address + 6.times.map{ Fog::Mock.random_numbers(2) }.join(":") + end end end diff --git a/lib/fog/cloudstack/compute.rb b/lib/fog/cloudstack/compute.rb index 860d39e6e..3ce9ab53c 100644 --- a/lib/fog/cloudstack/compute.rb +++ b/lib/fog/cloudstack/compute.rb @@ -15,6 +15,11 @@ module Fog :cloudstack_port, :cloudstack_path, :cloudstack_scheme, :cloudstack_persistent request_path 'fog/cloudstack/requests/compute' + + + model_path 'fog/cloudstack/models/compute' + model :server + collection :servers request :acquire_ip_address request :assign_to_load_balancer_rule @@ -135,14 +140,14 @@ module Fog class Real def initialize(options={}) - @cloudstack_api_key = options[:cloudstack_api_key] + @cloudstack_api_key = options[:cloudstack_api_key] @cloudstack_secret_access_key = options[:cloudstack_secret_access_key] - @cloudstack_session_id = options[:cloudstack_session_id] - @cloudstack_session_key = options[:cloudstack_session_key] - @host = options[:cloudstack_host] - @path = options[:cloudstack_path] || '/client/api' - @port = options[:cloudstack_port] || 443 - @scheme = options[:cloudstack_scheme] || 'https' + @cloudstack_session_id = options[:cloudstack_session_id] + @cloudstack_session_key = options[:cloudstack_session_key] + @host = options[:cloudstack_host] + @path = options[:cloudstack_path] || '/client/api' + @port = options[:cloudstack_port] || 443 + @scheme = options[:cloudstack_scheme] || 'https' @connection = Fog::Connection.new("#{@scheme}://#{@host}:#{@port}#{@path}", options[:cloudstack_persistent], {:ssl_verify_peer => false}) end @@ -245,9 +250,177 @@ module Fog raise Fog::Compute::Cloudstack::Error, error_text end end - + + end + end # Real + + class Mock + def self.data + @data ||= begin + rc_options = Fog.credentials[:cloudstack] || {} + zone_id = rc_options[:zone_id] ||"c554c592-e09c-9df5-7688-4a32754a4305" + template_id = rc_options[:template_id] || "8a31cf9c-f248-0588-256e-9dbf58785216" + service_offering_id = rc_options[:service_offering_id] || "4437ac6c-9fe3-477a-57ec-60a5a45896a4" + account_id = "8bec6f15-e2b8-44fc-a8f3-a022b2873440" + user_id = Fog::Cloudstack.uuid + domain_id = Fog::Cloudstack.uuid + network_id = (Array(rc_options[:network_ids]) || [Fog::Cloudstack.uuid]).first + domain_name = "exampleorg" + domain = { + "id" => domain_id, + "name" => domain_name, + "level" => 1, + "parentdomainid" => Fog::Cloudstack.uuid, + "parentdomainname" => "ROOT", + "haschild" => false, + "path" => "ROOT/accountname" + } + { + :networks => { network_id => { + "id" => network_id, + "name" => "10.56.23.0/26", + "displaytext" => "10.56.23.0/26", + "broadcastdomaintype" => "Vlan", + "traffictype" => "Guest", + "gateway" => "10.56.23.1", + "netmask" => "255.255.255.192", + "cidr" => "10.56.23.0/26", + "zoneid" => zone_id, + "zonename" => "zone-00", + "networkofferingid" => "af0c9bd5-a1b2-4ad0-bf4b-d6fa9b1b9d5b", + "networkofferingname" => "DefaultSharedNetworkOffering", + "networkofferingdisplaytext" => "Offering for Shared networks", + "networkofferingavailability" => "Optional", + "issystem" => false, + "state" => "Setup", + "related" => "86bbc9fc-d92e-49db-9fdc-296189090017", + "broadcasturi" => "vlan://800", + "dns1" => "10.0.80.11", + "type" => "Shared", + "vlan" => "800", + "acltype" => "Domain", + "subdomainaccess" => true, + "domainid" => domain_id, + "domain" => "ROOT", + "service" => [ + {"name" => "UserData"}, + {"name" => "Dhcp"}, + {"name" => "Dns", "capability" => [ + {"name" => "AllowDnsSuffixModification", + "value" => "true", + "canchooseservicecapability" => false}] + }], + "networkdomain" => "cs1cloud.internal", + "physicalnetworkid" => "8f4627c5-1fdd-4504-8a92-f61b4e9cb3e3", + "restartrequired" => false, + "specifyipranges" => true} + }, + :zones => { zone_id => { + "id" => zone_id, + "name"=> "zone-00", + "domainid" => 1, + "domainname" => "ROOT", + "networktype" => "Advanced", + "securitygroupsenabled" => false, + "allocationstate" => "Enabled", + "zonetoken" => Fog::Cloudstack.uuid, + "dhcpprovider" => "VirtualRouter"}}, + :templates => { template_id => { + "id" => template_id, + "name" => "CentOS 5.6(64-bit) no GUI (XenServer)", + "displaytext" => "CentOS 5.6(64-bit) no GUI (XenServer)", + "ispublic" => true, + "created" => "2012-05-09T15:29:33-0500", + "isready" => true, + "passwordenabled" => false, + "format" => "VHD", + "isfeatured" => true, + "crossZones" => true, + "ostypeid" => "a6a6694a-18f5-4765-8418-2b7a5f37cd0f", + "ostypename" => "CentOS 5.3 (64-bit)", + "account" => "system", + "zoneid" => zone_id, + "zonename" => "zone-00", + "status" => "Download Complete", + "size" => 21474836480, + "templatetype" => "BUILTIN", + "domain" => "ROOT", + "domainid" => "6023b6fe-5bef-4358-bc76-9f4e75afa52f", + "isextractable" => true, + "checksum" => "905cec879afd9c9d22ecc8036131a180"}}, + :service_offerings => { service_offering_id => { + "id" => service_offering_id, + "name" => "Medium Instance", + "displaytext" => "Medium Instance", + "cpunumber" => 1, + "cpuspeed" => 1000, + "memory" => 1024, + "created" => "2012-05-09T14:48:36-0500", + "storagetype" => "shared", + "offerha" => false, + "limitcpuuse" => false, + "issystem" => false, + "defaultuse" => false}}, + :accounts => { account_id => { + "id" => account_id, + "name" => "accountname", + "accounttype" => 2, + "domainid" => domain_id, + "domain" => domain_name, + "receivedbytes" => 0, + "sentbytes" => 0, + "vmlimit" => "Unlimited", + "vmtotal" => 0, + "vmavailable" => "Unlimited", + "iplimit" => "Unlimited", + "iptotal" => 0, + "ipavailable" => "Unlimited", + "volumelimit" => "Unlimited", + "volumetotal" => 0, + "volumeavailable" => "Unlimited", + "snapshotlimit" => "Unlimited", + "snapshottotal" => 0, + "snapshotavailable" => "Unlimited", + "templatelimit" => "Unlimited", + "templatetotal" => 0, + "templateavailable" => "Unlimited", + "vmstopped" => 0, + "vmrunning" => 0, + "projectlimit" => "Unlimited", + "projecttotal" => 1, + "projectavailable" => "Unlimited", + "networklimit" => "Unlimited", + "networktotal" => 0, + "networkavailable" => "Unlimited", + "state" => "enabled", + "user" => + [{"id" => user_id, + "username" => "username", + "firstname" => "Bob", + "lastname" => "Lastname", + "email" => "email@example.com", + "created" => "2012-05-14T16:25:17-0500", + "state" => "enabled", + "account" => "accountname", + "accounttype" => 2, + "domainid" => domain_id, + "domain" => domain_name, + "apikey" => Fog::Cloudstack.uuid, + "secretkey" => Fog::Cloudstack.uuid}]}}, + :domains => { domain_id => domain }, + :servers => {} + } + end + end + + def self.reset + @data= nil + end + + def data + self.class.data end end - end - end -end + end # Cloudstack + end # Compute +end # Fog diff --git a/lib/fog/cloudstack/models/compute/server.rb b/lib/fog/cloudstack/models/compute/server.rb new file mode 100644 index 000000000..c18838072 --- /dev/null +++ b/lib/fog/cloudstack/models/compute/server.rb @@ -0,0 +1,62 @@ +require 'fog/compute/models/server' + +module Fog + module Compute + class Cloudstack + class Server < Fog::Compute::Server + extend Fog::Deprecation + identity :id, :aliases => 'id' + attribute :name + attribute :display_name, :aliases => 'displayname' + attribute :account + attribute :domain_id, :aliases => 'domainid' + attribute :domain + attribute :created + attribute :state + attribute :haenable + attribute :zone_id, :aliases => 'zoneid' + attribute :zone_name, :aliases => 'zonename' + attribute :template_id, :aliases => 'templateid' + attribute :template_name, :aliases => 'templatename' + attribute :templated_display_text, :aliases => 'templatedisplaytext' + attribute :password_enabled, :aliases => 'passwordenabled' + attribute :service_offering_id, :aliases => 'serviceofferingid' + attribute :service_offering_name, :aliases => 'serviceofferingname' + attribute :cpu_number, :aliases => 'cpunumber' + attribute :cpu_speed, :aliases => 'cpuspeed' + attribute :memory + attribute :cpu_used, :aliases => 'cpuused' + attribute :network_kbs_read, :aliases => 'networkkbsread' + attribute :network_kbs_write, :aliases => 'networkkbswrite' + attribute :guest_os_id, :aliases => 'guestosid' + attribute :root_device_id, :aliases => 'rootdeviceid' + attribute :root_device_type, :aliases => 'rootdevicetype' + attribute :security_group, :aliases => 'securitygroup' + attribute :nics, :aliases => 'nic' + + attr_accessor :network_ids + + def ready? + true + end + + def save + requires :template_id, :service_offering_id, :zone_id + data = connection.deploy_virtual_machine( + :template_id => template_id, + :service_offering_id => service_offering_id, + :zone_id => zone_id, + :network_ids => network_ids + ) + merge_attributes(data['deployvirtualmachineresponse']) + end + + def destroy + requires :id + connection.destroy_virtual_machine(:id => id) + true + end + end # Server + end # Cloudstack + end # Compute +end # Fog diff --git a/lib/fog/cloudstack/models/compute/servers.rb b/lib/fog/cloudstack/models/compute/servers.rb new file mode 100644 index 000000000..c61ba380f --- /dev/null +++ b/lib/fog/cloudstack/models/compute/servers.rb @@ -0,0 +1,35 @@ +require 'fog/core/collection' +require 'fog/cloudstack/models/compute/server' + +module Fog + module Compute + class Cloudstack + + class Servers < Fog::Collection + + model Fog::Compute::Cloudstack::Server + + def all + data = connection.list_virtual_machines["listvirtualmachinesresponse"]["virtualmachine"] || [] + load(data) + end + + def bootstrap(new_attributes = {}) + server = create(new_attributes) + server.wait_for { ready? } + server.setup(:password => server.password) + server + end + + def get(server_id) + if server = connection.list_virtual_machines('id' => server_id)["listvirtualmachinesresponse"]["virtualmachine"].first + new(server) + end + rescue Fog::Compute::Cloudstack::BadRequest + nil + end + end + + end + end +end diff --git a/lib/fog/cloudstack/requests/compute/deploy_virtual_machine.rb b/lib/fog/cloudstack/requests/compute/deploy_virtual_machine.rb index 49c424f34..2fb5db753 100644 --- a/lib/fog/cloudstack/requests/compute/deploy_virtual_machine.rb +++ b/lib/fog/cloudstack/requests/compute/deploy_virtual_machine.rb @@ -11,23 +11,125 @@ module Fog 'command' => 'deployVirtualMachine' ) - if ( securitygroupids = options.delete('securitygroupids') ).is_a?(Array) - options.merge!('securitygroupids' => securitygroupids.join(',')) + security_group_ids = options.delete(:security_group_ids) + if security_group_ids + options.merge!('securitygroupids' => Array(security_group_ids).join(',')) end - - if ( securitygroupnames = options.delete('securitygroupnames') ).is_a?(Array) - options.merge!('securitygroupnames' => securitygroupnames.join(',')) + + security_group_names = options.delete(:security_group_names) + if security_group_names + options.merge!('securitygroupnames' => Array(security_group_names).join(',')) end - - if ( networkids = options.delete('networkids') ).is_a?(Array) - options.merge!('networkids' => networkids.join(',')) + + network_ids = options.delete(:network_ids) + if network_ids + options.merge!('networkids' => Array(network_ids).join(',')) end - - + + options["zoneid"]= options.delete(:zone_id) if options.key?(:zone_id) + options["templateid"]= options.delete(:template_id) if options.key?(:template_id) + options["serviceofferingid"]= options.delete(:service_offering_id) if options.key?(:service_offering_id) + request(options) end + end # Real - end - end - end -end + class Mock + + def deploy_virtual_machine(options={}) + zone_id = options[:zone_id] + unless zone_id + raise Fog::Compute::Cloudstack::BadRequest.new('Unable to execute API command deployvirtualmachine due to missing parameter zoneid') + end + unless zone = self.data[:zones][zone_id] + raise Fog::Compute::Cloudstack::BadRequest.new("Unable to execute API command deployvirtualmachine due to invalid value. Object networks(uuid: #{zone_id}) does not exist.") + end + zone_name = zone[:name] + + template_id = options[:template_id] + unless template = self.data[:templates][options[:template_id]] + raise Fog::Compute::Cloudstack::BadRequest.new('Unable to execute API command deployvirtualmachine due to missing parameter templateid') + end + template_name = template[:name] + template_display_text = template[:display_text] + + service_offering_id = options[:service_offering_id] + unless service_offering = self.data[:service_offerings][options[:service_offering_id]] + raise Fog::Compute::Cloudstack::BadRequest.new('Unable to execute API command deployvirtualmachine due to missing parameter serviceofferingid') + end + + service_offering_name = service_offering[:name] + service_offering_cpu_number = service_offering[:cpunumber] + service_offering_cpu_speed = service_offering[:cpuspeed] + service_offering_memory = service_offering[:cpumemory] + + identity = Fog::Cloudstack.uuid + name = options[:name] || Fog::Cloudstack.uuid + display_name = options[:display_name] || name + account_name = options[:account] || self.data[:accounts].first[1]["name"] + + domain = options[:domain_id] ? self.data[:domains][options[:domain_id]] : self.data[:domains].first[1] + domain_id = domain[:id] + domain_name = domain[:name] + + # how is this setup + password = nil + password_enabled = false + + guest_os_id = Fog::Cloudstack.uuid + + security_group_ids = options[:security_group_ids] || [] # TODO: for now + + network_ids = Array(options[:network_ids]) || [self.data[:networks].first[1]["id"]] + networks = network_ids.map{|nid| self.data[:networks][nid]} + nic = networks.map do |network| + { + "id" => Fog::Cloudstack.uuid, + "networkid" => network["id"], + "netmask" => Fog::Cloudstack.ip_address, + "gateway" => network["gateway"], + "ipaddress" => Fog::Cloudstack.ip_address, + "traffictype" => "Guest", # TODO: ? + "type" => network["type"], + "isdefault" => true, # TODO: ? + "macaddress" => Fog::Cloudstack.mac_address + } + end + + virtual_machine = { + "id" => identity, + "name" => name, + "displayname" => display_name, + "account" => account_name, + "domainid" => domain_id, + "domain" => domain_name, + "created" => Time.now.to_s, + "state" => "Running", + "haenable" => false, + "zoneid" => zone_id, + "zonename" => zone_name, + "templateid" => template_id, + "templatename" => template_name, + "templatedisplaytext" => template_display_text, + "passwordenabled" => false, + "serviceofferingid" => service_offering_id, + "serviceofferingname" => service_offering_name, + "cpunumber" => service_offering_cpu_number, + "cpuspeed" => service_offering_cpu_speed, + "memory" => service_offering_memory, + "cpuused" => "0%", + "networkkbsread" => 0, + "networkkbswrite" => 0, + "guestosid" => guest_os_id, + "rootdeviceid" => 0, + "rootdevicetype" => "NetworkFilesystem", + "securitygroup" => security_group_ids, # TODO: mayhaps? + "nic" => nic + } + self.data[:servers][identity]= virtual_machine + {'deployvirtualmachineresponse' => virtual_machine} + end + end # Mock + end # Cloudstack + end # Compute +end # Fog diff --git a/lib/fog/cloudstack/requests/compute/destroy_virtual_machine.rb b/lib/fog/cloudstack/requests/compute/destroy_virtual_machine.rb index 747782e1e..cbe2895ea 100644 --- a/lib/fog/cloudstack/requests/compute/destroy_virtual_machine.rb +++ b/lib/fog/cloudstack/requests/compute/destroy_virtual_machine.rb @@ -14,7 +14,16 @@ module Fog request(options) end + end # Real + + class Mock + + def destroy_virtual_machine(options={}) + identity = options[:id] + + self.data[:servers].delete(identity) + end end - end - end -end + end # Cloudstack + end # Compute +end # Fog diff --git a/lib/fog/cloudstack/requests/compute/list_capabilities.rb b/lib/fog/cloudstack/requests/compute/list_capabilities.rb index 2b56549d7..047fd776a 100644 --- a/lib/fog/cloudstack/requests/compute/list_capabilities.rb +++ b/lib/fog/cloudstack/requests/compute/list_capabilities.rb @@ -10,7 +10,7 @@ module Fog options.merge!( 'command' => 'listCapabilities' ) - + request(options) end diff --git a/lib/fog/cloudstack/requests/compute/list_virtual_machines.rb b/lib/fog/cloudstack/requests/compute/list_virtual_machines.rb index 2150619dc..7371a943f 100644 --- a/lib/fog/cloudstack/requests/compute/list_virtual_machines.rb +++ b/lib/fog/cloudstack/requests/compute/list_virtual_machines.rb @@ -10,11 +10,17 @@ module Fog options.merge!( 'command' => 'listVirtualMachines' ) - + request(options) end + end # Real - end - end - end -end + class Mock + def list_virtual_machines(options={}) + {"listvirtualmachinesresponse" => + {"count" => self.data[:servers].values.size, "virtualmachine" => self.data[:servers].values}} + end + end # Mock + end # Cloudstack + end # Compute +end # Fog diff --git a/tests/cloudstack/requests/virtual_machine_tests.rb b/tests/cloudstack/requests/virtual_machine_tests.rb index fa209e96b..9cc745579 100644 --- a/tests/cloudstack/requests/virtual_machine_tests.rb +++ b/tests/cloudstack/requests/virtual_machine_tests.rb @@ -4,35 +4,35 @@ Shindo.tests('Fog::Compute[:cloudstack] | virtual machine requests', ['cloudstac 'listvirtualmachinesresponse' => { 'count' => Integer, 'virtualmachine' => [ - 'id' => Integer, + 'id' => String, 'name' => String, 'displayname' => String, 'account' => String, - 'domainid' => Integer, + 'domainid' => String, 'domain' => String, 'created' => String, 'state' => String, 'haenable' => Fog::Boolean, - 'zoneid' => Integer, + 'zoneid' => String, 'zonename' => String, 'hostid' => Fog::Nullable::String, 'hostname' => Fog::Nullable::String, - 'templateid' => Integer, + 'templateid' => String, 'templatename' => String, 'templatedisplaytext' => String, 'passwordenabled' => Fog::Boolean, - 'serviceofferingid' => Integer, + 'serviceofferingid' => String, 'serviceofferingname' => String, 'cpunumber' => Integer, 'cpuspeed' => Integer, 'networkkbsread' => Fog::Nullable::Integer, 'memory' => Integer, 'cpuused' => Fog::Nullable::String, - 'guestosid' => Integer, + 'guestosid' => String, 'networkkbswrite' => Fog::Nullable::Integer, 'rootdeviceid' => Integer, 'rootdevicetype' => String, - 'hypervisor' => String, + 'hypervisor' => Fog::Nullable::String, 'group' => Fog::Nullable::String, 'groupid' => Fog::Nullable::Integer, 'isoname' => Fog::Nullable::String, @@ -43,8 +43,8 @@ Shindo.tests('Fog::Compute[:cloudstack] | virtual machine requests', ['cloudstac 'description' => Fog::Nullable::String ], 'nic' => [ - 'id' => Integer, - 'networkid' => Integer, + 'id' => String, + 'networkid' => String, 'netmask' => String, 'gateway' => String, 'ipaddress' => String, @@ -68,4 +68,4 @@ Shindo.tests('Fog::Compute[:cloudstack] | virtual machine requests', ['cloudstac end -end \ No newline at end of file +end diff --git a/tests/compute/helper.rb b/tests/compute/helper.rb index 6d4e6f34f..d651af13d 100644 --- a/tests/compute/helper.rb +++ b/tests/compute/helper.rb @@ -19,6 +19,14 @@ def compute_providers }, :mocked => false }, + :openstack => { :mocked => true}, + :cloudstack => { + :provider_attributes => { + :cloudstack_host => 'http://host.foo' + }, + :server_attributes => Fog.credentials[:cloudstack], + :mocked => true + }, :glesys => { :mocked => false }, diff --git a/tests/compute/models/servers_tests.rb b/tests/compute/models/servers_tests.rb index ccb78b93b..77fb105c5 100644 --- a/tests/compute/models/servers_tests.rb +++ b/tests/compute/models/servers_tests.rb @@ -1,4 +1,6 @@ + for provider, config in compute_providers + next if ENV['FOG_PROVIDER'] && provider.to_s != ENV['FOG_PROVIDER'] Shindo.tests("Fog::Compute[:#{provider}] | servers", [provider.to_s]) do diff --git a/tests/helpers/mock_helper.rb b/tests/helpers/mock_helper.rb index 02ff590b3..f4d9d4d34 100644 --- a/tests/helpers/mock_helper.rb +++ b/tests/helpers/mock_helper.rb @@ -15,6 +15,7 @@ if Fog.mock? :bluebox_customer_id => 'bluebox_customer_id', :brightbox_client_id => 'brightbox_client_id', :brightbox_secret => 'brightbox_secret', + :cloudstack_host => 'http://cloudstack.example.org', :clodo_api_key => 'clodo_api_key', :clodo_username => 'clodo_username', :dnsimple_email => 'dnsimple_email', @@ -77,5 +78,5 @@ if Fog.mock? :libvirt_uri => 'qemu:///system', :libvirt_username => 'root', :libvirt_password => 'password' - } + }.merge(Fog.credentials) end