Skip to content
This repository

GH-690 Joyent Cloud Provider #739

Closed
wants to merge 9 commits into from

4 participants

Kevin Chan Wesley Beary Rodrigo Dellacqua
Kevin Chan
Collaborator

This implements GH-690 Joyent CloudAPI provider support

API Documentation: https://us-west-1.api.joyentcloud.com/docs

Methods implemented (everything in the docs except those under Analytics and Datacenters)

add_machine_tags
create_key
create_machine
create_machine_snapshot
delete_all_machine_metadata
delete_all_machine_tags
delete_key
delete_machine
delete_machine_metadata
delete_machine_snapshot
delete_machine_tag
get_dataset
get_key
get_machine
get_machine_metadata
get_machine_snapshot
get_machine_tag
get_package
list_datasets
list_keys
list_machine_snapshots
list_machine_tags
list_machines
list_packages
reboot_machine
resize_machine
start_machine
start_machine_from_snapshot
stop_machine
update_machine_metadata

Models provided (and their corresponding vocabulary in CloudAPI terms):

Flavor # Machine
Image # Dataset
Key # Key
Server # Machine
Snapshot # Snapshot

+1
Any chance it will be merged soon?

Wesley Beary
Owner

Awesome, I'm hoping to find time to review/merge sometime this week.

Wesley Beary
Owner

@kevinykchan Looks great, a few questions though:

  • Why are all the credentials called :cloudapi_foo, seems like :joyent_foo might be clearer?
  • Where can I sign up for an account to test with? I tried to look at the joyent sight but wasn't sure which thing I really wanted.
  • Are you up for joining the core team to help maintain this moving forward? Thanks! wes
Kevin Chan
Collaborator

@geemus

To sign up for a Joyent account: https://my.joyentcloud.com

The credentials are prefixed with cloudapi because this provider is 100% compatible with deployments of our SmartDataCebter product in which cloudapi is a component of. But you are right, prefixing it with joyent would be more consistent. I'll fix it asap.

And sure, I'll join the team to maintain this going forward.

Thanks!

Wesley Beary
Owner

@kevinykchan Thanks for the info. I can see why cloudapi would also make sense, but I think joyent is probably the better prefix consistency-wise, so thanks for cleaning that up. Will work on getting this merged now.

Wesley Beary
Owner

@kevinykchan I did a little cleanup, but I'm having trouble with the tests. Seems that they aren't really setup to work in non-mock mode (and when I started fixing them up to work in real they started breaking in mock). You can see my initial changes here:
https://github.com/fog/fog/tree/joyent
My expectation is that the following two things should run without failures bundle exec shindo tests/joyent and FOG_MOCK=true bundle exec shindo tests/joyent. So if you could run through those and double check and cleanup the errors that would be awesome. Just let me know if anything is unclear or I can help out. Thanks!

added some commits February 21, 2012
Kevin Chan Revert "[joyent|compute] make password required"
This reverts commit 6e93321.

The reason for reverting this is Joyent's CloudAPI allows authenticating using
either a username/password credential pair OR using a username and SSH private key.

In http-signature authentication scheme, no password is used.
a295c9c
Kevin Chan Fixes dataset tests 9ef1c52
Kevin Chan - Fixes tests to run in both mock and non-mock mode
 - Clean ups and fixes
01c92c8
Kevin Chan
Collaborator

@geemus

I've push the fixes to the issues above to https://github.com/kevinykchan/fog/tree/joyent
I did revert your change to require cloudapi_password because both cloudapi_keyfile and cloudapi_password can be used to authenticate against our API

Test output:

$ bundle exec shindo tests/joyent
Skipping tests for dynect due to lacking credentials (add some to '~/.fog' to run them)
Skipping tests for ecloud due to lacking credentials (add some to '~/.fog' to run them)
Skipping tests for glesys due to lacking credentials (add some to '~/.fog' to run them)
Skipping tests for ninefold due to lacking credentials (add some to '~/.fog' to run them)
Skipping tests for ovirt due to lacking credentials (add some to '~/.fog' to run them)
Skipping tests for stormondemand due to lacking credentials (add some to '~/.fog' to run them)

Fog::Compute[:joyent] | dataset requests (joyent)
  #list_datasets
    + has proper format

Fog::Compute::Joyent | key requests (joyent)
  #list_keys + has proper format
  #get_key + has proper format
  #delete_key
    + returns status code 204
    + when a key no longer exists

Fog::Compute[:joyent] | machine requests (joyent)
  #list_machines
    + returns an Array of machines
    + has proper format
  #get_machine
    + has proper format

Fog::Compute[:joyent] | package requests (joyent)
  #list_packages
    + has proper format
  #get_package
    + has proper format

10 succeeded in 35.821114 seconds


  ======================

$ FOG_MOCK=true bundle exec shindo tests/joyent

Fog::Compute[:joyent] | dataset requests (joyent)
  #list_datasets
    + has proper format

Fog::Compute::Joyent | key requests (joyent)
  #list_keys + has proper format
  #get_key + has proper format
  #delete_key
    + returns status code 204
    + when a key no longer exists

Fog::Compute[:joyent] | machine requests (joyent)
  #list_machines
    + correct number of machines
    + returns an Array of machines
    + has proper format
  #get_machine
    + has proper format

Fog::Compute[:joyent] | package requests (joyent)
  #list_packages
    + has proper format
  #list_packages
    + has correct number of packages
  #get_package
    + has proper format

Please let me know if you run into more issues. I have plans to continue adding more tests in the future.

Wesley Beary
Owner

@kevinykchan thanks for continuing to help out on this. I'm still getting one failure though, from non-mocked get_machines. Here is what I'm seeing:

geemus  ~/geemus/fog[kevinykchan]  ⌘  be shindo tests/joyent/
  Skipping tests for openstack due to lacking credentials (add some to '~/.fog' to run them)
  Skipping tests for ovirt due to lacking credentials (add some to '~/.fog' to run them)
  Skipping tests for vsphere due to lacking credentials (add some to '~/.fog' to run them)

  Fog::Compute[:joyent] | dataset requests (joyent)
    #list_datasets
      + has proper format

  Fog::Compute::Joyent | key requests (joyent)
    #list_keys + has proper format
    #get_key + has proper format
    #delete_key
      + returns status code 204
      + when a key no longer exists

  Fog::Compute[:joyent] | machine requests (joyent)
    #list_machines
      + returns an Array of machines
      + has proper format
    #get_machine
      - has proper format
      undefined method `[]' for nil:NilClass (NoMethodError)
        /Users/geemus/geemus/fog/tests/joyent/requests/compute/machines_tests.rb:60:in `block (3 levels) in <top (required)>'
        tests/helpers/formats_helper.rb:32:in `instance_eval'
        tests/helpers/formats_helper.rb:32:in `block in formats'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:139:in `instance_eval'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:139:in `assert'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:115:in `test'
        tests/helpers/formats_helper.rb:31:in `formats'
        /Users/geemus/geemus/fog/tests/joyent/requests/compute/machines_tests.rb:59:in `block (2 levels) in <top (required)>'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:79:in `instance_eval'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:79:in `tests'
        /Users/geemus/geemus/fog/tests/joyent/requests/compute/machines_tests.rb:58:in `block in <top (required)>'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:79:in `instance_eval'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:79:in `tests'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:38:in `initialize'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:14:in `new'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo.rb:14:in `tests'
        /Users/geemus/geemus/fog/tests/joyent/requests/compute/machines_tests.rb:1:in `<top (required)>'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo/bin.rb:61:in `load'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo/bin.rb:61:in `block (2 levels) in run_in_thread'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo/bin.rb:58:in `each'
        /Users/geemus/.rvm/gems/ruby-1.9.2-p290/gems/shindo-0.3.4/lib/shindo/bin.rb:58:in `block in run_in_thread'
      Action? [c,q,r,?]? c



  Fog::Compute[:joyent] | package requests (joyent)
    #list_packages
      + has proper format
    #get_package
      + has proper format

  1 failed, 9 succeeded in 23.617851 seconds

Any thoughts?

Kevin Chan
Collaborator

Ah it looks like its because you don't have any machines, I will adjust the test to check the length of the resultant.

Rodrigo Dellacqua

looking forward to be able to use this :)
Great job guys, +1.

Wesley Beary
Owner

Merged, thanks!

Wesley Beary geemus closed this February 27, 2012
Rodrigo Dellacqua

Pro! New era has begun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 9 unique commits by 2 authors.

Feb 10, 2012
Kevin Chan GH-690 Joyent Cloud Provider
Squashed from fog/joyent
cb101d8
Feb 18, 2012
Kevin Chan Credentials: cloudapi_* -> joyent_* for consistency cf1c277
Feb 20, 2012
Wesley Beary [joyent|compute] rename _test files to _tests for shindo cae7bcb
Wesley Beary [joyent|compute] make password required 6e93321
Wesley Beary [joyent|compute] fix format of joyent to match real output and remove…
… mock-only test
76ac079
Feb 21, 2012
Kevin Chan Revert "[joyent|compute] make password required"
This reverts commit 6e93321.

The reason for reverting this is Joyent's CloudAPI allows authenticating using
either a username/password credential pair OR using a username and SSH private key.

In http-signature authentication scheme, no password is used.
a295c9c
Kevin Chan Fixes dataset tests 9ef1c52
Kevin Chan - Fixes tests to run in both mock and non-mock mode
 - Clean ups and fixes
01c92c8
Feb 22, 2012
Kevin Chan Cleanups + Fixes #get_machine test breaking when there are no machines 8c7a85b
This page is out of date. Refresh to see the latest.

Showing 53 changed files with 1,497 additions and 1 deletion. Show diff stats Hide diff stats

  1. 1  lib/fog/bin.rb
  2. 31  lib/fog/bin/joyent.rb
  3. 3  lib/fog/compute.rb
  4. 8  lib/fog/joyent.rb
  5. 194  lib/fog/joyent/compute.rb
  6. 17  lib/fog/joyent/models/compute/flavor.rb
  7. 25  lib/fog/joyent/models/compute/flavors.rb
  8. 18  lib/fog/joyent/models/compute/image.rb
  9. 25  lib/fog/joyent/models/compute/images.rb
  10. 19  lib/fog/joyent/models/compute/key.rb
  11. 34  lib/fog/joyent/models/compute/keys.rb
  12. 117  lib/fog/joyent/models/compute/server.rb
  13. 35  lib/fog/joyent/models/compute/servers.rb
  14. 45  lib/fog/joyent/models/compute/snapshot.rb
  15. 37  lib/fog/joyent/models/compute/snapshots.rb
  16. 20  lib/fog/joyent/requests/compute/add_machine_tags.rb
  17. 56  lib/fog/joyent/requests/compute/create_key.rb
  18. 16  lib/fog/joyent/requests/compute/create_machine.rb
  19. 18  lib/fog/joyent/requests/compute/create_machine_snapshot.rb
  20. 16  lib/fog/joyent/requests/compute/delete_all_machine_metadata.rb
  21. 15  lib/fog/joyent/requests/compute/delete_all_machine_tags.rb
  22. 29  lib/fog/joyent/requests/compute/delete_key.rb
  23. 15  lib/fog/joyent/requests/compute/delete_machine.rb
  24. 18  lib/fog/joyent/requests/compute/delete_machine_metadata.rb
  25. 21  lib/fog/joyent/requests/compute/delete_machine_snapshot.rb
  26. 15  lib/fog/joyent/requests/compute/delete_machine_tag.rb
  27. 28  lib/fog/joyent/requests/compute/get_dataset.rb
  28. 29  lib/fog/joyent/requests/compute/get_key.rb
  29. 29  lib/fog/joyent/requests/compute/get_machine.rb
  30. 25  lib/fog/joyent/requests/compute/get_machine_metadata.rb
  31. 14  lib/fog/joyent/requests/compute/get_machine_snapshot.rb
  32. 19  lib/fog/joyent/requests/compute/get_machine_tag.rb
  33. 33  lib/fog/joyent/requests/compute/get_package.rb
  34. 24  lib/fog/joyent/requests/compute/list_datasets.rb
  35. 25  lib/fog/joyent/requests/compute/list_keys.rb
  36. 14  lib/fog/joyent/requests/compute/list_machine_snapshots.rb
  37. 20  lib/fog/joyent/requests/compute/list_machine_tags.rb
  38. 25  lib/fog/joyent/requests/compute/list_machines.rb
  39. 36  lib/fog/joyent/requests/compute/list_packages.rb
  40. 15  lib/fog/joyent/requests/compute/reboot_machine.rb
  41. 13  lib/fog/joyent/requests/compute/resize_machine.rb
  42. 16  lib/fog/joyent/requests/compute/start_machine.rb
  43. 15  lib/fog/joyent/requests/compute/start_machine_from_snapshot.rb
  44. 16  lib/fog/joyent/requests/compute/stop_machine.rb
  45. 15  lib/fog/joyent/requests/compute/update_machine_metadata.rb
  46. 1  lib/fog/providers.rb
  47. 3  tests/compute/helper.rb
  48. 2  tests/helper.rb
  49. 2  tests/helpers/mock_helper.rb
  50. 49  tests/joyent/requests/compute/datasets_tests.rb
  51. 47  tests/joyent/requests/compute/keys_tests.rb
  52. 67  tests/joyent/requests/compute/machines_tests.rb
  53. 68  tests/joyent/requests/compute/packages_tests.rb
1  lib/fog/bin.rb
@@ -64,6 +64,7 @@ def collections
64 64
 require 'fog/bin/glesys'
65 65
 require 'fog/bin/go_grid'
66 66
 require 'fog/bin/google'
  67
+require 'fog/bin/joyent'
67 68
 require 'fog/bin/libvirt'
68 69
 require 'fog/bin/linode'
69 70
 require 'fog/bin/local'
31  lib/fog/bin/joyent.rb
... ...
@@ -0,0 +1,31 @@
  1
+class Joyent < Fog::Bin
  2
+  class << self
  3
+
  4
+    def class_for(key)
  5
+      case key
  6
+      when :compute
  7
+        Fog::Compute::Joyent
  8
+      else 
  9
+        raise ArgumentError, "Unrecognized service: #{key}"
  10
+      end
  11
+    end
  12
+
  13
+    def [](service)
  14
+      @@connections ||= Hash.new do |hash, key|
  15
+        hash[key] = case key
  16
+        when :compute
  17
+          Fog::Logger.warning("Joyent[:compute] is not recommended, use Compute[:joyent] for portability")
  18
+          Fog::Compute.new(:provider => 'Joyent')
  19
+        else
  20
+          raise ArgumentError, "Unrecognized service: #{key.inspect}"
  21
+        end
  22
+      end
  23
+      @@connections[service]
  24
+    end
  25
+
  26
+    def services
  27
+      Fog::Joyent.services
  28
+    end
  29
+
  30
+  end
  31
+end
3  lib/fog/compute.rb
@@ -32,6 +32,9 @@ def self.new(attributes)
32 32
       when :gogrid
33 33
         require 'fog/go_grid/compute'
34 34
         Fog::Compute::GoGrid.new(attributes)
  35
+      when :joyent
  36
+        require 'fog/joyent/compute'
  37
+        Fog::Compute::Joyent.new(attributes)
35 38
       when :libvirt
36 39
         require 'fog/libvirt/compute'
37 40
         Fog::Compute::Libvirt.new(attributes)
8  lib/fog/joyent.rb
... ...
@@ -0,0 +1,8 @@
  1
+module Fog
  2
+  module Joyent
  3
+    extend Fog::Provider
  4
+
  5
+    service(:compute, 'joyent/compute', 'Compute')
  6
+
  7
+  end
  8
+end
194  lib/fog/joyent/compute.rb
... ...
@@ -0,0 +1,194 @@
  1
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'brightbox'))
  2
+require 'fog/compute'
  3
+require 'multi_json'
  4
+
  5
+module Fog
  6
+  module Compute
  7
+    class Joyent < Fog::Service
  8
+      requires :joyent_username
  9
+
  10
+      recognizes :joyent_password
  11
+      recognizes :joyent_url
  12
+      recognizes :joyent_keyname
  13
+      recognizes :joyent_keyfile
  14
+
  15
+      model_path 'fog/joyent/models/compute'
  16
+      request_path 'fog/joyent/requests/compute'
  17
+
  18
+      # request :list_datacenters
  19
+      # request :get_datacenter
  20
+
  21
+      # Keys
  22
+      collection :keys
  23
+      model :key
  24
+
  25
+      request :list_keys
  26
+      request :get_key
  27
+      request :create_key
  28
+      request :delete_key
  29
+
  30
+      # Images
  31
+      collection :images
  32
+      model :image
  33
+      request :list_datasets
  34
+      request :get_dataset
  35
+
  36
+      # Flavors
  37
+      collection :flavors
  38
+      model :flavor
  39
+      request :list_packages
  40
+      request :get_package
  41
+
  42
+      # Servers
  43
+      collection :servers
  44
+      model :server
  45
+      request :list_machines
  46
+      request :get_machine
  47
+      request :create_machine
  48
+      request :start_machine
  49
+      request :stop_machine
  50
+      request :reboot_machine
  51
+      request :resize_machine
  52
+      request :delete_machine
  53
+
  54
+      # Snapshots
  55
+      collection :snapshots
  56
+      model :snapshot
  57
+      request :create_machine_snapshot
  58
+      request :start_machine_from_snapshot
  59
+      request :list_machine_snapshots
  60
+      request :get_machine_snapshot
  61
+      request :delete_machine_snapshot
  62
+      request :update_machine_metadata
  63
+      request :get_machine_metadata
  64
+      request :delete_machine_metadata
  65
+      request :delete_all_machine_metadata
  66
+
  67
+      # MachineTags
  68
+      request :add_machine_tags
  69
+      request :list_machine_tags
  70
+      request :get_machine_tag
  71
+      request :delete_machine_tag
  72
+      request :delete_all_machine_tags
  73
+
  74
+      class Mock
  75
+        def self.data
  76
+          @data ||= Hash.new do |hash, key|
  77
+            hash[key] = {}
  78
+          end
  79
+        end
  80
+
  81
+        def data
  82
+          self.class.data
  83
+        end
  84
+
  85
+        def initialize(options = {})
  86
+          @joyent_username = options[:joyent_username] || Fog.credentials[:joyent_username]
  87
+          @joyent_password = options[:joyent_password] || Fog.credentials[:joyent_password]
  88
+        end
  89
+
  90
+        def request(opts)
  91
+          raise "Not Implemented"
  92
+        end
  93
+      end # Mock
  94
+
  95
+      class Real
  96
+        def initialize(options = {})
  97
+          @connection_options = options[:connection_options] || {}
  98
+          @persistent = options[:persistent] || false
  99
+
  100
+          @joyent_url = options[:joyent_url] || 'https://us-sw-1.api.joyentcloud.com'
  101
+          @joyent_version = options[:joyent_version] || '~6.5'
  102
+
  103
+          @joyent_username = options[:joyent_username]
  104
+
  105
+          unless @joyent_username
  106
+            raise ArgumentError, "options[:joyent_username] required"
  107
+          end
  108
+
  109
+          if options[:joyent_keyname] && options[:joyent_keyfile]
  110
+            if File.exists?(options[:joyent_keyfile])
  111
+              @joyent_keyname = options[:joyent_keyname]
  112
+              @joyent_key = File.read(options[:joyent_keyfile])
  113
+
  114
+              @rsa = OpenSSL::PKey::RSA.new(@joyent_key)
  115
+
  116
+              @header_method = method(:header_for_signature)
  117
+            else
  118
+              raise ArgumentError, "options[:joyent_keyfile] provided does not exist."
  119
+            end
  120
+          elsif options[:joyent_password]
  121
+            @joyent_password = options[:joyent_password]
  122
+
  123
+            @header_method = method(:header_for_basic)
  124
+          else
  125
+            raise ArgumentError, "Must provide either a joyent_password or joyent_keyname and joyent_keyfile pair"
  126
+          end
  127
+
  128
+          @connection = Fog::Connection.new(
  129
+            @joyent_url,
  130
+            @persistent,
  131
+            @connection_options
  132
+          )
  133
+        end
  134
+
  135
+        def request(request_options = {})
  136
+          (request_options[:headers] ||= {}).merge!({
  137
+            "X-Api-Version" => @joyent_version,
  138
+            "Content-Type" => "application/json",
  139
+            "Accept" => "application/json"
  140
+          }).merge!(@header_method.call)
  141
+
  142
+          if request_options[:body]
  143
+            request_options[:body] = MultiJson.encode(request_options[:body])
  144
+          end
  145
+
  146
+          response = @connection.request(request_options)
  147
+
  148
+          if response.headers["Content-Type"] == "application/json"
  149
+            response.body = MultiJson.decode(response.body)
  150
+            response.body = decode_time_props(response.body)
  151
+          end
  152
+
  153
+          response
  154
+        end
  155
+
  156
+        private
  157
+
  158
+        def header_for_basic
  159
+          {
  160
+            "Authorization" => "Basic #{Base64.encode64("#{@joyent_username}:#{@joyent_password}").delete("\r\n")}"
  161
+          }
  162
+        end
  163
+
  164
+        def header_for_signature
  165
+          date = Time.now.utc.httpdate
  166
+          signature = Base64.encode64(@rsa.sign("sha256", date)).delete("\r\n")
  167
+          key_id = "/#{@joyent_username}/keys/#{@joyent_keyname}"
  168
+
  169
+          {
  170
+            "Date" => date,
  171
+            "Authorization" => "Signature keyId=\"#{key_id}\",algorithm=\"rsa-sha256\" #{signature}"
  172
+          }
  173
+        end
  174
+
  175
+        def decode_time_props(obj)
  176
+          if obj.kind_of?(Hash)
  177
+            if obj["created"]
  178
+              obj["created"] = Time.parse(obj["created"])
  179
+            end
  180
+
  181
+            if obj["updated"]
  182
+              obj["updated"] = Time.parse(obj["updated"])
  183
+            end
  184
+          elsif obj.kind_of?(Array)
  185
+            obj.map do |o|
  186
+              decode_time_props(o)
  187
+            end
  188
+          end
  189
+          obj
  190
+        end
  191
+      end # Real
  192
+    end
  193
+  end
  194
+end
17  lib/fog/joyent/models/compute/flavor.rb
... ...
@@ -0,0 +1,17 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Flavor < Fog::Model
  5
+
  6
+        identity :name
  7
+
  8
+        attribute :name
  9
+        attribute :memory
  10
+        attribute :swap
  11
+        attribute :disk
  12
+        attribute :default
  13
+
  14
+      end
  15
+    end
  16
+  end
  17
+end
25  lib/fog/joyent/models/compute/flavors.rb
... ...
@@ -0,0 +1,25 @@
  1
+require 'fog/core/collection'
  2
+require 'fog/joyent/models/compute/flavor'
  3
+
  4
+module Fog
  5
+  module Compute
  6
+
  7
+    class Joyent
  8
+      class Flavors < Fog::Collection
  9
+
  10
+        model Fog::Compute::Joyent::Flavor
  11
+
  12
+        def all
  13
+          load(connection.list_packages().body)
  14
+        end
  15
+
  16
+        def get(id)
  17
+          data = connection.get_package(id).body
  18
+          new(data)
  19
+        end
  20
+
  21
+      end
  22
+    end # Joyent
  23
+
  24
+  end # Compute
  25
+end # Fog
18  lib/fog/joyent/models/compute/image.rb
... ...
@@ -0,0 +1,18 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Image < Fog::Model
  5
+
  6
+        identity :id
  7
+
  8
+        attribute :name
  9
+        attribute :os
  10
+        attribute :type
  11
+        attribute :version
  12
+        attribute :created, :type => :time
  13
+        attribute :default
  14
+
  15
+      end
  16
+    end
  17
+  end
  18
+end
25  lib/fog/joyent/models/compute/images.rb
... ...
@@ -0,0 +1,25 @@
  1
+require 'fog/core/collection'
  2
+require 'fog/joyent/models/compute/image'
  3
+
  4
+module Fog
  5
+  module Compute
  6
+
  7
+    class Joyent
  8
+      class Images < Fog::Collection
  9
+
  10
+        model Fog::Compute::Joyent::Image
  11
+
  12
+        def all
  13
+          load(connection.list_datasets().body)
  14
+        end
  15
+
  16
+        def get(id)
  17
+          data = connection.get_dataset(id).body
  18
+          new(data)
  19
+        end
  20
+
  21
+      end # Images
  22
+    end # Joyent
  23
+
  24
+  end # Compute
  25
+end # Fog
19  lib/fog/joyent/models/compute/key.rb
... ...
@@ -0,0 +1,19 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Key < Fog::Model
  5
+        identity :name
  6
+
  7
+        attribute :name
  8
+        attribute :key
  9
+
  10
+        attribute :created, :type => :time
  11
+
  12
+        def destroy
  13
+          requires :name
  14
+          self.connection.delete_key(name)
  15
+        end
  16
+      end
  17
+    end
  18
+  end
  19
+end
34  lib/fog/joyent/models/compute/keys.rb
... ...
@@ -0,0 +1,34 @@
  1
+require 'fog/joyent/models/compute/key'
  2
+
  3
+module Fog
  4
+  module Compute
  5
+    class Joyent
  6
+      class Keys < Fog::Collection
  7
+
  8
+        model Fog::Compute::Joyent::Key
  9
+
  10
+        def all
  11
+          data = connection.list_keys.body
  12
+          load(data)
  13
+        end
  14
+
  15
+        def get(keyname)
  16
+          data = connection.get_key(keyname).body
  17
+          if data
  18
+            new(data)
  19
+          else
  20
+            nil
  21
+          end
  22
+        end
  23
+
  24
+        def create(params = {})
  25
+          raise ArgumentError, "Key name required" unless params.key?(:name)
  26
+          raise ArgumentError, "Key body required" unless params.key?(:body)
  27
+
  28
+          self.connection.create_key(params)
  29
+        end
  30
+
  31
+      end
  32
+    end
  33
+  end
  34
+end
117  lib/fog/joyent/models/compute/server.rb
... ...
@@ -0,0 +1,117 @@
  1
+require 'fog/compute/models/server'
  2
+module Fog
  3
+  module Compute
  4
+    class Joyent
  5
+
  6
+      class Server < Fog::Compute::Server
  7
+        identity :id
  8
+
  9
+        attribute :name
  10
+        attribute :state
  11
+        attribute :type
  12
+        attribute :dataset
  13
+        attribute :ips
  14
+        attribute :memory
  15
+        attribute :ips
  16
+        attribute :disk
  17
+        attribute :metadata
  18
+
  19
+        attribute :created, :type => :time
  20
+        attribute :updated, :type => :time
  21
+
  22
+        def ready?
  23
+          self.state == 'running'
  24
+        end
  25
+
  26
+        def stopped?
  27
+          requires :id
  28
+          self.state == 'stopped'
  29
+        end
  30
+
  31
+        def destroy
  32
+          requires :id
  33
+          self.connection.delete_machine(id)
  34
+          true
  35
+        end
  36
+
  37
+        def start
  38
+          requires :id
  39
+          self.connection.start_machine(id)
  40
+          self.wait_for { ready? }
  41
+          true
  42
+        end
  43
+
  44
+        def stop
  45
+          requires :id
  46
+          self.connection.stop_machine(id)
  47
+          self.wait_for { stopped? }
  48
+          true
  49
+        end
  50
+
  51
+        def resize(flavor)
  52
+          requires :id
  53
+          self.connection.resize(id, flavor)
  54
+          true
  55
+        end
  56
+
  57
+        def reboot
  58
+          requires :id
  59
+          self.connection.reboot_machine(id)
  60
+          true
  61
+        end
  62
+
  63
+        def snapshots
  64
+          requires :id
  65
+          self.connection.snapshots.all(id)
  66
+        end
  67
+
  68
+        def update_metadata(data = {})
  69
+          requires :id
  70
+          self.connection.update_machine_metadata(self.id, data)
  71
+          self.reload
  72
+          true
  73
+        end
  74
+
  75
+        def delete_metadata(keyname)
  76
+          raise ArgumentError, "Must provide a key name to delete" if keyname.nil? || keyname.empty?
  77
+          requires :id
  78
+
  79
+          self.connection.delete_machine_metadata(self.id, keyname)
  80
+          true
  81
+        end
  82
+
  83
+        def delete_all_metadata
  84
+          requires :id
  85
+          self.connection.delete_all_machine_metadata(self.id)
  86
+          true
  87
+        end
  88
+
  89
+        def tags
  90
+          requires :id
  91
+          self.connection.list_machine_tags(id).body
  92
+        end
  93
+
  94
+        def add_tags(tags_hash = {})
  95
+          requires :id
  96
+          self.connection.add_machine_tags(self.id, tags_hash).body
  97
+        end
  98
+
  99
+        def delete_tag(tagname)
  100
+          requires :id
  101
+
  102
+          raise ArgumentError, "Must provide a tag name to delete" if tagname.nil? || tagname.empty?
  103
+          self.connection.delete_machine_tag(self.id, tagname)
  104
+          true
  105
+        end
  106
+
  107
+        def delete_all_tags
  108
+          requires :id
  109
+
  110
+          self.connection.delete_all_machine_tags(self.id)
  111
+          true
  112
+        end
  113
+
  114
+      end
  115
+    end
  116
+  end
  117
+end
35  lib/fog/joyent/models/compute/servers.rb
... ...
@@ -0,0 +1,35 @@
  1
+require 'fog/core/collection'
  2
+require 'fog/joyent/models/compute/server'
  3
+
  4
+module Fog
  5
+  module Compute
  6
+
  7
+    class Joyent
  8
+      class Servers < Fog::Collection
  9
+        model Fog::Compute::Joyent::Server
  10
+
  11
+        def all
  12
+          load(self.connection.list_machines().body)
  13
+        end
  14
+
  15
+        def create(params = {})
  16
+          data = self.connection.create_machine(params).body
  17
+          server = new(data)
  18
+          server.wait_for { ready? }
  19
+          server
  20
+        end
  21
+
  22
+        def bootstrap
  23
+          # XXX TOXO
  24
+        end
  25
+
  26
+        def get(machine_id)
  27
+          data = self.connection.get_machine(machine_id).body
  28
+          new(data)
  29
+        end
  30
+
  31
+      end
  32
+    end # Joyent
  33
+
  34
+  end # Compute
  35
+end # Fog
45  lib/fog/joyent/models/compute/snapshot.rb
... ...
@@ -0,0 +1,45 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Snapshot < Fog::Model
  5
+        identity :name
  6
+
  7
+        attribute :name
  8
+        attribute :state
  9
+        attribute :machine_id
  10
+        attribute :created, :type => :time
  11
+        attribute :updated, :type => :time
  12
+
  13
+        def reload
  14
+          requires :name
  15
+          requires :machine_id
  16
+
  17
+          self.connection.snapshots.get(self.machine_id, self.name)
  18
+        end
  19
+
  20
+        def start
  21
+          requires :name
  22
+          requires :machine_id
  23
+
  24
+          self.connection.start_machine_from_snapshot(self.machine_id, self.name)
  25
+          true
  26
+        end
  27
+
  28
+        def destroy
  29
+          requires :name
  30
+          requires :machine_id
  31
+
  32
+          self.connection.delete_machine_snapshot(self.machine_id, self.name)
  33
+          true
  34
+        end
  35
+
  36
+        def machine
  37
+          requires :machine_id
  38
+
  39
+          self.connection.servers.get(self.machine_id)
  40
+        end
  41
+
  42
+      end
  43
+    end
  44
+  end
  45
+end
37  lib/fog/joyent/models/compute/snapshots.rb
... ...
@@ -0,0 +1,37 @@
  1
+require 'fog/joyent/models/compute/snapshot'
  2
+
  3
+module Fog
  4
+  module Compute
  5
+    class Joyent
  6
+      class Snapshots < Fog::Collection
  7
+
  8
+        model Fog::Compute::Joyent::Snapshot
  9
+
  10
+        def create(machine_id, snapshot_name)
  11
+          data = self.connection.create_machine_snapshot(machine_id, snapshot_name).body
  12
+          data['machine_id'] = machine_id
  13
+          new(data)
  14
+        end
  15
+
  16
+        def all(machine_id)
  17
+          data = self.connection.list_machine_snapshots(machine_id).body.map do |m|
  18
+            m["machine_id"] = machine_id
  19
+            m
  20
+          end
  21
+          load(data)
  22
+        end
  23
+
  24
+        def get(machine_id, snapshot_name)
  25
+          data = self.connection.get_machine_snapshot(machine_id, snapshot_name).body
  26
+          if data
  27
+            data["machine_id"] = machine_id
  28
+            new(data)
  29
+          else
  30
+            nil
  31
+          end
  32
+        end
  33
+
  34
+      end
  35
+    end
  36
+  end
  37
+end
20  lib/fog/joyent/requests/compute/add_machine_tags.rb
... ...
@@ -0,0 +1,20 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+
  5
+      class Real
  6
+        # https://us-west-1.api.joyentcloud.com/docs#AddMachineTags
  7
+        def add_machine_tags(machine_id, tags={})
  8
+          raise ArgumentError, "tags must be a Hash of tags" unless tags.is_a?(Hash)
  9
+
  10
+          request(
  11
+            :path => "/my/machines/#{machine_id}/tags",
  12
+            :method => "POST",
  13
+            :body => tags,
  14
+            :expects => 200
  15
+          )
  16
+        end
  17
+      end
  18
+    end
  19
+  end
  20
+end
56  lib/fog/joyent/requests/compute/create_key.rb
... ...
@@ -0,0 +1,56 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Mock
  5
+        #
  6
+        # https://us-west-1.api.joyentcloud.com/docs#CreateKey
  7
+        #
  8
+        def create_key(params)
  9
+          name = params[:name]
  10
+          key = params[:key]
  11
+
  12
+          record = {
  13
+            "name" => name,
  14
+            "key" => key,
  15
+            "created" => Time.now.utc,
  16
+            "updated" => Time.now.utc
  17
+          }
  18
+
  19
+          self.data[:keys][name] = record
  20
+
  21
+          response = Excon::Response.new
  22
+          response.status = 201
  23
+          response.body = record
  24
+          response
  25
+        end
  26
+      end # Mock
  27
+
  28
+      class Real
  29
+
  30
+        # Creates a new SSH Key
  31
+        # ==== Parameters
  32
+        # * name<~String> - Name to assign to this key
  33
+        # * key<~String> - OpenSSH formatted public key
  34
+        #
  35
+        # ==== Returns
  36
+        # * response<~Excon::Response>:
  37
+        #   * body<~Hash>:
  38
+        #     * 'name'<~String> - Name for this key
  39
+        #     * 'key'<~String> - OpenSSH formatted public key
  40
+        #
  41
+        def create_key(params={})
  42
+          raise ArgumentError, "error creating key: [name] is required" unless params[:name]
  43
+          raise ArgumentError, "error creating key: [key] is required" unless params[:key]
  44
+
  45
+          request(
  46
+            :method => "POST",
  47
+            :path => "/my/keys",
  48
+            :body => { "name" => params[:name], "key" => params[:key] },
  49
+            :expects => 201
  50
+          )
  51
+        end
  52
+
  53
+      end
  54
+    end
  55
+  end
  56
+end
16  lib/fog/joyent/requests/compute/create_machine.rb
... ...
@@ -0,0 +1,16 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Real
  5
+        def create_machine(params = {})
  6
+          request(
  7
+            :method => "POST",
  8
+            :path => "/my/machines",
  9
+            :body => params,
  10
+            :expects => [200, 201, 202]
  11
+          )
  12
+        end
  13
+      end
  14
+    end
  15
+  end
  16
+end
18  lib/fog/joyent/requests/compute/create_machine_snapshot.rb
... ...
@@ -0,0 +1,18 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Real
  5
+
  6
+        def create_machine_snapshot(machine_id, snapshot_name)
  7
+          request(
  8
+            :method => "POST",
  9
+            :path => "/my/machines/#{machine_id}/snapshots",
  10
+            :body => {"name" => snapshot_name },
  11
+            :expects => [201]
  12
+          )
  13
+        end
  14
+
  15
+      end
  16
+    end
  17
+  end
  18
+end
16  lib/fog/joyent/requests/compute/delete_all_machine_metadata.rb
... ...
@@ -0,0 +1,16 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Real
  5
+        # https://us-west-1.api.joyentcloud.com/docs#DeleteMachineMetadata
  6
+        def delete_all_machine_metadata(machine_id)
  7
+          request(
  8
+            :method => "DELETE",
  9
+            :path => "/my/machines/#{machine_id}/metadata",
  10
+            :expects => 204
  11
+          )
  12
+        end
  13
+      end
  14
+    end
  15
+  end
  16
+end
15  lib/fog/joyent/requests/compute/delete_all_machine_tags.rb
... ...
@@ -0,0 +1,15 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Real
  5
+        def delete_all_machine_tags(machine_id)
  6
+          request(
  7
+            :path => "/my/machines/#{machine_id}/tags",
  8
+            :method => "DELETE",
  9
+            :expects => 204
  10
+          )
  11
+        end
  12
+      end
  13
+    end
  14
+  end
  15
+end
29  lib/fog/joyent/requests/compute/delete_key.rb
... ...
@@ -0,0 +1,29 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+
  5
+      class Mock
  6
+        def delete_key(keyname)
  7
+          if self.data[:keys].delete(keyname)
  8
+            response = Excon::Response.new
  9
+            response.status = 204
  10
+            response
  11
+          else
  12
+            raise Excon::Errors::NotFound, "Not Found"
  13
+          end
  14
+        end
  15
+      end
  16
+
  17
+      class Real
  18
+        def delete_key(name)
  19
+          request(
  20
+            :method => "DELETE",
  21
+            :path => "/my/keys/#{name}",
  22
+            :expects => 204
  23
+          )
  24
+        end
  25
+      end # Real
  26
+
  27
+    end
  28
+  end
  29
+end
15  lib/fog/joyent/requests/compute/delete_machine.rb
... ...
@@ -0,0 +1,15 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Real
  5
+        def delete_machine(machine_id)
  6
+          request(
  7
+            :path => "/my/machines/#{machine_id}",
  8
+            :method => "DELETE",
  9
+            :expects => [200, 204]
  10
+          )
  11
+        end
  12
+      end
  13
+    end
  14
+  end
  15
+end
18  lib/fog/joyent/requests/compute/delete_machine_metadata.rb
... ...
@@ -0,0 +1,18 @@
  1
+module Fog
  2
+  module Compute
  3
+
  4
+    class Joyent
  5
+      class Real
  6
+
  7
+        # https://us-west-1.api.joyentcloud.com/docs#DeleteAllMachineMetadata
  8
+        def delete_machine_metadata(machine_id, key)
  9
+          request(
  10
+            :method => "DELETE",
  11
+            :path => "/my/machines/#{machine_id}/metadata/#{key}",
  12
+            :expects => [200, 204]
  13
+          )
  14
+        end
  15
+      end
  16
+    end
  17
+  end
  18
+end
21  lib/fog/joyent/requests/compute/delete_machine_snapshot.rb
... ...
@@ -0,0 +1,21 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+
  5
+      class Real
  6
+        def delete_machine_snapshot(machine_id, snapshot)
  7
+          request(
  8
+            :method => "DELETE",
  9
+            :path => "/my/machines/#{machine_id}/snapshots/#{snapshot}",
  10
+            :expects => [200, 204]
  11
+          )
  12
+        end
  13
+      end
  14
+
  15
+      class Mock
  16
+
  17
+      end
  18
+
  19
+    end
  20
+  end
  21
+end
15  lib/fog/joyent/requests/compute/delete_machine_tag.rb
... ...
@@ -0,0 +1,15 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Real
  5
+        def delete_machine_tag(machine_id, tagname)
  6
+          request(
  7
+            :path => "/my/machines/#{machine_id}/tags/#{tagname}",
  8
+            :method => "DELETE",
  9
+            :expects => 204
  10
+          )
  11
+        end
  12
+      end
  13
+    end
  14
+  end
  15
+end
28  lib/fog/joyent/requests/compute/get_dataset.rb
... ...
@@ -0,0 +1,28 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+
  5
+      class Mock
  6
+        def get_dataset(id)
  7
+          if ds = self.data[:datasets][id]
  8
+            res = Excon::Response.new
  9
+            res.status = 200
  10
+            res.body = ds
  11
+          else
  12
+            raise Excon::Errors::NotFound
  13
+          end
  14
+        end
  15
+      end
  16
+
  17
+      class Real
  18
+        def get_dataset
  19
+          request(
  20
+            :method => "GET",
  21
+            :path => "/my/datasets"
  22
+          )
  23
+        end
  24
+      end
  25
+
  26
+    end
  27
+  end
  28
+end
29  lib/fog/joyent/requests/compute/get_key.rb
... ...
@@ -0,0 +1,29 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+      class Mock
  5
+
  6
+        def get_key(keyid)
  7
+          if key = self.data[:keys][keyid]
  8
+            response = Excon::Response.new
  9
+            response.status = 200
  10
+            response.body = key
  11
+            response
  12
+          else
  13
+            raise Excon::Errors::NotFound
  14
+          end
  15
+        end
  16
+      end
  17
+
  18
+      class Real
  19
+        def get_key(keyid)
  20
+          request(
  21
+            :method => "GET",
  22
+            :path => "/my/keys/#{keyid}",
  23
+            :expects => 200
  24
+          )
  25
+        end
  26
+      end
  27
+    end
  28
+  end
  29
+end
29  lib/fog/joyent/requests/compute/get_machine.rb
... ...
@@ -0,0 +1,29 @@
  1
+module Fog
  2
+  module Compute
  3
+    class Joyent
  4
+
  5
+      class Mock
  6
+        def get_machine(uuid)
  7
+          if machine = self.data[:machines][uuid]
  8
+            res = Excon::Response.new
  9
+            res.status = 200
  10
+            res.body = machine