blob: 565fae2e7c5c5d6d24dfa13c8fbda50d6dd099e5 [file] [log] [blame]
{-# 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