swirl 1.0

Sign up to get free protection for your applications and to get access to all the features.
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