swirl 1.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.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2007, 2008, 2009 Blake Mizerany
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Swirl
2
+ =====
3
+
4
+ Swirl is an EC2 version agnostic client for EC2 writtin in Ruby. It gets
5
+ out of your way.
6
+
7
+ The secret is it's simple input extraction and output compacting. Your
8
+ input parameters and `expand`ed and EC2's (terrible) xml output is
9
+ `compact`ed.
10
+
11
+
12
+ Some simple examples:
13
+
14
+ # Input
15
+ { "InstanceId" => ["i-123k2h1", "i-0234d3"] }
16
+
17
+ is `expand`ed to:
18
+
19
+ { "InstanceId.0" => "i-123k2h1", "InstanceId.1" => "i-0234d3" }
20
+
21
+ and
22
+
23
+ # Output
24
+ {
25
+ "reservationSet" => {
26
+ "item" => {
27
+ "instancesSet" => { "item" => [ ... ] }
28
+ }
29
+ }
30
+ }
31
+
32
+ and it's varations are now `compact`ed to:
33
+
34
+ {
35
+ "reservationSet" => {
36
+ "instancesSet" => [ { ... }, { ... } ]
37
+ }
38
+ }
39
+
40
+
41
+ Some things worth noteing is that compact ignores Symbols. This
42
+ allows you to pass the params into `call` and use them later
43
+ without affecting the API call (i.e. chain of responsiblity); a
44
+ nifty trick we use in (Rack)[http://github.com/rack/rack]
45
+
46
+ Use
47
+ ---
48
+
49
+ ec2 = Swirl::EC2.new
50
+
51
+ # Describe all instances
52
+ ec2.call "DescribeInstances"
53
+
54
+ # Describe specific instances
55
+ ec2.call "DescribeInstances", "InstanceId" => ["i-38hdk2f", "i-93nndch"]
56
+
57
+
58
+ Shell
59
+ ---
60
+
61
+ $ swirl
62
+ >> c
63
+ <Swirl::EC2 ... >
64
+ >> c.call "DescribeInstances"
65
+ ...
66
+
67
+ The shell respects your ~/.swirl file for configuration
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ require 'irb'
3
+ require 'swirl'
4
+
5
+ def c
6
+ @c ||= Swirl::EC2.new
7
+ end
8
+
9
+ if $0 =~ %r|/swirl|
10
+ IRB.start(__FILE__)
11
+ end
@@ -0,0 +1 @@
1
+ require 'swirl/ec2'
@@ -0,0 +1,114 @@
1
+ require 'yaml'
2
+ require 'cgi'
3
+ require 'base64'
4
+ require 'net/https'
5
+
6
+ require 'crack/xml'
7
+ require 'hmac-sha2'
8
+
9
+ require 'swirl/helpers'
10
+
11
+ module Swirl
12
+
13
+ ## Errors
14
+ class InvalidRequest < StandardError ; end
15
+
16
+
17
+ class EC2
18
+ include Helpers::Compactor
19
+ include Helpers::Expander
20
+
21
+
22
+ def self.options(name=:default, file="~/.swirl")
23
+ YAML.load_file(File.expand_path(file))[name]
24
+ end
25
+
26
+ def initialize(options={}, file="~/.swirl")
27
+ account = options.fetch(:account) { :default }
28
+ options = self.class.options(account, file).merge(options)
29
+
30
+ @aws_access_key_id = options[:aws_access_key_id]
31
+ @aws_secret_access_key = options[:aws_secret_access_key]
32
+ @hmac = HMAC::SHA256.new(@aws_secret_access_key)
33
+ @version = options[:version] || "2009-11-30"
34
+ @url = URI(options[:url] || "https://ec2.amazonaws.com")
35
+ end
36
+
37
+ def escape(value)
38
+ CGI.escape(value).gsub(/\+/, "%20")
39
+ end
40
+
41
+ def compile_sorted_form_data(query)
42
+ valid = query.reject {|_, v| v.nil? }
43
+ valid.sort.map {|k,v| [k, escape(v)] * "=" } * "&"
44
+ end
45
+
46
+ def compile_signature(method, body)
47
+ string_to_sign = [method, @url.host, "/", body] * "\n"
48
+ hmac = @hmac.update(string_to_sign)
49
+ encoded_sig = Base64.encode64(hmac.digest).chomp
50
+ escape(encoded_sig)
51
+ end
52
+
53
+ ##
54
+ # Execute an EC2 command, expand the input,
55
+ # and compact the output
56
+ #
57
+ # Examples:
58
+ # ec2.call("DescribeInstances")
59
+ # ec2.call("TerminateInstances", "InstanceId" => ["i-1234", "i-993j"]
60
+ #
61
+ def call(action, query={})
62
+ code, data = call!(action, expand(query))
63
+
64
+ case code
65
+ when 200
66
+ compact(data)
67
+ when 400...500
68
+ messages = Array(data["Response"]["Errors"]).map {|_, e| e["Message"] }
69
+ raise InvalidRequest, messages.join(",")
70
+ else
71
+ msg = "unexpected response #{response.code} -> #{data.inspect}"
72
+ raise InvalidRequest, msg
73
+ end
74
+ end
75
+
76
+ def call!(action, query={})
77
+ # Hard coding this here until otherwise needed
78
+ method = "POST"
79
+
80
+ query["Action"] = action
81
+ query["AWSAccessKeyId"] = @aws_access_key_id
82
+ query["SignatureMethod"] = "HmacSHA256"
83
+ query["SignatureVersion"] = "2"
84
+ query["Timestamp"] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
85
+ query["Version"] = @version
86
+
87
+ body = compile_sorted_form_data(query)
88
+ body += "&" + ["Signature", compile_signature(method, body)].join("=")
89
+
90
+ response = post(body)
91
+ data = Crack::XML.parse(response.body)
92
+ [response.code.to_i, data]
93
+ end
94
+
95
+ def post(body)
96
+ headers = { "Content-Type" => "application/x-www-form-urlencoded" }
97
+
98
+ http = Net::HTTP.new(@url.host, @url.port)
99
+ http.use_ssl = true
100
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
101
+
102
+ request = Net::HTTP::Post.new(@url.request_uri, headers)
103
+ request.body = body
104
+
105
+ http.request(request)
106
+ end
107
+
108
+ def inspect
109
+ "<#{self.class.name} version: #{@version} url: #{@url} aws_access_key_id: #{@aws_access_key_id}>"
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,108 @@
1
+ module Swirl
2
+
3
+ module Helpers
4
+
5
+ module Compactor
6
+
7
+ Lists = [
8
+ "keySet",
9
+ "groupSet",
10
+ "blockDeviceMapping",
11
+ "instancesSet",
12
+ "reservationSet",
13
+ "imagesSet",
14
+ "ownersSet",
15
+ "executableBySet",
16
+ "securityGroupSet",
17
+ "ipPermissions",
18
+ "ipRanges",
19
+ "groups",
20
+ "securityGroupInfo",
21
+ "add",
22
+ "remove",
23
+ "launchPermission",
24
+ "productCodes",
25
+ "availabilityZoneSet",
26
+ "availabilityZoneInfo",
27
+ "publicIpsSet",
28
+ "addressesSet"
29
+ ]
30
+
31
+ def compact(response)
32
+ root_key = response.keys.first
33
+ base = response[root_key]
34
+ compact!(base)
35
+ end
36
+ module_function :compact
37
+
38
+ def compact!(data)
39
+ data.inject({}) do |com, (key, value)|
40
+ if Lists.include?(key)
41
+ converted = if value && value.has_key?("item")
42
+ items = value["item"]
43
+ items ||= []
44
+ items = items.is_a?(Array) ? items : [items]
45
+ items.map {|item| compact!(item) }
46
+ else
47
+ []
48
+ end
49
+ com[key] = converted
50
+ elsif key == "xmlns"
51
+ next(com)
52
+ else
53
+ com[key] = value
54
+ end
55
+ com
56
+ end
57
+ end
58
+ module_function :compact!
59
+
60
+ end
61
+
62
+ module Expander
63
+ def expand(request)
64
+ request.inject({}) do |exp, (key, value)|
65
+ next(exp) if !key.is_a?(String)
66
+
67
+ case value
68
+ when Array
69
+ value.each_with_index do |val, n|
70
+ exp["#{key}.#{n}"] = val
71
+ end
72
+ when Range
73
+ exp["From#{key}"] = value.min
74
+ exp["To#{key}"] = value.max
75
+ else
76
+ exp[key] = value
77
+ end
78
+ exp
79
+ end
80
+ end
81
+ module_function :expand
82
+ end
83
+
84
+ module Slop
85
+ class InvalidKey < StandardError ; end
86
+
87
+ def self.new(response)
88
+ sloppy = Hash.new do |hash, key|
89
+ camalized = Slop.camalize(key)
90
+ raise InvalidKey, key if !response.has_key?(camalized)
91
+ response[camalized]
92
+ end
93
+ end
94
+
95
+ def self.camalize(stringish)
96
+ head, tail = stringish.to_s.split("_")
97
+ rest = Array(tail).map! {|part| part.capitalize }
98
+ [head, *rest].join
99
+ end
100
+
101
+ def slopify(response)
102
+ Slop.new(response)
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'nokogiri'
5
+ rescue LoadError
6
+ abort <<-MSG
7
+ Unable to load nokogiri.
8
+ Ensure you have it installed and/or
9
+ export RUBYOPT=rubygems
10
+ MSG
11
+ end
12
+
13
+ require 'open-uri'
14
+ require 'stringio'
15
+ require 'pp'
16
+
17
+ DefaultWSDL = "http://s3.amazonaws.com/ec2-downloads/2008-02-01.ec2.wsdl"
18
+
19
+ wsdl_url = ARGV.shift || DefaultWSDL
20
+ Wsdl = Nokogiri::XML.parse(open(wsdl_url))
21
+
22
+ ##__END__
23
+
24
+ list_keys = []
25
+ item_types = Wsdl.search(%Q|//xs:complexType//xs:element[@name="item"]|)
26
+ item_types.each do |item_type|
27
+ ns, name = item_type["type"].split(":")
28
+ next if ns != "tns"
29
+ complex = item_type.parent.parent
30
+ uses = Wsdl.search(%Q|//xs:element[@type="#{ns}:#{complex["name"]}"]|)
31
+ uses.each do |u|
32
+ list_keys << u["name"]
33
+ end
34
+ end
35
+
36
+ pp list_keys.uniq
@@ -0,0 +1,73 @@
1
+ require 'contest'
2
+ require 'swirl/helpers'
3
+
4
+ class CompactorTest < Test::Unit::TestCase
5
+ include Swirl::Helpers
6
+
7
+ test "pivots on root" do
8
+ response = { "Foo" => { "requestId" => "abc123" } }
9
+ expected = { "requestId" => "abc123" }
10
+
11
+ assert_equal expected, Compactor.compact(response)
12
+ end
13
+
14
+ test "ignores xmlns" do
15
+ response = { "Foo" => { "requestId" => "abc123", "xmlns" => "suckit" } }
16
+ expected = { "requestId" => "abc123" }
17
+
18
+ assert_equal expected, Compactor.compact(response)
19
+ end
20
+
21
+ test "pivots list keys on item" do
22
+ response = { "Foo" => { "groupSet" => { "item" => [{ "foo" => "bar" }] } } }
23
+ expected = { "groupSet" => [ { "foo" => "bar" } ] }
24
+
25
+ assert_equal expected, Compactor.compact(response)
26
+ end
27
+
28
+ test "pivots list keys item and converts to Array not already an Array" do
29
+ response = { "Foo" => { "groupSet" => { "item" => { "foo" => "bar" } } } }
30
+ expected = { "groupSet" => [ { "foo" => "bar" } ] }
31
+
32
+ assert_equal expected, Compactor.compact(response)
33
+ end
34
+
35
+ test "pivots list keys item and makes empty Array if nil" do
36
+ response = { "Foo" => { "groupSet" => { "item" => nil } } }
37
+ expected = { "groupSet" => [] }
38
+
39
+ assert_equal expected, Compactor.compact(response)
40
+ end
41
+
42
+ test "makes value empty Array if nil" do
43
+ response = { "Foo" => { "groupSet" => nil } }
44
+ expected = { "groupSet" => [] }
45
+
46
+ assert_equal expected, Compactor.compact(response)
47
+ end
48
+
49
+ test "traverses list values and compacts" do
50
+ response = {
51
+ "Foo" => {
52
+ "groupSet" => {
53
+ "item" => {
54
+ "ipPermissions" => {
55
+ "item" => {
56
+ "proto" => "tcp"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ expected = {
65
+ "groupSet" => [
66
+ { "ipPermissions" => [ { "proto" => "tcp" } ] }
67
+ ]
68
+ }
69
+
70
+ assert_equal expected, Compactor.compact(response)
71
+ end
72
+
73
+ end
@@ -0,0 +1,46 @@
1
+ require 'contest'
2
+ require 'swirl/helpers'
3
+
4
+ class ExpanderTest < Test::Unit::TestCase
5
+ include Swirl::Helpers
6
+
7
+ test "leaves params as is by default" do
8
+ request = { "foo" => "bar" }
9
+
10
+ assert_equal request, Expander.expand(request)
11
+ end
12
+
13
+ test "ignores non-String keys" do
14
+ request = { "foo" => "bar", :ignore => "test" }
15
+ expected = { "foo" => "bar" }
16
+
17
+ assert_equal expected, Expander.expand(request)
18
+ end
19
+
20
+ test "expands Array values to .n key-values" do
21
+ request = { "group" => ["foo", "bar", "baz"] }
22
+
23
+ expected = {
24
+ "group.0" => "foo",
25
+ "group.1" => "bar",
26
+ "group.2" => "baz"
27
+ }
28
+
29
+ assert_equal expected, Expander.expand(request)
30
+ end
31
+
32
+ test "ignores empty Array values" do
33
+ request = { "group" => [] }
34
+ expected = {}
35
+
36
+ assert_equal expected, Expander.expand(request)
37
+ end
38
+
39
+ test "converts Key of Range to FromKey ToKey" do
40
+ request = { "Port" => 1..3 }
41
+ expected = { "FromPort" => 1, "ToPort" => 3 }
42
+
43
+ assert_equal expected, Expander.expand(request)
44
+ end
45
+
46
+ end
@@ -0,0 +1,29 @@
1
+ require 'contest'
2
+ require 'swirl/helpers'
3
+
4
+ class SlopTest < Test::Unit::TestCase
5
+ include Swirl::Helpers
6
+
7
+ test "camalize" do
8
+ assert_equal "foo", Slop.camalize(:foo)
9
+ assert_equal "fooBar", Slop.camalize(:foo_bar)
10
+ end
11
+
12
+ test "gets sloppy" do
13
+ slop = Slop.new({"fooBar" => "baz"})
14
+ assert_equal "baz", slop[:foo_bar]
15
+ end
16
+
17
+ test "honors keys already camalized" do
18
+ slop = Slop.new({"fooBar" => "baz"})
19
+ assert_equal "baz", slop["fooBar"]
20
+ end
21
+
22
+ test "raise InvalidKey if key not found" do
23
+ slop = Slop.new({})
24
+ assert_raises Slop::InvalidKey do
25
+ slop[:not_here]
26
+ end
27
+ end
28
+
29
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: swirl
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Blake Mizerany
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-26 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: crack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.1.4
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: ruby-hmac
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.2
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: contest
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.1.2
44
+ version:
45
+ description: A version agnostic EC2 ruby driver
46
+ email:
47
+ executables:
48
+ - swirl
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README.md
53
+ - LICENSE
54
+ files:
55
+ - LICENSE
56
+ - README.md
57
+ - list-types
58
+ - lib/swirl/ec2.rb
59
+ - lib/swirl/helpers.rb
60
+ - lib/swirl.rb
61
+ - test/compactor_test.rb
62
+ - test/expander_test.rb
63
+ - test/slop_test.rb
64
+ - bin/swirl
65
+ has_rdoc: true
66
+ homepage: http://github.com/bmizerany/swirl
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options:
71
+ - --line-numbers
72
+ - --inline-source
73
+ - --title
74
+ - Sinatra
75
+ - --main
76
+ - README.rdoc
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "0"
90
+ version:
91
+ requirements: []
92
+
93
+ rubyforge_project: swirl
94
+ rubygems_version: 1.3.5
95
+ signing_key:
96
+ specification_version: 2
97
+ summary: A version agnostic EC2 ruby driver
98
+ test_files:
99
+ - test/compactor_test.rb
100
+ - test/expander_test.rb
101
+ - test/slop_test.rb