MuranoCLI 3.0.0 → 3.0.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +50 -27
  3. data/.trustme.vim +12 -8
  4. data/bin/murano +23 -8
  5. data/docs/basic_example.rst +1 -1
  6. data/docs/completions/murano_completion-bash +88 -0
  7. data/lib/MrMurano/Account.rb +3 -3
  8. data/lib/MrMurano/Business.rb +6 -6
  9. data/lib/MrMurano/Config-Migrate.rb +1 -3
  10. data/lib/MrMurano/Config.rb +16 -8
  11. data/lib/MrMurano/Content.rb +56 -45
  12. data/lib/MrMurano/Gateway.rb +62 -21
  13. data/lib/MrMurano/Mock.rb +27 -19
  14. data/lib/MrMurano/Passwords.rb +7 -7
  15. data/lib/MrMurano/ReCommander.rb +171 -28
  16. data/lib/MrMurano/Setting.rb +38 -40
  17. data/lib/MrMurano/Solution-ServiceConfig.rb +2 -1
  18. data/lib/MrMurano/Solution-Services.rb +196 -61
  19. data/lib/MrMurano/Solution-Users.rb +7 -7
  20. data/lib/MrMurano/Solution.rb +22 -8
  21. data/lib/MrMurano/SolutionId.rb +10 -4
  22. data/lib/MrMurano/SubCmdGroupContext.rb +14 -5
  23. data/lib/MrMurano/SyncAllowed.rb +42 -0
  24. data/lib/MrMurano/SyncUpDown.rb +122 -65
  25. data/lib/MrMurano/Webservice-Cors.rb +9 -3
  26. data/lib/MrMurano/Webservice-Endpoint.rb +39 -33
  27. data/lib/MrMurano/Webservice-File.rb +35 -24
  28. data/lib/MrMurano/commands/business.rb +18 -18
  29. data/lib/MrMurano/commands/content.rb +3 -2
  30. data/lib/MrMurano/commands/devices.rb +137 -22
  31. data/lib/MrMurano/commands/globals.rb +8 -2
  32. data/lib/MrMurano/commands/keystore.rb +3 -2
  33. data/lib/MrMurano/commands/link.rb +13 -13
  34. data/lib/MrMurano/commands/login.rb +3 -2
  35. data/lib/MrMurano/commands/mock.rb +4 -3
  36. data/lib/MrMurano/commands/password.rb +4 -2
  37. data/lib/MrMurano/commands/postgresql.rb +5 -3
  38. data/lib/MrMurano/commands/settings.rb +78 -62
  39. data/lib/MrMurano/commands/show.rb +79 -74
  40. data/lib/MrMurano/commands/solution.rb +6 -4
  41. data/lib/MrMurano/commands/solution_picker.rb +5 -4
  42. data/lib/MrMurano/commands/status.rb +15 -4
  43. data/lib/MrMurano/commands/timeseries.rb +3 -2
  44. data/lib/MrMurano/commands/tsdb.rb +3 -2
  45. data/lib/MrMurano/hash.rb +6 -6
  46. data/lib/MrMurano/http.rb +66 -67
  47. data/lib/MrMurano/makePretty.rb +18 -12
  48. data/lib/MrMurano/progress.rb +9 -2
  49. data/lib/MrMurano/verbosing.rb +14 -2
  50. data/lib/MrMurano/version.rb +2 -2
  51. data/spec/GatewayDevice_spec.rb +190 -149
  52. data/spec/Mock_spec.rb +3 -3
  53. data/spec/Solution-ServiceEventHandler_spec.rb +170 -137
  54. data/spec/SyncUpDown_spec.rb +205 -191
  55. metadata +3 -2
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.22 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -59,10 +59,16 @@ module MrMurano
59
59
  if !file.nil?
60
60
  data = YAML.load_file(file)
61
61
  else
62
- data = $project['routes.cors']
62
+ file = $project['routes.cors']
63
63
  # If it is just a string, then is a file to load.
64
- data = YAML.load_file(data) if data.is_a? String
64
+ if file.is_a? String
65
+ data = YAML.load_file(file)
66
+ else
67
+ data = file
68
+ file = %(ProjectFile['routes.cors'])
69
+ end
65
70
  end
71
+ return unless upload_item_allowed(file)
66
72
  put('', data)
67
73
  end
68
74
  end
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.22 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -16,7 +16,6 @@ module MrMurano
16
16
  # …/endpoint
17
17
  module Webservice
18
18
  class Endpoint < WebserviceBase
19
-
20
19
  # Route Specific details on an Item
21
20
  class RouteItem < Item
22
21
  # @return [String] HTTP method for this endpoint
@@ -49,7 +48,7 @@ module MrMurano
49
48
  ret = get
50
49
  return [] unless ret.is_a?(Array)
51
50
  ret.map do |item|
52
- if item[:content_type].to_s.empty? then
51
+ if item[:content_type].to_s.empty?
53
52
  item[:content_type] = 'application/json'
54
53
  end
55
54
  # XXX should this update the script header?
@@ -69,9 +68,9 @@ module MrMurano
69
68
 
70
69
  ret[:content_type] = 'application/json' if ret[:content_type].empty?
71
70
 
72
- script = ret[:script].lines.map{|l|l.chomp}
71
+ script = ret[:script].lines.map(&:chomp)
73
72
 
74
- aheader = (script.first or "")
73
+ aheader = (script.first || '')
75
74
 
76
75
  rh = ['--#ENDPOINT', ret[:method].upcase, ret[:path]]
77
76
  rh << ret[:content_type] if ret[:content_type] != 'application/json'
@@ -81,19 +80,21 @@ module MrMurano
81
80
  # If header is wrong, replace it.
82
81
 
83
82
  md = @match_header.match(aheader)
84
- if md.nil? then
83
+ if md.nil?
85
84
  # header missing.
86
85
  script.unshift rheader
87
- elsif md[:method] != ret[:method] or
88
- md[:path] != ret[:path] or
89
- md[:ctype] != ret[:content_type] then
86
+ elsif (
87
+ md[:method] != ret[:method] ||
88
+ md[:path] != ret[:path] ||
89
+ md[:ctype] != ret[:content_type]
90
+ )
90
91
  # header is wrong.
91
92
  script[0] = rheader
92
93
  end
93
94
  # otherwise current header is good.
94
95
 
95
96
  script = script.join("\n") + "\n"
96
- if block_given? then
97
+ if block_given?
97
98
  yield script
98
99
  else
99
100
  script
@@ -105,23 +106,24 @@ module MrMurano
105
106
  # @param local [Pathname] path to file to push
106
107
  # @param remote [RouteItem] of method and endpoint path
107
108
  # @param modify [Boolean] True if item exists already and this is changing it
108
- def upload(local, remote, modify)
109
- local = Pathname.new(local) unless local.kind_of? Pathname
110
- raise "no file" unless local.exist?
109
+ def upload(local, remote, _modify)
110
+ local = Pathname.new(local) unless local.is_a? Pathname
111
+ raise 'no file' unless local.exist?
111
112
  # we assume these are small enough to slurp.
112
- if remote.script.nil? then
113
+ if remote.script.nil?
113
114
  script = local.read
114
115
  remote[:script] = script
115
116
  end
116
117
  limitkeys = [:method, :path, :script, :content_type, @itemkey]
117
- remote = remote.to_h.select{|k,v| limitkeys.include? k }
118
- # post('', remote)
119
- if remote.has_key? @itemkey then
118
+ remote = remote.to_h.select { |k, _v| limitkeys.include? k }
119
+ if remote.key? @itemkey
120
+ return unless upload_item_allowed(remote[@itemkey])
120
121
  put('/' + remote[@itemkey], remote) do |request, http|
121
122
  response = http.request(request)
122
123
  case response
123
124
  when Net::HTTPSuccess
124
125
  #return JSON.parse(response.body)
126
+ return
125
127
  when Net::HTTPNotFound
126
128
  verbose "\tDoesn't exist, creating"
127
129
  post('/', remote)
@@ -131,6 +133,8 @@ module MrMurano
131
133
  end
132
134
  else
133
135
  verbose "\tNo itemkey, creating"
136
+ #return unless upload_item_allowed(remote)
137
+ return unless upload_item_allowed(local)
134
138
  post('', remote)
135
139
  end
136
140
  end
@@ -138,29 +142,31 @@ module MrMurano
138
142
  ##
139
143
  # Delete an endpoint
140
144
  def remove(id)
145
+ return unless remove_item_allowed(id)
141
146
  delete('/' + id.to_s)
142
147
  end
143
148
 
144
- def tolocalname(item, key)
149
+ def tolocalname(item, _key)
145
150
  name = ''
146
151
  # 2017-07-02: Changing shovel operator << to +=
147
152
  # to support Ruby 3.0 frozen string literals.
148
- name += item[:path].split('/').reject { |i| i.empty? }.join('-')
153
+ name += item[:path].split('/').reject(&:empty?).join('-')
149
154
  name += '.'
150
155
  # This downcase is just for the filename.
151
156
  name += item[:method].downcase
152
157
  name += '.lua'
158
+ name
153
159
  end
154
160
 
155
- def to_remote_item(from, path)
161
+ def to_remote_item(_from, path)
156
162
  # Path could be have multiple endpoints in side, so a loop.
157
163
  items = []
158
- path = Pathname.new(path) unless path.kind_of? Pathname
164
+ path = Pathname.new(path) unless path.is_a? Pathname
159
165
  cur = nil
160
166
  lineno = 0
161
- path.readlines().each do |line|
167
+ path.readlines.each do |line|
162
168
  md = @match_header.match(line)
163
- if not md.nil? then
169
+ if !md.nil?
164
170
  # header line.
165
171
  cur[:line_end] = lineno unless cur.nil?
166
172
  items << cur unless cur.nil?
@@ -181,12 +187,12 @@ module MrMurano
181
187
  #method: md[:method],
182
188
  method: md[:method].upcase,
183
189
  path: md[:path],
184
- content_type: (md[:ctype] or 'application/json'),
190
+ content_type: (md[:ctype] || 'application/json'),
185
191
  local_path: path,
186
192
  line: lineno,
187
193
  script: up_line,
188
194
  )
189
- elsif not cur.nil? and not cur[:script].nil? then
195
+ elsif !cur.nil? && !cur[:script].nil?
190
196
  # 2017-07-02: Frozen string literal: change << to +=
191
197
  cur[:script] += line
192
198
  end
@@ -204,27 +210,27 @@ module MrMurano
204
210
  return false if md.nil?
205
211
  debug "match pattern: '#{md[:method]}' '#{md[:path]}'"
206
212
 
207
- unless md[:method].empty? then
213
+ unless md[:method].empty?
208
214
  return false unless item[:method].casecmp(md[:method]).zero?
209
215
  end
210
216
 
211
217
  return true if md[:path].empty?
212
218
 
213
- ::File.fnmatch(md[:path],item[:path])
219
+ ::File.fnmatch(md[:path], item[:path])
214
220
  end
215
221
 
216
222
  def synckey(item)
217
223
  "#{item[:method].upcase}_#{item[:path]}"
218
224
  end
219
225
 
220
- def docmp(itemA, itemB)
221
- if itemA[:script].nil? and itemA[:local_path] then
222
- itemA[:script] = itemA[:local_path].read
226
+ def docmp(item_a, item_b)
227
+ if item_a[:script].nil? && item_a[:local_path]
228
+ item_a[:script] = item_a[:local_path].read
223
229
  end
224
- if itemB[:script].nil? and itemB[:local_path] then
225
- itemB[:script] = itemB[:local_path].read
230
+ if item_b[:script].nil? && item_b[:local_path]
231
+ item_b[:script] = item_b[:local_path].read
226
232
  end
227
- return (itemA[:script] != itemB[:script] or itemA[:content_type] != itemB[:content_type])
233
+ (item_a[:script] != item_b[:script] || item_a[:content_type] != item_b[:content_type])
228
234
  end
229
235
  end
230
236
 
@@ -1,5 +1,12 @@
1
+ # Last Modified: 2017.08.22 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
7
+
1
8
  require 'digest/sha1'
2
- require "http/form_data"
9
+ require 'http/form_data'
3
10
  require 'mime/types'
4
11
  require 'net/http'
5
12
  require 'uri'
@@ -50,12 +57,12 @@ module MrMurano
50
57
  # Get one item of the static content.
51
58
  def fetch(path, &block)
52
59
  path = path[1..-1] if path[0] == '/'
53
- path = '/'+ URI.encode_www_form_component(path)
60
+ path = '/' + URI.encode_www_form_component(path)
54
61
  get(path) do |request, http|
55
62
  http.request(request) do |resp|
56
63
  case resp
57
64
  when Net::HTTPSuccess
58
- if block_given? then
65
+ if block_given?
59
66
  resp.read_body(&block)
60
67
  else
61
68
  resp.read_body do |chunk|
@@ -75,15 +82,14 @@ module MrMurano
75
82
  # @param path [String] The identifying key for this item
76
83
  def remove(path)
77
84
  path = path[1..-1] if path[0] == '/'
85
+ return unless remove_item_allowed(path)
78
86
  delete('/' + URI.encode_www_form_component(path))
79
87
  end
80
88
 
81
89
  def curldebug(request)
82
90
  # The upload will get printed out inside of upload.
83
91
  # Because we don't have the correct info here.
84
- if request.method != 'PUT' then
85
- super(request)
86
- end
92
+ super(request) if request.method != 'PUT'
87
93
  end
88
94
 
89
95
  ##
@@ -91,8 +97,8 @@ module MrMurano
91
97
  # @param src [Pathname] Full path of where to upload from
92
98
  # @param item [Hash] The item details to upload
93
99
  # @param modify [Boolean] True if item exists already and this is changing it
94
- def upload(local, remote, modify)
95
- local = Pathname.new(local) unless local.kind_of? Pathname
100
+ def upload(local, remote, _modify)
101
+ local = Pathname.new(local) unless local.is_a? Pathname
96
102
 
97
103
  path = remote[:path]
98
104
  path = path[1..-1] if path[0] == '/'
@@ -110,22 +116,25 @@ module MrMurano
110
116
  # Most of these pull into ram. So maybe just go with that. Would guess that
111
117
  # truely large static content is rare, and we can optimize/fix that later.
112
118
 
113
- file = HTTP::FormData::File.new(local.to_s, { content_type: remote[:mime_type] })
119
+ file = HTTP::FormData::File.new(local.to_s, content_type: remote[:mime_type])
114
120
  form = HTTP::FormData.create(file: file)
115
121
  req = Net::HTTP::Put.new(uri)
116
- set_def_headers(req)
122
+ add_headers(req)
123
+
124
+ return unless upload_item_allowed(remote[@itemkey])
125
+
117
126
  workit(req) do |request, http|
118
127
  request.content_type = form.content_type
119
128
  request.content_length = form.content_length
120
129
  request.body = form.to_s
121
130
 
122
- if $cfg['tool.curldebug'] then
131
+ if $cfg['tool.curldebug']
123
132
  a = []
124
- a << %{curl -s -H 'Authorization: #{request['authorization']}'}
125
- a << %{-H 'User-Agent: #{request['User-Agent']}'}
126
- a << %{-X #{request.method}}
127
- a << %{'#{request.uri.to_s}'}
128
- a << %{-F file=@#{local.to_s}}
133
+ a << %(curl -s -H 'Authorization: #{request['authorization']}')
134
+ a << %(-H 'User-Agent: #{request['User-Agent']}')
135
+ a << %(-X #{request.method})
136
+ a << %('#{request.uri}')
137
+ a << %(-F file=@#{local})
129
138
  if $cfg.curlfile_f.nil?
130
139
  puts a.join(' ')
131
140
  else
@@ -156,20 +165,22 @@ module MrMurano
156
165
  name = '/' if name == $cfg['files.default_page']
157
166
  name = "/#{name}" unless name.chars.first == '/'
158
167
 
159
- mime = MIME::Types.type_for(path.to_s)[0] || MIME::Types["application/octet-stream"][0]
168
+ mime = MIME::Types.type_for(path.to_s)[0] || MIME::Types['application/octet-stream'][0]
160
169
 
161
170
  # It does not actually take the SHA1 of the file.
162
171
  # It first converts the file to hex, then takes the SHA1 of that string
163
172
  #sha1 = Digest::SHA1.file(path.to_s).hexdigest
164
173
  sha1 = Digest::SHA1.new
165
174
  path.open('rb:ASCII-8BIT') do |io|
166
- while chunk = io.read(1048576) do
175
+ # rubocop:disable Lint/AssignmentInCondition
176
+ # "Assignment in condition - you probably meant to use ==."
177
+ while chunk = io.read(1_048_576)
167
178
  sha1 << Digest.hexencode(chunk)
168
179
  end
169
180
  end
170
181
  debug "Checking #{name} (#{mime.simplified} #{sha1.hexdigest})"
171
182
 
172
- FileItem.new(:path=>name, :mime_type=>mime.simplified, :checksum=>sha1.hexdigest)
183
+ FileItem.new(path: name, mime_type: mime.simplified, checksum: sha1.hexdigest)
173
184
  end
174
185
 
175
186
  # @param item [FileItem] The item to get a key from
@@ -179,11 +190,11 @@ module MrMurano
179
190
  end
180
191
 
181
192
  # Compare items.
182
- # @param itemA [FileItem]
183
- # @param itemB [FileItem]
184
- def docmp(itemA, itemB)
185
- return (itemA[:mime_type] != itemB[:mime_type] or
186
- itemA[:checksum] != itemB[:checksum])
193
+ # @param item_a [FileItem]
194
+ # @param item_b [FileItem]
195
+ def docmp(item_a, item_b)
196
+ (item_a[:mime_type] != item_b[:mime_type] ||
197
+ item_a[:checksum] != item_b[:checksum])
187
198
  end
188
199
  end
189
200
 
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -10,7 +10,7 @@ require 'MrMurano/Account'
10
10
  require 'MrMurano/Business'
11
11
  require 'MrMurano/ReCommander'
12
12
 
13
- MSG_BUSINESSES_NONE_FOUND = 'No businesses found' if !defined? MSG_BUSINESSES_NONE_FOUND
13
+ MSG_BUSINESSES_NONE_FOUND = 'No businesses found' unless defined? MSG_BUSINESSES_NONE_FOUND
14
14
 
15
15
  # *** Base business command help.
16
16
  # -------------------------------
@@ -22,9 +22,10 @@ command :business do |c|
22
22
  Commands for working with businesses.
23
23
  ).strip
24
24
  c.project_not_required = true
25
+ c.subcmdgrouphelp = true
25
26
 
26
27
  c.action do |_args, _options|
27
- ::Commander::UI.enable_paging
28
+ ::Commander::UI.enable_paging unless $cfg['tool.no-page']
28
29
  say MrMurano::SubCmdGroupHelp.new(c).get_help
29
30
  end
30
31
  end
@@ -49,19 +50,17 @@ def cmd_options_add_id_and_name(c)
49
50
  end
50
51
 
51
52
  def cmd_defaults_id_and_name(options)
52
- if !options.id.nil? && !options.name.nil?
53
- MrMurano::Verbose.error('Please only --id or --name but not both')
54
- exit 1
55
- end
53
+ return if options.id.nil? || options.name.nil?
54
+ MrMurano::Verbose.error('Please specify only --id or --name but not both')
55
+ exit 1
56
56
  end
57
57
 
58
58
  def cmd_verify_args_and_id_or_name!(args, options)
59
- if !args.any? && (options.id || options.name)
60
- MrMurano::Verbose.warning(
61
- 'The --id and --name options only apply when specifying a business name or ID.'
62
- )
63
- exit 1
64
- end
59
+ return unless args.none? && (options.id || options.name)
60
+ MrMurano::Verbose.warning(
61
+ 'The --id and --name options only apply when specifying a business name or ID.'
62
+ )
63
+ exit 1
65
64
  end
66
65
 
67
66
  def cmd_option_business_pickers(c)
@@ -123,7 +122,7 @@ Find business by name or ID.
123
122
  c.action do |args, options|
124
123
  # SKIP: c.verify_arg_count!(args)
125
124
  cmd_defaults_id_and_name(options)
126
- if !args.any? && !any_business_pickers?(options)
125
+ if args.none? && !any_business_pickers?(options)
127
126
  MrMurano::Verbose.error('What would you like to find?')
128
127
  exit 1
129
128
  end
@@ -149,9 +148,9 @@ def business_find_or_ask!(acc, options)
149
148
  biz = businesses_ask_which(acc) if biz.nil?
150
149
  end
151
150
 
152
- match_bid = $cfg.set('business.id', nil, :internal)
153
- match_name = $cfg.set('business.name', nil, :internal)
154
- match_fuzzy = $cfg.set('business.fuzzy', nil, :internal)
151
+ $cfg.set('business.id', nil, :internal)
152
+ $cfg.set('business.name', nil, :internal)
153
+ $cfg.set('business.fuzzy', nil, :internal)
155
154
 
156
155
  biz
157
156
  end
@@ -186,7 +185,8 @@ def business_from_config
186
185
  if biz.valid?
187
186
  say("Found Business #{biz.pretty_name_and_id}")
188
187
  else
189
- say("Could not find Business ‘#{biz.bid}’ referenced in the config")
188
+ biz_bid = MrMurano::Verbose.fancy_ticks(biz.bid)
189
+ say("Could not find Business #{biz_bid} referenced in the config")
190
190
  end
191
191
  puts('')
192
192
  end
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.16 /coding: utf-8
1
+ # Last Modified: 2017.08.22 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -17,9 +17,10 @@ This set of commands let you interact with the content area for a product.
17
17
  This is where OTA data can be stored so that devices can easily download it.
18
18
  ).strip
19
19
  c.project_not_required = true
20
+ c.subcmdgrouphelp = true
20
21
 
21
22
  c.action do |_args, _options|
22
- ::Commander::UI.enable_paging
23
+ ::Commander::UI.enable_paging unless $cfg['tool.no-page']
23
24
  say MrMurano::SubCmdGroupHelp.new(c).get_help
24
25
  end
25
26
  end
@@ -1,23 +1,25 @@
1
- # Last Modified: 2017.08.16 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
5
5
  # License: MIT. See LICENSE.txt.
6
6
  # vim:tw=0:ts=2:sw=2:et:ai
7
7
 
8
+ require 'date'
8
9
  require 'MrMurano/Gateway'
9
10
  require 'MrMurano/ReCommander'
10
11
 
11
- command 'device' do |c|
12
+ command :device do |c|
12
13
  c.syntax = %(murano device)
13
14
  c.summary = %(Interact with a device)
14
15
  c.description = %(
15
16
  Interact with a device.
16
17
  ).strip
17
18
  c.project_not_required = true
19
+ c.subcmdgrouphelp = true
18
20
 
19
21
  c.action do |_args, _options|
20
- ::Commander::UI.enable_paging
22
+ ::Commander::UI.enable_paging unless $cfg['tool.no-page']
21
23
  say MrMurano::SubCmdGroupHelp.new(c).get_help
22
24
  end
23
25
  end
@@ -90,7 +92,7 @@ end
90
92
  alias_command 'devices list', 'device list'
91
93
 
92
94
  command 'device read' do |c|
93
- c.syntax = %(murano device read <identifier> (<alias>...))
95
+ c.syntax = %(murano device read <identifier> [<alias>...] [--options])
94
96
  c.summary = %(Read state of a device)
95
97
  c.description = %(
96
98
  Read state of a device.
@@ -101,7 +103,7 @@ This reads the latest state values for the resources in a device.
101
103
  c.option '-o', '--output FILE', %(Download to file instead of STDOUT)
102
104
 
103
105
  c.action do |args, options|
104
- c.verify_arg_count!(args, nil, ['Identifier missing'])
106
+ c.verify_arg_count!(args, nil, ['Missing device identifier'])
105
107
 
106
108
  prd = MrMurano::Gateway::Device.new
107
109
 
@@ -142,7 +144,7 @@ If an alias is not settable, this will fail.
142
144
  ).strip
143
145
 
144
146
  c.action do |args, _options|
145
- c.verify_arg_count!(args, nil, ['Identifier missing'])
147
+ c.verify_arg_count!(args, nil, ['Missing device identifier'])
146
148
 
147
149
  resources = (MrMurano::Gateway::GweBase.new.info || {})[:resources]
148
150
 
@@ -174,45 +176,108 @@ If an alias is not settable, this will fail.
174
176
  end
175
177
 
176
178
  command 'device enable' do |c|
177
- c.syntax = %(murano device enable [<identifier>|--file <identifiers>])
178
- c.summary = %(Enable an Identifier; Creates device in Murano)
179
+ c.syntax = %(murano device enable (<identifier>|--file <path>) [--options])
180
+ c.summary = %(Enable Identifiers in Murano for real world devices)
179
181
  c.description = %(
180
- Enables Identifiers, creating the digial shadow in Murano.
182
+ Enables Identifiers, creating devices, or digital shadows, in Murano.
181
183
  ).strip
182
184
 
185
+ c.option '-e', '--expire HOURS', %(Devices that do not activate within HOURS hours will be deleted for security purposes)
183
186
  c.option '-f', '--file FILE', %(A file of serial numbers, one per line)
184
- c.option '--key FILE', %(Public TLS key for this device)
187
+ c.option '--key FILE', %(Path to file containing public TLS key for this device)
188
+ allowed_types = MrMurano::Gateway::Device::DEVICE_AUTH_TYPES.map(&:to_s).sort
189
+ c.option '--auth TYPE', %(Type of credential used to authenticate [#{allowed_types.join('|')}])
190
+ c.option '--cred KEY', %(The credential used to authenticate, e.g., token, password, etc.)
185
191
 
186
192
  c.action do |args, options|
187
- # SKIP: c.verify_arg_count!(args)
193
+ c.verify_arg_count!(args, 1)
194
+
188
195
  prd = MrMurano::Gateway::Device.new
189
- if !options.file.nil? && !options.key.nil?
190
- prd.error %(Cannot use both --file and --key)
196
+
197
+ if args.count.zero? && options.file.to_s.empty?
198
+ prd.error 'Missing device identifier or --file'
191
199
  exit 1
200
+ elsif !args.count.zero? && !options.file.to_s.empty?
201
+ prd.error 'Please specify an identifier or --file but not both'
202
+ exit 1
203
+ end
204
+
205
+ if !options.file.nil? && (!options.key.nil? || !options.auth.nil? || !options.cred.nil?)
206
+ prd.error %(Cannot use --file with any of: --key, --auth, or --cred)
207
+ exit 1
208
+ end
209
+ if !options.key.nil? && !options.cred.nil?
210
+ prd.error %(Please use either --cred or --key but not both)
211
+ exit 1
212
+ end
213
+ if options.auth.nil? ^ options.cred.nil?
214
+ prd.error %(Please specify both --auth and --cred or neither)
215
+ exit 1
216
+ end
217
+ options.auth = options.auth.to_sym unless options.auth.nil?
218
+ unless options.key.nil?
219
+ if !options.auth.nil? && options.auth != :certificate
220
+ prd.warning %(You probably mean to use "--auth certificate" with --key)
221
+ else
222
+ options.auth = :certificate
223
+ end
192
224
  end
193
- if options.file
225
+
226
+ unless options.expire.nil?
227
+ unless options.expire =~ /^[0-9]+$/
228
+ prd.error %(The --expire value is not a number of hours: #{prd.fancy_ticks(options.expire)})
229
+ exit 1
230
+ end
231
+ # The platform expects the expiration time to be an integer
232
+ # representing microseconds since the epoch, e.g.,
233
+ # hours * mins/hour * secs/min * msec/sec * μsec/msec
234
+ # or hours * 60 * 60 * 1000 * 1000
235
+ micros_since_epoch = DateTime.now.strftime('%Q').to_i * 1000
236
+ mircos_until_purge = options.expire.to_i * 60 * 60 * 1000 * 1000
237
+ options.expire = micros_since_epoch + mircos_until_purge
238
+ end
239
+
240
+ unless options.auth.nil?
241
+ options.auth = options.auth.to_sym
242
+ unless MrMurano::Gateway::Device.DEVICE_AUTH_TYPES.include?(options.auth)
243
+ MrMurano::Verbose.error("unrecognized --auth: #{options.auth}")
244
+ exit 1
245
+ end
246
+ end
247
+
248
+ if !options.file.to_s.empty?
194
249
  # Check file for headers.
195
- header = File.new(options.file).gets
250
+ begin
251
+ header = File.new(options.file).gets
252
+ rescue Errno::ENOENT => err
253
+ prd.error %(Unable to open file #{prd.fancy_ticks(options.file)}: #{err.message})
254
+ exit 2
255
+ end
196
256
  if header.nil?
197
257
  prd.error 'Nothing in file!'
198
258
  exit 1
199
259
  end
200
260
  unless header =~ /\s*ID\s*(,SSL Client Certificate\s*)?/
201
- prd.error "Missing column headers in file \"#{options.file}\""
261
+ prd.error %(Missing column headers in file "#{options.file}")
202
262
  prd.error %(First line in file should be either "ID" or "ID, SSL Client Certificate")
203
263
  exit 2
204
264
  end
205
- prd.enable_batch(options.file)
265
+ prd.enable_batch(options.file, options.expire)
206
266
  elsif args.count > 0
267
+ opts = {}
268
+ opts[:expire] = options.expire unless options.expire.nil?
269
+ opts[:type] = options.auth unless options.auth.nil?
207
270
  if options.key
208
271
  File.open(options.key, 'rb') do |io|
209
- prd.enable(args[0], type: :certificate, publickey: io)
272
+ prd.enable(args[0], **opts, key: io)
210
273
  end
211
274
  else
212
- prd.enable(args[0])
275
+ opts[:key] = options.cred unless options.cred.nil?
276
+ prd.enable(args[0], **opts)
213
277
  end
214
278
  else
215
- prd.error 'Missing an Identifier to enable'
279
+ # Impossible path: neither args nor --file; would've exited by now.
280
+ raise 'Impossible'
216
281
  end
217
282
  end
218
283
  end
@@ -236,7 +301,7 @@ you cannot retrive the CIK again.
236
301
  ).strip
237
302
 
238
303
  c.action do |args, _options|
239
- c.verify_arg_count!(args, nil, ['Identifier missing'])
304
+ c.verify_arg_count!(args, nil, ['Missing device identifier'])
240
305
  prd = MrMurano::Gateway::Device.new
241
306
  prd.outf prd.activate(args.first)
242
307
  end
@@ -250,7 +315,7 @@ Delete a device.
250
315
  ).strip
251
316
 
252
317
  c.action do |args, _options|
253
- c.verify_arg_count!(args, nil, ['Identifier missing'])
318
+ c.verify_arg_count!(args, nil, ['Missing device identifier'])
254
319
  prd = MrMurano::Gateway::Device.new
255
320
  snid = args.shift
256
321
  ret = prd.remove(snid)
@@ -273,6 +338,53 @@ Get the URL for the HTTP-Data-API for this Project.
273
338
  end
274
339
  end
275
340
 
341
+ command 'device lock' do |c|
342
+ c.syntax = %(murano device lock <identifier>)
343
+ c.summary = %(Lock a device, not allowing connections to it until unlocked)
344
+ c.description = %(
345
+ Lock a device, not allowing connections to it until unlocked.
346
+ ).strip
347
+
348
+ c.action do |args, _options|
349
+ c.verify_arg_count!(args, 1, ['Missing device identifier'])
350
+ prd = MrMurano::Gateway::Device.new
351
+ prd.lock(args[0])
352
+ end
353
+ end
354
+
355
+ command 'device unlock' do |c|
356
+ c.syntax = %(murano device unlock <identifier>)
357
+ c.summary = %(Unlock a device, allowing connections to it again)
358
+ c.description = %(
359
+ Unlock a device, allowing connections to it again.
360
+ ).strip
361
+
362
+ c.action do |args, _options|
363
+ c.verify_arg_count!(args, 1, ['Missing device identifier'])
364
+ prd = MrMurano::Gateway::Device.new
365
+ prd.unlock(args[0])
366
+ end
367
+ end
368
+
369
+ command 'device revoke' do |c|
370
+ c.syntax = %(murano device revoke <identifier>)
371
+ c.summary = %(Force device to reprovision)
372
+ c.description = %(
373
+ Force device to reprovision.
374
+
375
+ This will revoke the device's keys and cause it to temporarily disconnect. The will then reconnect and be provisioned with new keys.
376
+ ).strip
377
+
378
+ c.action do |args, _options|
379
+ c.verify_arg_count!(args, 1, ['Missing device identifier'])
380
+ prd = MrMurano::Gateway::Device.new
381
+ # MAYBE/2017-08-23: This command doesn't return an error if the device
382
+ # ID was not found, or if the keys were already revoked. Do we care?
383
+ # At least the lock command fails if the device ID is not found.
384
+ prd.revoke(args[0])
385
+ end
386
+ end
387
+
276
388
  alias_command 'product device', 'device'
277
389
  alias_command 'product device list', 'device list'
278
390
  alias_command 'product devices list', 'device list'
@@ -283,4 +395,7 @@ alias_command 'product device enable', 'device enable'
283
395
  alias_command 'product device activate', 'device activate'
284
396
  alias_command 'product device delete', 'device delete'
285
397
  alias_command 'product device httpurl', 'device httpurl'
398
+ alias_command 'product device lock', 'device lock'
399
+ alias_command 'product device unlock', 'device unlock'
400
+ alias_command 'product device revoke', 'device revoke'
286
401