#
# Author: Peter K. Lee (peter@corenova.com)
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Apache License, Version 2.0
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
#

name: opnfv-promise
description: Resource Management for Virtualized Infrastructure
author: Peter K. Lee <peter@corenova.com>
license: Apache-2.0
homepage: http://wiki.opnfv.org/promise
repository: git://github.com/opnfv/promise.git
yangforge: "0.11.x"
keywords:
  - opnfv
  - promise
  - vim
  - nfvi
  - infrastructure
  - openstack
  - nbi
  - yangforge
  - resource
  - reservation
  - capacity
  - allocation

schema: !yang schema/opnfv-promise.yang

# below config provides default parameters
# NOTE: uncomment locally for testing with pre-existing data
#config: !json config/demo.json

dependencies:
  access-control-models: !yang schema/access-control-models.yang
  nfv-infrastructure:    !yang schema/nfv-infrastructure.yang

# MODULE model active bindings
module:
  opnfv-promise:
    # rebind to be a computed property
    promise.capacity.total: !coffee/function |
      (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
    # rebind to be a computed property
    promise.capacity.reserved: !coffee/function |
      (prev) -> @computed (->
        combine = (a, b) ->
          for k, v of b.remaining 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
    promise.capacity.usage: !coffee/function |
      (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
    promise.capacity.available: !coffee/function |
      (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

# RPC definitions (INTENT interfaces)
rpc: !require spec/promise-intents.coffee

# COMPLEX-TYPE model active bindings (controller logic)
complex-type:
  ResourceElement:
    #properties
    id: !coffee/function |
      (prev) -> prev.set 'default', -> @uuid()

  ResourceCollection:
    # properties
    start: !coffee/function |
      (prev) -> prev.set 'default', -> (new Date).toJSON()

    active: !coffee/function |
      (prev) -> @computed (->
        now = new Date
        start = new Date (@get 'start')
        end = switch
          when (@get 'end')? then new Date (@get 'end')
          else now
        (@get 'enabled') and (start <= now <= end)
      ), type: prev

  ResourceReservation:
    end: !coffee/function |
      (prev) -> prev.set 'default', ->
        end = (new Date @get 'start')
        max = @parent.get 'promise.policy.reservation.max-duration'
        return unless max?
        end.setTime (end.getTime() + (max*60*60*1000))
        end.toJSON()

    allocations: !coffee/function |
      (prev) -> @computed (->
        res = (@store.find 'ResourceAllocation', reservation: @id)
        res.map (x) -> x.get 'id'
      ), type: 'array'

    remaining: !coffee/function |
      (prev) -> @computed (->
        total = @get 'capacity'
        records = @store.find 'ResourceAllocation', id: (@get 'allocations'), active: true
        for entry in records
          usage = entry.get 'capacity'
          for k, v of usage
            total[k] -= v
        total
      ), type: prev

    # methods
    validate: !coffee/function |
      (prev) -> (value={}, resolve, reject) ->
        # validate that request contains sufficient data
        for k, v of value.capacity when v? and !!v
          hasCapacity = true
        if (not hasCapacity) and value.elements.length is 0
          return reject "unable to validate reservation record without anything being reserved"
        # time range verifications
        now = new Date
        start = (new Date value.start) if value.start?
        end   = (new Date value.end) if value.end?
        # if start? and start < now
        #   return reject "requested start time #{value.start} cannot be in the past"
        if end? and end < now
          return reject "requested end time #{value.end} cannot be in the past"
        if start? and end? and start > end
          retun reject "requested start time must be earlier than end time"
        resolve this

    update: !coffee/function |
      (prev) -> (req, resolve, reject) ->
        req.start ?= @get 'start'
        req.end   ?= @get 'end'

        # TODO: should validate here...
        @parent.invoke 'query-capacity',
          start: req.start
          end: req.end
          capacity: 'available'
          without: @id
        .then (res) =>
          collections = res.get 'collections'
          unless collections.length > 0
            return reject 'no resource capacity available during requested start/end time'

          pools = collections.filter (e) -> /^ResourcePool/.test e
          # should do some policy or check to see if more than one pool acceptable to reservee

          entries = res.get 'utilization'
          start = new Date req.start
          end   = new Date req.end

          for x in [0..entries.length-1]
            t1 = new Date entries[x].timestamp
            break unless t1 < end

            if x < entries.length-1
              t2 = new Date entries[x+1].timestamp
              continue unless t2 > start

            available = entries[x].capacity
            for k, v of req.capacity when v? and !!v
              unless available[k] >= v
                return reject "requested #{k}=#{v} exceeds available #{available[k]} between #{t1} and #{t2}"

          @set req
          @set 'pools', pools
          resolve this
        .catch (err) -> reject err

    save: !coffee/function |
      (prev) -> (resolve, reject) ->
        @invoke 'validate', @get()
        .then (res) ->
          # should do something about this reservation record...
          now = (new Date).toJSON()
          unless (res.get 'created-on')?
            res.set 'created-on', now
          res.set 'modified-on', now
          resolve res
        .catch (e) -> reject e

  ResourceAllocation:
    # properties
    priority: !coffee/function |
      (prev) -> @computed (->
        switch
          when not (@get 'reservation')? then 3
          when not (@get 'active') then 2
          else 1
      ), type: prev

  ResourcePool:
    save: !coffee/function |
      (prev) -> (resolve, reject) ->
        # validate that record contains sufficient data
        value = @get()

        for k, v of value.capacity when v? and !!v
          hasCapacity = true
        if (not hasCapacity) and value.elements.length is 0
          return reject "unable to save pool record without any capacity values"

        resolve this

  ResourceProvider:
    # properties
    token: !coffee/function |
      (prev) -> prev.set 'private', true

    pools: !coffee/function |
      (prev) -> @computed (->
        (@store.find 'ResourcePool', source: (@get 'name')).map (x) -> x.get 'id'
      ), type: 'array'

    # methods
    # XXX - this method is OpenStack-specific only, will need to revise later
    update: !coffee/function |
      (prev) -> (services=[], resolve, reject) ->
        return reject "unable to update provider without list of services" unless services.length
        request = @store.parent.require 'superagent'
        services.forEach (service) =>
          switch service.type
            when 'compute'
              url = service.endpoints[0].publicURL
              @set 'services.compute.endpoint', url
              request
                .get "#{url}/limits"
                  .set 'X-Auth-Token', @get 'token'
                  .set 'Accept', 'application/json'
                  .end (err, res) =>
                    if err? or !res.ok
                      console.warn "request to discover capacity limits failed"
                      return

                    capacity = res.body.limits?.absolute
                    #console.log "\ndiscovered capacity:"
                    #console.log capacity

                    (@access 'capacity').set {
                      cores: capacity.maxTotalCores
                      ram: capacity.maxTotalRAMSize
                      instances: capacity.maxTotalInstances
                      addresses: capacity.maxTotalFloatingIps
                    }
              request
                .get "#{url}/flavors/detail"
                  .set 'X-Auth-Token', @get 'token'
                  .set 'Accept', 'application/json'
                  .end (err, res) =>
                    if err? or !res.ok
                      console.warn "request to discover compute flavors failed"
                      return

                    flavors = res.body.flavors
                    # console.log "\ndiscovered flavors:"
                    # console.log flavors
                    try
                      flavors = flavors.map (x) -> ResourceFlavor: x
                      (@access 'services.compute.flavors').push flavors...
                    catch er
                      console.warn "failed to update flavors into the provider due to validation errors"

        # XXX - update should do promise.all
        resolve this