{-# LANGUAGE TemplateHaskell, RankNTypes, FlexibleContexts #-}

{-| Pure functions for manipulating reservations of temporary objects

NOTE: Reservations aren't released specifically, they're just all
released at the end of a job. This could be improved in the future.

-}

{-

Copyright (C) 2014 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.WConfd.TempRes
  ( TempRes
  , mkTempRes
  , TempResState(..)
  , emptyTempResState
  , NodeUUID
  , InstanceUUID
  , DiskUUID
  , NetworkUUID
  , DRBDMinor
  , DRBDMap
  , trsDRBDL
  , computeDRBDMap
  , computeDRBDMap'
  , allocateDRBDMinor
  , releaseDRBDMinors
  , MAC
  , generateMAC
  , reserveMAC
  , generateDRBDSecret
  , reserveLV
  , IPv4ResAction(..)
  , IPv4Reservation(..)
  , reserveIp
  , releaseIp
  , generateIp
  , commitReleaseIp
  , commitReservedIps
  , listReservedIps
  , dropAllReservations
  , isReserved
  , reserve
  , dropReservationsFor
  , reserved
  ) where

import Control.Applicative
import Control.Lens.At
import Control.Monad.Error
import Control.Monad.State
import Control.Monad.Trans.Maybe
import qualified Data.ByteString as BS
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.Foldable as F
import Data.Maybe
import Data.Map (Map)
import qualified Data.Map as M
import Data.Monoid
import qualified Data.Set as S
import System.Random
import qualified Text.JSON as J

import Ganeti.BasicTypes
import Ganeti.Config
import qualified Ganeti.Constants as C
import Ganeti.Errors
import qualified Ganeti.JSON as J
import Ganeti.Lens
import qualified Ganeti.Network as N
import Ganeti.Locking.Locks (ClientId)
import Ganeti.Logging
import Ganeti.Objects
import Ganeti.THH
import Ganeti.Objects.Lens (configNetworksL)
import Ganeti.Utils
import Ganeti.Utils.Monad
import Ganeti.Utils.Random
import qualified Ganeti.Utils.MultiMap as MM

-- * The main reservation state

-- ** Aliases to make types more meaningful:

type NodeUUID = BS.ByteString

type InstanceUUID = BS.ByteString

type DiskUUID = BS.ByteString

type NetworkUUID = BS.ByteString

type DRBDMinor = Int

-- | A map of the usage of DRBD minors
type DRBDMap = Map NodeUUID (Map DRBDMinor DiskUUID)

-- | A map of the usage of DRBD minors with possible duplicates
type DRBDMap' = Map NodeUUID (Map DRBDMinor [DiskUUID])

-- * The state data structure

-- | Types of IPv4 reservation actions.
data IPv4ResAction = IPv4Reserve | IPv4Release
  deriving (Eq, Ord, Show, Bounded, Enum)

instance J.JSON IPv4ResAction where
  showJSON IPv4Reserve = J.JSString . J.toJSString $ C.reserveAction
  showJSON IPv4Release = J.JSString . J.toJSString $ C.releaseAction
  readJSON = J.readEitherString
             >=> \s -> case () of
                    _ | s == C.reserveAction -> return IPv4Reserve
                      | s == C.releaseAction -> return IPv4Release
                      | otherwise -> fail $ "Invalid IP reservation action: "
                                           ++ s

-- | The values stored in the IPv4 reservation table.
data IPv4Reservation = IPv4Res
  { ipv4ResAction :: IPv4ResAction
  , ipv4ResNetwork :: NetworkUUID
  , ipv4ResAddr :: Ip4Address
  } deriving (Eq, Ord, Show)

instance J.JSON IPv4Reservation where
  -- Notice that addr and net are in a different order, to be compatible
  -- with the original Python representation (while it's used).
  showJSON (IPv4Res a net addr) = J.showJSON (a, addr, net)
  readJSON = fmap (\(a, addr, net) -> IPv4Res a net addr) . J.readJSON

-- | A polymorphic data structure for managing temporary resources assigned
-- to jobs.
newtype TempRes j a = TempRes { getTempRes :: MM.MultiMap j a }
  deriving (Eq, Ord, Show)

instance (Ord j, Ord a) => Monoid (TempRes j a) where
  mempty = TempRes mempty
  mappend (TempRes x) (TempRes y) = TempRes $ x <> y

instance (J.JSON j, Ord j, J.JSON a, Ord a) => J.JSON (TempRes j a) where
  showJSON = J.showJSON . getTempRes
  readJSON = liftM TempRes . J.readJSON

-- | Create a temporary reservations from a given multi-map.
mkTempRes :: MM.MultiMap j a -> TempRes j a
mkTempRes = TempRes

-- | The state of the temporary reservations
$(buildObject "TempResState" "trs"
  [ simpleField "dRBD"             [t| DRBDMap |]
  , simpleField "mACs"             [t| TempRes ClientId MAC |]
  , simpleField "dRBDSecrets"      [t| TempRes ClientId DRBDSecret |]
  , simpleField "lVs"              [t| TempRes ClientId LogicalVolume |]
  , simpleField "iPv4s"            [t| TempRes ClientId IPv4Reservation |]
  ])

emptyTempResState :: TempResState
emptyTempResState = TempResState M.empty mempty mempty mempty mempty

$(makeCustomLenses ''TempResState)

-- ** Utility functions

-- | Issues a reservation error.
resError :: (MonadError GanetiException m) => String -> m a
resError = throwError . ReservationError

-- | Converts 'GenericError' into a 'ReservationError'.
toResError :: (MonadError GanetiException m) => m a -> m a
toResError = flip catchError (throwError . f)
  where
    f (GenericError msg) = ReservationError msg
    f e = e

-- | Filter values from the nested map and remove any nested maps
-- that become empty.
filterNested :: (Ord a, Ord b)
             => (c -> Bool) -> Map a (Map b c) -> Map a (Map b c)
filterNested p = M.filter (not . M.null) . fmap (M.filter p)

-- | Converts a lens that works on maybe values into a lens that works
-- on regular ones. A missing value on the input is replaced by
-- 'mempty'.
-- The output is is @Just something@ iff @something /= mempty@.
maybeLens :: (Monoid a, Monoid b, Eq b)
          => Lens s t (Maybe a) (Maybe b) -> Lens s t a b
maybeLens l f = l (fmap (mfilter (/= mempty) . Just) . f . fromMaybe mempty)

-- * DRBD functions

-- | Compute the map of used DRBD minor/nodes, including possible
-- duplicates.
-- An error is returned if the configuration isn't consistent
-- (for example if a referenced disk is missing etc.).
computeDRBDMap' :: (MonadError GanetiException m)
                => ConfigData -> TempResState -> m DRBDMap'
computeDRBDMap' cfg trs =
    flip execStateT (fmap (fmap (: [])) (trsDRBD trs))
    $ F.forM_ (configDisks cfg) addMinors
  where
    -- | Creates a lens for modifying the list of instances
    nodeMinor :: NodeUUID -> DRBDMinor -> Lens' DRBDMap' [DiskUUID]
    nodeMinor node minor = maybeLens (at node) . maybeLens (at minor)
    -- | Adds minors of a disk within the state monad
    addMinors disk = do
      let minors = getDrbdMinorsForDisk disk
      forM_ minors $ \(minor, node) ->
        nodeMinor (UTF8.fromString node) minor %=
            (UTF8.fromString (uuidOf disk) :)

-- | Compute the map of used DRBD minor/nodes.
-- Report any duplicate entries as an error.
--
-- Unlike 'computeDRBDMap'', includes entries for all nodes, even if empty.
computeDRBDMap :: (MonadError GanetiException m)
               => ConfigData -> TempResState -> m DRBDMap
computeDRBDMap cfg trs = do
  m <- computeDRBDMap' cfg trs
  let dups = filterNested ((>= 2) . length) m
  unless (M.null dups) . resError
    $ "Duplicate DRBD ports detected: " ++ show (M.toList $ fmap M.toList dups)
  return $ fmap (fmap head . M.filter ((== 1) . length)) m
           `M.union` (fmap (const mempty) . J.fromContainer . configNodes $ cfg)

-- Allocate a drbd minor.
--
-- The free minor will be automatically computed from the existing devices.
-- A node can not be given multiple times.
-- The result is the list of minors, in the same order as the passed nodes.
allocateDRBDMinor :: (MonadError GanetiException m, MonadState TempResState m)
                  => ConfigData -> DiskUUID -> [NodeUUID]
                  -> m [DRBDMinor]
allocateDRBDMinor cfg disk nodes = do
  unless (nodes == ordNub nodes) . resError
    $ "Duplicate nodes detected in list '" ++ show nodes ++ "'"
  dMap <- computeDRBDMap' cfg =<< get
  let usedMap = fmap M.keysSet dMap
  let alloc :: S.Set DRBDMinor -> Map DRBDMinor DiskUUID
            -> (DRBDMinor, Map DRBDMinor DiskUUID)
      alloc used m = let k = findFirst 0 (M.keysSet m `S.union` used)
                     in (k, M.insert k disk m)
  forM nodes $ \node -> trsDRBDL . maybeLens (at node)
                        %%= alloc (M.findWithDefault mempty node usedMap)

-- Release temporary drbd minors allocated for a given disk using
-- 'allocateDRBDMinor'.
releaseDRBDMinors :: (MonadState TempResState m) => DiskUUID -> m ()
releaseDRBDMinors disk = trsDRBDL %= filterNested (/= disk)

-- * Other temporary resources

-- | Tests if a given value is reserved for a given job.
isReserved :: (Ord a, Ord j) => a -> TempRes j a -> Bool
isReserved x = MM.elem x . getTempRes

-- | Tries to reserve a given value for a given job.
reserve :: (MonadError GanetiException m, Show a, Ord a, Ord j)
        => j -> a -> TempRes j a -> m (TempRes j a)
reserve jobid x tr = do
  when (isReserved x tr) . resError $ "Duplicate reservation for resource '"
                                       ++ show x ++ "'"
  return . TempRes . MM.insert jobid x $ getTempRes tr

dropReservationsFor :: (Ord a, Ord j) => j -> TempRes j a -> TempRes j a
dropReservationsFor jobid = TempRes . MM.deleteAll jobid . getTempRes

reservedFor :: (Ord a, Ord j) => j -> TempRes j a -> S.Set a
reservedFor jobid = MM.lookup jobid . getTempRes

reserved :: (Ord a, Ord j) => TempRes j a -> S.Set a
reserved = MM.values . getTempRes

-- | Computes the set of all reserved resources and passes it to
-- the given function.
-- This allows it to avoid resources that are already in use.
withReserved :: (MonadError GanetiException m, Show a, Ord a, Ord j)
             => j -> (S.Set a -> m a) -> TempRes j a -> m (a, TempRes j a)
withReserved jobid genfn tr = do
  x <- genfn (reserved tr)
  (,) x `liftM` reserve jobid x tr

-- | Repeatedly tries to run a given monadic function until it succeeds
-- and the returned value is free to reserve.
-- If such a value is found, it's reserved and returned.
-- Otherwise fails with an error.
generate :: (MonadError GanetiException m, Show a, Ord a, Ord j)
         => j -> S.Set a -> m (Maybe a) -> TempRes j a -> m (a, TempRes j a)
generate jobid existing genfn = withReserved jobid f
  where
    retries = 64 :: Int
    f res = do
      let vals = res `S.union` existing
      xOpt <- retryMaybeN retries
                          (\_ -> mfilter (`S.notMember` vals) (MaybeT genfn))
      maybe (resError "Not able generate new resource")
                       -- TODO: (last tried: " ++ %s)" % new_resource
            return xOpt

-- | A variant of 'generate' for randomized computations.
generateRand
  :: (MonadError GanetiException m, Show a, Ord a, Ord j, RandomGen g)
  => g -> j -> S.Set a -> (g -> (Maybe a, g)) -> TempRes j a
  -> m (a, TempRes j a)
generateRand rgen jobid existing genfn tr =
  evalStateT (generate jobid existing (state genfn) tr) rgen

-- | Embeds a stateful computation in a stateful monad.
stateM :: (MonadState s m) => (s -> m (a, s)) -> m a
stateM f = get >>= f >>= \(x, s) -> liftM (const x) (put s)

-- | Embeds a state-modifying computation in a stateful monad.
modifyM :: (MonadState s m) => (s -> m s) -> m ()
modifyM f = get >>= f >>= put

-- ** Functions common to all reservations

-- | Removes all resources reserved by a given job.
--
-- If a new reservation resource type is added, it must be added here as well.
dropAllReservations :: ClientId -> State TempResState ()
dropAllReservations jobId = modify $
    (trsMACsL %~ dropReservationsFor jobId)
  . (trsDRBDSecretsL %~ dropReservationsFor jobId)
  . (trsLVsL %~ dropReservationsFor jobId)
  . (trsIPv4sL %~ dropReservationsFor jobId)

-- | Looks up a network by its UUID.
lookupNetwork :: (MonadError GanetiException m)
              => ConfigData -> NetworkUUID -> m Network
lookupNetwork cd netId =
  J.lookupContainer (resError $ "Network '" ++ show netId ++ "' not found")
                    netId (configNetworks cd)

-- ** IDs

-- ** MAC addresses

-- Randomly generate a MAC for an instance.
-- Checks that the generated MAC isn't used by another instance.
--
-- Note that we only consume, but not return the state of a random number
-- generator. This is because the whole operation needs to be pure (for atomic
-- 'IORef' updates) and therefore we can't use 'getStdRandom'. Therefore the
-- approach we take is to instead use 'newStdGen' and discard the split
-- generator afterwards.
generateMAC
  :: (RandomGen g, MonadError GanetiException m, Functor m)
  => g -> ClientId -> Maybe NetworkUUID -> ConfigData
  -> StateT TempResState m MAC
generateMAC rgen jobId netId cd = do
  net <- case netId of
    Just n -> Just <$> lookupNetwork cd n
    Nothing -> return Nothing
  let prefix = fromMaybe (clusterMacPrefix . configCluster $ cd)
                         (networkMacPrefix =<< net)
  let existing = S.fromList $ getAllMACs cd
  StateT
    $ traverseOf2 trsMACsL
        (generateRand rgen jobId existing
                      (over _1 Just . generateOneMAC prefix))

-- Reserves a MAC for an instance in the list of temporary reservations.
reserveMAC
  :: (MonadError GanetiException m, MonadState TempResState m, Functor m)
  => ClientId -> MAC -> ConfigData -> m ()
reserveMAC jobId mac cd = do
  let existing = S.fromList $ getAllMACs cd
  when (S.member mac existing)
    $ resError "MAC already in use"
  modifyM $ traverseOf trsMACsL (reserve jobId mac)

-- ** DRBD secrets

generateDRBDSecret
  :: (RandomGen g, MonadError GanetiException m, Functor m)
  => g -> ClientId -> ConfigData -> StateT TempResState m DRBDSecret
generateDRBDSecret rgen jobId cd = do
  let existing = S.fromList $ getAllDrbdSecrets cd
  StateT $ traverseOf2 trsDRBDSecretsL
           (generateRand rgen jobId existing
                         (over _1 Just . generateSecret C.drbdSecretLength))

-- ** LVs

reserveLV
  :: (MonadError GanetiException m, MonadState TempResState m, Functor m)
  => ClientId -> LogicalVolume -> ConfigData -> m ()
reserveLV jobId lv cd = do
  existing <- toError $ getAllLVs cd
  when (S.member lv existing)
    $ resError "MAC already in use"
  modifyM $ traverseOf trsLVsL (reserve jobId lv)

-- ** IPv4 addresses

-- | Lists all IPv4 addresses reserved for a given network.
usedIPv4Addrs :: NetworkUUID -> S.Set IPv4Reservation -> S.Set Ip4Address
usedIPv4Addrs netuuid =
  S.map ipv4ResAddr . S.filter ((== netuuid) . ipv4ResNetwork)

-- | Reserve a given IPv4 address for use by an instance.
reserveIp
  :: (MonadError GanetiException m, MonadState TempResState m, Functor m)
  => ClientId -> NetworkUUID -> Ip4Address
  -> Bool -- ^ whether to check externally reserved IPs
  -> ConfigData -> m ()
reserveIp jobId netuuid addr checkExt cd = toResError $ do
    net <- lookupNetwork cd netuuid
    isres <- N.isReserved N.PoolInstances addr net
    when isres . resError $ "IP address already in use"
    when checkExt $ do
      isextres <- N.isReserved N.PoolExt addr net
      when isextres . resError $ "IP is externally reserved"
    let action = IPv4Res IPv4Reserve netuuid addr
    modifyM $ traverseOf trsIPv4sL (reserve jobId action)

-- | Give a specific IP address back to an IP pool.
-- The IP address is returned to the IP pool designated by network id
-- and marked as reserved.
releaseIp
  :: (MonadError GanetiException m, MonadState TempResState m, Functor m)
  => ClientId -> NetworkUUID -> Ip4Address -> m ()
releaseIp jobId netuuid addr =
  let action = IPv4Res { ipv4ResAction = IPv4Release
                       , ipv4ResNetwork = netuuid
                       , ipv4ResAddr = addr }
  in modifyM $ traverseOf trsIPv4sL (reserve jobId action)

-- Find a free IPv4 address for an instance and reserve it.
generateIp
  :: (MonadError GanetiException m, MonadState TempResState m, Functor m)
  => ClientId -> NetworkUUID -> ConfigData -> m Ip4Address
generateIp jobId netuuid cd = toResError $ do
    net <- lookupNetwork cd netuuid
    let f res = do
          let ips = usedIPv4Addrs netuuid res
          addr <- N.findFree (`S.notMember` ips) net
          maybe (resError "Cannot generate IP. Network is full")
                (return . IPv4Res IPv4Reserve netuuid) addr
    liftM ipv4ResAddr . stateM $ traverseOf2 trsIPv4sL (withReserved jobId f)

-- | Commit a reserved/released IP address to an IP pool.
-- The IP address is taken from the network's IP pool and marked as
-- reserved/free for instances.
commitIp
  :: (MonadError GanetiException m, Functor m)
  => IPv4Reservation -> ConfigData -> m ConfigData
commitIp (IPv4Res actType netuuid addr) cd = toResError $ do
    let call = case actType of
                IPv4Reserve -> N.reserve
                IPv4Release -> N.release
        f Nothing = resError $ "Network '" ++ show netuuid ++ "' not found"
        f (Just net) = Just `liftM` call N.PoolInstances addr net
    traverseOf (configNetworksL . J.alterContainerL netuuid) f cd

-- | Immediately release an IP address, without using the reservations pool.
commitReleaseIp
  :: (MonadError GanetiException m, Functor m)
  => NetworkUUID -> Ip4Address -> ConfigData -> m ConfigData
commitReleaseIp netuuid addr =
  commitIp (IPv4Res IPv4Release netuuid addr)

-- | Commit all reserved/released IP address to an IP pool.
-- The IP addresses are taken from the network's IP pool and marked as
-- reserved/free for instances.
--
-- Note that the reservations are kept, they are supposed to be cleaned
-- when a job finishes.
commitReservedIps
  :: (MonadError GanetiException m, Functor m, MonadLog m)
  => ClientId -> TempResState -> ConfigData -> m ConfigData
commitReservedIps jobId tr cd = do
  let res = reservedFor jobId (trsIPv4s tr)
  logDebug $ "Commiting reservations: " ++ show res
  F.foldrM commitIp cd res

listReservedIps :: ClientId -> TempResState -> S.Set IPv4Reservation
listReservedIps jobid = reservedFor jobid . trsIPv4s