diff options
Diffstat (limited to 'source/spec')
-rw-r--r-- | source/spec/openstack-intents.coffee | 6 | ||||
-rw-r--r-- | source/spec/promise-intents.coffee | 360 | ||||
-rw-r--r-- | source/spec/promise-module.coffee | 72 |
3 files changed, 438 insertions, 0 deletions
diff --git a/source/spec/openstack-intents.coffee b/source/spec/openstack-intents.coffee new file mode 100644 index 0000000..f1a10d2 --- /dev/null +++ b/source/spec/openstack-intents.coffee @@ -0,0 +1,6 @@ +request = require 'superagent' + +module.exports = + 'create-tenant': + (input, output, done) -> + # TODO - this requires OS-KSADM extension diff --git a/source/spec/promise-intents.coffee b/source/spec/promise-intents.coffee new file mode 100644 index 0000000..6ad3ae7 --- /dev/null +++ b/source/spec/promise-intents.coffee @@ -0,0 +1,360 @@ +module.exports = + 'create-reservation': + (input, output, done) -> + # 1. create the reservation record (empty) + reservation = @create 'ResourceReservation' + reservations = @access 'promise.reservations' + + # 2. update the record with requested input + reservation.invoke 'update', input.get() + .then (res) -> + # 3. save the record and add to list + res.save() + .then -> + reservations.push res + output.set result: 'ok', message: 'reservation request accepted' + output.set 'reservation-id', res.id + done() + .catch (err) -> + output.set result: 'error', message: err + done() + .catch (err) -> + output.set result: 'conflict', message: err + done() + + 'query-reservation': + (input, output, done) -> + query = input.get() + query.capacity = 'reserved' + @invoke 'query-capacity', query + .then (res) -> + output.set 'reservations', res.get 'collections' + output.set 'utilization', res.get 'utilization' + done() + .catch (e) -> done e + + 'update-reservation': + (input, output, done) -> + # TODO: we shouldn't need this... need to check why leaf mandatory: true not being enforced + unless (input.get 'reservation-id')? + output.set result: 'error', message: "must provide 'reservation-id' parameter" + return done() + + # 1. find the reservation + reservation = @find 'ResourceReservation', input.get 'reservation-id' + unless reservation? + output.set result: 'error', message: 'no reservation found for specified identifier' + return done() + + # 2. update the record with requested input + reservation.invoke 'update', input.get() + .then (res) -> + # 3. save the updated record + res.save() + .then -> + output.set result: 'ok', message: 'reservation update successful' + done() + .catch (err) -> + output.set result: 'error', message: err + done() + .catch (err) -> + output.set result: 'conflict', message: err + done() + + 'cancel-reservation': + (input, output, done) -> + # 1. find the reservation + reservation = @find 'ResourceReservation', input.get 'reservation-id' + unless reservation? + output.set result: 'error', message: 'no reservation found for specified identifier' + return done() + + # 2. destroy all traces of this reservation + reservation.destroy() + .then => + (@access 'promise.reservations').remove reservation.id + output.set 'result', 'ok' + output.set 'message', 'reservation canceled' + done() + .catch (e) -> + output.set 'result', 'error' + output.set 'message', e + done() + + 'query-capacity': + (input, output, done) -> + # 1. we gather up all collections that match the specified window + window = input.get 'window' + metric = input.get 'capacity' + + collections = switch metric + when 'total' then [ 'ResourcePool' ] + when 'reserved' then [ 'ResourceReservation' ] + when 'usage' then [ 'ResourceAllocation' ] + when 'available' then [ 'ResourcePool', 'ResourceReservation', 'ResourceAllocation' ] + + matches = collections.reduce ((a, name) => + res = @find name, + start: (value) -> (not window.end?) or (new Date value) <= (new Date window.end) + end: (value) -> (not window.start?) or (new Date value) >= (new Date window.start) + enabled: true + a.concat res... + ), [] + + if window.scope is 'exclusive' + # yes, we CAN query filter in one shot above but this makes logic cleaner... + matches = matches.where + start: (value) -> (not window.start?) or (new Date value) >= (new Date window.start) + end: (value) -> (not window.end?) or (new Date value) <= (new Date window.end) + + # exclude any identifiers specified + matches = matches.without id: (input.get 'without') + + if metric is 'available' + # excludes allocations with reservation property set (to prevent double count) + matches = matches.without reservation: (v) -> v? + + output.set 'collections', matches + unless (input.get 'show-utilization') is true + return done() + + # 2. we calculate the deltas based on start/end times of each match item + deltas = matches.reduce ((a, entry) -> + b = entry.get() + b.end ?= 'infiniteT' + [ skey, ekey ] = [ (b.start.split 'T')[0], (b.end.split 'T')[0] ] + a[skey] ?= count: 0, capacity: {} + a[ekey] ?= count: 0, capacity: {} + a[skey].count += 1 + a[ekey].count -= 1 + + for k, v of b.capacity when v? + a[skey].capacity[k] ?= 0 + a[ekey].capacity[k] ?= 0 + if entry.name is 'ResourcePool' + a[skey].capacity[k] += v + a[ekey].capacity[k] -= v + else + a[skey].capacity[k] -= v + a[ekey].capacity[k] += v + return a + ), {} + + # 3. we then sort the timestamps and aggregate the deltas + last = count: 0, capacity: {} + usages = for timestamp in Object.keys(deltas).sort() when timestamp isnt 'infinite' + entry = deltas[timestamp] + entry.timestamp = (new Date timestamp).toJSON() + entry.count += last.count + for k, v of entry.capacity + entry.capacity[k] += (last.capacity[k] ? 0) + last = entry + entry + + output.set 'utilization', usages + done() + + 'increase-capacity': + (input, output, done) -> + pool = @create 'ResourcePool', input.get() + pool.save() + .then (res) => + (@access 'promise.pools').push res + output.set result: 'ok', message: 'capacity increase successful' + output.set 'pool-id', res.id + done() + .catch (e) -> + output.set result: 'error', message: e + done() + + 'decrease-capacity': + (input, output, done) -> + request = input.get() + for k, v of request.capacity + request.capacity[k] = -v + pool = @create 'ResourcePool', request + pool.save() + .then (res) => + (@access 'promise.pools').push res + output.set result: 'ok', message: 'capacity decrease successful' + output.set 'pool-id', res.id + done() + .catch (e) -> + output.set result: 'error', message: e + done() + + # TEMPORARY (should go into VIM-specific module) + 'create-instance': + (input, output, done) -> + pid = input.get 'provider-id' + if pid? + provider = @find 'ResourceProvider', pid + unless provider? + output.set result: 'error', message: "no matching provider found for specified identifier: #{pid}" + return done() + else + provider = (@find 'ResourceProvider')[0] + unless provider? + output.set result: 'error', message: "no available provider found for create-instance" + return done() + + # calculate required capacity based on 'flavor' and other params + flavor = provider.access "services.compute.flavors.#{input.get 'flavor'}" + unless flavor? + output.set result: 'error', message: "no such flavor found for specified identifier: #{pid}" + return done() + + required = + instances: 1 + cores: flavor.get 'vcpus' + ram: flavor.get 'ram' + gigabytes: flavor.get 'disk' + + rid = input.get 'reservation-id' + if rid? + reservation = @find 'ResourceReservation', rid + unless reservation? + output.set result: 'error', message: 'no valid reservation found for specified identifier' + return done() + unless (reservation.get 'active') is true + output.set result: 'error', message: "reservation is currently not active" + return done() + available = reservation.get 'remaining' + else + available = @get 'promise.capacity.available' + + # TODO: need to verify whether 'provider' associated with this 'reservation' + + for k, v of required when v? and !!v + unless available[k] >= v + output.set result: 'conflict', message: "required #{k}=#{v} exceeds available #{available[k]}" + return done() + + @create 'ResourceAllocation', + reservation: rid + capacity: required + .save() + .then (instance) => + url = provider.get 'services.compute.endpoint' + payload = + server: + name: input.get 'name' + imageRef: input.get 'image' + flavorRef: input.get 'flavor' + networks = (input.get 'networks').filter (x) -> x? and !!x + if networks.length > 0 + payload.server.networks = networks.map (x) -> uuid: x + + request = @parent.require 'superagent' + request + .post "#{url}/servers" + .send payload + .set 'X-Auth-Token', provider.get 'token' + .set 'Accept', 'application/json' + .end (err, res) => + if err? or !res.ok + instance.destroy() + #console.error err + return done res.error + #console.log JSON.stringify res.body, null, 2 + instance.set 'instance-ref', + provider: provider + server: res.body.server.id + (@access 'promise.allocations').push instance + output.set result: 'ok', message: 'create-instance request accepted' + output.set 'instance-id', instance.id + done() + return instance + .catch (err) -> + output.set result: 'error', mesage: err + done() + + 'destroy-instance': + (input, output, done) -> + # 1. find the instance + instance = @find 'ResourceAllocation', input.get 'instance-id' + unless instance? + output.set result: 'error', message: 'no allocation found for specified identifier' + return done() + + # 2. destroy all traces of this instance + instance.destroy() + .then => + # always remove internally + (@access 'promise.allocations').remove instance.id + ref = instance.get 'instance-ref' + provider = (@access "promise.providers.#{ref.provider}") + url = provider.get 'services.compute.endpoint' + request = @parent.require 'superagent' + request + .delete "#{url}/servers/#{ref.server}" + .set 'X-Auth-Token', provider.get 'token' + .set 'Accept', 'application/json' + .end (err, res) => + if err? or !res.ok + console.error err + return done res.error + output.set 'result', 'ok' + output.set 'message', 'instance destroyed and resource released back to pool' + done() + return instance + .catch (e) -> + output.set 'result', 'error' + output.set 'message', e + done() + + # TEMPORARY (should go into VIM-specific module) + 'add-provider': + (input, output, done) -> + app = @parent + request = app.require 'superagent' + + payload = switch input.get 'provider-type' + when 'openstack' + auth: + tenantId: input.get 'tenant.id' + tenantName: input.get 'tenant.name' + passwordCredentials: input.get 'username', 'password' + + unless payload? + return done 'Sorry, only openstack supported at this time' + + url = input.get 'endpoint' + switch input.get 'strategy' + when 'keystone', 'oauth' + url += '/tokens' unless /\/tokens$/.test url + + providers = @access 'promise.providers' + request + .post url + .send payload + .set 'Accept', 'application/json' + .end (err, res) => + if err? or !res.ok then return done res.error + #console.log JSON.stringify res.body, null, 2 + access = res.body.access + provider = @create 'ResourceProvider', + token: access?.token?.id + name: access?.token?.tenant?.name + provider.invoke 'update', access.serviceCatalog + .then (res) -> + res.save() + .then -> + providers.push res + output.set 'result', 'ok' + output.set 'provider-id', res.id + done() + .catch (err) -> + output.set 'error', message: err + done() + .catch (err) -> + output.set 'error', message: err + done() + + # @using 'mano', -> + # @invoke 'add-provider', (input.get 'endpoint', 'region', 'username', 'password') + # .then (res) => + # (@access 'promise.providers').push res + # output.set 'result', 'ok' + # output.set 'provider-id', res.id + # done() diff --git a/source/spec/promise-module.coffee b/source/spec/promise-module.coffee new file mode 100644 index 0000000..3eea482 --- /dev/null +++ b/source/spec/promise-module.coffee @@ -0,0 +1,72 @@ +module.exports = + '/opnfv-promise/promise/capacity/total': (prev) -> + @computed (-> + combine = (a, b) -> + for k, v of b.capacity when v? + a[k] ?= 0 + a[k] += v + return a + (@parent.get 'pools') + .filter (entry) -> entry.active is true + .reduce combine, {} + ), type: prev + + '/opnfv-promise/promise/capacity/reserved', (prev) -> + @computed (-> + combine = (a, b) -> + for k, v of b.capacity when v? + a[k] ?= 0 + a[k] += v + return a + (@parent.get 'reservations') + .filter (entry) -> entry.active is true + .reduce combine, {} + ), type: prev + + # rebind to be a computed property + '/opnfv-promise/promise/capacity/usage': (prev) -> + @computed (-> + combine = (a, b) -> + for k, v of b.capacity when v? + a[k] ?= 0 + a[k] += v + return a + (@parent.get 'allocations') + .filter (entry) -> entry.active is true + .reduce combine, {} + ), type: prev + + # rebind to be a computed property + '/opnfv-promise/promise/capacity/available': (prev) -> + @computed (-> + total = @get 'total' + reserved = @get 'reserved' + usage = @get 'usage' + for k, v of total when v? + total[k] -= reserved[k] if reserved[k]? + total[k] -= usage[k] if usage[k]? + total + ), type: prev + + '/opnfv-promise/create-reservation': + (input, output, done) -> + # 1. create the reservation record (empty) + reservation = @create 'ResourceReservation' + reservations = @access 'promise.reservations' + + # 2. update the record with requested input + reservation.invoke 'update', input.get() + .then (res) -> + # 3. save the record and add to list + res.save() + .then -> + reservations.push res + output.set result: 'ok', message: 'reservation request accepted' + output.set 'reservation-id', res.id + done() + .catch (err) -> + output.set result: 'error', message: err + done() + .catch (err) -> + output.set result: 'conflict', message: err + done() |