pi 0.1.13
Sign up to get free protection for your applications and to get access to all the features.
- data/README +402 -0
- data/Rakefile +17 -0
- data/bin/pi +6 -0
- data/lib/cli.rb +25 -0
- data/lib/cli/commands/base.rb +47 -0
- data/lib/cli/commands/misc.rb +17 -0
- data/lib/cli/commands/projects.rb +294 -0
- data/lib/cli/commands/user.rb +153 -0
- data/lib/cli/config.rb +96 -0
- data/lib/cli/core_ext.rb +118 -0
- data/lib/cli/errors.rb +19 -0
- data/lib/cli/runner.rb +269 -0
- data/lib/cli/usage.rb +53 -0
- data/lib/cli/version.rb +5 -0
- data/lib/pi.rb +3 -0
- data/lib/pi/client.rb +250 -0
- data/lib/pi/const.rb +15 -0
- metadata +198 -0
data/lib/cli/config.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'json/pure'
|
6
|
+
|
7
|
+
module PI::Cli
|
8
|
+
class Config
|
9
|
+
|
10
|
+
DEFAULT_TARGET = 'api.staging.samsungcloud.org'
|
11
|
+
DEFAULT_SUGGEST = 'samsungcloud.org'
|
12
|
+
|
13
|
+
TARGET_FILE = '~/.pi_target'
|
14
|
+
TOKEN_FILE = '~/.pi_token'
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :colorize
|
18
|
+
attr_accessor :output
|
19
|
+
attr_accessor :trace
|
20
|
+
attr_reader :suggest_url
|
21
|
+
|
22
|
+
def target_url
|
23
|
+
target_file = File.expand_path(TARGET_FILE)
|
24
|
+
if File.exists? target_file
|
25
|
+
@target_url = lock_and_read(target_file).strip!
|
26
|
+
ha = @target_url.split('//')
|
27
|
+
ha.shift
|
28
|
+
@suggest_url = ha.join('.')
|
29
|
+
@suggest_url = DEFAULT_SUGGEST if @suggest_url.empty?
|
30
|
+
else
|
31
|
+
@target_url = DEFAULT_TARGET
|
32
|
+
@suggest_url = DEFAULT_SUGGEST
|
33
|
+
end
|
34
|
+
@target_url = "http://#{@target_url}" unless /^https?/ =~ @target_url
|
35
|
+
@target_url = @target_url.gsub(/\/+$/, '')
|
36
|
+
@target_url
|
37
|
+
end
|
38
|
+
|
39
|
+
def store_target(target_host)
|
40
|
+
target_file = File.expand_path(TARGET_FILE)
|
41
|
+
lock_and_write(target_file, target_host)
|
42
|
+
end
|
43
|
+
|
44
|
+
def all_tokens
|
45
|
+
token_file = File.expand_path(TOKEN_FILE)
|
46
|
+
return nil unless File.exists? token_file
|
47
|
+
contents = lock_and_read(token_file).strip
|
48
|
+
JSON.parse(contents)
|
49
|
+
end
|
50
|
+
|
51
|
+
alias :targets :all_tokens
|
52
|
+
|
53
|
+
def auth_token
|
54
|
+
return @token if @token
|
55
|
+
tokens = all_tokens
|
56
|
+
@token = tokens[target_url] if tokens
|
57
|
+
end
|
58
|
+
|
59
|
+
def remove_token_file
|
60
|
+
FileUtils.rm_f(File.expand_path(TOKEN_FILE))
|
61
|
+
end
|
62
|
+
|
63
|
+
def store_token(token)
|
64
|
+
tokens = all_tokens || {}
|
65
|
+
tokens[target_url] = token
|
66
|
+
token_file = File.expand_path(TOKEN_FILE)
|
67
|
+
lock_and_write(token_file, tokens.to_json)
|
68
|
+
end
|
69
|
+
|
70
|
+
def lock_and_read(file)
|
71
|
+
File.open(file, "r") {|f|
|
72
|
+
f.flock(File::LOCK_EX)
|
73
|
+
contents = f.read
|
74
|
+
f.flock(File::LOCK_UN)
|
75
|
+
contents
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def lock_and_write(file, contents)
|
80
|
+
File.open(file, File::RDWR | File::CREAT, 0600) {|f|
|
81
|
+
f.flock(File::LOCK_EX)
|
82
|
+
f.rewind
|
83
|
+
f.puts contents
|
84
|
+
f.flush
|
85
|
+
f.truncate(f.pos)
|
86
|
+
f.flock(File::LOCK_UN)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def initialize(work_dir = Dir.pwd)
|
92
|
+
@work_dir = work_dir
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
data/lib/cli/core_ext.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
module PIExtensions
|
2
|
+
|
3
|
+
def say(message)
|
4
|
+
PI::Cli::Config.output.puts(message) if PI::Cli::Config.output
|
5
|
+
end
|
6
|
+
|
7
|
+
def header(message, filler = '-')
|
8
|
+
say "\n"
|
9
|
+
say message
|
10
|
+
say filler.to_s * message.size
|
11
|
+
end
|
12
|
+
|
13
|
+
def banner(message)
|
14
|
+
say "\n"
|
15
|
+
say message
|
16
|
+
end
|
17
|
+
|
18
|
+
def display(message, nl=true)
|
19
|
+
if nl
|
20
|
+
say message
|
21
|
+
else
|
22
|
+
if PI::Cli::Config.output
|
23
|
+
PI::Cli::Config.output.print(message)
|
24
|
+
PI::Cli::Config.output.flush
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear(size=80)
|
30
|
+
return unless PI::Cli::Config.output
|
31
|
+
PI::Cli::Config.output.print("\r")
|
32
|
+
PI::Cli::Config.output.print(" " * size)
|
33
|
+
PI::Cli::Config.output.print("\r")
|
34
|
+
end
|
35
|
+
|
36
|
+
def err(message, prefix='Error: ')
|
37
|
+
raise PI::Cli::CliExit, "#{prefix}#{message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def quit(message = nil)
|
41
|
+
raise PI::Cli::GracefulExit, message
|
42
|
+
end
|
43
|
+
|
44
|
+
def blank?
|
45
|
+
self.to_s.blank?
|
46
|
+
end
|
47
|
+
|
48
|
+
def uptime_string(delta)
|
49
|
+
num_seconds = delta.to_i
|
50
|
+
days = num_seconds / (60 * 60 * 24);
|
51
|
+
num_seconds -= days * (60 * 60 * 24);
|
52
|
+
hours = num_seconds / (60 * 60);
|
53
|
+
num_seconds -= hours * (60 * 60);
|
54
|
+
minutes = num_seconds / 60;
|
55
|
+
num_seconds -= minutes * 60;
|
56
|
+
"#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
|
57
|
+
end
|
58
|
+
|
59
|
+
def pretty_size(size, prec=1)
|
60
|
+
return 'NA' unless size
|
61
|
+
return "#{size}B" if size < 1024
|
62
|
+
return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
|
63
|
+
return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024)
|
64
|
+
return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
module PIStringExtensions
|
70
|
+
|
71
|
+
def red
|
72
|
+
colorize("\e[0m\e[31m")
|
73
|
+
end
|
74
|
+
|
75
|
+
def green
|
76
|
+
colorize("\e[0m\e[32m")
|
77
|
+
end
|
78
|
+
|
79
|
+
def yellow
|
80
|
+
colorize("\e[0m\e[33m")
|
81
|
+
end
|
82
|
+
|
83
|
+
def bold
|
84
|
+
colorize("\e[0m\e[1m")
|
85
|
+
end
|
86
|
+
|
87
|
+
def colorize(color_code)
|
88
|
+
if PI::Cli::Config.colorize
|
89
|
+
"#{color_code}#{self}\e[0m"
|
90
|
+
else
|
91
|
+
self
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def blank?
|
96
|
+
self =~ /^\s*$/
|
97
|
+
end
|
98
|
+
|
99
|
+
def truncate(limit = 30)
|
100
|
+
return "" if self.blank?
|
101
|
+
etc = "..."
|
102
|
+
stripped = self.strip[0..limit]
|
103
|
+
if stripped.length > limit
|
104
|
+
stripped.gsub(/\s+?(\S+)?$/, "") + etc
|
105
|
+
else
|
106
|
+
stripped
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
class Object
|
113
|
+
include PIExtensions
|
114
|
+
end
|
115
|
+
|
116
|
+
class String
|
117
|
+
include PIStringExtensions
|
118
|
+
end
|
data/lib/cli/errors.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module PI::Cli
|
2
|
+
|
3
|
+
class CliError < StandardError
|
4
|
+
def self.error_code(code = nil)
|
5
|
+
define_method(:error_code) { code }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class UnknownCommand < CliError; error_code(100); end
|
10
|
+
class TargetMissing < CliError; error_code(102); end
|
11
|
+
class TargetInaccessible < CliError; error_code(103); end
|
12
|
+
|
13
|
+
class TargetError < CliError; error_code(201); end
|
14
|
+
class AuthError < TargetError; error_code(202); end
|
15
|
+
|
16
|
+
class CliExit < CliError; error_code(400); end
|
17
|
+
class GracefulExit < CliExit; error_code(401); end
|
18
|
+
|
19
|
+
end
|
data/lib/cli/runner.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/usage'
|
4
|
+
|
5
|
+
class PI::Cli::Runner
|
6
|
+
|
7
|
+
attr_reader :namespace
|
8
|
+
attr_reader :action
|
9
|
+
attr_reader :args
|
10
|
+
attr_reader :options
|
11
|
+
|
12
|
+
def self.run(args)
|
13
|
+
new(args).run
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(args=[])
|
17
|
+
@args = args
|
18
|
+
@options = { :colorize => true }
|
19
|
+
@exit_status = true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Collect all the available options for all commands
|
23
|
+
# Some duplicates exists to capture all scenarios
|
24
|
+
def parse_options!
|
25
|
+
opts_parser = OptionParser.new do |opts|
|
26
|
+
opts.banner = "\nAvailable options:\n\n"
|
27
|
+
# generic tracing and debugging
|
28
|
+
opts.on('-t [TKEY]') { |tkey| @options[:trace] = tkey || true }
|
29
|
+
opts.on('--trace [TKEY]') { |tkey| @options[:trace] = tkey || true }
|
30
|
+
opts.on('-q', '--quiet') { @options[:quiet] = true }
|
31
|
+
opts.on('--verbose') { @options[:verbose] = true }
|
32
|
+
opts.on('--json') { @options[:json] = true }
|
33
|
+
opts.on('-v', '--version') { set_cmd(:misc, :version) }
|
34
|
+
opts.on('-h', '--help') { puts "#{command_usage}\n"; exit }
|
35
|
+
opts.on_tail('--options') { puts "#{opts}\n"; exit }
|
36
|
+
end
|
37
|
+
instances_delta_arg = check_instances_delta!
|
38
|
+
@args = opts_parser.parse!(@args)
|
39
|
+
@args.concat instances_delta_arg
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_instances_delta!
|
44
|
+
return unless @args
|
45
|
+
instance_args = @args.select { |arg| /^[-]\d+$/ =~ arg } || []
|
46
|
+
@args.delete_if { |arg| instance_args.include? arg}
|
47
|
+
instance_args
|
48
|
+
end
|
49
|
+
|
50
|
+
def display_help
|
51
|
+
puts command_usage
|
52
|
+
exit
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_cmd(namespace, action, args_range=0)
|
56
|
+
return if @help_only
|
57
|
+
unless args_range == "*" || args_range.is_a?(Range)
|
58
|
+
args_range = (args_range.to_i..args_range.to_i)
|
59
|
+
end
|
60
|
+
|
61
|
+
if args_range == "*" || args_range.include?(@args.size)
|
62
|
+
@namespace = namespace
|
63
|
+
@action = action
|
64
|
+
else
|
65
|
+
@exit_status = false
|
66
|
+
if @args.size > args_range.last
|
67
|
+
usage_error("Too many arguments for [#{action}]: %s" % [ @args[args_range.last..-1].map{|a| "'#{a}'"}.join(', ') ])
|
68
|
+
else
|
69
|
+
usage_error("Not enough arguments for [#{action}]")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_command!
|
75
|
+
# just return if already set, happends with -v, -h
|
76
|
+
return if @namespace && @action
|
77
|
+
|
78
|
+
verb = @args.shift
|
79
|
+
case verb
|
80
|
+
|
81
|
+
###############################################################################
|
82
|
+
# Users
|
83
|
+
###############################################################################
|
84
|
+
|
85
|
+
when 'login'
|
86
|
+
usage('pi login [url]')
|
87
|
+
set_cmd(:user, :login, @args.size == 1 ? 1 : 0)
|
88
|
+
|
89
|
+
when 'logout'
|
90
|
+
usage('pi logout')
|
91
|
+
set_cmd(:user, :logout)
|
92
|
+
|
93
|
+
when 'info'
|
94
|
+
usage ('pi info')
|
95
|
+
set_cmd(:user, :info)
|
96
|
+
|
97
|
+
when 'user'
|
98
|
+
usage ('pi user')
|
99
|
+
set_cmd(:user, :user)
|
100
|
+
|
101
|
+
when 'targets'
|
102
|
+
usage ('pi targets')
|
103
|
+
set_cmd(:user, :targets)
|
104
|
+
|
105
|
+
when 'password'
|
106
|
+
usage ('pi password [newpassword]')
|
107
|
+
set_cmd(:user, :password, @args.size == 1 ? 1 : 0)
|
108
|
+
|
109
|
+
when 'github'
|
110
|
+
usage ('pi github ')
|
111
|
+
set_cmd(:user, :github)
|
112
|
+
|
113
|
+
when 'runtimes'
|
114
|
+
usage('pi runtimes')
|
115
|
+
set_cmd(:user, :runtimes)
|
116
|
+
|
117
|
+
when 'frameworks'
|
118
|
+
usage('pi frameworks')
|
119
|
+
set_cmd(:user, :frameworks)
|
120
|
+
|
121
|
+
when 'version', 'v'
|
122
|
+
usage('pi version')
|
123
|
+
set_cmd(:misc, :version)
|
124
|
+
|
125
|
+
when 'help'
|
126
|
+
display_help if @args.size == 0
|
127
|
+
@help_only = true
|
128
|
+
parse_command!
|
129
|
+
|
130
|
+
when 'options'
|
131
|
+
@args = @args.unshift('--options')
|
132
|
+
parse_options!
|
133
|
+
|
134
|
+
###############################################################################
|
135
|
+
# Projects
|
136
|
+
###############################################################################
|
137
|
+
|
138
|
+
when 'projects'
|
139
|
+
usage('pi projects')
|
140
|
+
set_cmd(:projects, :projects)
|
141
|
+
|
142
|
+
when 'create-project'
|
143
|
+
usage('pi create-project [projectname]')
|
144
|
+
set_cmd(:projects, :create_project, @args.size == 1 ? 1 : 0)
|
145
|
+
|
146
|
+
when 'delete-project'
|
147
|
+
usage('pi delete-project [projectname]')
|
148
|
+
set_cmd(:projects, :delete_project, @args.size == 1 ? 1 : 0)
|
149
|
+
|
150
|
+
when 'upload'
|
151
|
+
usage('pi upload [projectname]')
|
152
|
+
set_cmd(:projects, :upload, @args.size == 1 ? 1 : 0)
|
153
|
+
|
154
|
+
when 'project-events'
|
155
|
+
usage('pi project-events [projectname]')
|
156
|
+
set_cmd(:projects, :project_events, @args.size == 1 ? 1 : 0)
|
157
|
+
|
158
|
+
when 'project-tags'
|
159
|
+
usage('pi project-tags [projectname]')
|
160
|
+
set_cmd(:projects, :project_tags, @args.size == 1 ? 1 : 0)
|
161
|
+
|
162
|
+
when 'project-commits'
|
163
|
+
usage('pi project-commits [projectname]')
|
164
|
+
set_cmd(:projects, :project_commits, @args.size == 1 ? 1 : 0)
|
165
|
+
|
166
|
+
when 'project-apps'
|
167
|
+
usage('pi project-apps [projectname]')
|
168
|
+
set_cmd(:projects, :project_apps, @args.size == 1 ? 1 : 0)
|
169
|
+
|
170
|
+
when 'usage'
|
171
|
+
display basic_usage
|
172
|
+
exit(true)
|
173
|
+
|
174
|
+
else
|
175
|
+
if verb
|
176
|
+
display "pi: Unknown command [#{verb}]"
|
177
|
+
display basic_usage
|
178
|
+
exit(false)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def usage(msg = nil)
|
184
|
+
@usage = msg if msg
|
185
|
+
@usage
|
186
|
+
end
|
187
|
+
|
188
|
+
def usage_error(msg = nil)
|
189
|
+
@usage_error = msg if msg
|
190
|
+
@usage_error
|
191
|
+
end
|
192
|
+
|
193
|
+
def run
|
194
|
+
|
195
|
+
trap('TERM') { print "\nTerminated\n"; exit(false)}
|
196
|
+
|
197
|
+
parse_options!
|
198
|
+
|
199
|
+
@options[:colorize] = false unless STDOUT.tty?
|
200
|
+
|
201
|
+
PI::Cli::Config.colorize = @options.delete(:colorize)
|
202
|
+
PI::Cli::Config.trace = @options.delete(:trace)
|
203
|
+
PI::Cli::Config.output ||= STDOUT unless @options[:quiet]
|
204
|
+
|
205
|
+
parse_command!
|
206
|
+
if @namespace && @action
|
207
|
+
eval("PI::Cli::Command::#{@namespace.to_s.capitalize}").new(@options).send(@action.to_sym, *@args)
|
208
|
+
elsif @help_only || @usage
|
209
|
+
display_usage
|
210
|
+
else
|
211
|
+
display basic_usage
|
212
|
+
exit(false)
|
213
|
+
end
|
214
|
+
|
215
|
+
rescue OptionParser::InvalidOption => e
|
216
|
+
rescue OptionParser::AmbiguousOption => e
|
217
|
+
puts(e.message.red)
|
218
|
+
puts("\n")
|
219
|
+
puts(basic_usage)
|
220
|
+
@exit_status = false
|
221
|
+
rescue PI::Client::AuthError => e
|
222
|
+
if PI::Cli::Config.auth_token.nil?
|
223
|
+
puts "Login Required".red
|
224
|
+
else
|
225
|
+
puts "Not Authorized".red
|
226
|
+
end
|
227
|
+
@exit_status = false
|
228
|
+
rescue PI::Client::TargetError, PI::Client::NotFound, PI::Client::BadTarget => e
|
229
|
+
puts e.message.red
|
230
|
+
@exit_status = false
|
231
|
+
rescue PI::Client::HTTPException => e
|
232
|
+
puts e.message.red
|
233
|
+
@exit_status = false
|
234
|
+
rescue PI::Cli::GracefulExit => e
|
235
|
+
# Redirected commands end up generating this exception (kind of goto)
|
236
|
+
rescue PI::Cli::CliExit => e
|
237
|
+
puts e.message.red
|
238
|
+
@exit_status = false
|
239
|
+
rescue PI::Cli::CliError => e
|
240
|
+
say("Error #{e.error_code}: #{e.message}".red)
|
241
|
+
@exit_status = false
|
242
|
+
rescue SystemExit => e
|
243
|
+
@exit_status = e.success?
|
244
|
+
rescue SyntaxError => e
|
245
|
+
puts e.message.red
|
246
|
+
puts e.backtrace
|
247
|
+
@exit_status = false
|
248
|
+
rescue Interrupt => e
|
249
|
+
say("\nInterrupted".red)
|
250
|
+
@exit_status = false
|
251
|
+
rescue => e
|
252
|
+
puts e.message.red
|
253
|
+
puts e.backtrace
|
254
|
+
@exit_status = false
|
255
|
+
ensure
|
256
|
+
say("\n")
|
257
|
+
@exit_status == true if @exit_status.nil?
|
258
|
+
if @options[:verbose]
|
259
|
+
if @exit_status
|
260
|
+
puts "[#{@namespace}:#{@action}] SUCCEEDED".green
|
261
|
+
else
|
262
|
+
puts "[#{@namespace}:#{@action}] FAILED".red
|
263
|
+
end
|
264
|
+
say("\n")
|
265
|
+
end
|
266
|
+
exit(@exit_status)
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|