afb0e241b837438fb1496fb6450ef8f765a69093
[promise.git] / source / spec / promise-intents.coffee
1 #
2 # Author: Peter K. Lee (peter@corenova.com)
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 module.exports =
10   'create-reservation':
11     (input, output, done) ->
12       # 1. create the reservation record (empty)
13       reservation = @create 'ResourceReservation'
14       reservations = @access 'promise.reservations'
15
16       # 2. update the record with requested input
17       reservation.invoke 'update', input.get()
18       .then (res) ->
19         # 3. save the record and add to list
20         res.save()
21         .then ->
22           reservations.push res
23           output.set result: 'ok', message: 'reservation request accepted'
24           output.set 'reservation-id', res.id
25           done()
26         .catch (err) ->
27           output.set result: 'error', message: err
28           done()
29       .catch (err) ->
30         output.set result: 'conflict', message: err
31         done()
32
33   'query-reservation':
34     (input, output, done) ->
35       query = input.get()
36       query.capacity = 'reserved'
37       @invoke 'query-capacity', query
38       .then (res) ->
39         output.set 'reservations', res.get 'collections'
40         output.set 'utilization', res.get 'utilization'
41         done()
42       .catch (e) -> done e
43
44   'update-reservation':
45     (input, output, done) ->
46       # TODO: we shouldn't need this... need to check why leaf mandatory: true not being enforced
47       unless (input.get 'reservation-id')?
48         output.set result: 'error', message: "must provide 'reservation-id' parameter"
49         return done()
50
51       # 1. find the reservation
52       reservation = @find 'ResourceReservation', input.get 'reservation-id'
53       unless reservation?
54         output.set result: 'error', message: 'no reservation found for specified identifier'
55         return done()
56
57       # 2. update the record with requested input
58       reservation.invoke 'update', input.get()
59       .then (res) ->
60         # 3. save the updated record
61         res.save()
62         .then ->
63           output.set result: 'ok', message: 'reservation update successful'
64           done()
65         .catch (err) ->
66           output.set result: 'error', message: err
67           done()
68       .catch (err) ->
69         output.set result: 'conflict', message: err
70         done()
71
72   'cancel-reservation':
73     (input, output, done) ->
74       # 1. find the reservation
75       reservation = @find 'ResourceReservation', input.get 'reservation-id'
76       unless reservation?
77         output.set result: 'error', message: 'no reservation found for specified identifier'
78         return done()
79
80       # 2. destroy all traces of this reservation
81       reservation.destroy()
82       .then =>
83         (@access 'promise.reservations').remove reservation.id
84         output.set 'result', 'ok'
85         output.set 'message', 'reservation canceled'
86         done()
87       .catch (e) ->
88         output.set 'result', 'error'
89         output.set 'message', e
90         done()
91
92   'query-capacity':
93     (input, output, done) ->
94       # 1. we gather up all collections that match the specified window
95       window = input.get 'window'
96       metric = input.get 'capacity'
97
98       collections = switch metric
99         when 'total'     then [ 'ResourcePool' ]
100         when 'reserved'  then [ 'ResourceReservation' ]
101         when 'usage'     then [ 'ResourceAllocation' ]
102         when 'available' then [ 'ResourcePool', 'ResourceReservation', 'ResourceAllocation' ]
103
104       matches = collections.reduce ((a, name) =>
105         res = @find name,
106           start: (value) -> (not window.end?)   or (new Date value) <= (new Date window.end)
107           end:   (value) -> (not window.start?) or (new Date value) >= (new Date window.start)
108           enabled: true
109         a.concat res...
110       ), []
111
112       if window.scope is 'exclusive'
113         # yes, we CAN query filter in one shot above but this makes logic cleaner...
114         matches = matches.where
115           start: (value) -> (not window.start?) or (new Date value) >= (new Date window.start)
116           end:   (value) -> (not window.end?) or (new Date value) <= (new Date window.end)
117
118       # exclude any identifiers specified
119       matches = matches.without id: (input.get 'without')
120
121       if metric is 'available'
122         # excludes allocations with reservation property set (to prevent double count)
123         matches = matches.without reservation: (v) -> v?
124
125       output.set 'collections', matches
126       unless (input.get 'show-utilization') is true
127         return done()
128
129       # 2. we calculate the deltas based on start/end times of each match item
130       deltas = matches.reduce ((a, entry) ->
131         b = entry.get()
132         b.end ?= 'infiniteT'
133         [ skey, ekey ] = [ (b.start.split 'T')[0], (b.end.split 'T')[0] ]
134         a[skey] ?= count: 0, capacity: {}
135         a[ekey] ?= count: 0, capacity: {}
136         a[skey].count += 1
137         a[ekey].count -= 1
138
139         for k, v of b.capacity when v?
140           a[skey].capacity[k] ?= 0
141           a[ekey].capacity[k] ?= 0
142           if entry.name is 'ResourcePool'
143             a[skey].capacity[k] += v
144             a[ekey].capacity[k] -= v
145           else
146             a[skey].capacity[k] -= v
147             a[ekey].capacity[k] += v
148         return a
149       ), {}
150
151       # 3. we then sort the timestamps and aggregate the deltas
152       last = count: 0, capacity: {}
153       usages = for timestamp in Object.keys(deltas).sort() when timestamp isnt 'infinite'
154         entry = deltas[timestamp]
155         entry.timestamp = (new Date timestamp).toJSON()
156         entry.count += last.count
157         for k, v of entry.capacity
158           entry.capacity[k] += (last.capacity[k] ? 0)
159         last = entry
160         entry
161
162       output.set 'utilization', usages
163       done()
164
165   'increase-capacity':
166     (input, output, done) ->
167       pool = @create 'ResourcePool', input.get()
168       pool.save()
169       .then (res) =>
170         (@access 'promise.pools').push res
171         output.set result: 'ok', message: 'capacity increase successful'
172         output.set 'pool-id', res.id
173         done()
174       .catch (e) ->
175         output.set result: 'error', message: e
176         done()
177
178   'decrease-capacity':
179     (input, output, done) ->
180       request = input.get()
181       for k, v of request.capacity
182         request.capacity[k] = -v
183       pool = @create 'ResourcePool', request
184       pool.save()
185       .then (res) =>
186         (@access 'promise.pools').push res
187         output.set result: 'ok', message: 'capacity decrease successful'
188         output.set 'pool-id', res.id
189         done()
190       .catch (e) ->
191         output.set result: 'error', message: e
192         done()
193
194   # TEMPORARY (should go into VIM-specific module)
195   'create-instance':
196     (input, output, done) ->
197       pid = input.get 'provider-id'
198       if pid?
199         provider = @find 'ResourceProvider', pid
200         unless provider?
201           output.set result: 'error', message: "no matching provider found for specified identifier: #{pid}"
202           return done()
203       else
204         provider = (@find 'ResourceProvider')[0]
205         unless provider?
206           output.set result: 'error', message: "no available provider found for create-instance"
207           return done()
208
209       # calculate required capacity based on 'flavor' and other params
210       flavor = provider.access "services.compute.flavors.#{input.get 'flavor'}"
211       unless flavor?
212         output.set result: 'error', message: "no such flavor found for specified identifier: #{pid}"
213         return done()
214
215       required =
216         instances: 1
217         cores:     flavor.get 'vcpus'
218         ram:       flavor.get 'ram'
219         gigabytes: flavor.get 'disk'
220
221       rid = input.get 'reservation-id'
222       if rid?
223         reservation = @find 'ResourceReservation', rid
224         unless reservation?
225           output.set result: 'error', message: 'no valid reservation found for specified identifier'
226           return done()
227         unless (reservation.get 'active') is true
228           output.set result: 'error', message: "reservation is currently not active"
229           return done()
230         available = reservation.get 'remaining'
231       else
232         available = @get 'promise.capacity.available'
233
234       # TODO: need to verify whether 'provider' associated with this 'reservation'
235
236       for k, v of required when v? and !!v
237         unless available[k] >= v
238           output.set result: 'conflict', message: "required #{k}=#{v} exceeds available #{available[k]}"
239           return done()
240
241       @create 'ResourceAllocation',
242         reservation: rid
243         capacity: required
244       .save()
245       .then (instance) =>
246         url = provider.get 'services.compute.endpoint'
247         payload =
248           server:
249             name: input.get 'name'
250             imageRef: input.get 'image'
251             flavorRef: input.get 'flavor'
252         networks = (input.get 'networks').filter (x) -> x? and !!x
253         if networks.length > 0
254           payload.server.networks = networks.map (x) -> uuid: x
255
256         request = @parent.require 'superagent'
257         request
258           .post "#{url}/servers"
259           .send payload
260           .set 'X-Auth-Token', provider.get 'token'
261           .set 'Accept', 'application/json'
262           .end (err, res) =>
263             if err? or !res.ok
264               instance.destroy()
265               #console.error err
266               return done res.error
267             #console.log JSON.stringify res.body, null, 2
268             instance.set 'instance-ref',
269               provider: provider
270               server: res.body.server.id
271             (@access 'promise.allocations').push instance
272             output.set result: 'ok', message: 'create-instance request accepted'
273             output.set 'instance-id', instance.id
274             done()
275          return instance
276       .catch (err) ->
277         output.set result: 'error', mesage: err
278         done()
279
280   'destroy-instance':
281     (input, output, done) ->
282       # 1. find the instance
283       instance = @find 'ResourceAllocation', input.get 'instance-id'
284       unless instance?
285         output.set result: 'error', message: 'no allocation found for specified identifier'
286         return done()
287
288       # 2. destroy all traces of this instance
289       instance.destroy()
290       .then =>
291         # always remove internally
292         (@access 'promise.allocations').remove instance.id
293         ref = instance.get 'instance-ref'
294         provider = (@access "promise.providers.#{ref.provider}")
295         url = provider.get 'services.compute.endpoint'
296         request = @parent.require 'superagent'
297         request
298           .delete "#{url}/servers/#{ref.server}"
299           .set 'X-Auth-Token', provider.get 'token'
300           .set 'Accept', 'application/json'
301           .end (err, res) =>
302             if err? or !res.ok
303               console.error err
304               return done res.error
305             output.set 'result', 'ok'
306             output.set 'message', 'instance destroyed and resource released back to pool'
307             done()
308         return instance
309       .catch (e) ->
310         output.set 'result', 'error'
311         output.set 'message', e
312         done()
313
314   # TEMPORARY (should go into VIM-specific module)
315   'add-provider':
316     (input, output, done) ->
317       app = @parent
318       request = app.require 'superagent'
319
320       payload = switch input.get 'provider-type'
321         when 'openstack'
322           auth:
323             tenantId: input.get 'tenant.id'
324             tenantName: input.get 'tenant.name'
325             passwordCredentials: input.get 'username', 'password'
326
327       unless payload?
328         return done 'Sorry, only openstack supported at this time'
329
330       url = input.get 'endpoint'
331       switch input.get 'strategy'
332         when 'keystone', 'oauth'
333           url += '/tokens' unless /\/tokens$/.test url
334
335       providers = @access 'promise.providers'
336       request
337         .post url
338         .send payload
339         .set 'Accept', 'application/json'
340         .end (err, res) =>
341           if err? or !res.ok then return done res.error
342           #console.log JSON.stringify res.body, null, 2
343           access = res.body.access
344           provider = @create 'ResourceProvider',
345             token: access?.token?.id
346             name: access?.token?.tenant?.name
347           provider.invoke 'update', access.serviceCatalog
348           .then (res) ->
349             res.save()
350             .then ->
351               providers.push res
352               output.set 'result', 'ok'
353               output.set 'provider-id', res.id
354               done()
355             .catch (err) ->
356               output.set 'error', message: err
357               done()
358           .catch (err) ->
359             output.set 'error', message: err
360             done()
361
362       # @using 'mano', ->
363       #   @invoke 'add-provider', (input.get 'endpoint', 'region', 'username', 'password')
364       #   .then (res) =>
365       #     (@access 'promise.providers').push res
366       #     output.set 'result', 'ok'
367       #     output.set 'provider-id', res.id
368       #     done()