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