MuranoCLI 3.2.0.beta.1 → 3.2.0.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/.trustme.plugin +137 -0
  4. data/.trustme.sh +217 -117
  5. data/.trustme.vim +9 -3
  6. data/Gemfile +9 -3
  7. data/MuranoCLI.gemspec +8 -5
  8. data/Rakefile +1 -0
  9. data/dockers/Dockerfile.2.2.9 +6 -3
  10. data/dockers/Dockerfile.2.3.6 +6 -3
  11. data/dockers/Dockerfile.2.4.3 +6 -3
  12. data/dockers/Dockerfile.2.5.0 +6 -3
  13. data/dockers/Dockerfile.GemRelease +10 -8
  14. data/dockers/Dockerfile.m4 +23 -5
  15. data/dockers/docker-test.sh +65 -28
  16. data/docs/completions/murano_completion-bash +751 -57
  17. data/docs/develop.rst +10 -9
  18. data/lib/MrMurano/AccountBase.rb +95 -6
  19. data/lib/MrMurano/Commander-Entry.rb +9 -4
  20. data/lib/MrMurano/Config-Migrate.rb +2 -0
  21. data/lib/MrMurano/Config.rb +94 -26
  22. data/lib/MrMurano/Content.rb +1 -1
  23. data/lib/MrMurano/Exchange.rb +77 -42
  24. data/lib/MrMurano/Gateway.rb +1 -1
  25. data/lib/MrMurano/HttpAuthed.rb +20 -7
  26. data/lib/MrMurano/Logs.rb +10 -1
  27. data/lib/MrMurano/ProjectFile.rb +1 -1
  28. data/lib/MrMurano/ReCommander.rb +129 -73
  29. data/lib/MrMurano/Solution-ServiceConfig.rb +18 -11
  30. data/lib/MrMurano/Solution-Services.rb +78 -50
  31. data/lib/MrMurano/Solution-Users.rb +1 -1
  32. data/lib/MrMurano/Solution.rb +13 -63
  33. data/lib/MrMurano/SyncUpDown-Core.rb +185 -77
  34. data/lib/MrMurano/SyncUpDown-Item.rb +29 -4
  35. data/lib/MrMurano/SyncUpDown.rb +11 -11
  36. data/lib/MrMurano/Webservice-Cors.rb +1 -1
  37. data/lib/MrMurano/Webservice-Endpoint.rb +28 -17
  38. data/lib/MrMurano/Webservice-File.rb +103 -43
  39. data/lib/MrMurano/commands/domain.rb +1 -0
  40. data/lib/MrMurano/commands/element.rb +585 -0
  41. data/lib/MrMurano/commands/exchange.rb +211 -204
  42. data/lib/MrMurano/commands/gb.rb +1 -0
  43. data/lib/MrMurano/commands/globals.rb +17 -7
  44. data/lib/MrMurano/commands/init.rb +115 -101
  45. data/lib/MrMurano/commands/keystore.rb +1 -1
  46. data/lib/MrMurano/commands/logs.rb +2 -1
  47. data/lib/MrMurano/commands/postgresql.rb +17 -7
  48. data/lib/MrMurano/commands/service.rb +572 -0
  49. data/lib/MrMurano/commands/show.rb +7 -3
  50. data/lib/MrMurano/commands/solution.rb +2 -1
  51. data/lib/MrMurano/commands/solution_picker.rb +31 -15
  52. data/lib/MrMurano/commands/status.rb +205 -169
  53. data/lib/MrMurano/commands/sync.rb +70 -38
  54. data/lib/MrMurano/commands/token.rb +59 -14
  55. data/lib/MrMurano/commands/usage.rb +1 -0
  56. data/lib/MrMurano/commands.rb +2 -0
  57. data/lib/MrMurano/hash.rb +91 -0
  58. data/lib/MrMurano/http.rb +55 -6
  59. data/lib/MrMurano/makePretty.rb +47 -0
  60. data/lib/MrMurano/optparse.rb +60 -45
  61. data/lib/MrMurano/variegated/TruthyFalsey.rb +48 -0
  62. data/lib/MrMurano/variegated/ruby_dig.rb +64 -0
  63. data/lib/MrMurano/verbosing.rb +113 -3
  64. data/lib/MrMurano/version.rb +1 -1
  65. data/spec/Account_spec.rb +34 -20
  66. data/spec/Business_spec.rb +12 -9
  67. data/spec/Config_spec.rb +7 -1
  68. data/spec/Content_spec.rb +17 -1
  69. data/spec/GatewayBase_spec.rb +5 -2
  70. data/spec/GatewayDevice_spec.rb +4 -2
  71. data/spec/GatewayResource_spec.rb +4 -1
  72. data/spec/GatewaySettings_spec.rb +4 -1
  73. data/spec/HttpAuthed_spec.rb +73 -0
  74. data/spec/Http_spec.rb +32 -35
  75. data/spec/ProjectFile_spec.rb +1 -1
  76. data/spec/Solution-ServiceConfig_spec.rb +4 -1
  77. data/spec/Solution-ServiceEventHandler_spec.rb +6 -3
  78. data/spec/Solution-ServiceModules_spec.rb +4 -1
  79. data/spec/Solution-UsersRoles_spec.rb +4 -1
  80. data/spec/Solution_spec.rb +4 -1
  81. data/spec/SyncUpDown_spec.rb +1 -1
  82. data/spec/Webservice-Cors_spec.rb +4 -1
  83. data/spec/Webservice-Endpoint_spec.rb +9 -6
  84. data/spec/Webservice-File_spec.rb +17 -4
  85. data/spec/Webservice-Setting_spec.rb +6 -2
  86. data/spec/_workspace.rb +2 -0
  87. data/spec/cmd_common.rb +42 -13
  88. data/spec/cmd_content_spec.rb +17 -7
  89. data/spec/cmd_device_spec.rb +1 -1
  90. data/spec/cmd_domain_spec.rb +2 -2
  91. data/spec/cmd_element_spec.rb +400 -0
  92. data/spec/cmd_exchange_spec.rb +2 -2
  93. data/spec/cmd_init_spec.rb +59 -25
  94. data/spec/cmd_keystore_spec.rb +6 -3
  95. data/spec/cmd_link_spec.rb +10 -5
  96. data/spec/cmd_logs_spec.rb +1 -1
  97. data/spec/cmd_setting_application_spec.rb +18 -15
  98. data/spec/cmd_setting_product_spec.rb +7 -7
  99. data/spec/cmd_status_spec.rb +27 -17
  100. data/spec/cmd_syncdown_application_spec.rb +30 -3
  101. data/spec/cmd_syncdown_both_spec.rb +72 -18
  102. data/spec/cmd_syncup_spec.rb +71 -5
  103. data/spec/cmd_token_spec.rb +2 -2
  104. data/spec/cmd_usage_spec.rb +2 -2
  105. data/spec/dry_run_formatter.rb +27 -0
  106. data/spec/fixtures/dumped_config +8 -0
  107. data/spec/fixtures/exchange_element/element-show.json +1 -0
  108. data/spec/fixtures/exchange_element/swagger-mur-6407__10k.yaml +282 -0
  109. data/spec/fixtures/exchange_element/swagger-mur-6407__20k.yaml +588 -0
  110. data/spec/variegated_TruthyFalsey_spec.rb +29 -0
  111. metadata +51 -25
@@ -6,6 +6,7 @@
6
6
  # Unauthorized copying of this file is strictly prohibited.
7
7
 
8
8
  require 'MrMurano/AccountBase'
9
+ require 'MrMurano/Business'
9
10
  require 'MrMurano/Exchange-Element'
10
11
  require 'MrMurano/verbosing'
11
12
 
@@ -19,56 +20,47 @@ module MrMurano
19
20
  super
20
21
  end
21
22
 
23
+ def put(element_id, body={}, &block)
24
+ super('exchange/' + bid + '/element/' + element_id, body, &block)
25
+ end
26
+
22
27
  def element(element_id)
28
+ element_by_id(element_id)
29
+ end
30
+
31
+ def element_by_id(element_id)
23
32
  ret = get('exchange/' + bid + '/element/' + element_id)
24
33
  return nil unless ret.is_a?(Hash) && !ret.key?(:error)
25
34
  ret
26
35
  end
27
36
 
28
- def fetch_type(part)
29
- # [lb] not super happy about mixing presentation with other logic
30
- # but this is quick and dirty.
31
- case part
32
- when '/element/'
33
- qualifier = 'All'
34
- when '/purchase/'
35
- qualifier = 'Purchased'
36
- else
37
- raise 'Unexpected error'
38
- end
39
- whirly_start("Fetching #{qualifier} Elements...")
37
+ def element_quick(element_id)
38
+ # Check if ID, a 24-character hexadecimal string.
39
+ smells_like_id = element_id =~ /^[\h]{24}$/
40
+ return element_by_id(element_id) if smells_like_id
41
+ nil
42
+ end
40
43
 
41
- # FIXME/2017-09-27: Support Pagination. BizAPI accepts four settings:
42
- # type, offset, limit, and select.
43
- # <bizapi>/lib/api/route/exchange/schemas/element.js
44
- ret = get('exchange/' + bid + part) do |request, http|
45
- response = http.request(request)
46
- case response
47
- when Net::HTTPSuccess
48
- workit_response(response)
49
- else
50
- showHttpError(request, response)
51
- end
52
- end
53
- whirly_stop
54
- return [] unless ret.is_a?(Hash) && !ret.key?(:error)
55
- return [] unless ret.key?(:items)
56
- unless ret[:count] == ret[:items].length
57
- warning(
58
- 'Unexpected: ret[:count] != ret[:items].length: ' \
59
- "#{ret[:count]} != #{ret[:items].length}"
60
- )
44
+ def elements(skip_purchased: false, **opts)
45
+ # Get the user's Business metadata, including their Business tier.
46
+ overview if @ometa.nil?
47
+
48
+ elems = nil
49
+ id_maybe = opts[:filter_id] || opts[:filter_fuzzy]
50
+ if id_maybe && skip_purchased
51
+ elem = element_quick(id_maybe)
52
+ elems = [MrMurano::ExchangeElement.new(elem)] unless elem.nil?
61
53
  end
62
- ret[:items]
54
+ elems = elements_all(skip_purchased: skip_purchased) if elems.nil?
55
+
56
+ prepare_elements(elems, **opts)
63
57
  end
64
58
 
65
- def elements(**opts)
59
+ def elements_all(skip_purchased: false)
66
60
  lookp = {}
67
- # Get the user's Business metadata, including their Business tier.
68
- overview if @ometa.nil?
69
61
  elems = fetch_elements(lookp)
70
- fetch_purchased(lookp)
71
- prepare_elements(elems, **opts)
62
+ fetch_purchased(lookp) unless skip_purchased
63
+ elems
72
64
  end
73
65
 
74
66
  def fetch_elements(lookp)
@@ -82,9 +74,11 @@ module MrMurano
82
74
  end
83
75
  end
84
76
 
85
- def fetch_purchased(lookp)
77
+ def fetch_purchased(lookp=nil, services_only: false)
78
+ query = [%w[type service]] if services_only
86
79
  # Fetch the list of Purchased elements.
87
- items = fetch_type('/purchase/')
80
+ items = fetch_type('/purchase/', query)
81
+ return items if lookp.nil?
88
82
  # Update the list of all Elements to indicate which have been purchased.
89
83
  items.each do |meta|
90
84
  elem = lookp[meta[:elementId]]
@@ -99,15 +93,56 @@ module MrMurano
99
93
  )
100
94
  end
101
95
  else
102
- warning("Unexpected: No Element found for Exchange Purchase: elementId: #{meta[:elementId]}")
96
+ warning(
97
+ "No Exchange Element for Purchase: elementId: #{meta[:elementId]}"
98
+ )
99
+ end
100
+ end
101
+ end
102
+
103
+ def fetch_type(part, query=nil)
104
+ # [lb] not super happy about mixing presentation with other logic
105
+ # but this is quick and dirty.
106
+ case part
107
+ when '/element/'
108
+ qualifier = 'All'
109
+ when '/purchase/'
110
+ qualifier = 'Purchased'
111
+ else
112
+ raise 'Unexpected error'
113
+ end
114
+ whirly_start("Fetching #{qualifier} Elements...")
115
+
116
+ # FIXME/2017-09-27: Support Pagination. BizAPI accepts four settings:
117
+ # type, offset, limit, and select.
118
+ # <bizapi>/lib/api/route/exchange/schemas/element.js
119
+ ret = get('exchange/' + bid + part, query) do |request, http|
120
+ response = http.request(request)
121
+ case response
122
+ when Net::HTTPSuccess
123
+ workit_response(response)
124
+ else
125
+ showHttpError(request, response)
103
126
  end
104
127
  end
128
+ whirly_stop
129
+ return [] unless ret.is_a?(Hash) && !ret.key?(:error)
130
+ return [] unless ret.key?(:items)
131
+ unless ret[:count] == ret[:items].length
132
+ warning(
133
+ 'Unexpected: ret[:count] != ret[:items].length: ' \
134
+ "#{ret[:count]} != #{ret[:items].length}"
135
+ )
136
+ end
137
+ ret[:items]
105
138
  end
106
139
 
107
140
  def verify_purchase_vs_element(elem, key, val)
108
141
  elem.send(key) == val
109
142
  rescue NoMethodError
110
- verbose("Unexpected: Exchange Element missing key found in Purchase Element: #{key}")
143
+ verbose(
144
+ "Unexpected: Exchange Element missing key found in Purchase Element: #{key}"
145
+ )
111
146
  true
112
147
  end
113
148
 
@@ -386,7 +386,7 @@ module MrMurano
386
386
 
387
387
  ## Get one device
388
388
  # @param id [String] The identity to fetch
389
- def fetch(id)
389
+ def fetch(id, _untainted=false)
390
390
  get("/#{CGI.escape(id.to_s)}")
391
391
  end
392
392
 
@@ -22,14 +22,12 @@ module MrMurano
22
22
  include Http
23
23
  include Verbose
24
24
 
25
+ attr_accessor :logging_on
25
26
  attr_accessor :login_info
26
27
  attr_reader :token_biz
27
28
 
28
29
  def initialize
29
- @login_info = nil
30
- @pass_file = nil
31
- @token_file = nil
32
- token_reset
30
+ credentials_reset
33
31
  end
34
32
 
35
33
  def add_headers(request)
@@ -52,6 +50,15 @@ module MrMurano
52
50
  request
53
51
  end
54
52
 
53
+ def credentials_reset
54
+ @logging_on = false
55
+ @login_info = nil
56
+ @pass_file = nil
57
+ @token_file = nil
58
+ token_reset
59
+ @password_user = ''
60
+ end
61
+
55
62
  # ---------------------------------------------------------------------
56
63
 
57
64
  def pass_file
@@ -118,8 +125,10 @@ module MrMurano
118
125
  @token_biz
119
126
  end
120
127
 
121
- def token_reset
122
- @token_biz = ''
128
+ def token_reset(new_token='')
129
+ # Setting the empty token will cause it to be re-fetched
130
+ # on next login_info.
131
+ @token_biz = new_token
123
132
  end
124
133
 
125
134
  def ensure_token!
@@ -140,7 +149,7 @@ module MrMurano
140
149
  acc_token = token_file.lookup(net_host, user_name)
141
150
  if acc_token.to_s.empty?
142
151
  nil
143
- elsif acc_token !~ /^[a-fA-F0-9]+$/
152
+ elsif !token_looks_legit(acc_token)
144
153
  warning "Malformed '#{net_host}:#{user_name}' token: #{acc_token}"
145
154
  nil
146
155
  else
@@ -148,6 +157,10 @@ module MrMurano
148
157
  end
149
158
  end
150
159
 
160
+ def token_looks_legit(acc_token)
161
+ (acc_token =~ /^[a-fA-F0-9]{40}$/) == 0
162
+ end
163
+
151
164
  def token_save(net_host=nil, user_name=nil)
152
165
  net_host, user_name = get_host_user_pair(net_host, user_name)
153
166
  return unless $cfg['auth.persist-token']
data/lib/MrMurano/Logs.rb CHANGED
@@ -5,7 +5,16 @@
5
5
  # vim:tw=0:ts=2:sw=2:et:ai
6
6
  # Unauthorized copying of this file is strictly prohibited.
7
7
 
8
- require 'eventmachine'
8
+ begin
9
+ require 'eventmachine'
10
+ rescue StandardError => _err
11
+ # (lb): I started developing in tmux, and I get this error only within it:
12
+ # require 'eventmachine'
13
+ # Unable to load the EventMachine C extension;
14
+ # To use the pure-ruby reactor, require 'em/pure_ruby'
15
+ require 'em/pure_ruby'
16
+ end
17
+
9
18
  require 'faye/websocket'
10
19
 
11
20
  require 'MrMurano/http'
@@ -8,9 +8,9 @@
8
8
  # 2017-07-01: This ordered list hacks around having
9
9
  # individual files include all the files they need.
10
10
 
11
- require 'yaml'
12
11
  require 'json-schema'
13
12
  require 'pathname'
13
+ require 'yaml'
14
14
  require 'MrMurano/verbosing'
15
15
  require 'MrMurano/Config'
16
16
  require 'MrMurano/hash'
@@ -127,12 +127,29 @@ module Commander
127
127
  hooked.check_run_post_hook
128
128
  end
129
129
 
130
+ alias old_args_without_command_name args_without_command_name
131
+ def args_without_command_name
132
+ args_sans = old_args_without_command_name
133
+ args_sans += ['--'] + @positional unless @positional.empty?
134
+ args_sans
135
+ end
136
+
137
+ alias old_remove_global_options remove_global_options
138
+ def remove_global_options(options, args)
139
+ # Look for sole '--' argument, which signals start of positionals.
140
+ i_positional = args.index('--')
141
+ args = args[0, i_positional] unless i_positional.nil?
142
+ old_remove_global_options(options, args)
143
+ end
144
+
130
145
  alias old_parse_global_options parse_global_options
131
146
  def parse_global_options
132
147
  # User can specify, e.g., status.options, to set options for specific commands.
133
148
  defopts = ($cfg["#{active_command.name}.options"] || '').split
134
149
  @args.push(*defopts)
135
- help_hack
150
+ end_of_options_hack
151
+ do_help = help_hack
152
+ $cfg.validate_cmd_business_and_project(active_command) unless do_help
136
153
  parse_global_options_real
137
154
  end
138
155
 
@@ -150,12 +167,21 @@ module Commander
150
167
  exit 2
151
168
  end
152
169
 
170
+ def end_of_options_hack
171
+ @positional = []
172
+ i_positional = @args.index('--')
173
+ return if i_positional.nil?
174
+ @positional = @args[i_positional + 1..-1]
175
+ @args = @args[0, i_positional]
176
+ end
177
+
153
178
  # 2017-08-22: Commander's help infrastructure is either really weak,
154
179
  # or we did something elsewhere that seriously cripples it. In any
155
180
  # case, this fixes all its quirks.
156
181
  def help_hack
157
- do_help = fix_args
182
+ do_help = fix_args_for_help
158
183
  show_alias_help_maybe! if do_help
184
+ do_help
159
185
  end
160
186
 
161
187
  def help_opts
@@ -166,7 +192,7 @@ module Commander
166
192
  %w[-v --version].freeze
167
193
  end
168
194
 
169
- def fix_args
195
+ def fix_args_for_help
170
196
  # If `murano --help` is specified, let rb-commander handle it.
171
197
  # But if `murano command --help` is specified, don't let cmdr
172
198
  # handle it, otherwise it just shows the command description,
@@ -175,79 +201,110 @@ module Commander
175
201
  # 'help' might really be a command argument (e.g., a solution name).
176
202
  # Note: `murano -v`'s active_command is 'help'.
177
203
  do_help = (@args & %w[-h --help]).any? || active_command.name == 'help'
178
- if do_help
179
- # If there are options in addition to --help, then Commander
180
- # runs the command! So remove all options.
181
- #
182
- # E.g., this shows the help for usage:
183
- # $ murano --help usage
184
- # but if a flag is added, the command gets run, e.g.,
185
- # $ murano usage --id 1234 --help
186
- # runs the usage command.
187
- #
188
- # I [lb] walked the code and it looks like when Commander tries to
189
- # validate the args, it doesn't like the --id flag (maybe because
190
- # the "help" command is being used to validate, and it does not
191
- # define any options?). Then it removes the --id switch (after the
192
- # --help switch was previously removed) and tries the command *again*
193
- # (in gems/commander-4.4.3/lib/commander/runner.rb, look for the
194
- # comment, "Remove the offending args and retry"). So in the example
195
- # given above, `murano usage --id 1234 --help`, both the `--id` flag
196
- # and `--help` flag are moved from @args, and then `murano usage 1234`
197
- # is executed (and args are not validated by Commander but merely passed
198
- # to the command action, so `usage` gets args=['1234']). Whadda wreck.
199
- reject_next = false
200
- @args.reject! do |arg|
201
- reject = false
202
- if reject_next
203
- reject = true
204
- reject_next = false
205
- elsif arg.start_with?('-') && !help_opts.include?(arg) && !vers_opts.include?(arg)
206
- reject = true
207
- # See if the next argument should also be consumed.
208
- arg_opts = @options.select do |opt|
209
- if arg =~ /^-[^-]/
210
- # Single char abbrev: look for exact match.
211
- opt[:switches].include?(arg)
212
- else
213
- # Long switch: Commander matches abbrevs of longs...
214
- opt[:switches].any? { |oarg| oarg =~ /^#{arg}/ }
215
- end
216
- end
217
- if arg =~ /^--/
218
- exact = arg_opts.select do |opt|
219
- opt[:switches].include?(arg) || opt[:switches].any? do |sw|
220
- sw.start_with?(arg + ' ')
221
- end
222
- end
223
- arg_opts = exact unless exact.empty?
224
- end
225
- if arg_opts.length > 1
226
- # MAYBE/2017-08-23: Always do this check, not just for help.
227
- ambig = MrMurano::Verbose.fancy_ticks(arg)
228
- match = arg_opts.map do |opt|
229
- opt[:switches].map { |sw| MrMurano::Verbose.fancy_ticks(sw) }.join('|')
230
- end
231
- match = match.flatten
232
- match[-1] = "and #{match[-1]}" if match.length > 1
233
- match = match.join(', ')
234
- MrMurano::Verbose.error("Ambiguous option: #{ambig} matches: #{match}")
235
- exit 2
236
- elsif arg_opts.length == 1
237
- if arg_opts.first[:switches].any? { |opt| opt.start_with?('--') && opt.include?(' ') }
238
- # There's a space in the --long setting, e.g., '--config KEY=VAL',
239
- # so we know there's an argument following.
240
- reject_next = true
241
- end
242
- end
243
- end
244
- reject
245
- end
246
- end
204
+ trim_options_from_args if do_help
247
205
  @purargs = @args - help_opts
248
206
  return do_help if active_command.name == 'help'
249
207
  # Any command other than `murano help` or `murano --help`.
250
208
  return do_help if !do_help || active_command.name.include?(' ')
209
+ # Command is not --help, or it's help for a single-word command.
210
+ cleanup_args_simple_command_help
211
+ do_help
212
+ end
213
+
214
+ # If there are options in addition to --help, then Commander
215
+ # runs the command! So remove all options.
216
+ #
217
+ # E.g., this shows the help for usage:
218
+ #
219
+ # $ murano --help usage
220
+ #
221
+ # but if a flag is added, the command gets run, e.g.,
222
+ #
223
+ # $ murano usage --id 1234 --help
224
+ #
225
+ # runs the usage command.
226
+ #
227
+ # (lb): I walked the code and it looks like when Commander tries to
228
+ # validate the args, it doesn't like the --id flag (maybe because
229
+ # the "help" command is being used to validate, and it does not
230
+ # define any options?). Then it removes the --id switch (after the
231
+ # --help switch was previously removed) and tries the command *again*
232
+ # (in gems/commander-4.4.3/lib/commander/runner.rb, look for the
233
+ # comment, "Remove the offending args and retry"). So in the example
234
+ # given above, `murano usage --id 1234 --help`, both the `--id` flag
235
+ # and `--help` flag are moved from @args, and then `murano usage 1234`
236
+ # is executed (and args are not validated by Commander but merely passed
237
+ # to the command action, so `usage` gets args=['1234']). Whadda wreck.
238
+ def trim_options_from_args
239
+ reject_next = false
240
+ @args.reject! do |arg|
241
+ if reject_next
242
+ reject_next = false
243
+ true
244
+ elsif (
245
+ arg.start_with?('-') &&
246
+ !help_opts.include?(arg) &&
247
+ !vers_opts.include?(arg)
248
+ )
249
+ # See if the next argument should also be consumed.
250
+ arg_opts = find_matching_options(arg)
251
+ arg_opts = find_exact_match_maybe(arg, arg_opts)
252
+ reject_next = must_be_sane_option!(arg_opts)
253
+ true
254
+ else
255
+ false
256
+ end
257
+ end
258
+ end
259
+
260
+ def find_matching_options(arg)
261
+ @options.select do |opt|
262
+ if arg =~ /^-[^-]/
263
+ # Single char abbrev: look for exact match.
264
+ opt[:switches].include?(arg)
265
+ else
266
+ # Long switch: Commander matches abbrevs of longs...
267
+ opt[:switches].any? { |oarg| oarg =~ /^#{arg}/ }
268
+ end
269
+ end
270
+ end
271
+
272
+ def find_exact_match_maybe(arg, arg_opts)
273
+ return arg_opts unless arg =~ /^--/
274
+ exact = arg_opts.select do |opt|
275
+ opt[:switches].include?(arg) || opt[:switches].any? do |sw|
276
+ sw.start_with?(arg + ' ')
277
+ end
278
+ end
279
+ exact.empty? && exact || arg_opts
280
+ end
281
+
282
+ # Returns true if next arg is input to current --option.
283
+ def must_be_sane_option!(arg_opts)
284
+ if arg_opts.length > 1
285
+ # MAYBE/2017-08-23: Always do this check, not just for help.
286
+ ambig = MrMurano::Verbose.fancy_ticks(arg)
287
+ match = arg_opts.map do |opt|
288
+ opt[:switches].map { |sw| MrMurano::Verbose.fancy_ticks(sw) }.join('|')
289
+ end
290
+ match = match.flatten
291
+ match[-1] = "and #{match[-1]}" if match.length > 1
292
+ match = match.join(', ')
293
+ MrMurano::Verbose.error("Ambiguous option: #{ambig} matches: #{match}")
294
+ exit 2
295
+ elsif arg_opts.length == 1
296
+ # See if this option (the only option) has input.
297
+ arg_opts.first[:switches].any? do |opt|
298
+ # There may be a space in a --long option, e.g.,
299
+ # '--config KEY=VAL', which means an argument follows.
300
+ opt.start_with?('--') && opt.include?(' ')
301
+ end
302
+ else
303
+ false
304
+ end
305
+ end
306
+
307
+ def cleanup_args_simple_command_help
251
308
  # This is a single-word command, e.g., 'link', not 'link list',
252
309
  # as in `murano link --help`, not `murano link list --help`.
253
310
  # Positional parameters break Commander. E.g.,
@@ -258,7 +315,6 @@ module Commander
258
315
  @args = @args[0..0]
259
316
  # Add back in the --help if the command is not a subcommand help.
260
317
  @args.push('--help') unless active_command.subcmdgrouphelp
261
- do_help
262
318
  end
263
319
 
264
320
  def show_alias_help_maybe!
@@ -21,8 +21,6 @@ module MrMurano
21
21
  return [] unless ret.is_a?(Hash) && !ret.key?(:error)
22
22
  return [] unless ret.key?(:items)
23
23
  ret[:items]
24
- # MAYBE/2017-08-17:
25
- # sort_by_name(ret[:items])
26
24
  end
27
25
 
28
26
  def search(svc_name)
@@ -33,8 +31,14 @@ module MrMurano
33
31
  super(svc_name, path)
34
32
  end
35
33
 
36
- def fetch(id)
37
- get('/' + id.to_s)
34
+ def fetch(id, _untainted=false, &block)
35
+ must_id! id
36
+ get('/' + id.to_s, &block)
37
+ end
38
+
39
+ def must_id!(id)
40
+ return unless id.nil?
41
+ raise "ID not set, or Service '#{@service_name}' not enabled for this Solution"
38
42
  end
39
43
 
40
44
  def scid_for_name(name)
@@ -69,21 +73,24 @@ module MrMurano
69
73
  )
70
74
  end
71
75
 
72
- def remove(id)
76
+ def remove(id, &block)
77
+ must_id! id
73
78
  return unless remove_item_allowed(id)
74
- delete("/#{id}")
79
+ delete("/#{id}", &block)
75
80
  end
76
81
 
77
- def info(id=scid)
78
- get("/#{id}/info")
82
+ def info(id=scid, &block)
83
+ must_id! id
84
+ get("/#{id}/info", &block)
79
85
  end
80
86
 
81
- def logs(id=scid)
82
- get("/#{id}/logs")
87
+ def logs(id=scid, &block)
88
+ must_id! id
89
+ get("/#{id}/logs", &block)
83
90
  end
84
91
 
85
92
  def call(opid, meth=:get, data=nil, id=scid, &block)
86
- raise "Service '#{@service_name}' not enabled for this Solution" if id.nil?
93
+ must_id! id
87
94
  call = "/#{id}/call/#{opid}"
88
95
  debug "Will call: #{call}"
89
96
  case meth