MuranoCLI 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.agignore +3 -0
  3. data/.gitignore +18 -1
  4. data/.rubocop.yml +222 -0
  5. data/.trustme.sh +185 -0
  6. data/.trustme.vim +24 -0
  7. data/Gemfile +23 -4
  8. data/LICENSE.txt +1 -1
  9. data/MuranoCLI.gemspec +43 -8
  10. data/README.markdown +9 -11
  11. data/Rakefile +187 -143
  12. data/TODO.taskpaper +2 -2
  13. data/bin/murano +51 -52
  14. data/docs/basic_example.rst +436 -0
  15. data/docs/completions/murano_completion-bash +3484 -0
  16. data/docs/demo.md +32 -32
  17. data/docs/develop.rst +391 -0
  18. data/lib/MrMurano.rb +21 -7
  19. data/lib/MrMurano/Account.rb +159 -174
  20. data/lib/MrMurano/Business.rb +381 -0
  21. data/lib/MrMurano/Config-Migrate.rb +32 -26
  22. data/lib/MrMurano/Config.rb +407 -128
  23. data/lib/MrMurano/Content.rb +191 -0
  24. data/lib/MrMurano/Gateway.rb +489 -0
  25. data/lib/MrMurano/Keystore.rb +48 -0
  26. data/lib/MrMurano/Passwords.rb +103 -0
  27. data/lib/MrMurano/ProjectFile.rb +121 -79
  28. data/lib/MrMurano/ReCommander.rb +114 -10
  29. data/lib/MrMurano/Setting.rb +90 -0
  30. data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
  31. data/lib/MrMurano/Solution-Services.rb +461 -166
  32. data/lib/MrMurano/Solution-Users.rb +70 -31
  33. data/lib/MrMurano/Solution.rb +372 -13
  34. data/lib/MrMurano/SolutionId.rb +73 -0
  35. data/lib/MrMurano/SyncRoot.rb +137 -0
  36. data/lib/MrMurano/SyncUpDown.rb +594 -284
  37. data/lib/MrMurano/Webservice-Cors.rb +71 -0
  38. data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
  39. data/lib/MrMurano/Webservice-File.rb +193 -0
  40. data/lib/MrMurano/Webservice.rb +51 -0
  41. data/lib/MrMurano/commands.rb +18 -15
  42. data/lib/MrMurano/commands/business.rb +300 -6
  43. data/lib/MrMurano/commands/completion-bash.erb +166 -0
  44. data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
  45. data/lib/MrMurano/commands/completion.rb +76 -39
  46. data/lib/MrMurano/commands/config.rb +108 -44
  47. data/lib/MrMurano/commands/content.rb +115 -72
  48. data/lib/MrMurano/commands/cors.rb +29 -14
  49. data/lib/MrMurano/commands/devices.rb +286 -0
  50. data/lib/MrMurano/commands/domain.rb +52 -12
  51. data/lib/MrMurano/commands/gb.rb +24 -9
  52. data/lib/MrMurano/commands/globals.rb +64 -0
  53. data/lib/MrMurano/commands/init.rb +377 -155
  54. data/lib/MrMurano/commands/keystore.rb +92 -82
  55. data/lib/MrMurano/commands/link.rb +300 -0
  56. data/lib/MrMurano/commands/login.rb +74 -11
  57. data/lib/MrMurano/commands/logs.rb +63 -32
  58. data/lib/MrMurano/commands/mock.rb +57 -29
  59. data/lib/MrMurano/commands/password.rb +57 -39
  60. data/lib/MrMurano/commands/postgresql.rb +127 -94
  61. data/lib/MrMurano/commands/settings.rb +203 -0
  62. data/lib/MrMurano/commands/show.rb +79 -38
  63. data/lib/MrMurano/commands/solution.rb +423 -5
  64. data/lib/MrMurano/commands/solution_picker.rb +547 -0
  65. data/lib/MrMurano/commands/status.rb +195 -61
  66. data/lib/MrMurano/commands/sync.rb +78 -39
  67. data/lib/MrMurano/commands/timeseries.rb +71 -55
  68. data/lib/MrMurano/commands/tsdb.rb +113 -87
  69. data/lib/MrMurano/commands/usage.rb +57 -15
  70. data/lib/MrMurano/hash.rb +100 -10
  71. data/lib/MrMurano/http.rb +187 -43
  72. data/lib/MrMurano/makePretty.rb +16 -14
  73. data/lib/MrMurano/optparse.rb +2178 -0
  74. data/lib/MrMurano/progress.rb +138 -0
  75. data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
  76. data/lib/MrMurano/template/projectFile.murano.erb +16 -13
  77. data/lib/MrMurano/verbosing.rb +166 -29
  78. data/lib/MrMurano/version.rb +30 -1
  79. data/spec/Account-Passwords_spec.rb +21 -4
  80. data/spec/Account_spec.rb +69 -146
  81. data/spec/Business_spec.rb +290 -0
  82. data/spec/ConfigFile_spec.rb +1 -0
  83. data/spec/ConfigMigrate_spec.rb +12 -8
  84. data/spec/Config_spec.rb +40 -34
  85. data/spec/Content_spec.rb +363 -0
  86. data/spec/GatewayBase_spec.rb +54 -0
  87. data/spec/GatewayDevice_spec.rb +321 -0
  88. data/spec/GatewayResource_spec.rb +266 -0
  89. data/spec/GatewaySettings_spec.rb +120 -0
  90. data/spec/Http_spec.rb +18 -8
  91. data/spec/Mock_spec.rb +2 -2
  92. data/spec/ProjectFile_spec.rb +25 -14
  93. data/spec/Setting_spec.rb +110 -0
  94. data/spec/Solution-ServiceConfig_spec.rb +44 -5
  95. data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
  96. data/spec/Solution-ServiceModules_spec.rb +47 -37
  97. data/spec/Solution-UsersRoles_spec.rb +10 -8
  98. data/spec/Solution_spec.rb +17 -8
  99. data/spec/SyncRoot_spec.rb +46 -20
  100. data/spec/SyncUpDown_spec.rb +437 -201
  101. data/spec/Verbosing_spec.rb +12 -4
  102. data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
  103. data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
  104. data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
  105. data/spec/Webservice-Setting_spec.rb +89 -0
  106. data/spec/_workspace.rb +4 -4
  107. data/spec/cmd_business_spec.rb +9 -4
  108. data/spec/cmd_common.rb +44 -1
  109. data/spec/cmd_content_spec.rb +43 -17
  110. data/spec/cmd_cors_spec.rb +4 -4
  111. data/spec/cmd_device_spec.rb +61 -16
  112. data/spec/cmd_domain_spec.rb +29 -6
  113. data/spec/cmd_init_spec.rb +281 -126
  114. data/spec/cmd_keystore_spec.rb +3 -3
  115. data/spec/cmd_link_spec.rb +98 -0
  116. data/spec/cmd_password_spec.rb +1 -1
  117. data/spec/cmd_setting_application_spec.rb +260 -0
  118. data/spec/cmd_setting_product_spec.rb +220 -0
  119. data/spec/cmd_status_spec.rb +223 -114
  120. data/spec/cmd_syncdown_spec.rb +115 -35
  121. data/spec/cmd_syncup_spec.rb +68 -15
  122. data/spec/cmd_usage_spec.rb +35 -8
  123. data/spec/fixtures/dumped_config +6 -4
  124. data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
  125. data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
  126. data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
  127. data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
  128. data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
  129. data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
  130. data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
  131. data/spec/spec_helper.rb +5 -0
  132. metadata +262 -171
  133. data/bin/mr +0 -8
  134. data/lib/MrMurano/Product-1P-Device.rb +0 -145
  135. data/lib/MrMurano/Product-Resources.rb +0 -205
  136. data/lib/MrMurano/Product.rb +0 -358
  137. data/lib/MrMurano/Solution-Cors.rb +0 -47
  138. data/lib/MrMurano/Solution-Endpoint.rb +0 -191
  139. data/lib/MrMurano/Solution-File.rb +0 -166
  140. data/lib/MrMurano/commands/assign.rb +0 -57
  141. data/lib/MrMurano/commands/businessList.rb +0 -45
  142. data/lib/MrMurano/commands/product.rb +0 -14
  143. data/lib/MrMurano/commands/productCreate.rb +0 -39
  144. data/lib/MrMurano/commands/productDelete.rb +0 -33
  145. data/lib/MrMurano/commands/productDevice.rb +0 -87
  146. data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
  147. data/lib/MrMurano/commands/productList.rb +0 -45
  148. data/lib/MrMurano/commands/productWrite.rb +0 -27
  149. data/lib/MrMurano/commands/solutionCreate.rb +0 -41
  150. data/lib/MrMurano/commands/solutionDelete.rb +0 -34
  151. data/lib/MrMurano/commands/solutionList.rb +0 -45
  152. data/spec/ProductBase_spec.rb +0 -113
  153. data/spec/ProductContent_spec.rb +0 -162
  154. data/spec/ProductResources_spec.rb +0 -329
  155. data/spec/Product_1P_Device_spec.rb +0 -202
  156. data/spec/Product_1P_RPC_spec.rb +0 -175
  157. data/spec/Product_spec.rb +0 -153
  158. data/spec/Solution-ServiceDevice_spec.rb +0 -176
  159. data/spec/cmd_assign_spec.rb +0 -51
@@ -1,112 +1,188 @@
1
- require 'uri'
1
+ # Last Modified: 2017.08.18 /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
+
8
+ require 'abbrev'
2
9
  require 'cgi'
3
- require 'net/http'
4
- require 'json'
5
- require 'yaml'
6
10
  require 'date'
7
11
  require 'digest/sha1'
12
+ require 'json'
13
+ require 'net/http'
14
+ require 'uri'
15
+ require 'yaml'
16
+ require 'MrMurano/progress'
8
17
  require 'MrMurano/Solution'
18
+ require 'MrMurano/SyncRoot'
9
19
 
10
20
  module MrMurano
11
21
  ##
12
22
  # Things that servers do that is common.
13
23
  class ServiceBase < SolutionBase
24
+ def initialize(sid=nil)
25
+ super
26
+ end
14
27
 
15
- def mkalias(remote)
28
+ def mkalias(_remote)
16
29
  # :nocov:
17
- raise "Needs to be implemented in child"
30
+ raise 'Needs to be implemented in child'
18
31
  # :nocov:
19
32
  end
20
33
 
21
- def mkname(remote)
34
+ def mkname(_remote)
22
35
  # :nocov:
23
- raise "Needs to be implemented in child"
36
+ raise 'Needs to be implemented in child'
24
37
  # :nocov:
25
38
  end
26
39
 
27
40
  def fetch(name)
28
- raise "Missing name!" if name.nil?
29
- raise "Empty name!" if name.empty?
30
- ret = get('/'+CGI.escape(name))
31
- error "Unexpected result type, assuming empty instead: #{ret}" unless ret.kind_of? Hash
32
- ret = {} unless ret.kind_of? Hash
33
- if block_given? then
34
- yield (ret[:script] or '')
41
+ raise 'Missing name!' if name.nil?
42
+ raise 'Empty name!' if name.empty?
43
+ ret = get('/' + CGI.escape(name))
44
+ unless ret.is_a?(Hash) && !ret.key?(:error)
45
+ error "#{UNEXPECTED_TYPE_OR_ERROR_MSG}: #{ret}"
46
+ ret = {}
47
+ end
48
+ if block_given?
49
+ yield (ret[:script] || '')
35
50
  else
36
- ret[:script] or ''
51
+ ret[:script] || ''
37
52
  end
38
53
  end
39
54
 
40
55
  # ??? remove
41
56
  def remove(name)
42
- delete('/'+name)
57
+ delete('/' + name)
43
58
  end
44
59
 
45
60
  # @param modify Bool: True if item exists already and this is changing it
46
- def upload(local, remote, modify=false)
47
- local = Pathname.new(local) unless local.kind_of? Pathname
48
- raise "no file" unless local.exist?
49
-
50
- # we assume these are small enough to slurp.
51
- script = local.read
52
-
53
- pst = remote.to_h.merge ({
54
- :solution_id => $cfg['solution.id'],
55
- :script => script,
56
- :alias=>mkalias(remote),
57
- :name=>mkname(remote),
58
- })
59
- debug "f: #{local} >> #{pst.reject{|k,_| k==:script}.to_json}"
60
- # try put, if 404, then post.
61
- put('/'+mkalias(remote), pst) do |request, http|
61
+ def upload(localpath, thereitem, _modify=false)
62
+ localpath = Pathname.new(localpath) unless localpath.is_a?(Pathname)
63
+ if localpath.exist?
64
+ # we assume these are small enough to slurp.
65
+ script = localpath.read
66
+ else
67
+ # I.e., thereitem.phantom, an "undeletable" file that does not
68
+ # exist locally but should not be deleted from server.
69
+ raise 'no file' unless thereitem.script
70
+ script = thereitem.script
71
+ end
72
+ localpath = Pathname.new(localpath) unless localpath.is_a?(Pathname)
73
+ name = mkname(thereitem)
74
+ pst = thereitem.to_h.merge(
75
+ #solution_id: $cfg[@solntype],
76
+ solution_id: @sid,
77
+ script: script,
78
+ alias: mkalias(thereitem),
79
+ name: name,
80
+ )
81
+ debug "f: #{localpath} >> #{pst.reject { |k, _| k == :script }.to_json}"
82
+ # Try PUT. If 404, then POST.
83
+ # I.e., PUT if not exists, else POST to create.
84
+ updated_at = nil
85
+ put('/' + mkalias(thereitem), pst) do |request, http|
62
86
  response = http.request(request)
63
- case response
64
- when Net::HTTPSuccess
87
+ isj, jsn = isJSON(response.body)
88
+ # ORDER: An HTTPNoContent is also a HTTPSuccess, so the latter comes first.
89
+ # EXPLAIN: How come `case response ... when Net:HTTPNoContent` works?
90
+ # It seems magical, since response is a class and here we use is_a?.
91
+ if response.is_a?(Net::HTTPNoContent)
92
+ # 2017-08-07: When did Murano start returning 204?
93
+ # This seems to happen when updating an existing service.
94
+ # Unfortunately, we don't get the latest updated_at, so
95
+ # a subsequent status will show this module as dirty.
96
+ ret = get('/' + CGI.escape(name))
97
+ if ret.is_a?(Hash) && ret.key?(:updated_at)
98
+ updated_at = ret[:updated_at]
99
+ else
100
+ warning "Failed to verify updated_at: #{ret}"
101
+ end
102
+ elsif response.is_a?(Net::HTTPSuccess)
103
+ # A first upload will see a 200 response and a JSON body.
104
+ # A subsequent upload of the same item sees 204 and no body.
65
105
  #return JSON.parse(response.body)
66
- when Net::HTTPNotFound
106
+ # MAYBE/EXPLAIN: Spit out error if no JSON, or explain why it's okay.
107
+ updated_at = jsn[:updated_at] unless jsn.nil?
108
+ elsif response == Net::HTTPNotFound
67
109
  verbose "Doesn't exist, creating"
68
110
  post('/', pst)
69
111
  else
70
- showHttpError(request, response)
112
+ relpath = localpath.sub(File.join(Dir.pwd, ''), '')
113
+ if response.is_a?(Net::HTTPBadRequest) && isj && jsn[:message] == 'Validation errors'
114
+ warning "Validation errors detected in #{relpath}:"
115
+ puts MrMurano::Pretties.makeJsonPretty(jsn[:errors], Struct.new(:pretty).new(true))
116
+ else
117
+ showHttpError(request, response)
118
+ end
119
+ warning "Failed to upload: #{relpath}"
71
120
  end
72
121
  end
73
- cacheUpdateTimeFor(local)
122
+ cache_update_time_for(localpath, updated_at)
74
123
  end
75
124
 
76
- def docmp(itemA, itemB)
77
- if itemA[:updated_at].nil? and itemA[:local_path] then
78
- ct = cachedUpdateTimeFor(itemA[:local_path])
79
- itemA[:updated_at] = ct unless ct.nil?
80
- itemA[:updated_at] = itemA[:local_path].mtime.getutc if ct.nil?
81
- elsif itemA[:updated_at].kind_of? String then
82
- itemA[:updated_at] = DateTime.parse(itemA[:updated_at]).to_time.getutc
125
+ def docmp(item_a, item_b)
126
+ if item_a[:updated_at].nil? && item_a[:local_path]
127
+ ct = cached_update_time_for(item_a[:local_path])
128
+ item_a[:updated_at] = ct unless ct.nil?
129
+ # The item might not exist if it was resurrected (item.phantom).
130
+ if ct.nil? && item_a[:local_path].exist?
131
+ item_a[:updated_at] = item_a[:local_path].mtime.getutc
132
+ end
133
+ elsif item_a[:updated_at].is_a?(String)
134
+ item_a[:updated_at] = DateTime.parse(item_a[:updated_at]).to_time.getutc
83
135
  end
84
- if itemB[:updated_at].nil? and itemB[:local_path] then
85
- ct = cachedUpdateTimeFor(itemB[:local_path])
86
- itemB[:updated_at] = ct unless ct.nil?
87
- itemB[:updated_at] = itemB[:local_path].mtime.getutc if ct.nil?
88
- elsif itemB[:updated_at].kind_of? String then
89
- itemB[:updated_at] = DateTime.parse(itemB[:updated_at]).to_time.getutc
136
+ if item_b[:updated_at].nil? && item_b[:local_path]
137
+ ct = cached_update_time_for(item_b[:local_path])
138
+ item_b[:updated_at] = ct unless ct.nil?
139
+ if ct.nil? && item_b[:local_path].exist?
140
+ item_b[:updated_at] = item_b[:local_path].mtime.getutc
141
+ end
142
+ elsif item_b[:updated_at].is_a?(String)
143
+ item_b[:updated_at] = DateTime.parse(item_b[:updated_at]).to_time.getutc
144
+ end
145
+ return false if item_a[:updated_at].nil? && item_b[:updated_at].nil?
146
+ return true if item_a[:updated_at].nil? && !item_b[:updated_at].nil?
147
+ return true if !item_a[:updated_at].nil? && item_b[:updated_at].nil?
148
+ item_a[:updated_at].to_time.round != item_b[:updated_at].to_time.round
149
+ end
150
+
151
+ def dodiff(merged, local, there, asdown=false)
152
+ mrg_diff = super
153
+ if mrg_diff.empty?
154
+ mrg_diff = '<Nothing changed (was timestamp difference)>'
155
+ # FIXME/2017-08-08: This isn't exactly working: setting mtime...
156
+ cache_update_time_for(local.local_path, there.updated_at)
90
157
  end
91
- return itemA[:updated_at].to_time.round != itemB[:updated_at].to_time.round
158
+ mrg_diff
92
159
  end
93
160
 
94
- def cacheFileName
95
- ['cache',
96
- self.class.to_s.gsub(/\W+/,'_'),
97
- @sid,
98
- 'yaml'].join('.')
161
+ def cache_file_name
162
+ [
163
+ 'cache',
164
+ self.class.to_s.gsub(/\W+/, '_'),
165
+ @sid,
166
+ 'yaml',
167
+ ].join('.')
99
168
  end
100
169
 
101
- def cacheUpdateTimeFor(local_path, time=nil)
102
- time = Time.now.getutc if time.nil?
170
+ def cache_update_time_for(local_path, time=nil)
171
+ if time.nil?
172
+ time = Time.now.getutc
173
+ elsif time.is_a?(String)
174
+ time = DateTime.parse(time)
175
+ end
176
+ file_hash = local_path_file_hash(local_path)
103
177
  entry = {
104
- :sha1=>Digest::SHA1.file(local_path.to_s).hexdigest,
105
- :updated_at=>time.to_datetime.iso8601(3)
178
+ sha1: file_hash,
179
+ updated_at: time.to_datetime.iso8601(3),
106
180
  }
107
- cacheFile = $cfg.file_at(cacheFileName)
108
- if cacheFile.file? then
109
- cacheFile.open('r+') do |io|
181
+ cache_file = $cfg.file_at(cache_file_name)
182
+ if cache_file.file?
183
+ cache_file.open('r+') do |io|
184
+ # FIXME/2017-07-02: "Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load."
185
+ # rubocop:disable Security/YAMLLoad
110
186
  cache = YAML.load(io)
111
187
  cache = {} unless cache
112
188
  io.rewind
@@ -114,7 +190,7 @@ module MrMurano
114
190
  io << cache.to_yaml
115
191
  end
116
192
  else
117
- cacheFile.open('w') do |io|
193
+ cache_file.open('w') do |io|
118
194
  cache = {}
119
195
  cache[local_path.to_s] = entry
120
196
  io << cache.to_yaml
@@ -123,21 +199,22 @@ module MrMurano
123
199
  time
124
200
  end
125
201
 
126
- def cachedUpdateTimeFor(local_path)
127
- cksm = Digest::SHA1.file(local_path.to_s).hexdigest
128
- cacheFile = $cfg.file_at(cacheFileName)
129
- return nil unless cacheFile.file?
202
+ def cached_update_time_for(local_path)
203
+ cksm = local_path_file_hash(local_path)
204
+ cache_file = $cfg.file_at(cache_file_name)
205
+ return nil unless cache_file.file?
130
206
  ret = nil
131
- cacheFile.open('r') do |io|
207
+ cache_file.open('r') do |io|
208
+ # FIXME/2017-07-02: "Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load."
132
209
  cache = YAML.load(io)
133
210
  return nil unless cache
134
- if cache.has_key?(local_path.to_s) then
211
+ if cache.key?(local_path.to_s)
135
212
  entry = cache[local_path.to_s]
136
213
  debug("For #{local_path}:")
137
- debug(" cached: #{entry.to_s}")
214
+ debug(" cached: #{entry}")
138
215
  debug(" cm: #{cksm}")
139
- if entry.kind_of?(Hash) then
140
- if entry[:sha1] == cksm and entry.has_key?(:updated_at) then
216
+ if entry.is_a?(Hash)
217
+ if entry[:sha1] == cksm && entry.key?(:updated_at)
141
218
  ret = DateTime.parse(entry[:updated_at])
142
219
  end
143
220
  end
@@ -145,66 +222,114 @@ module MrMurano
145
222
  end
146
223
  ret
147
224
  end
225
+
226
+ def local_path_file_hash(local_path)
227
+ if local_path.exist?
228
+ Digest::SHA1.file(local_path.to_s).hexdigest
229
+ else
230
+ # For item.phantom. Return the empty string, hashed:
231
+ # da39a3ee5e6b4b0d3255bfef95601890afd80709
232
+ Digest::SHA1.hexdigest('')
233
+ # MAYBE: Pass in the item and check for item.script?
234
+ end
235
+ end
148
236
  end
149
237
 
150
- # Libraries or better known as Modules.
151
- class Library < ServiceBase
238
+ # What Murano calls "Modules". Snippets of Lua code.
239
+ class Module < ServiceBase
152
240
  # Module Specific details on an Item
153
- class LibraryItem < Item
241
+ class ModuleItem < Item
154
242
  # @return [String] Internal Alias name
155
243
  attr_accessor :alias
156
244
  # @return [String] Timestamp when this was updated.
157
245
  attr_accessor :updated_at
158
246
  # @return [String] Timestamp when this was created.
159
247
  attr_accessor :created_at
160
- # @return [String] The solution.id that this is in
248
+ # @return [String] The application solution's ID.
161
249
  attr_accessor :solution_id
162
250
  end
163
251
 
164
- def initialize
252
+ def initialize(sid=nil)
253
+ # FIXME/VERIFY/2017-07-02: Check that products do not have Modules.
254
+ @solntype = 'application.id'
165
255
  super
166
- @uriparts << 'library'
256
+ @uriparts << :module
167
257
  @itemkey = :alias
168
258
  @project_section = :modules
169
259
  end
170
260
 
171
- def tolocalname(item, key)
261
+ def self.description
262
+ # MAYBE/2017-07-31: Rename to "Script Modules", per Renaud's suggestion? [lb]
263
+ %(Module)
264
+ end
265
+
266
+ def tolocalname(item, _key)
172
267
  name = item[:name]
268
+ # Nested Lua support: the platform dot-delimits modules in a require.
269
+ name = File.join(name.split('.')) unless $cfg['modules.no-nesting']
270
+ # NOTE: On syncup, user can specify file extension (or use * glob),
271
+ # but on syncdown, the ".lua" extension is hardcoded (here).
173
272
  "#{name}.lua"
174
273
  end
175
274
 
176
275
  def mkalias(remote)
177
- unless remote.name.nil? then
178
- [$cfg['solution.id'], remote[:name]].join('_')
179
- else
180
- raise "Missing parts! #{remote.to_h.to_json}"
181
- end
276
+ raise "Missing parts! #{remote.to_h.to_json}" if remote.name.nil?
277
+ #[$cfg[@solntype], remote[:name]].join('_')
278
+ [@sid, remote[:name]].join('_')
182
279
  end
183
280
 
184
281
  def mkname(remote)
185
- unless remote.name.nil? then
186
- remote[:name]
187
- else
188
- raise "Missing parts! #{remote.to_h.to_json}"
189
- end
282
+ raise "Missing parts! #{remote.to_h.to_json}" if remote.name.nil?
283
+ remote[:name]
190
284
  end
191
285
 
192
286
  def list
193
- ret = get()
194
- return [] if ret.is_a? Hash and ret.has_key? :error
195
- ret[:items].map{|i| LibraryItem.new(i)}
287
+ ret = get
288
+ return [] unless ret.is_a?(Hash) && !ret.key?(:error)
289
+ return [] unless ret.key?(:items)
290
+ ret[:items].map { |i| ModuleItem.new(i) }
291
+ # MAYBE/2017-08-17:
292
+ # ret[:items].map!
293
+ # sort_by_name(ret[:items])
196
294
  end
197
295
 
198
- def toRemoteItem(from, path)
199
- name = path.basename.to_s.sub(/\..*/, '')
200
- LibraryItem.new(:name => name)
296
+ def to_remote_item(root, path)
297
+ if $cfg['modules.no-nesting']
298
+ name = path.basename.to_s.sub(/\..*/, '')
299
+ else
300
+ name = remote_item_nested_name(root, path)
301
+ end
302
+ ModuleItem.new(name: name)
303
+ end
304
+
305
+ def remote_item_nested_name(root, path)
306
+ # 2017-07-26: Nested Lua support.
307
+ root = root.expand_path
308
+ if path.basename.sub(/\.lua$/i, '').to_s.include?('.')
309
+ warning(
310
+ "WARNING: Do not use periods in filenames. Rename: ‘#{path.basename}’"
311
+ )
312
+ end
313
+ path.dirname.ascend do |ancestor|
314
+ break if ancestor == root
315
+ if ancestor.basename.to_s.include?('.')
316
+ warning(
317
+ "WARNING: Do not use periods in directory names. Rename: ‘#{ancestor.basename}’"
318
+ )
319
+ end
320
+ end
321
+ relpath = path.relative_path_from(root).to_s
322
+ # MAYBE: Use ALT_SEPARATOR to support Windows?
323
+ # ::File::ALT_SEPARATOR || ::File::SEPARATOR
324
+ #relpath.sub(/\..*$/, '').tr(::File::SEPARATOR, '.')
325
+ relpath.sub(/\.lua$/i, '').tr(::File::SEPARATOR, '.')
201
326
  end
202
327
 
203
328
  def synckey(item)
204
329
  item[:name]
205
330
  end
206
331
  end
207
- SyncRoot.add('modules', Library, 'M', %{Modules}, true)
332
+ SyncRoot.instance.add('modules', Module, 'M', true)
208
333
 
209
334
  # Services aka EventHandlers
210
335
  class EventHandler < ServiceBase
@@ -216,114 +341,183 @@ module MrMurano
216
341
  attr_accessor :updated_at
217
342
  # @return [String] Timestamp when this was created.
218
343
  attr_accessor :created_at
219
- # @return [String] The solution.id that this is in
344
+ # @return [String] The soln's product.id or application.id (Murano's apiId).
220
345
  attr_accessor :solution_id
221
- # @return [String] Which service triggers this script
346
+ # @return [String] Which service triggers this script.
222
347
  attr_accessor :service
223
- # @return [String] Which event triggers this script
348
+ # @return [String] Which event triggers this script.
224
349
  attr_accessor :event
350
+ # @return [String] For device2 events, the type of event.
351
+ attr_accessor :type
352
+ # @return [Boolean] True if local phantom item via eventhandler.undeletable.
353
+ attr_accessor :phantom
225
354
  end
226
355
 
227
- def initialize
356
+ def initialize(sid=nil)
228
357
  super
229
- @uriparts << 'eventhandler'
358
+ @uriparts << :eventhandler
230
359
  @itemkey = :alias
231
- @project_section = :services
360
+ #@project_section = :services
361
+ raise 'Subclass missing @project_section' unless @project_section
232
362
  @match_header = /--#EVENT (?<service>\S+) (?<event>\S+)/
233
363
  end
234
364
 
235
365
  def mkalias(remote)
236
- if remote.service.nil? or remote.event.nil? then
237
- raise "Missing parts! #{remote.to_h.to_json}"
238
- else
239
- [$cfg['solution.id'], remote[:service], remote[:event]].join('_')
240
- end
366
+ raise "Missing parts! #{remote.to_h.to_json}" if remote.service.nil? || remote.event.nil?
367
+ #[$cfg[@solntype], remote[:service], remote[:event]].join('_')
368
+ [@sid, remote[:service], remote[:event]].join('_')
241
369
  end
242
370
 
243
371
  def mkname(remote)
244
- if remote.service.nil? or remote.event.nil? then
245
- raise "Missing parts! #{remote.to_h.to_json}"
246
- else
247
- [remote[:service], remote[:event]].join('_')
248
- end
372
+ raise "Missing parts! #{remote.to_h.to_json}" if remote.service.nil? || remote.event.nil?
373
+ [remote[:service], remote[:event]].join('_')
249
374
  end
250
375
 
251
- def list
252
- ret = get()
253
- return [] if ret.is_a? Hash and ret.has_key? :error
254
- # eventhandler.skiplist is a list of whitespace seperated dot-paired values.
376
+ def list(call=nil, data=nil, &block)
377
+ ret = get(call, data, &block)
378
+ return [] unless ret.is_a?(Hash) && !ret.key?(:error)
379
+ return [] unless ret.key?(:items)
380
+ # eventhandler.skiplist is a list of whitespace separated dot-paired values.
255
381
  # fe: service.event service service service.event
256
- skiplist = ($cfg['eventhandler.skiplist'] or '').split
257
- ret[:items].reject { |i|
258
- i.has_key?(:service) and i.has_key?(:event) and
259
- ( skiplist.include? i[:service] or
260
- skiplist.include? "#{i[:service]}.#{i[:event]}"
261
- )
262
- }.map{|i| EventHandlerItem.new(i)}
382
+ skiplist = ($cfg['eventhandler.skiplist'] || '').split
383
+ items = ret[:items].reject do |item|
384
+ toss = skip?(item, skiplist)
385
+ debug "skiplist excludes: #{item[:service]}.#{item[:event]}" if toss
386
+ toss
387
+ end
388
+ items.map { |item| EventHandlerItem.new(item) }
389
+ # MAYBE/2017-08-17:
390
+ # items.map! ...
391
+ # sort_by_name(items)
392
+ end
393
+
394
+ def skip?(item, skiplist)
395
+ return false unless item.key?(:service) && item.key?(:event)
396
+ skiplist.any? do |svc_evt|
397
+ cmp_svc_evt(item, svc_evt)
398
+ end
399
+ end
400
+
401
+ def cmp_svc_evt(item, svc_evt)
402
+ service, event = svc_evt.split('.', 2)
403
+ if event.nil? || item[:event] == '*'
404
+ service == item[:service]
405
+ else
406
+ # rubocop:disable Style/IfInsideElse
407
+ #svc_evt == "#{item[:service]}.#{item[:event]}"
408
+ if service == '*'
409
+ event == item[:event]
410
+ else
411
+ service == item[:service] && event == item[:event]
412
+ end
413
+ end
263
414
  end
264
415
 
265
416
  def fetch(name)
266
- ret = get('/'+CGI.escape(name))
267
- if ret.nil? then
268
- error "Fetch for #{name} returned nil; skipping"
417
+ ret = get('/' + CGI.escape(name))
418
+ unless ret.is_a?(Hash) && !ret.key?(:error)
419
+ error "Fetch for #{name} returned nil or error; skipping"
269
420
  return ''
270
421
  end
271
- aheader = (ret[:script].lines.first or "").chomp
422
+ aheader = (ret[:script].lines.first || '').chomp
272
423
  dheader = "--#EVENT #{ret[:service]} #{ret[:event]}"
273
- if block_given? then
424
+ if block_given?
274
425
  yield dheader + "\n" if aheader != dheader
275
426
  yield ret[:script]
276
427
  else
428
+ # 2017-07-02: Changing shovel operator << to +=
429
+ # to support Ruby 3.0 frozen string literals.
277
430
  res = ''
278
- res << dheader + "\n" if aheader != dheader
279
- res << ret[:script]
431
+ res += dheader + "\n" if aheader != dheader
432
+ res += ret[:script]
280
433
  res
281
434
  end
282
435
  end
283
436
 
284
- def tolocalname(item, key)
437
+ def default_event_script(service_or_sid, &block)
438
+ post(
439
+ '/',
440
+ {
441
+ solution_id: @sid,
442
+ service: service_or_sid,
443
+ event: 'event',
444
+ script: 'print(event)',
445
+ },
446
+ &block
447
+ )
448
+ end
449
+
450
+ def tolocalname(item, _key)
285
451
  "#{item[:name]}.lua"
286
452
  end
287
453
 
288
- def toRemoteItem(from, path)
454
+ def to_remote_item(from, path)
289
455
  # This allows multiple events to be in the same file. This is a lie.
290
456
  # This only finds the last event in a file.
291
- # :legacy support doesn't allow for that. but that's ok.
292
- path = Pathname.new(path) unless path.kind_of? Pathname
457
+ # :legacy support doesn't allow for that. But that's ok.
458
+ path = Pathname.new(path) unless path.is_a?(Pathname)
293
459
  cur = nil
294
- lineno=0
295
- path.readlines().each do |line|
460
+ lineno = 0
461
+ path.readlines.each do |line|
462
+ # @match_header finds a service and an event string, e.g., "--EVENT svc evt\n"
296
463
  md = @match_header.match(line)
297
- if not md.nil? then
464
+ if !md.nil?
465
+ # FIXME/2017-08-09: device2.event is now in the skiplist,
466
+ # but some tests have a "device2 data_in" script, which
467
+ # gets changed to "device2.event" here and then uploaded
468
+ # (note that skiplist does not apply to local items).
469
+ # You can test this code via:
470
+ # rspec ./spec/cmd_syncdown_spec.rb
471
+ # which has a fixture with device2.data_in specified.
472
+ # QUESTION: Does writing device2.event do anything?
473
+ # You cannot edit that handler from the web UI...
474
+ # - Should we change the test?
475
+ # - Should we get rid of this device2 hack?
476
+ if md[:service] == 'device2'
477
+ event_event = 'event'
478
+ event_type = md[:event]
479
+ # FIXME/CONFIRM/2017-07-02: 'data_in' was the old event name? It's now 'event'?
480
+ # Want this?:
481
+ # event_type = 'event' if event_type == 'data_in'
482
+ else
483
+ event_event = md[:event]
484
+ event_type = nil
485
+ end
298
486
  # header line.
299
- cur = EventHandlerItem.new(:service=>md[:service],
300
- :event=>md[:event],
301
- :local_path=>path,
302
- :line=>lineno,
303
- :script=>line)
304
- elsif not cur.nil? and not cur[:script].nil? then
305
- cur[:script] << line
487
+ cur = EventHandlerItem.new(
488
+ service: md[:service],
489
+ event: event_event,
490
+ type: event_type,
491
+ local_path: path,
492
+ line: lineno,
493
+ script: line,
494
+ )
495
+ elsif !cur.nil? && !cur[:script].nil?
496
+ # 2017-07-02: Frozen string literal: change << to +=
497
+ cur[:script] += line
306
498
  end
307
499
  lineno += 1
308
500
  end
309
501
  cur[:line_end] = lineno unless cur.nil?
310
502
 
311
503
  # If cur is nil here, then we need to do a :legacy check.
312
- if cur.nil? and $project['services.legacy'].kind_of? Hash then
504
+ if cur.nil? && $project['services.legacy'].is_a?(Hash)
313
505
  spath = path.relative_path_from(from)
314
506
  debug "No headers: #{spath}"
315
507
  service, event = $project['services.legacy'][spath.to_s]
316
508
  debug "Legacy lookup #{spath} => [#{service}, #{event}]"
317
- unless service.nil? or event.nil? then
318
- warning "Event in #{spath} missing header, but has legacy support."
319
- warning "Please add the header \"--#EVENT #{service} #{event}\""
320
- cur = EventHandlerItem.new(:service=>service,
321
- :event=>event,
322
- :local_path=>path,
323
- :line=>0,
324
- :line_end => lineno,
325
- :script=>path.read() # FIXME: ick, fix this.
326
- )
509
+ unless service.nil? || event.nil?
510
+ warning %(Event in #{spath} missing header, but has legacy support.)
511
+ warning %(Please add the header "--#EVENT #{service} #{event}")
512
+ cur = EventHandlerItem.new(
513
+ service: service,
514
+ event: event,
515
+ type: nil,
516
+ local_path: path,
517
+ line: 0,
518
+ line_end: lineno,
519
+ script: path.read, # FIXME: ick, fix this.
520
+ )
327
521
  end
328
522
  end
329
523
  cur
@@ -335,23 +529,124 @@ module MrMurano
335
529
  md = pattern_pattern.match(pattern)
336
530
  return false if md.nil?
337
531
  debug "match pattern: '#{md[:service]}' '#{md[:event]}'"
532
+ return false unless md[:service].empty? || item[:service].casecmp(md[:service]).zero?
533
+ return false unless md[:event].empty? || item[:event].casecmp(md[:event]).zero?
534
+ true # Both match (or are empty.)
535
+ end
536
+
537
+ def synckey(item)
538
+ "#{item[:service]}_#{item[:event]}"
539
+ end
338
540
 
339
- unless md[:service].empty? then
340
- return false unless item[:service].downcase == md[:service].downcase
541
+ def resurrect_undeletables(localbox, therebox)
542
+ undeletables = ($cfg['eventhandler.undeletable'] || '').split
543
+ (therebox.keys - localbox.keys).each do |key|
544
+ # key exists in therebox but not localbox.
545
+ thereitem = therebox[key]
546
+ next unless undeletable?(thereitem, undeletables)
547
+ debug "Undeletable: #{key}"
548
+ undeletable = EventHandlerItem.new(thereitem)
549
+ undeletable.id = nil
550
+ undeletable.created_at = nil
551
+ undeletable.updated_at = nil
552
+ #undeletable.local_path
553
+ #undeletable.line
554
+ # Even if the user deletes the contents of a script,
555
+ # the platform still sends the magic header.
556
+ #undeletable.script = ''
557
+ undeletable.script = (
558
+ "--#EVENT #{therebox[key].service} #{therebox[key].event}\n"
559
+ )
560
+ undeletable.local_path = Pathname.new(
561
+ File.join(location, tolocalname(thereitem, key))
562
+ )
563
+ undeletable.phantom = true
564
+ localbox[key] = undeletable
341
565
  end
566
+ localbox
567
+ end
342
568
 
343
- unless md[:event].empty? then
344
- return false unless item[:event].downcase == md[:event].downcase
569
+ def undeletable?(item, undeletables)
570
+ return false if item.service.to_s.empty? || item.event.to_s.empty?
571
+ undeletables.any? do |svc_evt|
572
+ cmp_svc_evt(item, svc_evt)
345
573
  end
574
+ end
575
+ end
346
576
 
347
- true # Both match (or are empty.)
577
+ PRODUCT_SERVICES = %w[device2 interface].freeze
578
+
579
+ class EventHandlerSolnPrd < EventHandler
580
+ def initialize(sid=nil)
581
+ @solntype = 'product.id'
582
+ # FIXME/2017-06-20: Should we use separate directories for prod vs app?
583
+ # See also :services in PrfFile and elsewhere;
584
+ # we could use @@class_var to DRY.
585
+ @project_section = :services
586
+ super
348
587
  end
349
588
 
350
- def synckey(item)
351
- "#{item[:service]}_#{item[:event]}"
589
+ def self.description
590
+ %(Interface)
591
+ end
592
+
593
+ ##
594
+ # Get a list of local items filtered by solution type.
595
+ # @return [Array<Item>] Product solution events found
596
+ def locallist(skip_warn: false)
597
+ llist = super
598
+ # This is a kludge to distinguish between Product services
599
+ # and Application services: assume that Murano only ever
600
+ # identifies Product services as 'device2' || 'interface'.
601
+ # If this weren't always the case, we'd have two obvious options:
602
+ # 1) Store Product and Application eventhandlers in separate
603
+ # directories (and update the SyncRoot.instance.add()s, below); or,
604
+ # 2) Put the solution type in the Lua script,
605
+ # e.g., change this:
606
+ # --#EVENT device2 data_in
607
+ # to this:
608
+ # --#EVENT product device2 data_in
609
+ # For now, the @service indicator is sufficient.
610
+ llist.select! { |i| PRODUCT_SERVICES.include? i.service }
611
+ llist
352
612
  end
353
613
  end
354
- SyncRoot.add('eventhandlers', EventHandler, 'E', %{Event Handlers}, true)
355
614
 
615
+ class EventHandlerSolnApp < EventHandler
616
+ def initialize(sid=nil)
617
+ @solntype = 'application.id'
618
+ # FIXME/2017-06-20: Should we use separate directories for prod vs app?
619
+ @project_section = :services
620
+ super
621
+ end
622
+
623
+ def self.description
624
+ %(Service)
625
+ end
626
+
627
+ ##
628
+ # Get a list of local items filtered by solution type.
629
+ # @return [Array<Item>] Application solution events found
630
+ def locallist(skip_warn: false)
631
+ llist = super
632
+ # "Style/InverseMethods: Use reject! instead of inverting select!."
633
+ #llist.select! { |i| !PRODUCT_SERVICES.include? i.service }
634
+ llist.reject! { |i| PRODUCT_SERVICES.include? i.service }
635
+ llist
636
+ end
637
+ end
638
+
639
+ # Order here matters, because spec/cmd_init_spec.rb
640
+ # NOTE/2017-08-07: There was one syncable in 2.x for events, but in ADC,
641
+ # there are different events for Applications and Products.
642
+ # Except there aren't any product events the user should care about (yet?).
643
+ SyncRoot.instance.add(
644
+ 'services', EventHandlerSolnApp, 'S', true, %w[eventhandlers]
645
+ )
646
+ # 2017-08-08: device2 and interface are now part of the skiplist, so no
647
+ # product event handlers will be found, unless the user modifies the skiplist.
648
+ SyncRoot.instance.add(
649
+ 'interfaces', EventHandlerSolnPrd, 'I', true, %w[]
650
+ )
356
651
  end
357
- # vim: set ai et sw=2 ts=2 :
652
+