ec2cli 0.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/README ADDED
@@ -0,0 +1,71 @@
1
+ = ec2cli
2
+
3
+ == Description
4
+
5
+ ec2cli is an interactive command-line client of Amazon EC2.
6
+
7
+ == Source Code
8
+
9
+ https://bitbucket.org/winebarrel/ec2cli
10
+
11
+ == Install
12
+
13
+ shell> gem install ec2cli
14
+ shell> export AWS_ACCESS_KEY_ID='...'
15
+ shell> export AWS_SECRET_ACCESS_KEY='...'
16
+ shell> export EC2_REGION=ap-northeast-1
17
+ shell> ec2cli -h
18
+ Usage: ec2cli [options]
19
+ -k, --access-key=ACCESS_KEY
20
+ -s, --secret-key=SECRET_KEY
21
+ -r, --region=REGION_OR_ENDPOINT
22
+ --debug
23
+ -h, --help
24
+ ...
25
+ shell> ec2cli # show prompt
26
+ ap-northeast-1> desc instances;
27
+ [
28
+ ["cthulhu","i-01bd4903","stopped","10.0.114.178","t1.micro",["default"],{"Name":"cthulhu"},"ap-northeast-1a"],
29
+ ["hastur","i-e78d79e5","stopped","10.0.130.129","m1.large",["default"],{"Name":"hastur","aaa":"1100"},"ap-northeast-1b"],
30
+ ["nyar","i-a5e119a7","stopped","10.0.254.210","c1.xlarge",["default"],{"aaa":"1100","Name":"nyar"},"ap-northeast-1b"],
31
+ ]
32
+ // 3 rows in set (0.23 sec)
33
+
34
+ == Help
35
+
36
+ ##### Query #####
37
+
38
+ DESC[RIBE] INSTANCES [WHERE name = '...' AND ...]
39
+ Describes one or more of your instances.
40
+
41
+ DESC[RIBE] IMAGES [WHERE name = '...' AND ...]
42
+ DESC[RIBE] ALL IMAGES [WHERE [Owner = {amazon|aws-marketplace|self} AND] name = '...' AND ...]
43
+ Describes the images.
44
+
45
+ RUN INSTANCES image_id
46
+ Launches instances
47
+
48
+ START INSTANCES id_or_name
49
+ START INSTANCES (id_or_name, ...)
50
+ Starts an Amazon EBS-backed AMI that you've previously stopped.
51
+
52
+ STOP INSTANCES id_or_name
53
+ STOP INSTANCES (id_or_name, ...)
54
+ Stops an Amazon EBS-backed instance
55
+
56
+ SET TAGS name = value, ... [WHERE name = '...' AND ...]
57
+ Adds or overwrites one or more tags for the specified EC2 resource or resources.
58
+
59
+ USE region_or_endpoint
60
+ changes an endpoint
61
+
62
+ SHOW REGIONS
63
+ displays a region list
64
+
65
+ ##### Command #####
66
+
67
+ .help displays this message
68
+ .quit | .exit exits sdbcli
69
+ .debug (true|false)? displays a debug status or changes it
70
+ .version displays a version
71
+
data/bin/ec2cli ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ Version = '0.1.0'
5
+
6
+ HISTORY_FILE = File.join((ENV['HOME'] || ENV['USERPROFILE'] || '.'), '.ec2cli_history')
7
+ HISTSIZE = 500
8
+
9
+ require 'rubygems'
10
+ require 'ec2cli'
11
+ require 'readline'
12
+
13
+ options = parse_options
14
+
15
+ driver = EC2::Driver.new(
16
+ options.access_key_id,
17
+ options.secret_access_key,
18
+ options.ec2_endpoint_or_region)
19
+
20
+ driver.debug = options.debug
21
+
22
+ # load history file
23
+ if File.exist?(HISTORY_FILE)
24
+ open(HISTORY_FILE) do |f|
25
+ f.each_line do |line|
26
+ line = line.strip
27
+ Readline::HISTORY.push(line) unless line.empty?
28
+ end
29
+ end
30
+ end
31
+
32
+ # interactive mode
33
+ src = ''
34
+ prompt1 = lambda { "#{driver.region || 'unknown'}> " }
35
+ prompt2 = lambda { "#{' ' * (prompt1.call.length - 3)}-> " }
36
+
37
+ while buf = Readline.readline((src.empty? ? prompt1.call : prompt2.call), true)
38
+ # ignore blank lines
39
+ if /\A\s*\Z/ =~ buf
40
+ Readline::HISTORY.pop
41
+ next
42
+ end
43
+
44
+ if src.empty? and buf =~ /\A\.(.+)/
45
+ evaluate_command(driver, $1)
46
+ else
47
+ begin
48
+ src << (src.empty? ? buf : ("\n" + buf))
49
+ evaluate_query(driver, src, :show_rows => true)
50
+ rescue => e
51
+ print_error(e.message)
52
+ print_error(e.backtrace) if driver.debug
53
+ end
54
+
55
+ prompt = src.empty? ? prompt1.call : prompt2.call
56
+ end
57
+ end # of while
58
+
59
+ # save history file
60
+ unless Readline::HISTORY.empty?
61
+ open(HISTORY_FILE, 'wb') do |f|
62
+ (Readline::HISTORY.to_a.slice(-(Readline::HISTORY.length < HISTSIZE ? Readline::HISTORY.length : HISTSIZE)..-1) || []).each do |line|
63
+ next if /\A\s*\Z/ =~ line
64
+ f.puts line
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,53 @@
1
+ require 'ec2cli/ec2-driver'
2
+
3
+ def evaluate_query(driver, src, opts = {})
4
+ ss = StringScanner.new(src.dup)
5
+ buf = ''
6
+
7
+ until ss.eos?
8
+ if (tok = ss.scan %r{[^`'";\\/#]+}) #'
9
+ buf << tok
10
+ elsif (tok = ss.scan /`(?:[^`]|``)*`/)
11
+ buf << tok
12
+ elsif (tok = ss.scan /'(?:[^']|'')*'/) #'
13
+ buf << tok
14
+ elsif (tok = ss.scan /"(?:[^"]|"")*"/) #"
15
+ buf << tok
16
+ elsif (tok = ss.scan %r{/\*/?(?:\n|[^/]|[^*]/)*\*/})
17
+ # nothing to do
18
+ elsif (tok = ss.scan /--[^\r\n]*(?:\r\n|\r|\n|\Z)/)
19
+ # nothing to do
20
+ elsif (tok = ss.scan /#[^\r\n]*(?:\r\n|\r|\n|\Z)/)
21
+ # nothing to do
22
+ elsif (tok = ss.scan /(?:\\;)/)
23
+ buf << ';' # escape of ';'
24
+ elsif (tok = ss.scan /(?:;|\\G)/)
25
+ src.replace(ss.rest)
26
+ query = buf
27
+ buf = ''
28
+
29
+ if query.strip.empty?
30
+ print_error('No query specified')
31
+ next
32
+ end
33
+
34
+ start_time = Time.new
35
+ out = driver.execute(query)
36
+ elapsed = Time.now - start_time
37
+
38
+ if out.kind_of?(EC2::Driver::Rownum)
39
+ print_rownum(out, :time => elapsed)
40
+ elsif out.kind_of?(String)
41
+ puts out
42
+ elsif out
43
+ opts = opts.merge(:inline => (tok != '\G'), :time => elapsed)
44
+ print_json(out, opts)
45
+ end
46
+ elsif (tok = ss.scan /./)
47
+ buf << tok # 落ち穂拾い
48
+ end
49
+ end
50
+
51
+ src.replace(buf.strip)
52
+ buf
53
+ end
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+
3
+ def print_error(errmsg, opts = {})
4
+ errmsg = errmsg.join("\n") if errmsg.kind_of?(Array)
5
+ errmsg = errmsg.strip.split("\n").map {|i| "// #{i.strip}" }.join("\n")
6
+ errmsg += "\n\n" unless opts[:strip]
7
+ $stderr.puts errmsg
8
+ end
9
+
10
+ def print_rownum(data, opts = {})
11
+ rownum = data.to_i
12
+ msg = "// #{rownum} #{rownum > 1 ? 'rows' : 'row'} changed"
13
+ msg << " (%.2f sec)" % opts[:time] if opts[:time]
14
+ msg << "\n\n"
15
+ puts msg
16
+ end
17
+
18
+ def print_json(data, opts = {})
19
+ str = nil
20
+
21
+ if data.kind_of?(Array) and opts[:inline]
22
+ str = "[\n"
23
+
24
+ data.each_with_index do |item, i|
25
+ str << " #{item.to_json}"
26
+ str << ',' if i < (data.length - 1)
27
+ str << "\n"
28
+ end
29
+
30
+ str << "]"
31
+ else
32
+ if data.kind_of?(Array) or data.kind_of?(Hash)
33
+ str = JSON.pretty_generate(data)
34
+ else
35
+ str = data.to_json
36
+ end
37
+ end
38
+
39
+ str.sub!(/(?:\r\n|\r|\n)*\Z/, "\n")
40
+
41
+ if opts[:show_rows] and data.kind_of?(Array)
42
+ str << "// #{data.length} #{data.length > 1 ? 'rows' : 'row'} in set"
43
+ str << " (%.2f sec)" % opts[:time] if opts[:time]
44
+ str << "\n"
45
+ end
46
+
47
+ str << "\n"
48
+ puts str
49
+ end
50
+
51
+ def print_version
52
+ puts "ec2cli #{Version}"
53
+ end
54
+
55
+ def evaluate_command(driver, cmd_arg)
56
+ cmd, arg = cmd_arg.split(/\s+/, 2).map {|i| i.strip }
57
+ arg = nil if (arg || '').strip.empty?
58
+
59
+ r = /\A#{Regexp.compile(cmd)}/i
60
+
61
+ commands = {
62
+ 'help' => lambda {
63
+ print_help
64
+ },
65
+
66
+ ['exit', 'quit'] => lambda {
67
+ exit 0
68
+ },
69
+
70
+ 'debug' => lambda {
71
+ if arg
72
+ r_arg = /\A#{Regexp.compile(arg)}/i
73
+
74
+ if r_arg =~ 'true'
75
+ driver.debug = true
76
+ elsif r_arg =~ 'false'
77
+ driver.debug = false
78
+ else
79
+ print_error('Invalid argument')
80
+ end
81
+ else
82
+ puts driver.debug
83
+ end
84
+ },
85
+
86
+ 'version' => lambda {
87
+ print_version
88
+ }
89
+ }
90
+
91
+ cmd_name, cmd_proc = commands.find do |name, proc|
92
+ if name.kind_of?(Array)
93
+ name.any? {|i| r =~ i }
94
+ else
95
+ r =~ name
96
+ end
97
+ end
98
+
99
+ if cmd_proc
100
+ cmd_proc.call
101
+ else
102
+ print_error('Unknown command')
103
+ end
104
+ end
@@ -0,0 +1,40 @@
1
+ def print_help
2
+ puts <<EOS
3
+ ##### Query #####
4
+
5
+ DESC[RIBE] INSTANCES [WHERE name = '...' AND ...]
6
+ Describes one or more of your instances.
7
+
8
+ DESC[RIBE] IMAGES [WHERE name = '...' AND ...]
9
+ DESC[RIBE] ALL IMAGES [WHERE [Owner = {amazon|aws-marketplace|self} AND] name = '...' AND ...]
10
+ Describes the images.
11
+
12
+ RUN INSTANCES image_id
13
+ Launches instances
14
+
15
+ START INSTANCES id_or_name
16
+ START INSTANCES (id_or_name, ...)
17
+ Starts an Amazon EBS-backed AMI that you've previously stopped.
18
+
19
+ STOP INSTANCES id_or_name
20
+ STOP INSTANCES (id_or_name, ...)
21
+ Stops an Amazon EBS-backed instance
22
+
23
+ SET TAGS name = value, ... [WHERE name = '...' AND ...]
24
+ Adds or overwrites one or more tags for the specified EC2 resource or resources.
25
+
26
+ USE region_or_endpoint
27
+ changes an endpoint
28
+
29
+ SHOW REGIONS
30
+ displays a region list
31
+
32
+ ##### Command #####
33
+
34
+ .help displays this message
35
+ .quit | .exit exits sdbcli
36
+ .debug (true|false)? displays a debug status or changes it
37
+ .version displays a version
38
+
39
+ EOS
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ def parse_options
5
+ options = OpenStruct.new
6
+ options.access_key_id = ENV['AWS_ACCESS_KEY_ID']
7
+ options.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
8
+ options.ec2_endpoint_or_region = ENV['EC2_ENDPOINT'] || ENV['EC2_REGION'] || 'ec2.us-east-1.amazonaws.com'
9
+
10
+ # default value
11
+ options.debug = false
12
+
13
+ ARGV.options do |opt|
14
+ opt.on('-k', '--access-key=ACCESS_KEY') {|v| options.access_key_id = v }
15
+ opt.on('-s', '--secret-key=SECRET_KEY') {|v| options.secret_access_key = v }
16
+ opt.on('-r', '--region=REGION_OR_ENDPOINT') {|v| options.ddb_endpoint_or_region = v }
17
+ opt.on('', '--debug') { options.debug = true }
18
+
19
+ opt.on('-h', '--help') {
20
+ puts opt.help
21
+ puts
22
+ print_help
23
+ exit
24
+ }
25
+
26
+ opt.parse!
27
+
28
+ unless options.access_key_id and options.secret_access_key and options.ec2_endpoint_or_region
29
+ puts opt.help
30
+ exit 1
31
+ end
32
+ end
33
+
34
+ options
35
+ end
@@ -0,0 +1,108 @@
1
+ require 'cgi'
2
+ require 'base64'
3
+ require 'net/https'
4
+ require 'openssl'
5
+ require 'optparse'
6
+ require 'pp'
7
+ require 'rexml/document'
8
+
9
+ require 'ec2cli/ec2-error'
10
+ require 'ec2cli/ec2-endpoint'
11
+
12
+ module EC2
13
+ class Client
14
+
15
+ API_VERSION = '2013-02-01'
16
+ SIGNATURE_VERSION = 2
17
+ SIGNATURE_ALGORITHM = :SHA256
18
+
19
+ attr_reader :endpoint
20
+ attr_reader :region
21
+ attr_accessor :debug
22
+
23
+ def initialize(accessKeyId, secretAccessKey, endpoint_or_region)
24
+ @accessKeyId = accessKeyId
25
+ @secretAccessKey = secretAccessKey
26
+ set_endpoint_and_region(endpoint_or_region)
27
+ @debug = false
28
+ end
29
+
30
+ def set_endpoint_and_region(endpoint_or_region)
31
+ @endpoint, @region = EC2::Endpoint.endpoint_and_region(endpoint_or_region)
32
+ end
33
+
34
+ def query(action, params = {})
35
+ params = {
36
+ :Action => action,
37
+ :Version => API_VERSION,
38
+ :Timestamp => Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'),
39
+ :SignatureVersion => SIGNATURE_VERSION,
40
+ :SignatureMethod => "Hmac#{SIGNATURE_ALGORITHM}",
41
+ :AWSAccessKeyId => @accessKeyId,
42
+ }.merge(params)
43
+
44
+ signature = aws_sign(params)
45
+ params[:Signature] = signature
46
+
47
+ if @debug
48
+ $stderr.puts(<<EOS)
49
+ ---request begin---
50
+ #{params.pretty_inspect}
51
+ ---request end---
52
+ EOS
53
+ end
54
+
55
+ https = Net::HTTP.new(@endpoint, 443)
56
+ https.use_ssl = true
57
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
58
+
59
+ source = nil
60
+
61
+ https.start do |w|
62
+ req = Net::HTTP::Post.new('/',
63
+ 'Host' => @endpoint,
64
+ 'Content-Type' => 'application/x-www-form-urlencoded'
65
+ )
66
+
67
+ req.set_form_data(params)
68
+ res = w.request(req)
69
+
70
+ unless res.kind_of?(Net::HTTPOK)
71
+ raise EC2::Error, "#{res.code} #{res.message}"
72
+ end
73
+
74
+ source = res.body
75
+ end
76
+
77
+ if /<Response>\s*<Errors>\s*<Error>/ =~ source
78
+ errors = []
79
+
80
+ REXML::Document.new(source).each_element('//Errors/Error') do |element|
81
+ code = element.text('Code')
82
+ message = element.text('Message')
83
+ errors << "#{code}:#{message}"
84
+ end
85
+
86
+ rraise EC2::Error, errors.join(', ') unless errors.empty?
87
+ end
88
+
89
+ if @debug
90
+ $stderr.puts(<<EOS)
91
+ ---response begin---
92
+ #{source}
93
+ ---response end---
94
+ EOS
95
+ end
96
+
97
+ return source
98
+ end
99
+
100
+ def aws_sign(params)
101
+ params = params.sort_by {|a, b| a.to_s }.map {|k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
102
+ string_to_sign = "POST\n#{@endpoint}\n/\n#{params}"
103
+ digest = OpenSSL::HMAC.digest(OpenSSL::Digest.const_get(SIGNATURE_ALGORITHM).new, @secretAccessKey, string_to_sign)
104
+ Base64.encode64(digest).gsub("\n", '')
105
+ end
106
+
107
+ end # Client
108
+ end # EC2