raca 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.markdown +28 -3
- data/Rakefile +14 -1
- data/lib/raca/account.rb +66 -10
- data/lib/raca/container.rb +68 -26
- data/lib/raca/containers.rb +32 -4
- data/lib/raca/errors.rb +17 -0
- data/lib/raca/server.rb +31 -129
- data/lib/raca/servers.rb +186 -1
- data/lib/raca.rb +2 -0
- metadata +18 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 0f887cfa6d624450eb2805122c998cc29acc0b01
         | 
| 4 | 
            +
              data.tar.gz: 89109fe3d3b728239a0ab9795d24508b96bff2ae
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 65b7f065c88ec68907b6580fc65b1ed05187988807a450bfee8dff001d2bb0c2ac552b973bdc188567edd0bbcc1bb12ef5ca2de9e732dd9713df64220d8f8c80
         | 
| 7 | 
            +
              data.tar.gz: ce61916a750f740b82b569be8913eaa806dffab65e3ad34013d49702ceaca9aa63ab7a01333a277adf51dc674427984c2fb534fc8f6cde915438ad1ce5007df8
         | 
    
        data/README.markdown
    CHANGED
    
    | @@ -113,9 +113,34 @@ is the temp URL key that can be set using Raca::Containers#set_temp_url_key | |
| 113 113 |  | 
| 114 114 | 
             
            ### Cloud Servers
         | 
| 115 115 |  | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 116 | 
            +
            Using an existing Raca::Account object, retrieve a collection of Cloud Servers
         | 
| 117 | 
            +
            from a region like so:
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                ord_servers = account.servers(:ord)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
            You can retrieve a existing server from the collection:
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                a_server = ord_servers.get("server_name")
         | 
| 124 | 
            +
             | 
| 125 | 
            +
            Retrieve some details on the server:
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                put a_server.metadata
         | 
| 128 | 
            +
             | 
| 129 | 
            +
            You can use the collection to create a brand new server:
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                a_server = ord_servers.create("server_name", "1Gb", "Ubuntu 10.04 LTS")
         | 
| 132 | 
            +
             | 
| 133 | 
            +
            ## General API principles
         | 
| 134 | 
            +
             | 
| 135 | 
            +
            Methods that make calls to an API should never return a raw HTTP response
         | 
| 136 | 
            +
            object. If a sensible return value is expected (retrieving metadata, listing
         | 
| 137 | 
            +
            matches, etc) then that should always be returned. If return value isn't obvious
         | 
| 138 | 
            +
            (change remote state, deleting an object, etc) then a simple boolean or similar
         | 
| 139 | 
            +
            should be returned to indicate success.
         | 
| 140 | 
            +
             | 
| 141 | 
            +
            If an unexpected error occurs (a network timeout, a 500, etc) then an exception
         | 
| 142 | 
            +
            should be raised.
         | 
| 143 | 
            +
             | 
| 119 144 |  | 
| 120 145 | 
             
            ## Why not fog?
         | 
| 121 146 |  | 
    
        data/Rakefile
    CHANGED
    
    | @@ -1,9 +1,22 @@ | |
| 1 1 | 
             
            require 'rspec/core/rake_task'
         | 
| 2 | 
            +
            require "cane/rake_task"
         | 
| 2 3 |  | 
| 3 | 
            -
            task default: :spec
         | 
| 4 | 
            +
            task default: [:cane, :spec]
         | 
| 4 5 |  | 
| 5 6 | 
             
            desc "Run all rspec files"
         | 
| 6 7 | 
             
            RSpec::Core::RakeTask.new("spec") do |t|
         | 
| 7 8 | 
             
              t.rspec_opts  = ["--color", "--format progress"]
         | 
| 8 9 | 
             
              t.ruby_opts = "-w"
         | 
| 9 10 | 
             
            end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            desc "Run cane to check quality metrics"
         | 
| 13 | 
            +
            Cane::RakeTask.new(:cane) do |cane|
         | 
| 14 | 
            +
              # keep the ABC complexity of methods to something reasonable
         | 
| 15 | 
            +
              cane.abc_max = 15
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              # keep line lengths to something that fit into a reasonable split terminal
         | 
| 18 | 
            +
              cane.style_measure = 148
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              # 0 is the goal
         | 
| 21 | 
            +
              cane.max_violations = 3
         | 
| 22 | 
            +
            end
         | 
    
        data/lib/raca/account.rb
    CHANGED
    
    | @@ -3,12 +3,9 @@ require 'json' | |
| 3 3 |  | 
| 4 4 | 
             
            module Raca
         | 
| 5 5 |  | 
| 6 | 
            -
              #  | 
| 7 | 
            -
              #  | 
| 8 | 
            -
              #  | 
| 9 | 
            -
              #
         | 
| 10 | 
            -
              # This class caches these settings so we don't have to continually use our
         | 
| 11 | 
            -
              # username/key to retrieve them.
         | 
| 6 | 
            +
              # This is your entrypoint to the rackspace API. Start by creating a
         | 
| 7 | 
            +
              # Raca::Account object and then use the instance method to access each of
         | 
| 8 | 
            +
              # the supported rackspace APIs.
         | 
| 12 9 | 
             
              #
         | 
| 13 10 | 
             
              class Account
         | 
| 14 11 |  | 
| @@ -21,10 +18,25 @@ module Raca | |
| 21 18 | 
             
                  end
         | 
| 22 19 | 
             
                end
         | 
| 23 20 |  | 
| 21 | 
            +
                # Return the temporary token that should be used when making further API
         | 
| 22 | 
            +
                # requests.
         | 
| 23 | 
            +
                #
         | 
| 24 | 
            +
                #     account = Raca::Account.new("username", "secret")
         | 
| 25 | 
            +
                #     puts account.auth_token
         | 
| 26 | 
            +
                #
         | 
| 24 27 | 
             
                def auth_token
         | 
| 25 | 
            -
                  extract_value( | 
| 28 | 
            +
                  extract_value(identity_data, "access", "token", "id")
         | 
| 26 29 | 
             
                end
         | 
| 27 30 |  | 
| 31 | 
            +
                # Return the public API URL for a particular rackspace service.
         | 
| 32 | 
            +
                #
         | 
| 33 | 
            +
                # Use Account#service_names to see a list of valid service_name's for this.
         | 
| 34 | 
            +
                #
         | 
| 35 | 
            +
                # Check the project README for an updated list of the available regions.
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                #     account = Raca::Account.new("username", "secret")
         | 
| 38 | 
            +
                #     puts account.public_endpoint("cloudServers", :syd)
         | 
| 39 | 
            +
                #
         | 
| 28 40 | 
             
                def public_endpoint(service_name, region)
         | 
| 29 41 | 
             
                  region = region.to_s.upcase
         | 
| 30 42 | 
             
                  endpoints = service_endpoints(service_name)
         | 
| @@ -32,14 +44,45 @@ module Raca | |
| 32 44 | 
             
                  regional_endpoint["publicURL"]
         | 
| 33 45 | 
             
                end
         | 
| 34 46 |  | 
| 47 | 
            +
                # Return the names of the available services. As rackspace add new services and
         | 
| 48 | 
            +
                # APIs they should appear here.
         | 
| 49 | 
            +
                #
         | 
| 50 | 
            +
                # Any name returned from here can be passe to #public_endpoint to get the API
         | 
| 51 | 
            +
                # endpoint for that service
         | 
| 52 | 
            +
                #
         | 
| 53 | 
            +
                #     account = Raca::Account.new("username", "secret")
         | 
| 54 | 
            +
                #     puts account.service_names
         | 
| 55 | 
            +
                #
         | 
| 56 | 
            +
                def service_names
         | 
| 57 | 
            +
                  catalog = extract_value(identity_data, "access", "serviceCatalog") || {}
         | 
| 58 | 
            +
                  catalog.map { |service|
         | 
| 59 | 
            +
                    service["name"]
         | 
| 60 | 
            +
                  }
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                # Return a Raca::Containers object for a region. Use this to interact with the
         | 
| 64 | 
            +
                # cloud files service.
         | 
| 65 | 
            +
                #
         | 
| 66 | 
            +
                #     account = Raca::Account.new("username", "secret")
         | 
| 67 | 
            +
                #     puts account.containers(:ord)
         | 
| 68 | 
            +
                #
         | 
| 35 69 | 
             
                def containers(region)
         | 
| 36 70 | 
             
                  Raca::Containers.new(self, region)
         | 
| 37 71 | 
             
                end
         | 
| 38 72 |  | 
| 73 | 
            +
                # Return a Raca::Containers object for a region. Use this to interact with the
         | 
| 74 | 
            +
                # next gen cloud servers service.
         | 
| 75 | 
            +
                #
         | 
| 76 | 
            +
                #     account = Raca::Account.new("username", "secret")
         | 
| 77 | 
            +
                #     puts account.servers(:ord)
         | 
| 78 | 
            +
                #
         | 
| 39 79 | 
             
                def servers(region)
         | 
| 40 80 | 
             
                  Raca::Servers.new(self, region)
         | 
| 41 81 | 
             
                end
         | 
| 42 82 |  | 
| 83 | 
            +
                # Raca classes use this method to occasionally re-authenticate with the rackspace
         | 
| 84 | 
            +
                # servers. You can probable ignore it.
         | 
| 85 | 
            +
                #
         | 
| 43 86 | 
             
                def refresh_cache
         | 
| 44 87 | 
             
                  Net::HTTP.new('identity.api.rackspacecloud.com', 443).tap {|http|
         | 
| 45 88 | 
             
                    http.use_ssl = true
         | 
| @@ -57,14 +100,27 @@ module Raca | |
| 57 100 | 
             
                      JSON.dump(payload),
         | 
| 58 101 | 
             
                      {'Content-Type' => 'application/json'},
         | 
| 59 102 | 
             
                    )
         | 
| 60 | 
            -
                    if response.is_a? | 
| 103 | 
            +
                    if response.is_a?(Net::HTTPSuccess)
         | 
| 61 104 | 
             
                      cache_write(cache_key, JSON.load(response.body))
         | 
| 105 | 
            +
                    else
         | 
| 106 | 
            +
                      raise_on_error(response)
         | 
| 62 107 | 
             
                    end
         | 
| 63 108 | 
             
                  }
         | 
| 64 109 | 
             
                end
         | 
| 65 110 |  | 
| 66 111 | 
             
                private
         | 
| 67 112 |  | 
| 113 | 
            +
                def raise_on_error(response)
         | 
| 114 | 
            +
                  error_klass = case response.code.to_i
         | 
| 115 | 
            +
                  when 400 then BadRequestError
         | 
| 116 | 
            +
                  when 404 then NotFoundError
         | 
| 117 | 
            +
                  when 500 then ServerError
         | 
| 118 | 
            +
                  else
         | 
| 119 | 
            +
                    HTTPError
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
                  raise error_klass, "Rackspace returned HTTP status #{response.code}"
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 68 124 | 
             
                # This method is opaque, but it was the best I could come up with using just
         | 
| 69 125 | 
             
                # the standard library. Sorry.
         | 
| 70 126 | 
             
                #
         | 
| @@ -94,7 +150,7 @@ module Raca | |
| 94 150 | 
             
                # cloud servers, dns, etc)
         | 
| 95 151 | 
             
                #
         | 
| 96 152 | 
             
                def service_endpoints(service_name)
         | 
| 97 | 
            -
                  catalog = extract_value( | 
| 153 | 
            +
                  catalog = extract_value(identity_data, "access", "serviceCatalog") || {}
         | 
| 98 154 | 
             
                  service = catalog.detect { |s| s["name"] == service_name } || {}
         | 
| 99 155 | 
             
                  service["endpoints"] || []
         | 
| 100 156 | 
             
                end
         | 
| @@ -115,7 +171,7 @@ module Raca | |
| 115 171 | 
             
                  end
         | 
| 116 172 | 
             
                end
         | 
| 117 173 |  | 
| 118 | 
            -
                def  | 
| 174 | 
            +
                def identity_data
         | 
| 119 175 | 
             
                  refresh_cache unless cache_read(cache_key)
         | 
| 120 176 |  | 
| 121 177 | 
             
                  cache_read(cache_key) || {}
         | 
    
        data/lib/raca/container.rb
    CHANGED
    
    | @@ -4,13 +4,18 @@ require 'openssl' | |
| 4 4 | 
             
            require 'uri'
         | 
| 5 5 |  | 
| 6 6 | 
             
            module Raca
         | 
| 7 | 
            -
             | 
| 8 | 
            -
              #  | 
| 9 | 
            -
              #  | 
| 7 | 
            +
             | 
| 8 | 
            +
              # Represents a single cloud files container. Contains methods for uploading,
         | 
| 9 | 
            +
              # downloading, collecting stats, listing files, etc.
         | 
| 10 | 
            +
              #
         | 
| 11 | 
            +
              # You probably don't want to instantiate this directly,
         | 
| 12 | 
            +
              # see Raca::Account#containers
         | 
| 13 | 
            +
              #
         | 
| 10 14 | 
             
              class Container
         | 
| 11 15 | 
             
                MAX_ITEMS_PER_LIST = 10_000
         | 
| 12 16 | 
             
                LARGE_FILE_THRESHOLD = 5_368_709_120 # 5 Gb
         | 
| 13 17 | 
             
                LARGE_FILE_SEGMENT_SIZE = 104_857_600 # 100 Mb
         | 
| 18 | 
            +
                RETRY_PAUSE = 5
         | 
| 14 19 |  | 
| 15 20 | 
             
                attr_reader :container_name
         | 
| 16 21 |  | 
| @@ -24,6 +29,7 @@ module Raca | |
| 24 29 | 
             
                end
         | 
| 25 30 |  | 
| 26 31 | 
             
                # Upload data_or_path (which may be a filename or an IO) to the container, as key.
         | 
| 32 | 
            +
                #
         | 
| 27 33 | 
             
                def upload(key, data_or_path)
         | 
| 28 34 | 
             
                  case data_or_path
         | 
| 29 35 | 
             
                  when StringIO, File
         | 
| @@ -39,9 +45,11 @@ module Raca | |
| 39 45 |  | 
| 40 46 | 
             
                # Delete +key+ from the container. If the container is on the CDN, the object will
         | 
| 41 47 | 
             
                # still be served from the CDN until the TTL expires.
         | 
| 48 | 
            +
                #
         | 
| 42 49 | 
             
                def delete(key)
         | 
| 43 50 | 
             
                  log "deleting #{key} from #{container_path}"
         | 
| 44 | 
            -
                  storage_request(Net::HTTP::Delete.new(File.join(container_path, key)))
         | 
| 51 | 
            +
                  response = storage_request(Net::HTTP::Delete.new(File.join(container_path, key)))
         | 
| 52 | 
            +
                  (200..299).cover?(response.code.to_i)
         | 
| 45 53 | 
             
                end
         | 
| 46 54 |  | 
| 47 55 | 
             
                # Remove +key+ from the CDN edge nodes on which it is currently cached. The object is
         | 
| @@ -50,12 +58,14 @@ module Raca | |
| 50 58 | 
             
                #
         | 
| 51 59 | 
             
                # This shouldn't be used except when it's really required (e.g. when a piece has to be
         | 
| 52 60 | 
             
                # taken down) because it's expensive: it lodges a support ticket at Akamai. (!)
         | 
| 61 | 
            +
                #
         | 
| 53 62 | 
             
                def purge_from_akamai(key, email_address)
         | 
| 54 63 | 
             
                  log "Requesting #{File.join(container_path, key)} to be purged from the CDN"
         | 
| 55 | 
            -
                  cdn_request(Net::HTTP::Delete.new(
         | 
| 64 | 
            +
                  response = cdn_request(Net::HTTP::Delete.new(
         | 
| 56 65 | 
             
                    File.join(container_path, key),
         | 
| 57 66 | 
             
                    'X-Purge-Email' => email_address
         | 
| 58 67 | 
             
                  ))
         | 
| 68 | 
            +
                  (200..299).cover?(response.code.to_i)
         | 
| 59 69 | 
             
                end
         | 
| 60 70 |  | 
| 61 71 | 
             
                # Returns some metadata about a single object in this container.
         | 
| @@ -71,15 +81,20 @@ module Raca | |
| 71 81 | 
             
                  }
         | 
| 72 82 | 
             
                end
         | 
| 73 83 |  | 
| 84 | 
            +
                # Download the object at key into a local file at filepath.
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                # Returns the number of downloaded bytes.
         | 
| 87 | 
            +
                #
         | 
| 74 88 | 
             
                def download(key, filepath)
         | 
| 75 89 | 
             
                  log "downloading #{key} from #{container_path}"
         | 
| 76 | 
            -
                  storage_request(Net::HTTP::Get.new(File.join(container_path, key))) do |response|
         | 
| 90 | 
            +
                  response = storage_request(Net::HTTP::Get.new(File.join(container_path, key))) do |response|
         | 
| 77 91 | 
             
                    File.open(filepath, 'wb') do |io|
         | 
| 78 92 | 
             
                      response.read_body do |chunk|
         | 
| 79 93 | 
             
                        io.write(chunk)
         | 
| 80 94 | 
             
                      end
         | 
| 81 95 | 
             
                    end
         | 
| 82 96 | 
             
                  end
         | 
| 97 | 
            +
                  response["Content-Length"].to_i
         | 
| 83 98 | 
             
                end
         | 
| 84 99 |  | 
| 85 100 | 
             
                # Return an array of files in the container.
         | 
| @@ -89,6 +104,7 @@ module Raca | |
| 89 104 | 
             
                # max - the maximum number of items to return
         | 
| 90 105 | 
             
                # marker - return items alphabetically after this key. Useful for pagination
         | 
| 91 106 | 
             
                # prefix - only return items that start with this string
         | 
| 107 | 
            +
                #
         | 
| 92 108 | 
             
                def list(options = {})
         | 
| 93 109 | 
             
                  max = options.fetch(:max, MAX_ITEMS_PER_LIST)
         | 
| 94 110 | 
             
                  marker = options.fetch(:marker, nil)
         | 
| @@ -112,11 +128,18 @@ module Raca | |
| 112 128 | 
             
                  }
         | 
| 113 129 | 
             
                end
         | 
| 114 130 |  | 
| 131 | 
            +
                # Returns an array of object keys that start with prefix. This is a convenience
         | 
| 132 | 
            +
                # method that is equivilant to:
         | 
| 133 | 
            +
                #
         | 
| 134 | 
            +
                #     container.list(prefix: "foo/bar/")
         | 
| 135 | 
            +
                #
         | 
| 115 136 | 
             
                def search(prefix)
         | 
| 116 137 | 
             
                  log "retrieving container listing from #{container_path} items starting with #{prefix}"
         | 
| 117 138 | 
             
                  list(prefix: prefix)
         | 
| 118 139 | 
             
                end
         | 
| 119 140 |  | 
| 141 | 
            +
                # Return some basic stats on the current container.
         | 
| 142 | 
            +
                #
         | 
| 120 143 | 
             
                def metadata
         | 
| 121 144 | 
             
                  log "retrieving container metadata from #{container_path}"
         | 
| 122 145 | 
             
                  response = storage_request(Net::HTTP::Head.new(container_path))
         | 
| @@ -126,6 +149,9 @@ module Raca | |
| 126 149 | 
             
                  }
         | 
| 127 150 | 
             
                end
         | 
| 128 151 |  | 
| 152 | 
            +
                # Return the key details for CDN access to this container. Can be called
         | 
| 153 | 
            +
                # on non CDN enabled containers, but the details won't make much sense.
         | 
| 154 | 
            +
                #
         | 
| 129 155 | 
             
                def cdn_metadata
         | 
| 130 156 | 
             
                  log "retrieving container CDN metadata from #{container_path}"
         | 
| 131 157 | 
             
                  response = cdn_request(Net::HTTP::Head.new(container_path))
         | 
| @@ -143,10 +169,13 @@ module Raca | |
| 143 169 | 
             
                # via the CDN. CDN enabling can be done via the web UI but only with a TTL of 72 hours.
         | 
| 144 170 | 
             
                # Using the API it's possible to set a TTL of 50 years.
         | 
| 145 171 | 
             
                #
         | 
| 146 | 
            -
                 | 
| 172 | 
            +
                # TTL is defined in seconds, default is 72 hours.
         | 
| 173 | 
            +
                #
         | 
| 174 | 
            +
                def cdn_enable(ttl = 259200)
         | 
| 147 175 | 
             
                  log "enabling CDN access to #{container_path} with a cache expiry of #{ttl / 60} minutes"
         | 
| 148 176 |  | 
| 149 | 
            -
                  cdn_request | 
| 177 | 
            +
                  response = cdn_request(Net::HTTP::Put.new(container_path, "X-TTL" => ttl.to_i.to_s))
         | 
| 178 | 
            +
                  (200..299).cover?(response.code.to_i)
         | 
| 150 179 | 
             
                end
         | 
| 151 180 |  | 
| 152 181 | 
             
                # Generate a expiring URL for a file that is otherwise private. useful for providing temporary
         | 
| @@ -184,8 +213,8 @@ module Raca | |
| 184 213 | 
             
                  if io.respond_to?(:path)
         | 
| 185 214 | 
             
                    headers['Content-Type'] ||= extension_content_type(io.path)
         | 
| 186 215 | 
             
                    headers['Content-Type'] ||= file_content_type(io.path)
         | 
| 187 | 
            -
                    headers['Etag']           = md5(io.path)
         | 
| 188 216 | 
             
                  end
         | 
| 217 | 
            +
                    headers['Etag']           = md5_io(io)
         | 
| 189 218 | 
             
                  headers['Content-Type']   ||= "application/octet-stream"
         | 
| 190 219 | 
             
                  if content_type_needs_cors(key)
         | 
| 191 220 | 
             
                    headers['Access-Control-Allow-Origin'] = "*"
         | 
| @@ -196,7 +225,8 @@ module Raca | |
| 196 225 | 
             
                  request = Net::HTTP::Put.new(full_path, headers)
         | 
| 197 226 | 
             
                  request.body_stream = io
         | 
| 198 227 | 
             
                  request.content_length = byte_count
         | 
| 199 | 
            -
                  storage_request(request)
         | 
| 228 | 
            +
                  response = storage_request(request)
         | 
| 229 | 
            +
                  response['ETag']
         | 
| 200 230 | 
             
                end
         | 
| 201 231 |  | 
| 202 232 | 
             
                def upload_io_large(key, io, byte_count)
         | 
| @@ -207,8 +237,8 @@ module Raca | |
| 207 237 | 
             
                    segment_key = "%s.%03d" % [key, segments.size]
         | 
| 208 238 | 
             
                    io.seek(start_pos)
         | 
| 209 239 | 
             
                    segment_io = StringIO.new(io.read(LARGE_FILE_SEGMENT_SIZE))
         | 
| 210 | 
            -
                     | 
| 211 | 
            -
                    segments << {path: "#{@container_name}/#{segment_key}", etag:  | 
| 240 | 
            +
                    etag = upload_io_standard(segment_key, segment_io, segment_io.size)
         | 
| 241 | 
            +
                    segments << {path: "#{@container_name}/#{segment_key}", etag: etag, size_bytes: segment_io.size}
         | 
| 212 242 | 
             
                  end
         | 
| 213 243 | 
             
                  manifest_key = "#{key}?multipart-manifest=put"
         | 
| 214 244 | 
             
                  manifest_body = StringIO.new(JSON.dump(segments))
         | 
| @@ -230,14 +260,12 @@ module Raca | |
| 230 260 | 
             
                  end
         | 
| 231 261 | 
             
                rescue Timeout::Error
         | 
| 232 262 | 
             
                  if retries >= 3
         | 
| 233 | 
            -
                    raise "Timeout from Rackspace  | 
| 263 | 
            +
                    raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
         | 
| 234 264 | 
             
                  end
         | 
| 235 265 |  | 
| 236 | 
            -
                   | 
| 237 | 
            -
             | 
| 238 | 
            -
             | 
| 239 | 
            -
                    sleep(retry_interval)
         | 
| 240 | 
            -
                  end
         | 
| 266 | 
            +
                  retry_interval = RETRY_PAUSE + (retries.to_i * RETRY_PAUSE) # Retry after 5, 10, 15 and 20 seconds
         | 
| 267 | 
            +
                  log "Rackspace timed out: retrying after #{retry_interval}s"
         | 
| 268 | 
            +
                  sleep(retry_interval)
         | 
| 241 269 |  | 
| 242 270 | 
             
                  cloud_request(request, hostname, retries + 1, &block)
         | 
| 243 271 | 
             
                end
         | 
| @@ -253,11 +281,25 @@ module Raca | |
| 253 281 | 
             
                      @account.refresh_cache
         | 
| 254 282 | 
             
                      response = block.call http
         | 
| 255 283 | 
             
                    end
         | 
| 256 | 
            -
                     | 
| 257 | 
            -
             | 
| 284 | 
            +
                    if response.is_a?(Net::HTTPSuccess)
         | 
| 285 | 
            +
                      response
         | 
| 286 | 
            +
                    else
         | 
| 287 | 
            +
                      raise_on_error(response)
         | 
| 288 | 
            +
                    end
         | 
| 258 289 | 
             
                  end
         | 
| 259 290 | 
             
                end
         | 
| 260 291 |  | 
| 292 | 
            +
                def raise_on_error(response)
         | 
| 293 | 
            +
                  error_klass = case response.code.to_i
         | 
| 294 | 
            +
                  when 400 then BadRequestError
         | 
| 295 | 
            +
                  when 404 then NotFoundError
         | 
| 296 | 
            +
                  when 500 then ServerError
         | 
| 297 | 
            +
                  else
         | 
| 298 | 
            +
                    HTTPError
         | 
| 299 | 
            +
                  end
         | 
| 300 | 
            +
                  raise error_klass, "Rackspace returned HTTP status #{response.code}"
         | 
| 301 | 
            +
                end
         | 
| 302 | 
            +
             | 
| 261 303 | 
             
                def log(msg)
         | 
| 262 304 | 
             
                  if @logger.respond_to?(:debug)
         | 
| 263 305 | 
             
                    @logger.debug msg
         | 
| @@ -308,14 +350,14 @@ module Raca | |
| 308 350 | 
             
                  [".eot",".ttf",".woff"].include?(File.extname(path))
         | 
| 309 351 | 
             
                end
         | 
| 310 352 |  | 
| 311 | 
            -
                def  | 
| 353 | 
            +
                def md5_io(io)
         | 
| 354 | 
            +
                  io.seek(0)
         | 
| 312 355 | 
             
                  digest = Digest::MD5.new
         | 
| 313 | 
            -
                   | 
| 314 | 
            -
             | 
| 315 | 
            -
                     | 
| 316 | 
            -
                      digest << chunk
         | 
| 317 | 
            -
                    end
         | 
| 356 | 
            +
                  # read in 128K chunks
         | 
| 357 | 
            +
                  io.each(1024 * 128) do |chunk|
         | 
| 358 | 
            +
                    digest << chunk
         | 
| 318 359 | 
             
                  end
         | 
| 360 | 
            +
                  io.seek(0)
         | 
| 319 361 | 
             
                  digest.hexdigest
         | 
| 320 362 | 
             
                end
         | 
| 321 363 | 
             
              end
         | 
    
        data/lib/raca/containers.rb
    CHANGED
    
    | @@ -1,4 +1,12 @@ | |
| 1 1 | 
             
            module Raca
         | 
| 2 | 
            +
              # Represents a collection of cloud files containers within a single region.
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # There's a handful of methods that relate to the entire collection, but this
         | 
| 5 | 
            +
              # is primarily used to retrieve a single Raca::Container object.
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # You probably don't want to instantiate this directly,
         | 
| 8 | 
            +
              # see Raca::Account#containers
         | 
| 9 | 
            +
              #
         | 
| 2 10 | 
             
              class Containers
         | 
| 3 11 | 
             
                def initialize(account, region, opts = {})
         | 
| 4 12 | 
             
                  @account, @region = account, region
         | 
| @@ -23,14 +31,19 @@ module Raca | |
| 23 31 | 
             
                  }
         | 
| 24 32 | 
             
                end
         | 
| 25 33 |  | 
| 26 | 
            -
                # Set the secret key that will be used to generate expiring URLs for all cloud | 
| 34 | 
            +
                # Set the secret key that will be used to generate expiring URLs for all cloud
         | 
| 35 | 
            +
                # files containers on the current account. This value should be passed to the
         | 
| 36 | 
            +
                # expiring_url() method.
         | 
| 27 37 | 
             
                #
         | 
| 28 | 
            -
                # Use this with caution, this will invalidate all previously generated expiring | 
| 38 | 
            +
                # Use this with caution, this will invalidate all previously generated expiring
         | 
| 39 | 
            +
                # URLS *FOR THE ENTIRE ACCOUNT*
         | 
| 29 40 | 
             
                #
         | 
| 30 41 | 
             
                def set_temp_url_key(secret)
         | 
| 31 42 | 
             
                  log "setting Account Temp URL Key on #{storage_path}"
         | 
| 32 43 |  | 
| 33 | 
            -
                   | 
| 44 | 
            +
                  request = Net::HTTP::Post.new(storage_path, "X-Account-Meta-Temp-Url-Key" => secret.to_s)
         | 
| 45 | 
            +
                  response = storage_request(request)
         | 
| 46 | 
            +
                  (200..299).cover?(response.code.to_i)
         | 
| 34 47 | 
             
                end
         | 
| 35 48 |  | 
| 36 49 | 
             
                private
         | 
| @@ -65,11 +78,26 @@ module Raca | |
| 65 78 | 
             
                      @account.refresh_cache
         | 
| 66 79 | 
             
                      response = block.call http
         | 
| 67 80 | 
             
                    end
         | 
| 68 | 
            -
                     | 
| 81 | 
            +
                    if response.is_a?(Net::HTTPSuccess)
         | 
| 82 | 
            +
                      response
         | 
| 83 | 
            +
                    else
         | 
| 84 | 
            +
                      raise_on_error(response)
         | 
| 85 | 
            +
                    end
         | 
| 69 86 | 
             
                    response
         | 
| 70 87 | 
             
                  end
         | 
| 71 88 | 
             
                end
         | 
| 72 89 |  | 
| 90 | 
            +
                def raise_on_error(response)
         | 
| 91 | 
            +
                  error_klass = case response.code.to_i
         | 
| 92 | 
            +
                  when 400 then BadRequestError
         | 
| 93 | 
            +
                  when 404 then NotFoundError
         | 
| 94 | 
            +
                  when 500 then ServerError
         | 
| 95 | 
            +
                  else
         | 
| 96 | 
            +
                    HTTPError
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                  raise error_klass, "Rackspace returned HTTP status #{response.code}"
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 73 101 | 
             
                def log(msg)
         | 
| 74 102 | 
             
                  if @logger.respond_to?(:debug)
         | 
| 75 103 | 
             
                    @logger.debug msg
         | 
    
        data/lib/raca/errors.rb
    ADDED
    
    | @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module Raca
         | 
| 2 | 
            +
              # base error for unexpected HTTP responses from rackspace
         | 
| 3 | 
            +
              class HTTPError < RuntimeError; end
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              # for 400 responses from rackspace
         | 
| 6 | 
            +
              class BadRequestError < HTTPError; end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              # for 404 responses from rackspace
         | 
| 9 | 
            +
              class NotFoundError < HTTPError; end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              # for 500 responses from rackspace
         | 
| 12 | 
            +
              class ServerError < HTTPError; end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              # for rackspace timeouts
         | 
| 15 | 
            +
              class TimeoutError < RuntimeError; end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            end
         | 
    
        data/lib/raca/server.rb
    CHANGED
    
    | @@ -3,69 +3,24 @@ require 'base64' | |
| 3 3 | 
             
            require 'net/http'
         | 
| 4 4 |  | 
| 5 5 | 
             
            module Raca
         | 
| 6 | 
            -
              #  | 
| 7 | 
            -
              #  | 
| 8 | 
            -
              # | 
| 6 | 
            +
              # Represents a single cloud server. Contains methods for deleting a server,
         | 
| 7 | 
            +
              # listing IP addresses, checking the state, etc.
         | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              # You probably don't want to instantiate this directly,
         | 
| 10 | 
            +
              # see Raca::Account#servers
         | 
| 11 | 
            +
              #
         | 
| 9 12 | 
             
              class Server
         | 
| 10 13 |  | 
| 11 | 
            -
                attr_reader : | 
| 14 | 
            +
                attr_reader :server_id
         | 
| 12 15 |  | 
| 13 | 
            -
                def initialize(account, region,  | 
| 16 | 
            +
                def initialize(account, region, server_id)
         | 
| 14 17 | 
             
                  @account = account
         | 
| 15 18 | 
             
                  @region = region
         | 
| 16 19 | 
             
                  @servers_url = @account.public_endpoint("cloudServersOpenStack", region)
         | 
| 17 | 
            -
                  @ | 
| 18 | 
            -
                  @server_id = find_server_id(server_name)
         | 
| 19 | 
            -
                end
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                # return true if this server exists on Rackspace
         | 
| 22 | 
            -
                #
         | 
| 23 | 
            -
                def exists?
         | 
| 24 | 
            -
                  @server_id != nil
         | 
| 25 | 
            -
                end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                # create this server on Rackspace.
         | 
| 28 | 
            -
                #
         | 
| 29 | 
            -
                # flavor_name is a string that describes the amount of RAM. If you enter
         | 
| 30 | 
            -
                # an invalid option a list of valid options will be raised.
         | 
| 31 | 
            -
                #
         | 
| 32 | 
            -
                # image_name is a string that describes the OS image to use. If you enter
         | 
| 33 | 
            -
                # an invalid option a list of valid options will be raised. I suggest
         | 
| 34 | 
            -
                # starting with 'Ubuntu 10.04 LTS'
         | 
| 35 | 
            -
                #
         | 
| 36 | 
            -
                # files is an optional Hash of path to blobs. Use it to place a file on the
         | 
| 37 | 
            -
                # disk of the new server.
         | 
| 38 | 
            -
                #
         | 
| 39 | 
            -
                # Use it like this:
         | 
| 40 | 
            -
                #
         | 
| 41 | 
            -
                #     server.create(512, "Ubuntu 10.04 LTS", "/root/.ssh/authorised_keys" => File.read("/foo"))
         | 
| 42 | 
            -
                #
         | 
| 43 | 
            -
                def create(flavor_name, image_name, files = {})
         | 
| 44 | 
            -
                  raise ArgumentError, "server already exists" if exists?
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  request = {
         | 
| 47 | 
            -
                    "server" => {
         | 
| 48 | 
            -
                      "name" => @server_name,
         | 
| 49 | 
            -
                      "imageRef" => image_name_to_id(image_name),
         | 
| 50 | 
            -
                      "flavorRef" => flavor_name_to_id(flavor_name),
         | 
| 51 | 
            -
                    }
         | 
| 52 | 
            -
                  }
         | 
| 53 | 
            -
                  files.each do |path, blob|
         | 
| 54 | 
            -
                    request['server']['personality'] ||= []
         | 
| 55 | 
            -
                    request['server']['personality'] << {
         | 
| 56 | 
            -
                      'path' => path,
         | 
| 57 | 
            -
                      'contents' => Base64.encode64(blob)
         | 
| 58 | 
            -
                    }
         | 
| 59 | 
            -
                  end
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                  data = cloud_request(Net::HTTP::Post.new(servers_path), JSON.dump(request)).body
         | 
| 62 | 
            -
                  data = JSON.parse(data)['server']
         | 
| 63 | 
            -
                  @server_id = data['id']
         | 
| 20 | 
            +
                  @server_id = server_id
         | 
| 64 21 | 
             
                end
         | 
| 65 22 |  | 
| 66 23 | 
             
                def delete!
         | 
| 67 | 
            -
                  raise ArgumentError, "server doesn't exist" unless exists?
         | 
| 68 | 
            -
             | 
| 69 24 | 
             
                  response = cloud_request(Net::HTTP::Delete.new(server_path))
         | 
| 70 25 | 
             
                  response.is_a? Net::HTTPSuccess
         | 
| 71 26 | 
             
                end
         | 
| @@ -95,42 +50,12 @@ module Raca | |
| 95 50 | 
             
                # A Hash of various matadata about the server
         | 
| 96 51 | 
             
                #
         | 
| 97 52 | 
             
                def details
         | 
| 98 | 
            -
                  raise ArgumentError, "server doesn't exist" unless exists?
         | 
| 99 | 
            -
             | 
| 100 53 | 
             
                  data = cloud_request(Net::HTTP::Get.new(server_path)).body
         | 
| 101 54 | 
             
                  JSON.parse(data)['server']
         | 
| 102 55 | 
             
                end
         | 
| 103 56 |  | 
| 104 57 | 
             
                private
         | 
| 105 58 |  | 
| 106 | 
            -
                def list
         | 
| 107 | 
            -
                  json = cloud_request(Net::HTTP::Get.new(servers_path)).body
         | 
| 108 | 
            -
                  JSON.parse(json)['servers']
         | 
| 109 | 
            -
                end
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                def find_server_id(server_name)
         | 
| 112 | 
            -
                  server = list.detect {|row|
         | 
| 113 | 
            -
                    row["name"] == server_name
         | 
| 114 | 
            -
                  }
         | 
| 115 | 
            -
                  server ? server["id"] : nil
         | 
| 116 | 
            -
                end
         | 
| 117 | 
            -
             | 
| 118 | 
            -
                def flavors_path
         | 
| 119 | 
            -
                  @flavors_path ||= File.join(account_path, "flavors")
         | 
| 120 | 
            -
                end
         | 
| 121 | 
            -
             | 
| 122 | 
            -
                def images_path
         | 
| 123 | 
            -
                  @images_path ||= File.join(account_path, "images")
         | 
| 124 | 
            -
                end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                def server_path
         | 
| 127 | 
            -
                  @server_path ||= File.join(account_path, "servers", @server_id.to_s)
         | 
| 128 | 
            -
                end
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                def servers_path
         | 
| 131 | 
            -
                  @servers_path ||= File.join(account_path, "servers")
         | 
| 132 | 
            -
                end
         | 
| 133 | 
            -
             | 
| 134 59 | 
             
                def servers_host
         | 
| 135 60 | 
             
                  @servers_host ||= URI.parse(@servers_url).host
         | 
| 136 61 | 
             
                end
         | 
| @@ -139,48 +64,8 @@ module Raca | |
| 139 64 | 
             
                  @account_path ||= URI.parse(@servers_url).path
         | 
| 140 65 | 
             
                end
         | 
| 141 66 |  | 
| 142 | 
            -
                def  | 
| 143 | 
            -
                  @ | 
| 144 | 
            -
                    data = cloud_request(Net::HTTP::Get.new(flavors_path)).body
         | 
| 145 | 
            -
                    JSON.parse(data)['flavors']
         | 
| 146 | 
            -
                  end
         | 
| 147 | 
            -
                end
         | 
| 148 | 
            -
             | 
| 149 | 
            -
                def flavor_names
         | 
| 150 | 
            -
                  flavors.map {|row| row['name'] }
         | 
| 151 | 
            -
                end
         | 
| 152 | 
            -
             | 
| 153 | 
            -
                def flavor_name_to_id(str)
         | 
| 154 | 
            -
                  flavor = flavors.detect {|row|
         | 
| 155 | 
            -
                    row['name'].downcase.include?(str.to_s.downcase)
         | 
| 156 | 
            -
                  }
         | 
| 157 | 
            -
                  if flavor
         | 
| 158 | 
            -
                    flavor['id']
         | 
| 159 | 
            -
                  else
         | 
| 160 | 
            -
                    raise ArgumentError, "valid flavors are: #{flavor_names.join(', ')}"
         | 
| 161 | 
            -
                  end
         | 
| 162 | 
            -
                end
         | 
| 163 | 
            -
             | 
| 164 | 
            -
                def images
         | 
| 165 | 
            -
                  @images ||= begin
         | 
| 166 | 
            -
                    data = cloud_request(Net::HTTP::Get.new(images_path)).body
         | 
| 167 | 
            -
                    JSON.parse(data)['images']
         | 
| 168 | 
            -
                  end
         | 
| 169 | 
            -
                end
         | 
| 170 | 
            -
             | 
| 171 | 
            -
                def image_names
         | 
| 172 | 
            -
                  images.map {|row| row['name'] }
         | 
| 173 | 
            -
                end
         | 
| 174 | 
            -
             | 
| 175 | 
            -
                def image_name_to_id(str)
         | 
| 176 | 
            -
                  image = images.detect {|row|
         | 
| 177 | 
            -
                    row['name'].downcase.include?(str.to_s.downcase)
         | 
| 178 | 
            -
                  }
         | 
| 179 | 
            -
                  if image
         | 
| 180 | 
            -
                    image['id']
         | 
| 181 | 
            -
                  else
         | 
| 182 | 
            -
                    raise ArgumentError, "valid images are: #{image_names.join(', ')}"
         | 
| 183 | 
            -
                  end
         | 
| 67 | 
            +
                def server_path
         | 
| 68 | 
            +
                  @server_path ||= File.join(account_path, "servers", @server_id.to_s)
         | 
| 184 69 | 
             
                end
         | 
| 185 70 |  | 
| 186 71 | 
             
                def cloud_request(request, body = nil)
         | 
| @@ -202,15 +87,32 @@ module Raca | |
| 202 87 | 
             
                      @account.refresh_cache
         | 
| 203 88 | 
             
                      response = block.call http
         | 
| 204 89 | 
             
                    end
         | 
| 205 | 
            -
                     | 
| 90 | 
            +
                    if response.is_a?(Net::HTTPSuccess)
         | 
| 91 | 
            +
                      response
         | 
| 92 | 
            +
                    else
         | 
| 93 | 
            +
                      raise_on_error(response)
         | 
| 94 | 
            +
                    end
         | 
| 206 95 | 
             
                    response
         | 
| 207 96 | 
             
                  end
         | 
| 208 | 
            -
                rescue Timeout::Error | 
| 209 | 
            -
                   | 
| 97 | 
            +
                rescue Timeout::Error
         | 
| 98 | 
            +
                  if retries <= 0
         | 
| 99 | 
            +
                    raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
         | 
| 100 | 
            +
                  end
         | 
| 210 101 |  | 
| 211 102 | 
             
                  cloud_http(hostname, retries - 1, &block)
         | 
| 212 103 | 
             
                end
         | 
| 213 104 |  | 
| 105 | 
            +
                def raise_on_error(response)
         | 
| 106 | 
            +
                  error_klass = case response.code.to_i
         | 
| 107 | 
            +
                  when 400 then BadRequestError
         | 
| 108 | 
            +
                  when 404 then NotFoundError
         | 
| 109 | 
            +
                  when 500 then ServerError
         | 
| 110 | 
            +
                  else
         | 
| 111 | 
            +
                    HTTPError
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
                  raise error_klass, "Rackspace returned HTTP status #{response.code}"
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 214 116 | 
             
                def log(msg)
         | 
| 215 117 | 
             
                  if defined?(Rails)
         | 
| 216 118 | 
             
                    Rails.logger.info msg
         | 
    
        data/lib/raca/servers.rb
    CHANGED
    
    | @@ -1,11 +1,196 @@ | |
| 1 1 | 
             
            module Raca
         | 
| 2 | 
            +
              # Represents a collection of cloud servers within a single region.
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # There's currently no methods that relate to the entire collection,
         | 
| 5 | 
            +
              # this is primarily used to retrieve a single Raca::Server object.
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # You probably don't want to instantiate this directly,
         | 
| 8 | 
            +
              # see Raca::Account#servers
         | 
| 9 | 
            +
              #
         | 
| 2 10 | 
             
              class Servers
         | 
| 3 11 | 
             
                def initialize(account, region)
         | 
| 4 12 | 
             
                  @account, @region = account, region
         | 
| 13 | 
            +
                  @servers_url = @account.public_endpoint("cloudServersOpenStack", region)
         | 
| 5 14 | 
             
                end
         | 
| 6 15 |  | 
| 7 16 | 
             
                def get(server_name)
         | 
| 8 | 
            -
                   | 
| 17 | 
            +
                  server_id = find_server_id(server_name)
         | 
| 18 | 
            +
                  if server_id
         | 
| 19 | 
            +
                    Raca::Server.new(@account, @region, server_id)
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    nil
         | 
| 22 | 
            +
                  end
         | 
| 9 23 | 
             
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                # create a new server on Rackspace.
         | 
| 26 | 
            +
                #
         | 
| 27 | 
            +
                # server_name is a free text name you want to assign the server.
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # flavor_name is a string that describes the amount of RAM. If you enter
         | 
| 30 | 
            +
                # an invalid option a list of valid options will be raised.
         | 
| 31 | 
            +
                #
         | 
| 32 | 
            +
                # image_name is a string that describes the OS image to use. If you enter
         | 
| 33 | 
            +
                # an invalid option a list of valid options will be raised. I suggest
         | 
| 34 | 
            +
                # starting with 'Ubuntu 10.04 LTS'
         | 
| 35 | 
            +
                #
         | 
| 36 | 
            +
                # files is an optional Hash of path to blobs. Use it to place a file on the
         | 
| 37 | 
            +
                # disk of the new server.
         | 
| 38 | 
            +
                #
         | 
| 39 | 
            +
                # Use it like this:
         | 
| 40 | 
            +
                #
         | 
| 41 | 
            +
                #     server.create("my-server", 512, "Ubuntu 10.04 LTS", "/root/.ssh/authorised_keys" => File.read("/foo"))
         | 
| 42 | 
            +
                #
         | 
| 43 | 
            +
                def create(server_name, flavor_name, image_name, files = {})
         | 
| 44 | 
            +
                  request = {
         | 
| 45 | 
            +
                    "server" => {
         | 
| 46 | 
            +
                      "name" => server_name,
         | 
| 47 | 
            +
                      "imageRef" => image_name_to_id(image_name),
         | 
| 48 | 
            +
                      "flavorRef" => flavor_name_to_id(flavor_name),
         | 
| 49 | 
            +
                    }
         | 
| 50 | 
            +
                  }
         | 
| 51 | 
            +
                  files.each do |path, blob|
         | 
| 52 | 
            +
                    request['server']['personality'] ||= []
         | 
| 53 | 
            +
                    request['server']['personality'] << {
         | 
| 54 | 
            +
                      'path' => path,
         | 
| 55 | 
            +
                      'contents' => Base64.encode64(blob)
         | 
| 56 | 
            +
                    }
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  data = cloud_request(Net::HTTP::Post.new(servers_path), JSON.dump(request)).body
         | 
| 60 | 
            +
                  data = JSON.parse(data)['server']
         | 
| 61 | 
            +
                  Raca::Server.new(@account, @region, data['id'])
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                private
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def servers_host
         | 
| 67 | 
            +
                  @servers_host ||= URI.parse(@servers_url).host
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def account_path
         | 
| 71 | 
            +
                  @account_path ||= URI.parse(@servers_url).path
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def flavors_path
         | 
| 75 | 
            +
                  @flavors_path ||= File.join(account_path, "flavors")
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def images_path
         | 
| 79 | 
            +
                  @images_path ||= File.join(account_path, "images")
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def servers_path
         | 
| 83 | 
            +
                  @servers_path ||= File.join(account_path, "servers")
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def list
         | 
| 87 | 
            +
                  json = cloud_request(Net::HTTP::Get.new(servers_path)).body
         | 
| 88 | 
            +
                  JSON.parse(json)['servers']
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def find_server_id(server_name)
         | 
| 92 | 
            +
                  server = list.detect {|row|
         | 
| 93 | 
            +
                    row["name"] == server_name
         | 
| 94 | 
            +
                  }
         | 
| 95 | 
            +
                  server ? server["id"] : nil
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                def flavors
         | 
| 99 | 
            +
                  @flavors ||= begin
         | 
| 100 | 
            +
                    data = cloud_request(Net::HTTP::Get.new(flavors_path)).body
         | 
| 101 | 
            +
                    JSON.parse(data)['flavors']
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def flavor_names
         | 
| 106 | 
            +
                  flavors.map {|row| row['name'] }
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def flavor_name_to_id(str)
         | 
| 110 | 
            +
                  flavor = flavors.detect {|row|
         | 
| 111 | 
            +
                    row['name'].downcase.include?(str.to_s.downcase)
         | 
| 112 | 
            +
                  }
         | 
| 113 | 
            +
                  if flavor
         | 
| 114 | 
            +
                    flavor['id']
         | 
| 115 | 
            +
                  else
         | 
| 116 | 
            +
                    raise ArgumentError, "valid flavors are: #{flavor_names.join(', ')}"
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def images
         | 
| 121 | 
            +
                  @images ||= begin
         | 
| 122 | 
            +
                    data = cloud_request(Net::HTTP::Get.new(images_path)).body
         | 
| 123 | 
            +
                    JSON.parse(data)['images']
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def image_names
         | 
| 128 | 
            +
                  images.map {|row| row['name'] }
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def image_name_to_id(str)
         | 
| 132 | 
            +
                  image = images.detect {|row|
         | 
| 133 | 
            +
                    row['name'].downcase.include?(str.to_s.downcase)
         | 
| 134 | 
            +
                  }
         | 
| 135 | 
            +
                  if image
         | 
| 136 | 
            +
                    image['id']
         | 
| 137 | 
            +
                  else
         | 
| 138 | 
            +
                    raise ArgumentError, "valid images are: #{image_names.join(', ')}"
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def cloud_request(request, body = nil)
         | 
| 143 | 
            +
                  request['X-Auth-Token'] = @account.auth_token
         | 
| 144 | 
            +
                  request['Content-Type'] = 'application/json'
         | 
| 145 | 
            +
                  request['Accept']       = 'application/json'
         | 
| 146 | 
            +
                  cloud_http(servers_host) do |http|
         | 
| 147 | 
            +
                    http.request(request, body)
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def cloud_http(hostname, retries = 3, &block)
         | 
| 152 | 
            +
                  http = Net::HTTP.new(hostname, 443)
         | 
| 153 | 
            +
                  http.use_ssl = true
         | 
| 154 | 
            +
                  http.start do |http|
         | 
| 155 | 
            +
                    response = block.call http
         | 
| 156 | 
            +
                    if response.is_a?(Net::HTTPUnauthorized)
         | 
| 157 | 
            +
                      log "Rackspace returned HTTP 401; refreshing auth before retrying."
         | 
| 158 | 
            +
                      @account.refresh_cache
         | 
| 159 | 
            +
                      response = block.call http
         | 
| 160 | 
            +
                    end
         | 
| 161 | 
            +
                    if response.is_a?(Net::HTTPSuccess)
         | 
| 162 | 
            +
                      response
         | 
| 163 | 
            +
                    else
         | 
| 164 | 
            +
                      raise_on_error(response)
         | 
| 165 | 
            +
                    end
         | 
| 166 | 
            +
                    response
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                rescue Timeout::Error
         | 
| 169 | 
            +
                  if retries <= 0
         | 
| 170 | 
            +
                    raise Raca::TimeoutError, "Timeout from Rackspace while trying #{request.class} to #{request.path}"
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                  cloud_http(hostname, retries - 1, &block)
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                def raise_on_error(response)
         | 
| 177 | 
            +
                  error_klass = case response.code.to_i
         | 
| 178 | 
            +
                  when 400 then BadRequestError
         | 
| 179 | 
            +
                  when 404 then NotFoundError
         | 
| 180 | 
            +
                  when 500 then ServerError
         | 
| 181 | 
            +
                  else
         | 
| 182 | 
            +
                    HTTPError
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
                  raise error_klass, "Rackspace returned HTTP status #{response.code}"
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                def log(msg)
         | 
| 188 | 
            +
                  if defined?(Rails)
         | 
| 189 | 
            +
                    Rails.logger.info msg
         | 
| 190 | 
            +
                  else
         | 
| 191 | 
            +
                    puts msg
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
             | 
| 10 195 | 
             
              end
         | 
| 11 196 | 
             
            end
         | 
    
        data/lib/raca.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: raca
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.2.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - James Healy
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2014- | 
| 11 | 
            +
            date: 2014-03-19 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rake
         | 
| @@ -66,6 +66,20 @@ dependencies: | |
| 66 66 | 
             
                - - ">="
         | 
| 67 67 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 68 | 
             
                    version: '0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: cane
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - ">="
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '0'
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '0'
         | 
| 69 83 | 
             
            description: A simple wrapper for the Rackspace Cloud API with no dependencies
         | 
| 70 84 | 
             
            email:
         | 
| 71 85 | 
             
            - james.healy@theconversation.edu.au
         | 
| @@ -79,6 +93,7 @@ files: | |
| 79 93 | 
             
            - lib/raca/account.rb
         | 
| 80 94 | 
             
            - lib/raca/container.rb
         | 
| 81 95 | 
             
            - lib/raca/containers.rb
         | 
| 96 | 
            +
            - lib/raca/errors.rb
         | 
| 82 97 | 
             
            - lib/raca/server.rb
         | 
| 83 98 | 
             
            - lib/raca/servers.rb
         | 
| 84 99 | 
             
            homepage: http://github.com/conversation/raca
         | 
| @@ -104,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 104 119 | 
             
                  version: '0'
         | 
| 105 120 | 
             
            requirements: []
         | 
| 106 121 | 
             
            rubyforge_project: 
         | 
| 107 | 
            -
            rubygems_version: 2.2. | 
| 122 | 
            +
            rubygems_version: 2.2.2
         | 
| 108 123 | 
             
            signing_key: 
         | 
| 109 124 | 
             
            specification_version: 4
         | 
| 110 125 | 
             
            summary: A simple wrapper for the Rackspace Cloud API with no dependencies
         |