{-# LANGUAGE RankNTypes #-}

{-| Implementation of the Ganeti network objects.

This is does not (yet) cover all methods that are provided in the
corresponding python implementation (network.py).

-}

{-

Copyright (C) 2011, 2012, 2013 Google Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-}

module Ganeti.Network
  ( PoolPart(..)
  , netIpv4NumHosts
  , ip4BaseAddr
  , getReservedCount
  , getFreeCount
  , isFull
  , getMap
  , isReserved
  , reserve
  , release
  , findFree
  , allReservations
  , reservations
  , extReservations
  ) where

import Control.Monad
import Control.Monad.Error.Class (MonadError)
import Control.Monad.State
import Data.Bits ((.&.))
import Data.Function (on)

import Ganeti.BasicTypes
import qualified Ganeti.Constants as C
import Ganeti.Lens
import Ganeti.Objects
import Ganeti.Objects.Lens
import qualified Ganeti.Objects.BitArray as BA

ip4BaseAddr :: Ip4Network -> Ip4Address
ip4BaseAddr net =
  let m = ip4netMask net
      mask = 2^(32 :: Integer) - 2^(32 - m)
  in ip4AddressFromNumber .  (.&.) mask . ip4AddressToNumber $ ip4netAddr net

ipv4NumHosts :: (Integral n) => n -> Integer
ipv4NumHosts mask = 2^(32 - mask)

ipv4NetworkMinNumHosts :: Integer
ipv4NetworkMinNumHosts = ipv4NumHosts C.ipv4NetworkMinSize

ipv4NetworkMaxNumHosts :: Integer
ipv4NetworkMaxNumHosts = ipv4NumHosts C.ipv4NetworkMaxSize

data PoolPart = PoolInstances | PoolExt

addressPoolIso :: Iso' AddressPool BA.BitArray
addressPoolIso = iso apReservations AddressPool

poolLens :: PoolPart -> Lens' Network (Maybe AddressPool)
poolLens PoolInstances = networkReservationsL
poolLens PoolExt = networkExtReservationsL

poolArrayLens :: PoolPart -> Lens' Network (Maybe BA.BitArray)
poolArrayLens part = poolLens part . mapping addressPoolIso

netIpv4NumHosts :: Network -> Integer
netIpv4NumHosts = ipv4NumHosts . ip4netMask . networkNetwork

-- | Creates a new bit array pool of the appropriate size
newPoolArray :: (MonadError e m, FromString e) => Network -> m BA.BitArray
newPoolArray net = do
  let numhosts = netIpv4NumHosts net
  when (numhosts > ipv4NetworkMaxNumHosts) . failError $
    "A big network with " ++ show numhosts ++ " host(s) is currently"
    ++ " not supported, please specify at most a /"
    ++ show ipv4NetworkMaxNumHosts ++ " network"
  when (numhosts < ipv4NetworkMinNumHosts) . failError $
    "A network with only " ++ show numhosts ++ " host(s) is too small,"
    ++ " please specify at least a /"
    ++ show ipv4NetworkMinNumHosts ++ " network"
  return $ BA.zeroes (fromInteger numhosts)

-- | Creates a new bit array pool of the appropriate size
newPool :: (MonadError e m, FromString e) => Network -> m AddressPool
newPool = liftM AddressPool . newPoolArray

-- | A helper function that creates a bit array pool, of it's missing.
orNewPool :: (MonadError e m, FromString e)
          => Network -> Maybe AddressPool -> m AddressPool
orNewPool net = maybe (newPool net) return

withPool :: (MonadError e m, FromString e)
         => PoolPart -> (Network -> BA.BitArray -> m (a, BA.BitArray))
         -> StateT Network m a
withPool part f = StateT $ \n -> mapMOf2 (poolLens part) (f' n) n
  where
    f' net = liftM (over _2 Just)
             . mapMOf2 addressPoolIso (f net)
             <=< orNewPool net

withPool_ :: (MonadError e m, FromString e)
          => PoolPart -> (Network -> BA.BitArray -> m BA.BitArray)
          -> Network -> m Network
withPool_ part f = execStateT $ withPool part ((liftM ((,) ()) .) . f)

readPool :: PoolPart -> Network -> Maybe BA.BitArray
readPool = view . poolArrayLens

readPoolE :: (MonadError e m, FromString e)
          => PoolPart -> Network -> m BA.BitArray
readPoolE part net =
  liftM apReservations $ orNewPool net ((view . poolLens) part net)

readAllE :: (MonadError e m, FromString e)
         => Network -> m BA.BitArray
readAllE net = do
  let toRes = liftM apReservations . orNewPool net
  res <- toRes $ networkReservations net
  ext <- toRes $ networkExtReservations net
  return $ res BA.-|- ext

reservations :: Network -> Maybe BA.BitArray
reservations = readPool PoolInstances

extReservations :: Network -> Maybe BA.BitArray
extReservations = readPool PoolExt

-- | Get a bit vector of all reservations (internal and external) combined.
allReservations :: Network -> Maybe BA.BitArray
allReservations a = (BA.-|-) `liftM` reservations a `ap` extReservations a

-- | Get the count of reserved addresses.
getReservedCount :: Network -> Int
getReservedCount = maybe 0 BA.count1 . allReservations

-- | Get the count of free addresses.
getFreeCount :: Network -> Int
getFreeCount = maybe 0 BA.count0 . allReservations

-- | Check whether the network is full.
isFull :: Network -> Bool
isFull = (0 ==) . getFreeCount

-- | Return a textual representation of the network's occupation status.
getMap :: Network -> String
getMap = maybe "" (BA.asString '.' 'X') . allReservations

-- * Functions used for manipulating the reservations

-- | Returns an address index wrt a network.
-- Fails if the address isn't in the network range.
addrIndex :: (MonadError e m, FromString e) => Ip4Address -> Network -> m Int
addrIndex addr net = do
  let n = networkNetwork net
      i = on (-) ip4AddressToNumber addr (ip4BaseAddr n)
  when ((i < 0) || (i >= ipv4NumHosts (ip4netMask n))) . failError
    $ "Address '" ++ show addr ++ "' not in the network '" ++ show net ++ "'"
  return $ fromInteger i

-- | Returns an address of a given index wrt a network.
-- Fails if the index isn't in the network range.
addrAt :: (MonadError e m, FromString e) => Int -> Network -> m Ip4Address
addrAt i net | (i' < 0) || (i' >= ipv4NumHosts (ip4netMask n)) =
    failError $ "Requested index " ++ show i
                ++ " outside the range of network '" ++ show net ++ "'"
             | otherwise =
    return $ ip4AddressFromNumber (ip4AddressToNumber (ip4BaseAddr n) + i')
  where
    n = networkNetwork net
    i' = toInteger i

-- | Checks if a given address is reserved.
-- Fails if the address isn't in the network range.
isReserved :: (MonadError e m, FromString e) =>
              PoolPart -> Ip4Address -> Network -> m Bool
isReserved part addr net =
  (BA.!) `liftM` readPoolE part net `ap` addrIndex addr net

-- | Marks an address as used.
reserve :: (MonadError e m, FromString e) =>
           PoolPart -> Ip4Address -> Network -> m Network
reserve part addr =
    withPool_ part $ \net ba -> do
      idx <- addrIndex addr net
      let addrs = show addr
      when (ba BA.! idx) . failError $ case part of
        PoolExt -> "IP " ++ addrs ++ " is already externally reserved"
        PoolInstances -> "IP " ++ addrs ++ " is already used by an instance"
      BA.setAt idx True ba

-- | Marks an address as unused.
release :: (MonadError e m, FromString e) =>
           PoolPart -> Ip4Address -> Network -> m Network
release part addr =
    withPool_ part $ \net ba -> do
      idx <- addrIndex addr net
      let addrs = show addr
      unless (ba BA.! idx) . failError $ case part of
        PoolExt -> "IP " ++ addrs ++ " is not externally reserved"
        PoolInstances -> "IP " ++ addrs ++ " is not used by an instance"
      BA.setAt idx False ba

-- | Get the first free address in the network
-- that satisfies a given predicate.
findFree :: (MonadError e m, FromString e)
         => (Ip4Address -> Bool) -> Network -> m (Maybe Ip4Address)
findFree p net = readAllE net >>= BA.foldr f (return Nothing)
  where
    addrAtEither = addrAt :: Int -> Network -> Either String Ip4Address
    f False i _ | Right a <- addrAtEither i net, p a = return (Just a)
    f _ _ x = x