pi 0.1.13
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 +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
|