startapp 0.1.11 → 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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/features/app_feature.rb +25 -0
  4. data/features/core_feature.rb +4 -2
  5. data/features/members_feature.rb +21 -1
  6. data/lib/rhc/commands/app.rb +64 -13
  7. data/lib/rhc/commands/apps.rb +7 -3
  8. data/lib/rhc/commands/create.rb +2 -1
  9. data/lib/rhc/commands/domain.rb +2 -1
  10. data/lib/rhc/commands/member.rb +348 -76
  11. data/lib/rhc/commands/scp.rb +15 -6
  12. data/lib/rhc/commands/snapshot.rb +5 -80
  13. data/lib/rhc/commands/team.rb +103 -0
  14. data/lib/rhc/context_helper.rb +57 -15
  15. data/lib/rhc/exceptions.rb +26 -2
  16. data/lib/rhc/git_helpers.rb +1 -1
  17. data/lib/rhc/helpers.rb +1 -1
  18. data/lib/rhc/output_helpers.rb +33 -8
  19. data/lib/rhc/rest/api.rb +1 -1
  20. data/lib/rhc/rest/cartridge.rb +6 -1
  21. data/lib/rhc/rest/client.rb +124 -5
  22. data/lib/rhc/rest/membership.rb +51 -6
  23. data/lib/rhc/rest/mock.rb +82 -8
  24. data/lib/rhc/rest/team.rb +34 -0
  25. data/lib/rhc/rest.rb +1 -0
  26. data/lib/rhc/ssh_helpers.rb +88 -0
  27. data/lib/rhc/usage_templates/command_help.erb +13 -3
  28. data/lib/rhc/usage_templates/command_syntax_help.erb +11 -0
  29. data/spec/direct_execution_helper.rb +1 -0
  30. data/spec/rhc/command_spec.rb +22 -3
  31. data/spec/rhc/commands/app_spec.rb +81 -3
  32. data/spec/rhc/commands/apps_spec.rb +41 -1
  33. data/spec/rhc/commands/domain_spec.rb +1 -1
  34. data/spec/rhc/commands/member_spec.rb +393 -22
  35. data/spec/rhc/commands/scp_spec.rb +14 -3
  36. data/spec/rhc/commands/snapshot_spec.rb +1 -2
  37. data/spec/rhc/commands/team_spec.rb +191 -0
  38. data/spec/rhc/helpers_spec.rb +2 -0
  39. data/spec/rhc/rest_client_spec.rb +23 -0
  40. data/spec/rhc/rest_spec.rb +5 -5
  41. data/spec/spec_helper.rb +36 -0
  42. metadata +8 -2
@@ -2,75 +2,213 @@ require 'rhc/commands/base'
2
2
 
3
3
  module RHC::Commands
4
4
  class Member < Base
5
- summary "Manage membership on domains"
5
+ summary "Manage membership on domains and teams"
6
6
  syntax "<action>"
7
7
  description <<-DESC
8
- Teams of developers can collaborate on applications by adding people to
9
- domains as members: each member has a role (admin, editor, or viewer),
10
- and those roles determine what the user can do with the domain and the
11
- applications contained within.
8
+ Domain Membership
9
+ Developers can collaborate on applications by adding people or teams to
10
+ domains as members. Each member has a role (admin, edit, or view),
11
+ and those roles determine what the user can do with the domain and the
12
+ applications contained within.
12
13
 
13
- Roles:
14
+ Domain Member Roles
14
15
 
15
- view - able to see information about the domain and its apps, but not make any changes
16
- edit - create, update, and delete applications, and has Git and SSH access
17
- admin - can update membership of a domain
16
+ view - able to see the domain and its apps, but not make any changes
17
+ edit - create, update, and delete applications, and has Git and SSH access
18
+ admin - can update membership of a domain
19
+
20
+ The default role granted to domain members is 'edit' - use the '--role'
21
+ argument to specify a different role. When adding and removing members, you
22
+ can use their 'login' value (typically their email or a short unique name for
23
+ them), or their 'id'. Both login and ID are visible via the 'app account'
24
+ command.
25
+
26
+ To see existing members of a domain or application, use:
27
+
28
+ app members -n DOMAIN_NAME [-a APP_NAME]
29
+
30
+ To change the role for a domain member, simply call the update-member command
31
+ with the new role. You cannot change the role of the owner.
32
+
33
+ Team Membership
34
+ People who typically share the same role can be added to a team. The team can
35
+ then be added as a member of a domain, and all of the people in the team will
36
+ inherit the team's role on the domain.
18
37
 
19
- The default role granted to members when added is 'edit' - use the '--role'
20
- argument to use another. When adding and removing members, you can use their
21
- 'login' value (typically their email or a short unique name for them) or their
22
- 'id'. Both login and ID are visible via the 'rhc account' command.
38
+ If a person is a member of multiple teams which are members of a domain, or
39
+ is also added as a domain member individually, their effective role is the
40
+ higher of their individual role or their teams' roles on the domain.
23
41
 
24
- To see existing members of a domain or application, use:
42
+ Team Member Roles
43
+ view - able to see information about the team and its members, and
44
+ has access to all domains the team is a member of
25
45
 
26
- rhc members -n <domain_name> [-a <app_name>]
46
+ To see existing members of a team, use:
47
+
48
+ app members -t TEAM_NAME
27
49
 
28
- To change the role for a user, simply call the add-member command with the new role. You
29
- cannot change the role of the owner.
30
50
  DESC
31
51
  syntax "<action>"
32
52
  default_action :help
33
53
 
34
- summary "List members of a domain or application"
35
- syntax "<domain_or_app_name> [-n DOMAIN_NAME] [-a APP_NAME]"
54
+ summary "List members of a domain, application, or team"
55
+ syntax [
56
+ "<domain_name>[/<app_name>] [--all]",
57
+ "-n DOMAIN_NAME [--all]",
58
+ "-n DOMAIN_NAME -a APP_NAME [--all]",
59
+ nil,
60
+ "-t TEAM_NAME"
61
+ ]
36
62
  description <<-DESC
37
- Show the existing members of a domain or application - you can pass the name
38
- of your domain with '-n', the name of your application with '-a', or combine
39
- them in the first argument to the command like:
63
+ Show the existing members of a domain, application, or team.
64
+
65
+ To show the members of a domain or application, you can pass the name of your
66
+ domain with '-n', the name of your application with '-a', or combine them in
67
+ the first argument to the command like:
68
+ app members <domain_name>[/<app_name>]
40
69
 
41
- rhc members <domain_name>/[<app_name>]
70
+ To show the members of a team, you can pass the name of the team with '-t':
71
+ app members -t TEAM_NAME
42
72
 
43
- The owner is always listed first. To see the unique ID of members, pass
44
- '--ids'.
73
+ The owner is always listed first. To see the unique ID of members, pass '--ids'.
45
74
  DESC
46
75
  option ['--ids'], "Display the IDs of each member", :optional => true
47
- takes_application_or_domain :argument => true
76
+ option ['--all'], "Display all members, including members of teams", :optional => true
77
+ takes_membership_container :argument => true
48
78
  alias_action :members, :root_command => true
49
- def list(path)
50
- target = find_app_or_domain(path)
51
- members = target.members.sort_by{ |m| [m.owner? ? 0 : 1, m.role_weight, m.name] }
52
- show_name = members.any?{ |m| m.name && m.name != m.login }
53
- members.map! do |m|
54
- [
55
- ((m.name || "") if show_name),
56
- m.login || "",
57
- m.owner? ? "#{m.role} (owner)" : m.role,
58
- (m.id if options.ids)
59
- ].compact
79
+ def list(_)
80
+ target = find_membership_container
81
+
82
+ members = target.members
83
+ if options.all
84
+ show_members = members.sort
85
+ else
86
+ show_members = members.select do |m|
87
+ if m.owner?
88
+ true
89
+ elsif m.explicit_role?
90
+ true
91
+ elsif m.from.any? {|f| f["type"] != "team" }
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end.sort
97
+ end
98
+ show_name = show_members.any?{ |m| m.name.presence && m.name != m.login }
99
+ show_login = show_members.any?{ |m| m.login.presence }
100
+
101
+ if show_members.present?
102
+ say table(show_members.map do |member|
103
+ [
104
+ ((member.name || "") if show_name),
105
+ ((member.login || "") if show_login),
106
+ role_description(member, member.teams(members)),
107
+ (member.id if options.ids),
108
+ member.type
109
+ ].compact
110
+ end, :header => [
111
+ ('Name' if show_name),
112
+ ('Login' if show_login),
113
+ 'Role',
114
+ ("ID" if options.ids),
115
+ "Type"
116
+ ].compact)
117
+ else
118
+ info "The #{target.class.model_name.downcase} #{target.name} does not have any members."
119
+ end
120
+
121
+ if show_members.count < members.count
122
+ paragraph do
123
+ info "Pass --all to display all members, including members of teams."
124
+ end
60
125
  end
61
- say table(members, :header => [('Name' if show_name), 'Login', 'Role', ("ID" if options.ids)].compact)
62
126
 
63
127
  0
64
128
  end
65
129
 
66
- summary "Add or update a member on a domain"
67
- syntax "<login> [<login>...] [-n DOMAIN_NAME] [--role view|edit|admin] [--ids]"
130
+ summary "Add a member to a domain or team"
131
+ syntax [
132
+ "-n DOMAIN_NAME [--role view|edit|admin] <login>...",
133
+ "-n DOMAIN_NAME [--role view|edit|admin] <team_name>... --type team [--global]",
134
+ "-n DOMAIN_NAME [--role view|edit|admin] <id>... --ids [--type user|team]",
135
+ nil,
136
+ "-t TEAM_NAME <login>...",
137
+ "-t TEAM_NAME <id>... --ids",
138
+ ]
68
139
  description <<-DESC
69
- Adds or updates members on a domain by passing one or more login
70
- or ids for other people on OpenShift. The login and ID values for each
71
- account are displayed in 'rhc account'. To change the role for a user, simply
72
- call the add-member command with the new role. You cannot change the role of
73
- the owner.
140
+ Domain Membership
141
+ Add members to a domain by passing a user login, team name, or ID for each
142
+ member. The login and ID for each account are displayed in 'app account'.
143
+ To change the role for an existing domain member, use the 'app member update'
144
+ command.
145
+
146
+ Domain Member Roles
147
+ view - able to see information about the domain and its apps,
148
+ but not make any changes
149
+ edit - create, update, and delete applications, and has Git
150
+ and SSH access
151
+ admin - can update membership of a domain
152
+
153
+ The default role granted to domain members is 'edit'.
154
+ Use the '--role' argument for 'view' or 'admin'.
155
+
156
+ Team Membership
157
+ Add users to a team by passing a user login, or ID for each member.
158
+
159
+ Team Member Roles
160
+ view - able to see information about the team and its members, and
161
+ has access to all domains the team is a member of
162
+
163
+ Examples
164
+ app add-member sally joe -n mydomain
165
+ Gives the accounts with logins 'sally' and 'joe' edit access on mydomain
166
+
167
+ app add-member bob --role admin -n mydomain
168
+ Gives the account with login 'bob' admin access on mydomain
169
+
170
+ app add-member team1 --type team --role admin -n mydomain
171
+ Gives your team named 'team1' admin access on mydomain
172
+
173
+ app add-member steve -t team1
174
+ Adds the account with login 'steve' as a member of your team named 'team1'
175
+ DESC
176
+ takes_membership_container :writable => true
177
+ option ['--ids'], "Add member(s) by ID", :optional => true
178
+ option ['-r', '--role ROLE'], "The role to give to each member - view, edit, or admin (default is 'edit' for domains, 'view' for teams)", :type => Role, :optional => true
179
+ option ['--type TYPE'], "Type of member(s) being added - user or team (default is 'user').", :optional => true
180
+ option ['--global'], "Add global-scoped teams as members. Must be used with '--type team'.", :optional => true
181
+ argument :members, "A list of members (user logins, team names, or IDs) to add. Pass --ids to treat this as a list of IDs.", [], :type => :list
182
+ def add(members)
183
+ target = find_membership_container :writable => true
184
+
185
+ role = get_role_option(options, target)
186
+ type = get_type_option(options)
187
+ global = !!options.global
188
+
189
+ raise ArgumentError, 'You must pass at least one member to this command.' unless members.present?
190
+ raise ArgumentError, "The --global option can only be used with '--type team'." if global && !team?(type)
191
+
192
+ say "Adding #{pluralize(members.length, role_name(role))} to #{target.class.model_name.downcase} ... "
193
+
194
+ members = search_teams(members, global).map{|member| member.id} if team?(type) && !options.ids
195
+ target.update_members(changes_for(members, role, type))
196
+
197
+ success "done"
198
+
199
+ 0
200
+ end
201
+
202
+ summary "Update a member on a domain"
203
+ syntax [
204
+ "-n DOMAIN_NAME --role view|edit|admin <login>...",
205
+ "-n DOMAIN_NAME --role view|edit|admin <team_name>... --type team",
206
+ "-n DOMAIN_NAME --role view|edit|admin <id>... --ids [--type user|team]",
207
+ ]
208
+ description <<-DESC
209
+ Updates members on a domain by passing a user login, team name, or ID for
210
+ each member. You can use the 'app members' command to list the existing
211
+ members of your domain. You cannot change the role of the owner.
74
212
 
75
213
  Roles
76
214
  view - able to see information about the domain and its apps,
@@ -79,47 +217,67 @@ module RHC::Commands
79
217
  and SSH access
80
218
  admin - can update membership of a domain
81
219
 
82
- The default role granted to members when added is 'edit' - use the '--role'
83
- argument for 'view' or 'admin'.
84
-
85
220
  Examples
86
- rhc add-member sally joe -n mydomain
87
- Gives the accounts with logins 'sally' and 'joe' edit access on mydomain
221
+ app update-member -n mydomain --role view bob
222
+ Adds or updates the user with login 'bob' to 'admin' role on mydomain
88
223
 
89
- rhc add-member bob@example.com --role admin -n mydomain
90
- Gives the account with login 'bob@example.com' admin access on mydomain
224
+ app update-member -n mydomain --role admin team1 --type team
225
+ Updates the team member with name 'team1' to the 'admin' role on mydomain
226
+
227
+ app update-member -n mydomain --role admin team1_id --ids --type team
228
+ Adds or updates the team with ID 'team1_id' to the 'admin' role on mydomain
91
229
 
92
230
  DESC
93
231
  takes_domain
94
- option ['--ids'], "Treat the arguments as a list of IDs", :optional => true
95
- option ['-r', '--role ROLE'], "The role to give to each member - view, edit, or admin (default 'edit')", :type => Role, :optional => true
96
- argument :members, "A list of members logins to add. Pass --ids to treat this as a list of IDs.", [], :type => :list
97
- def add(members)
232
+ option ['--ids'], "Update member(s) by ID", :optional => true
233
+ option ['-r', '--role ROLE'], "The role to give to each member - view, edit, or admin", :type => Role, :optional => false
234
+ option ['--type TYPE'], "Type of member(s) being updated - user or team (default is 'user').", :optional => true
235
+ argument :members, "A list of members (user logins, team names, or IDs) to update. Pass --ids to treat this as a list of IDs.", [], :type => :list
236
+ def update(members)
98
237
  target = find_domain
99
- role = options.role || 'edit'
100
- raise ArgumentError, 'You must pass one or more logins or ids to this command' unless members.present?
101
- say "Adding #{pluralize(members.length, role_name(role))} to #{target.class.model_name.downcase} ... "
102
- target.update_members(changes_for(members, role))
238
+ role = get_role_option(options, target)
239
+ type = get_type_option(options)
240
+
241
+ raise ArgumentError, 'You must pass at least one member to this command.' unless members.present?
242
+
243
+ say "Updating #{pluralize(members.length, role_name(role))} to #{target.class.model_name.downcase} ... "
244
+
245
+ members = search_team_members(target.members, members).map{|member| member.id} if team?(type) && !options.ids
246
+ target.update_members(changes_for(members, role, type))
247
+
103
248
  success "done"
104
249
 
105
250
  0
106
251
  end
107
252
 
108
- summary "Remove a member from a domain"
109
- syntax "<login> [<login>...] [-n DOMAIN_NAME] [--ids]"
253
+ summary "Remove a member from a domain or team"
254
+ syntax [
255
+ "-n DOMAIN_NAME <login>...",
256
+ "-n DOMAIN_NAME <team_name>... --type team",
257
+ "-n DOMAIN_NAME <id>... --ids [--type user|team]",
258
+ nil,
259
+ "-t TEAM_NAME <login>...",
260
+ "-t TEAM_NAME <id>... --ids",
261
+ ]
110
262
  description <<-DESC
111
- Remove members on a domain by passing one or more login or ids for each
263
+ Remove members from a domain by passing a user login, team name, or ID for each
264
+ member you wish to remove. View the list of existing members with
265
+ app members -n DOMAIN_NAME
266
+
267
+ Remove members from a team by passing a user login, or ID for each
112
268
  member you wish to remove. View the list of existing members with
113
- 'rhc members <domain_name>'.
269
+ app members -t TEAM_NAME
114
270
 
115
- Pass '--all' to remove all but the owner from the domain.
271
+ Pass '--all' to remove all members but the owner.
116
272
  DESC
117
- takes_domain
118
- option ['--ids'], "Treat the arguments as a list of IDs"
119
- option ['--all'], "Remove all members from this domain."
120
- argument :members, "Member logins to remove from the domain. Pass --ids to treat this as a list of IDs.", [], :type => :list
273
+ takes_membership_container :writable => true
274
+ option ['--ids'], "Remove member(s) by ID."
275
+ option ['--all'], "Remove all members"
276
+ option ['--type TYPE'], "Type of member(s) being removed - user or team (default is 'user').", :optional => true
277
+ argument :members, "A list of members (user logins, team names, or IDs) to remove. Pass --ids to treat this as a list of IDs.", [], :type => :list
121
278
  def remove(members)
122
- target = find_domain
279
+ target = find_membership_container :writable => true
280
+ type = get_type_option(options)
123
281
 
124
282
  if options.all
125
283
  say "Removing all members from #{target.class.model_name.downcase} ... "
@@ -127,9 +285,13 @@ module RHC::Commands
127
285
  success "done"
128
286
 
129
287
  else
130
- raise ArgumentError, 'You must pass one or more logins or ids to this command' unless members.present?
288
+ raise ArgumentError, 'You must pass at least one member to this command.' unless members.present?
289
+
131
290
  say "Removing #{pluralize(members.length, 'member')} from #{target.class.model_name.downcase} ... "
132
- target.update_members(changes_for(members, 'none'))
291
+
292
+ members = search_team_members(target.members, members).map{|member| member.id} if team?(type) && !options.ids
293
+ target.update_members(changes_for(members, 'none', type))
294
+
133
295
  success "done"
134
296
  end
135
297
 
@@ -137,12 +299,122 @@ module RHC::Commands
137
299
  end
138
300
 
139
301
  protected
140
- def changes_for(members, role)
302
+ def get_role_option(options, target)
303
+ options.role || target.default_member_role
304
+ end
305
+
306
+ def get_type_option(options)
307
+ type = options.__hash__[:type]
308
+ type || 'user'
309
+ end
310
+
311
+ def changes_for(members, role, type)
141
312
  members.map do |m|
142
- h = {:role => role}
143
- h[options.ids ? :id : :login] = m
313
+ h = {:role => role, :type => type}
314
+ h[options.ids || team?(type) ? :id : :login] = m
144
315
  h
145
316
  end
146
317
  end
318
+
319
+ def team?(type)
320
+ type == 'team'
321
+ end
322
+
323
+ def search_teams(team_names, global=false)
324
+ r = []
325
+ team_names.each do |team_name|
326
+ teams_for_name =
327
+ global ?
328
+ rest_client.search_teams(team_name, global) :
329
+ rest_client.search_owned_teams(team_name)
330
+
331
+ team_for_name = nil
332
+ suggestions = nil
333
+
334
+ if (exact_matches = teams_for_name.select {|t| t.name == team_name }).present?
335
+ if exact_matches.length == 1
336
+ team_for_name = exact_matches.first
337
+ else
338
+ raise RHC::TeamNotFoundException.new("There is more than one team named '#{team_name}'. " +
339
+ "Please use the --ids flag and specify the exact id of the team you want to manage.")
340
+ end
341
+
342
+ elsif (case_insensitive_matches = teams_for_name.select {|t| t.name =~ /^#{Regexp.escape(team_name)}$/i }).present?
343
+ if case_insensitive_matches.length == 1
344
+ team_for_name = case_insensitive_matches.first
345
+ else
346
+ suggestions = case_insensitive_matches
347
+ end
348
+
349
+ else
350
+ suggestions = teams_for_name
351
+ end
352
+
353
+
354
+ if team_for_name
355
+ r << team_for_name
356
+ elsif suggestions.present?
357
+ msg = global ? "No global team found with the name '#{team_name}'." : "You do not have a team named '#{team_name}'."
358
+ raise RHC::TeamNotFoundException.new(msg + " Did you mean one of the following?\n#{suggestions[0..50].map(&:name).join(", ")}")
359
+ else
360
+ msg = global ? "No global team found with the name '#{team_name}'." : "You do not have a team named '#{team_name}'."
361
+ raise RHC::TeamNotFoundException.new(msg)
362
+ end
363
+
364
+ end
365
+ r.flatten
366
+ end
367
+
368
+ def search_team_members(members, names)
369
+ r = []
370
+ team_members = members.select(&:team?)
371
+ names.each do |name|
372
+
373
+ team_for_name = nil
374
+ suggestions = nil
375
+
376
+ if (exact_matches = team_members.select{|team| team.name == name }).present?
377
+ if exact_matches.length == 1
378
+ team_for_name = exact_matches.first
379
+ else
380
+ raise RHC::MemberNotFoundException.new("There is more than one member team named '#{name}'. " +
381
+ "Please use the --ids flag and specify the exact id of the team you want to manage.")
382
+ end
383
+
384
+ elsif (case_insensitive_matches = team_members.select{|team| team.name =~ /^#{Regexp.escape(name)}$/i}).present?
385
+ if case_insensitive_matches.length == 1
386
+ team_for_name = case_insensitive_matches.first
387
+ else
388
+ suggestions = case_insensitive_matches
389
+ end
390
+
391
+ else
392
+ suggestions = team_members.select{|t| t.name =~ /#{Regexp.escape(name)}/i}
393
+ end
394
+
395
+ if team_for_name
396
+ r << team_for_name
397
+ elsif suggestions.present?
398
+ raise RHC::MemberNotFoundException.new("No member team found with the name '#{name}'. " +
399
+ "Did you mean one of the following?\n#{suggestions[0..50].map(&:name).join(", ")}")
400
+ else
401
+ raise RHC::MemberNotFoundException.new("No member team found with the name '#{name}'.")
402
+ end
403
+
404
+ end
405
+ r.flatten
406
+ end
407
+
408
+ def role_description(member, teams=[])
409
+ if member.owner?
410
+ "#{member.role} (owner)"
411
+ elsif member.explicit_role != member.role && member.from.all? {|f| f['type'] == 'domain'}
412
+ "#{member.role} (via domain)"
413
+ elsif member.explicit_role != member.role && teams.present? && (teams_with_role = teams.select{|t| t.role == member.role }).present?
414
+ "#{member.role} (via #{teams_with_role.map(&:name).sort.join(', ')})"
415
+ else
416
+ member.role
417
+ end
418
+ end
147
419
  end
148
- end
420
+ end
@@ -13,7 +13,7 @@ module RHC::Commands
13
13
  cartridge) by default.
14
14
 
15
15
  Examples:
16
- Uploading a file from your workding directory to your app-root/data directory
16
+ Uploading a file from your working directory to your app-root/data directory
17
17
  app scp myapp upload somefile.txt app-root/data
18
18
 
19
19
  Downloading a file from your app-root/data directory to your working directory
@@ -21,9 +21,9 @@ module RHC::Commands
21
21
  DESC
22
22
  syntax "[<app> --] <action> <local_path> <remote_path>"
23
23
  takes_application :argument => true
24
- argument :action, "Transfer direction: upload|download", ["-t", "--transfer_direction upload|download"], :optional => false
25
- argument :local_path, "Local filesystem path", ["-l", "--local_path file_path"], :optional => false
26
- argument :remote_path, "Remote filesystem path", ["-r", "--remote_path file_path"], :optional => false
24
+ argument :action, "Transfer direction: upload|download", ["-t", "--transfer-direction upload|download"], :optional => false
25
+ argument :local_path, "Local filesystem path", ["-f", "--local-path file_path"], :optional => false
26
+ argument :remote_path, "Remote filesystem path", ["-r", "--remote-path file_path"], :optional => false
27
27
  alias_action 'app scp', :root_command => true
28
28
  def run(_, action, local_path, remote_path)
29
29
  rest_app = find_app
@@ -34,17 +34,26 @@ module RHC::Commands
34
34
 
35
35
  begin
36
36
  start_time = Time.now
37
+ last_sent = nil
37
38
  Net::SCP.send("#{action}!".to_sym, ssh_opts[1], ssh_opts[0], (action == 'upload' ? local_path : remote_path), (action == 'upload' ? remote_path : local_path)) do |ch, name, sent, total|
38
39
  #:nocov:
39
- $stderr.print "\r #{action}ing #{name}: #{((sent.to_f/total.to_f)*100).to_i}% complete. #{sent}/#{total} bytes transferred " + (sent == total ? "in #{Time.now - start_time} seconds \n" : "")
40
+ if sent != last_sent
41
+ last_sent = sent
42
+ complete = total == 0 ? 100 : ((sent.to_f/total.to_f)*100).to_i
43
+ $stderr.print "\r #{action}ing #{name}: #{complete}% complete. #{sent}/#{total} bytes transferred " + (sent == total ? "in #{Time.now - start_time} seconds \n" : "")
44
+ end
40
45
  #:nocov:
41
46
  end
42
47
  rescue Errno::ECONNREFUSED
43
48
  raise RHC::SSHConnectionRefused.new(ssh_opts[0], ssh_opts[1])
44
49
  rescue SocketError => e
45
50
  raise RHC::ConnectionFailed, "The connection to #{ssh_opts[1]} failed: #{e.message}"
51
+ rescue Net::SSH::AuthenticationFailed => e
52
+ debug_error e
53
+ raise RHC::SSHAuthenticationFailed.new(ssh_opts[1], ssh_opts[0])
46
54
  rescue Net::SCP::Error => e
47
- raise RHC::RemoteFileOrPathNotFound.new("Remote file, file_path, or directory could not be found.")
55
+ debug_error e
56
+ raise RHC::RemoteFileOrPathNotFound.new("An unknown error occurred: #{e.message}")
48
57
  end
49
58
  end
50
59