MuranoCLI 2.2.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.agignore +3 -0
- data/.gitignore +18 -1
- data/.rubocop.yml +222 -0
- data/.trustme.sh +185 -0
- data/.trustme.vim +24 -0
- data/Gemfile +23 -4
- data/LICENSE.txt +1 -1
- data/MuranoCLI.gemspec +43 -8
- data/README.markdown +9 -11
- data/Rakefile +187 -143
- data/TODO.taskpaper +2 -2
- data/bin/murano +51 -52
- data/docs/basic_example.rst +436 -0
- data/docs/completions/murano_completion-bash +3484 -0
- data/docs/demo.md +32 -32
- data/docs/develop.rst +391 -0
- data/lib/MrMurano.rb +21 -7
- data/lib/MrMurano/Account.rb +159 -174
- data/lib/MrMurano/Business.rb +381 -0
- data/lib/MrMurano/Config-Migrate.rb +32 -26
- data/lib/MrMurano/Config.rb +407 -128
- data/lib/MrMurano/Content.rb +191 -0
- data/lib/MrMurano/Gateway.rb +489 -0
- data/lib/MrMurano/Keystore.rb +48 -0
- data/lib/MrMurano/Passwords.rb +103 -0
- data/lib/MrMurano/ProjectFile.rb +121 -79
- data/lib/MrMurano/ReCommander.rb +114 -10
- data/lib/MrMurano/Setting.rb +90 -0
- data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
- data/lib/MrMurano/Solution-Services.rb +461 -166
- data/lib/MrMurano/Solution-Users.rb +70 -31
- data/lib/MrMurano/Solution.rb +372 -13
- data/lib/MrMurano/SolutionId.rb +73 -0
- data/lib/MrMurano/SyncRoot.rb +137 -0
- data/lib/MrMurano/SyncUpDown.rb +594 -284
- data/lib/MrMurano/Webservice-Cors.rb +71 -0
- data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
- data/lib/MrMurano/Webservice-File.rb +193 -0
- data/lib/MrMurano/Webservice.rb +51 -0
- data/lib/MrMurano/commands.rb +18 -15
- data/lib/MrMurano/commands/business.rb +300 -6
- data/lib/MrMurano/commands/completion-bash.erb +166 -0
- data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
- data/lib/MrMurano/commands/completion.rb +76 -39
- data/lib/MrMurano/commands/config.rb +108 -44
- data/lib/MrMurano/commands/content.rb +115 -72
- data/lib/MrMurano/commands/cors.rb +29 -14
- data/lib/MrMurano/commands/devices.rb +286 -0
- data/lib/MrMurano/commands/domain.rb +52 -12
- data/lib/MrMurano/commands/gb.rb +24 -9
- data/lib/MrMurano/commands/globals.rb +64 -0
- data/lib/MrMurano/commands/init.rb +377 -155
- data/lib/MrMurano/commands/keystore.rb +92 -82
- data/lib/MrMurano/commands/link.rb +300 -0
- data/lib/MrMurano/commands/login.rb +74 -11
- data/lib/MrMurano/commands/logs.rb +63 -32
- data/lib/MrMurano/commands/mock.rb +57 -29
- data/lib/MrMurano/commands/password.rb +57 -39
- data/lib/MrMurano/commands/postgresql.rb +127 -94
- data/lib/MrMurano/commands/settings.rb +203 -0
- data/lib/MrMurano/commands/show.rb +79 -38
- data/lib/MrMurano/commands/solution.rb +423 -5
- data/lib/MrMurano/commands/solution_picker.rb +547 -0
- data/lib/MrMurano/commands/status.rb +195 -61
- data/lib/MrMurano/commands/sync.rb +78 -39
- data/lib/MrMurano/commands/timeseries.rb +71 -55
- data/lib/MrMurano/commands/tsdb.rb +113 -87
- data/lib/MrMurano/commands/usage.rb +57 -15
- data/lib/MrMurano/hash.rb +100 -10
- data/lib/MrMurano/http.rb +187 -43
- data/lib/MrMurano/makePretty.rb +16 -14
- data/lib/MrMurano/optparse.rb +2178 -0
- data/lib/MrMurano/progress.rb +138 -0
- data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
- data/lib/MrMurano/template/projectFile.murano.erb +16 -13
- data/lib/MrMurano/verbosing.rb +166 -29
- data/lib/MrMurano/version.rb +30 -1
- data/spec/Account-Passwords_spec.rb +21 -4
- data/spec/Account_spec.rb +69 -146
- data/spec/Business_spec.rb +290 -0
- data/spec/ConfigFile_spec.rb +1 -0
- data/spec/ConfigMigrate_spec.rb +12 -8
- data/spec/Config_spec.rb +40 -34
- data/spec/Content_spec.rb +363 -0
- data/spec/GatewayBase_spec.rb +54 -0
- data/spec/GatewayDevice_spec.rb +321 -0
- data/spec/GatewayResource_spec.rb +266 -0
- data/spec/GatewaySettings_spec.rb +120 -0
- data/spec/Http_spec.rb +18 -8
- data/spec/Mock_spec.rb +2 -2
- data/spec/ProjectFile_spec.rb +25 -14
- data/spec/Setting_spec.rb +110 -0
- data/spec/Solution-ServiceConfig_spec.rb +44 -5
- data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
- data/spec/Solution-ServiceModules_spec.rb +47 -37
- data/spec/Solution-UsersRoles_spec.rb +10 -8
- data/spec/Solution_spec.rb +17 -8
- data/spec/SyncRoot_spec.rb +46 -20
- data/spec/SyncUpDown_spec.rb +437 -201
- data/spec/Verbosing_spec.rb +12 -4
- data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
- data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
- data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
- data/spec/Webservice-Setting_spec.rb +89 -0
- data/spec/_workspace.rb +4 -4
- data/spec/cmd_business_spec.rb +9 -4
- data/spec/cmd_common.rb +44 -1
- data/spec/cmd_content_spec.rb +43 -17
- data/spec/cmd_cors_spec.rb +4 -4
- data/spec/cmd_device_spec.rb +61 -16
- data/spec/cmd_domain_spec.rb +29 -6
- data/spec/cmd_init_spec.rb +281 -126
- data/spec/cmd_keystore_spec.rb +3 -3
- data/spec/cmd_link_spec.rb +98 -0
- data/spec/cmd_password_spec.rb +1 -1
- data/spec/cmd_setting_application_spec.rb +260 -0
- data/spec/cmd_setting_product_spec.rb +220 -0
- data/spec/cmd_status_spec.rb +223 -114
- data/spec/cmd_syncdown_spec.rb +115 -35
- data/spec/cmd_syncup_spec.rb +68 -15
- data/spec/cmd_usage_spec.rb +35 -8
- data/spec/fixtures/dumped_config +6 -4
- data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
- data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
- data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
- data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
- data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
- data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
- data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
- data/spec/spec_helper.rb +5 -0
- metadata +262 -171
- data/bin/mr +0 -8
- data/lib/MrMurano/Product-1P-Device.rb +0 -145
- data/lib/MrMurano/Product-Resources.rb +0 -205
- data/lib/MrMurano/Product.rb +0 -358
- data/lib/MrMurano/Solution-Cors.rb +0 -47
- data/lib/MrMurano/Solution-Endpoint.rb +0 -191
- data/lib/MrMurano/Solution-File.rb +0 -166
- data/lib/MrMurano/commands/assign.rb +0 -57
- data/lib/MrMurano/commands/businessList.rb +0 -45
- data/lib/MrMurano/commands/product.rb +0 -14
- data/lib/MrMurano/commands/productCreate.rb +0 -39
- data/lib/MrMurano/commands/productDelete.rb +0 -33
- data/lib/MrMurano/commands/productDevice.rb +0 -87
- data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
- data/lib/MrMurano/commands/productList.rb +0 -45
- data/lib/MrMurano/commands/productWrite.rb +0 -27
- data/lib/MrMurano/commands/solutionCreate.rb +0 -41
- data/lib/MrMurano/commands/solutionDelete.rb +0 -34
- data/lib/MrMurano/commands/solutionList.rb +0 -45
- data/spec/ProductBase_spec.rb +0 -113
- data/spec/ProductContent_spec.rb +0 -162
- data/spec/ProductResources_spec.rb +0 -329
- data/spec/Product_1P_Device_spec.rb +0 -202
- data/spec/Product_1P_RPC_spec.rb +0 -175
- data/spec/Product_spec.rb +0 -153
- data/spec/Solution-ServiceDevice_spec.rb +0 -176
- data/spec/cmd_assign_spec.rb +0 -51
@@ -1,112 +1,188 @@
|
|
1
|
-
|
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(
|
28
|
+
def mkalias(_remote)
|
16
29
|
# :nocov:
|
17
|
-
raise
|
30
|
+
raise 'Needs to be implemented in child'
|
18
31
|
# :nocov:
|
19
32
|
end
|
20
33
|
|
21
|
-
def mkname(
|
34
|
+
def mkname(_remote)
|
22
35
|
# :nocov:
|
23
|
-
raise
|
36
|
+
raise 'Needs to be implemented in child'
|
24
37
|
# :nocov:
|
25
38
|
end
|
26
39
|
|
27
40
|
def fetch(name)
|
28
|
-
raise
|
29
|
-
raise
|
30
|
-
ret = get('/'+CGI.escape(name))
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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]
|
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(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
122
|
+
cache_update_time_for(localpath, updated_at)
|
74
123
|
end
|
75
124
|
|
76
|
-
def docmp(
|
77
|
-
if
|
78
|
-
ct =
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
85
|
-
ct =
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
158
|
+
mrg_diff
|
92
159
|
end
|
93
160
|
|
94
|
-
def
|
95
|
-
[
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
102
|
-
|
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
|
-
:
|
105
|
-
:
|
178
|
+
sha1: file_hash,
|
179
|
+
updated_at: time.to_datetime.iso8601(3),
|
106
180
|
}
|
107
|
-
|
108
|
-
if
|
109
|
-
|
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
|
-
|
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
|
127
|
-
cksm =
|
128
|
-
|
129
|
-
return nil unless
|
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
|
-
|
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.
|
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
|
214
|
+
debug(" cached: #{entry}")
|
138
215
|
debug(" cm: #{cksm}")
|
139
|
-
if entry.
|
140
|
-
if entry[:sha1] == cksm
|
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
|
-
#
|
151
|
-
class
|
238
|
+
# What Murano calls "Modules". Snippets of Lua code.
|
239
|
+
class Module < ServiceBase
|
152
240
|
# Module Specific details on an Item
|
153
|
-
class
|
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.
|
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 <<
|
256
|
+
@uriparts << :module
|
167
257
|
@itemkey = :alias
|
168
258
|
@project_section = :modules
|
169
259
|
end
|
170
260
|
|
171
|
-
def
|
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
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
186
|
-
|
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 []
|
195
|
-
|
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
|
199
|
-
|
200
|
-
|
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',
|
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
|
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 <<
|
358
|
+
@uriparts << :eventhandler
|
230
359
|
@itemkey = :alias
|
231
|
-
|
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?
|
237
|
-
|
238
|
-
|
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?
|
245
|
-
|
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 []
|
254
|
-
|
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']
|
257
|
-
ret[:items].reject
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
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
|
422
|
+
aheader = (ret[:script].lines.first || '').chomp
|
272
423
|
dheader = "--#EVENT #{ret[:service]} #{ret[:event]}"
|
273
|
-
if block_given?
|
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
|
279
|
-
res
|
431
|
+
res += dheader + "\n" if aheader != dheader
|
432
|
+
res += ret[:script]
|
280
433
|
res
|
281
434
|
end
|
282
435
|
end
|
283
436
|
|
284
|
-
def
|
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
|
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.
|
292
|
-
path = Pathname.new(path) unless path.
|
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
|
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
|
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(
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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?
|
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?
|
318
|
-
warning
|
319
|
-
warning
|
320
|
-
cur = EventHandlerItem.new(
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
340
|
-
|
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
|
-
|
344
|
-
|
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
|
-
|
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
|
351
|
-
|
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
|
-
|
652
|
+
|