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/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
@@ -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