MrMurano 1.0.0

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