MrMurano 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ require 'terminal-table'
2
+
3
+ module MrMurano
4
+ # …/serviceconfig
5
+ class ServiceConfig < SolutionBase
6
+ def initialize
7
+ super
8
+ @uriparts << 'serviceconfig'
9
+ end
10
+
11
+ def list
12
+ get()['items']
13
+ end
14
+ def fetch(id)
15
+ get('/' + id.to_s)
16
+ end
17
+
18
+
19
+ def assignTriggers(products)
20
+ scr = list().select{|i| i['service'] == 'device' or i[:service] == 'device'}.first
21
+ scid = scr['id'] or scr[:id]
22
+
23
+ details = Hash.transform_keys_to_symbols(fetch(scid))
24
+ products = [products] unless products.kind_of? Array
25
+ details[:triggers] = {:pid=>products}
26
+
27
+ put('/'+scid, details)
28
+
29
+ end
30
+
31
+ def showTriggers
32
+ scr = list().select{|i| i['service'] == 'device' or i[:service] == 'device'}.first
33
+ scid = scr['id'] or scr[:id]
34
+
35
+ details = Hash.transform_keys_to_symbols(fetch(scid))
36
+
37
+ return [] if details[:triggers].nil?
38
+ details[:triggers][:pid]
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ command :assign do |c|
45
+ c.syntax = 'mr assign [product]'
46
+ c.description = 'Assign a product to a eventhandler'
47
+
48
+ c.option '--list', %{List assigned products}
49
+ c.option '--idonly', 'Only return the ids'
50
+
51
+ c.action do |args, options|
52
+ sol = MrMurano::ServiceConfig.new
53
+
54
+ if options.list then
55
+ trigs = sol.showTriggers()
56
+ if options.idonly then
57
+ say trigs.join(' ')
58
+ else
59
+ acc = MrMurano::Account.new
60
+ products = acc.products.map{|p| Hash.transform_keys_to_symbols(p)}
61
+ products.select!{|p| trigs.include? p[:pid] }
62
+ busy = products.map{|r| [r[:label], r[:type], r[:pid], r[:modelId]]}
63
+ table = Terminal::Table.new :rows => busy, :headings => ['Label', 'Type', 'PID', 'ModelID']
64
+ say table
65
+ end
66
+
67
+ else
68
+ prname = args.shift
69
+ if prname.nil? then
70
+ prid = $cfg['product.id']
71
+ else
72
+ acc = MrMurano::Account.new
73
+ products = acc.products.map{|p| Hash.transform_keys_to_symbols(p)}
74
+ products.select!{|p| p[:label] == prname or p[:pid] == prname }
75
+ prid = products.map{|p| p[:pid]}
76
+ end
77
+ raise "No product ID!" if prid.nil?
78
+ say "Assigning #{prid} to solution" if $cfg['tool.verbose']
79
+ sol.assignTriggers(prid) unless $cfg['tool.dry']
80
+ end
81
+
82
+
83
+ end
84
+ end
85
+
86
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,163 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'pp'
5
+
6
+ module MrMurano
7
+ ##
8
+ # Things that servers do that is common.
9
+ class ServiceBase < SolutionBase
10
+ # not quite sure why this is needed, but…
11
+ def mkalias(name)
12
+ case name
13
+ when String
14
+ "/#{$cfg['solution.id']}_#{name}"
15
+ when Hash
16
+ if name.has_key? :name then
17
+ "/#{$cfg['solution.id']}_#{name[:name]}"
18
+ elsif name.has_key? :service and name.has_key? :event then
19
+ "/#{$cfg['solution.id']}_#{name[:service]}_#{name[:event]}"
20
+ else
21
+ raise "unknown keys. #{name}"
22
+ end
23
+ else
24
+ raise "unknown type. #{name}"
25
+ end
26
+ end
27
+
28
+ def list
29
+ ret = get()
30
+ ret['items']
31
+ end
32
+
33
+ def fetch(name)
34
+ ret = get('/'+name)
35
+ if block_given? then
36
+ yield ret['script']
37
+ else
38
+ ret['script']
39
+ end
40
+ end
41
+
42
+ # ??? remove
43
+ def remove(name)
44
+ delete('/'+name)
45
+ end
46
+
47
+ def upload(local, remote)
48
+ local = Pathname.new(local) unless local.kind_of? Pathname
49
+ raise "no file" unless local.exist?
50
+
51
+ # we assume these are small enough to slurp.
52
+ script = local.read
53
+
54
+ pst = remote.merge ({
55
+ :solution_id => $cfg['solution.id'],
56
+ :script => script
57
+ })
58
+
59
+ # try put, if 404, then post.
60
+ put(mkalias(remote), pst) do |request, http|
61
+ response = http.request(request)
62
+ case response
63
+ when Net::HTTPSuccess
64
+ #return JSON.parse(response.body)
65
+ when Net::HTTPNotFound
66
+ verbose "Doesn't exist, creating"
67
+ post('/', pst)
68
+ else
69
+ say_error "got #{response} from #{request} #{request.uri.to_s}"
70
+ say_error ":: #{response.body}"
71
+ end
72
+ end
73
+ end
74
+
75
+ def docmp(itemA, itemB)
76
+ if itemA[:updated_at].nil? and itemA[:local_path] then
77
+ itemA[:updated_at] = itemA[:local_path].mtime.getutc
78
+ elsif itemA[:updated_at].kind_of? String then
79
+ itemA[:updated_at] = DateTime.parse(itemA[:updated_at]).to_time.getutc
80
+ end
81
+ if itemB[:updated_at].nil? and itemB[:local_path] then
82
+ itemB[:updated_at] = itemB[:local_path].mtime.getutc
83
+ elsif itemB[:updated_at].kind_of? String then
84
+ itemB[:updated_at] = DateTime.parse(itemB[:updated_at]).to_time.getutc
85
+ end
86
+ return itemA[:updated_at] != itemB[:updated_at]
87
+ end
88
+
89
+ end
90
+
91
+ # …/library
92
+ class Library < ServiceBase
93
+ def initialize
94
+ super
95
+ @uriparts << 'library'
96
+ @itemkey = :alias
97
+ end
98
+
99
+ def tolocalname(item, key)
100
+ name = item[:name]
101
+ "#{name}.lua"
102
+ end
103
+
104
+
105
+ def toremotename(from, path)
106
+ name = path.basename.to_s.sub(/\..*/, '')
107
+ {:name => name}
108
+ end
109
+
110
+ def synckey(item)
111
+ item[:name]
112
+ end
113
+ end
114
+
115
+ # …/eventhandler
116
+ class EventHandler < ServiceBase
117
+ def initialize
118
+ super
119
+ @uriparts << 'eventhandler'
120
+ @itemkey = :alias
121
+ end
122
+
123
+ def list
124
+ ret = get()
125
+ skiplist = ($cfg['eventhandler.skiplist'] or '').split
126
+ ret['items'].reject{|i| i.has_key?('service') and skiplist.include? i['service'] }
127
+ end
128
+
129
+ def fetch(name)
130
+ ret = get('/'+name)
131
+ aheader = (ret['script'].lines.first or "").chomp
132
+ dheader = "--#EVENT #{ret['service']} #{ret['event']}"
133
+ if block_given? then
134
+ yield dheader + "\n" if aheader != dheader
135
+ yield ret['script']
136
+ else
137
+ res = ''
138
+ res << dheader + "\n" if aheader != dheader
139
+ res << ret['script']
140
+ res
141
+ end
142
+ end
143
+
144
+ def tolocalname(item, key)
145
+ "#{item[:name]}.lua"
146
+ end
147
+
148
+ def toremotename(from, path)
149
+ path = Pathname.new(path) unless path.kind_of? Pathname
150
+ aheader = path.readlines().first
151
+ md = /--#EVENT (\S+) (\S+)/.match(aheader)
152
+ raise "Not an Event handler: #{path.to_s}" if md.nil?
153
+ {:service=>md[1], :event=>md[2]}
154
+ end
155
+
156
+ def synckey(item)
157
+ "#{item[:service]}_#{item[:event]}"
158
+ end
159
+ end
160
+
161
+ # How do we enable product.id to flow into the eventhandler?
162
+ end
163
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,123 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'yaml'
5
+ require 'pp'
6
+
7
+ module MrMurano
8
+ ##
9
+ # User Management common things
10
+ class UserBase < SolutionBase
11
+ def list()
12
+ get()
13
+ end
14
+
15
+ def fetch(id)
16
+ get('/' + id.to_s)
17
+ end
18
+
19
+ def remove(id)
20
+ delete('/' + id.to_s)
21
+ end
22
+
23
+ def upload(local, remote)
24
+ # Roles cannot be modified, so must delete and post.
25
+ delete('/' + remote[@itemkey]) do |request, http|
26
+ response = http.request(request)
27
+ case response
28
+ when Net::HTTPSuccess
29
+ when Net::HTTPNotFound
30
+ else
31
+ say_error "got #{response} from #{request} #{request.uri.to_s}"
32
+ say_error ":: #{response.body}"
33
+ end
34
+ end
35
+ post('/', remote)
36
+ end
37
+
38
+ def download(local, item)
39
+ # needs to append/merge with file
40
+ # for now, we'll read, modify, write
41
+ here = []
42
+ if local.exist? then
43
+ local.open('rb') {|io| here = YAML.load(io)}
44
+ here = [] if here == false
45
+ end
46
+ here.delete_if do |i|
47
+ Hash.transform_keys_to_symbols(i)[@itemkey] == item[@itemkey]
48
+ end
49
+ here << item
50
+ local.open('wb') do |io|
51
+ io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
52
+ end
53
+ end
54
+
55
+ def removelocal(dest, item)
56
+ # needs to append/merge with file
57
+ # for now, we'll read, modify, write
58
+ here = []
59
+ if local.exist? then
60
+ local.open('rb') {|io| here = YAML.load(io)}
61
+ here = [] if here == false
62
+ end
63
+ key = @itemkey.to_sym
64
+ here.delete_if do |it|
65
+ Hash.transform_keys_to_symbols(it)[key] == item[key]
66
+ end
67
+ local.open('wb') do|io|
68
+ io.write here.map{|i| Hash.transform_keys_to_strings(i)}.to_yaml
69
+ end
70
+ end
71
+
72
+ def tolocalpath(into, item)
73
+ into
74
+ end
75
+
76
+ def locallist(from)
77
+ from = Pathname.new(from) unless from.kind_of? Pathname
78
+ if not from.exist? then
79
+ say_warning "Skipping missing #{from.to_s}"
80
+ return []
81
+ end
82
+ unless from.file? then
83
+ say_warning "Cannot read from #{from.to_s}"
84
+ return []
85
+ end
86
+ key = @itemkey.to_sym
87
+
88
+ here = []
89
+ from.open {|io| here = YAML.load(io) }
90
+ here = [] if here == false
91
+
92
+ here
93
+ end
94
+ end
95
+
96
+ # …/role
97
+ class Role < UserBase
98
+ def initialize
99
+ super
100
+ @uriparts << 'role'
101
+ @itemkey = :role_id
102
+ end
103
+ end
104
+
105
+ # …/user
106
+ class User < UserBase
107
+ def initialize
108
+ super
109
+ @uriparts << 'user'
110
+ end
111
+
112
+ def upload(local, remote)
113
+ # TODO figure out APIs for updating users.
114
+ say_warning "Updating Users isn't working currently."
115
+ # post does work if the :password field is set.
116
+ end
117
+
118
+ def synckey(item)
119
+ item[:email]
120
+ end
121
+ end
122
+ end
123
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,318 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'pp'
5
+
6
+ module MrMurano
7
+ class SolutionBase
8
+ # This might also be a valid ProductBase.
9
+ def initialize
10
+ @token = Account.new.token
11
+ @sid = $cfg['solution.id']
12
+ raise "No solution!" if @sid.nil?
13
+ @uriparts = [:solution, @sid]
14
+ @itemkey = :id
15
+ end
16
+
17
+ def verbose(msg)
18
+ if $cfg['tool.verbose'] then
19
+ say msg
20
+ end
21
+ end
22
+
23
+ def endPoint(path='')
24
+ parts = ['https:/', $cfg['net.host'], 'api:1'] + @uriparts
25
+ s = parts.map{|v| v.to_s}.join('/')
26
+ URI(s + path.to_s)
27
+ end
28
+ def http
29
+ uri = URI('https://' + $cfg['net.host'])
30
+ if @http.nil? then
31
+ @http = Net::HTTP.new(uri.host, uri.port)
32
+ @http.use_ssl = true
33
+ @http.start
34
+ end
35
+ @http
36
+ end
37
+
38
+ def set_req_defaults(request)
39
+ request.content_type = 'application/json'
40
+ request['authorization'] = 'token ' + @token
41
+ request['User-Agent'] = "MrMurano/#{MrMurano::VERSION}"
42
+ request
43
+ end
44
+
45
+ def workit(request, &block)
46
+ set_req_defaults(request)
47
+ if block_given? then
48
+ yield request, http()
49
+ else
50
+ response = http().request(request)
51
+ case response
52
+ when Net::HTTPSuccess
53
+ return {} if response.body.nil?
54
+ begin
55
+ return JSON.parse(response.body)
56
+ rescue
57
+ return response.body
58
+ end
59
+ else
60
+ say_error "got #{response} from #{request} #{request.uri.to_s}"
61
+ say_error ":: #{response.body}"
62
+ raise response
63
+ end
64
+ end
65
+ end
66
+
67
+ def get(path='', &block)
68
+ uri = endPoint(path)
69
+ workit(Net::HTTP::Get.new(uri), &block)
70
+ end
71
+
72
+ def post(path='', body={}, &block)
73
+ uri = endPoint(path)
74
+ req = Net::HTTP::Post.new(uri)
75
+ req.body = JSON.generate(body)
76
+ workit(req, &block)
77
+ end
78
+
79
+ def put(path='', body={}, &block)
80
+ uri = endPoint(path)
81
+ req = Net::HTTP::Put.new(uri)
82
+ req.body = JSON.generate(body)
83
+ workit(req, &block)
84
+ end
85
+
86
+ def delete(path='', &block)
87
+ uri = endPoint(path)
88
+ workit(Net::HTTP::Delete.new(uri), &block)
89
+ end
90
+
91
+ # …
92
+
93
+ def toremotename(root, path)
94
+ path = Pathname.new(path) unless path.kind_of? Pathname
95
+ root = Pathname.new(root) unless root.kind_of? Pathname
96
+ path.relative_path_from(root).to_s
97
+ end
98
+ def tolocalpath(into, item)
99
+ into.mkpath unless $cfg['tool.dry']
100
+ return item[:local_path] if item.has_key? :local_path
101
+ itemkey = @itemkey.to_sym
102
+ name = tolocalname(item, itemkey)
103
+ raise "Bad key(#{itemkey}) for #{item}" if name.nil?
104
+ dest = into + name
105
+ end
106
+
107
+ def locallist(from)
108
+ from = Pathname.new(from) unless from.kind_of? Pathname
109
+ unless from.exist? then
110
+ return []
111
+ end
112
+ raise "Not a directory: #{from.to_s}" unless from.directory?
113
+
114
+ Pathname.glob(from.to_s + '/**/*').map do |path|
115
+ name = toremotename(from, path)
116
+ case name
117
+ when Hash
118
+ name[:local_path] = path
119
+ name
120
+ else
121
+ {:local_path => path, :name => name}
122
+ end
123
+ end
124
+ end
125
+
126
+ def synckey(item)
127
+ key = @itemkey.to_sym
128
+ item[key]
129
+ end
130
+
131
+ def syncup(from, options={})
132
+ there = list()
133
+ here = locallist(from)
134
+ itemkey = @itemkey.to_sym
135
+
136
+ # split into three lists.
137
+ # - Items here and not there. (toadd)
138
+ # - Items there and not here. (todel)
139
+ # - Items here and there. (tomod)
140
+ therebox = {}
141
+ there.each do |item|
142
+ item = Hash.transform_keys_to_symbols(item)
143
+ therebox[ synckey(item) ] = item
144
+ end
145
+ herebox = {}
146
+ here.each do |item|
147
+ item = Hash.transform_keys_to_symbols(item)
148
+ herebox[ synckey(item) ] = item
149
+ end
150
+ toadd = herebox.keys - therebox.keys
151
+ todel = therebox.keys - herebox.keys
152
+ tomod = herebox.keys & therebox.keys
153
+
154
+ if options.delete then
155
+ todel.each do |key|
156
+ verbose "Removing item #{key}"
157
+ unless $cfg['tool.dry'] then
158
+ item = therebox[key]
159
+ remove(item[itemkey])
160
+ end
161
+ end
162
+ end
163
+ if options.create then
164
+ toadd.each do |key|
165
+ verbose "Adding item #{key}"
166
+ unless $cfg['tool.dry'] then
167
+ item = herebox[key]
168
+ upload(item[:local_path], item.reject{|k,v| k==:local_path})
169
+ end
170
+ end
171
+ end
172
+ if options.update then
173
+ tomod.each do |key|
174
+ verbose "Updating item #{key}"
175
+ unless $cfg['tool.dry'] then
176
+ #item = therebox[key].merge herebox[key] # need to be consistent with key types for this to work
177
+ id = therebox[key][itemkey]
178
+ item = herebox[key].dup
179
+ item[itemkey] = id
180
+ upload(item[:local_path], item.reject{|k,v| k==:local_path})
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def syncdown(into, options={})
187
+ there = list()
188
+ into = Pathname.new(into) unless into.kind_of? Pathname
189
+ here = locallist(into)
190
+ itemkey = @itemkey.to_sym
191
+
192
+ # split into three lists.
193
+ # - Items here and not there. (todel)
194
+ # - Items there and not here. (toadd)
195
+ # - Items here and there. (tomod)
196
+ therebox = {}
197
+ there.each do |item|
198
+ item = Hash.transform_keys_to_symbols(item)
199
+ therebox[ synckey(item) ] = item
200
+ end
201
+ herebox = {}
202
+ here.each do |item|
203
+ item = Hash.transform_keys_to_symbols(item)
204
+ herebox[ synckey(item) ] = item
205
+ end
206
+ todel = herebox.keys - therebox.keys
207
+ toadd = therebox.keys - herebox.keys
208
+ tomod = herebox.keys & therebox.keys
209
+
210
+ if options.delete then
211
+ todel.each do |key|
212
+ verbose "Removing item #{key}"
213
+ unless $cfg['tool.dry'] then
214
+ item = herebox[key]
215
+ dest = tolocalpath(into, item)
216
+ removelocal(dest, item)
217
+ end
218
+ end
219
+ end
220
+ if options.create then
221
+ toadd.each do |key|
222
+ verbose "Adding item #{key}"
223
+ unless $cfg['tool.dry'] then
224
+ item = therebox[key]
225
+ dest = tolocalpath(into, item)
226
+
227
+ download(dest, item)
228
+ end
229
+ end
230
+ end
231
+ if options.update then
232
+ tomod.each do |key|
233
+ verbose "Updating item #{key}"
234
+ unless $cfg['tool.dry'] then
235
+ item = therebox[key]
236
+ dest = tolocalpath(into, herebox[key].merge(item) )
237
+
238
+ download(dest, item)
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ def download(local, item)
245
+ id = item[@itemkey.to_sym]
246
+ local.open('wb') do |io|
247
+ fetch(id) do |chunk|
248
+ io.write chunk
249
+ end
250
+ end
251
+ end
252
+
253
+ def removelocal(dest, item)
254
+ dest.unlink
255
+ end
256
+
257
+ def status(from, options={})
258
+ there = list()
259
+ here = locallist(from)
260
+ itemkey = @itemkey.to_sym
261
+
262
+ therebox = {}
263
+ there.each do |item|
264
+ item = Hash.transform_keys_to_symbols(item)
265
+ item[:synckey] = synckey(item)
266
+ therebox[ item[:synckey] ] = item
267
+ end
268
+ herebox = {}
269
+ here.each do |item|
270
+ item = Hash.transform_keys_to_symbols(item)
271
+ item[:synckey] = synckey(item)
272
+ herebox[ item[:synckey] ] = item
273
+ end
274
+ if options.asdown then
275
+ todel = herebox.keys - therebox.keys
276
+ toadd = therebox.keys - herebox.keys
277
+ tomod = herebox.keys & therebox.keys
278
+ {
279
+ :toadd=> toadd.map{|key| therebox[key] },
280
+ :todel=> todel.map{|key| herebox[key] },
281
+ # FIXME what if therebox[key] is nil?
282
+ :tomod=> tomod.map{|key| therebox[key].merge(herebox[key]) }
283
+ }
284
+ else
285
+ toadd = herebox.keys - therebox.keys
286
+ todel = therebox.keys - herebox.keys
287
+ tomod = herebox.keys & therebox.keys
288
+ {
289
+ :toadd=> toadd.map{|key| herebox[key] },
290
+ :todel=> todel.map{|key| therebox[key] },
291
+ :tomod=> tomod.map{|key| therebox[key].merge(herebox[key]) }
292
+ }
293
+ end
294
+ end
295
+ end
296
+
297
+ class Solution < SolutionBase
298
+ def version
299
+ get('/version')
300
+ end
301
+
302
+ def info
303
+ get()
304
+ end
305
+
306
+ def list
307
+ get('/')
308
+ end
309
+
310
+ def log
311
+ get('/logs')
312
+ end
313
+
314
+ end
315
+
316
+ end
317
+
318
+ # vim: set ai et sw=2 ts=2 :