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 +71 -0
- data/bin/ec2cli +67 -0
- data/lib/ec2cli/cli/evaluate.rb +53 -0
- data/lib/ec2cli/cli/functions.rb +104 -0
- data/lib/ec2cli/cli/help.rb +40 -0
- data/lib/ec2cli/cli/options.rb +35 -0
- data/lib/ec2cli/ec2-client.rb +108 -0
- data/lib/ec2cli/ec2-driver.rb +322 -0
- data/lib/ec2cli/ec2-endpoint.rb +32 -0
- data/lib/ec2cli/ec2-error.rb +10 -0
- data/lib/ec2cli/ec2-parser.tab.rb +519 -0
- data/lib/ec2cli/ec2-parser.y +233 -0
- data/lib/ec2cli.rb +11 -0
- metadata +74 -0
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
|