MuranoCLI 3.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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