{-| Implementation of cluster-wide logic.

This module holds all pure cluster-logic; I\/O related functionality
goes into the /Main/ module for the individual binaries.

-}

{-

Copyright (C) 2009, 2010, 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.HTools.Cluster
  (
    -- * Types
    AllocDetails(..)
  , Table(..)
  , CStats(..)
  , AllocNodes
  , AllocResult
  , AllocMethod
  , GenericAllocSolutionList
  , AllocSolutionList
  -- * Generic functions
  , totalResources
  , computeAllocationDelta
  , hasRequiredNetworks
  -- * First phase functions
  , computeBadItems
  -- * Second phase functions
  , printSolutionLine
  , formatCmds
  , involvedNodes
  , getMoves
  , splitJobs
  -- * Display functions
  , printNodes
  , printInsts
  -- * Balacing functions
  , doNextBalance
  , tryBalance
  , iMoveToJob
  -- * IAllocator functions
  , genAllocNodes
  , tryAlloc
  , tryGroupAlloc
  , tryMGAlloc
  , filterMGResults
  , sortMGResults
  , tryChangeGroup
  , allocList
  -- * Allocation functions
  , iterateAlloc
  , tieredAlloc
  -- * Node group functions
  , instanceGroup
  , findSplitInstances
  ) where

import Prelude ()
import Ganeti.Prelude

import Control.Applicative (liftA2)
import Control.Arrow ((&&&))
import Control.Monad (unless)
import qualified Data.IntSet as IntSet
import qualified Data.Set as Set
import Data.List ( nub
                 , sortBy
                 , foldl'
                 , intersect
                 , partition
                 , (\\)
                 , sort
                 , intercalate)
import Data.Maybe (fromJust, fromMaybe, isJust, isNothing)
import Data.Ord (comparing)
import Text.Printf (printf)

import Ganeti.BasicTypes
import Ganeti.HTools.AlgorithmParams (AlgorithmOptions(..), defaultOptions)
import qualified Ganeti.HTools.Container as Container
import Ganeti.HTools.Cluster.AllocatePrimitives ( allocateOnSingle
                                                , allocateOnPair)
import Ganeti.HTools.Cluster.AllocationSolution
    ( GenericAllocSolution(..) , AllocSolution, emptyAllocSolution
    , sumAllocs, extractNl, updateIl
    , annotateSolution, solutionDescription, collapseFailures
    , emptyAllocCollection, concatAllocCollections, collectionToSolution )
import Ganeti.HTools.Cluster.Evacuate ( EvacSolution(..), emptyEvacSolution
                                      , updateEvacSolution, reverseEvacSolution
                                      , nodeEvacInstance)
import Ganeti.HTools.Cluster.Metrics (compCV, compClusterStatistics)
import Ganeti.HTools.Cluster.Moves (applyMoveEx)
import Ganeti.HTools.Cluster.Utils (splitCluster, instancePriGroup
                                   , availableGroupNodes, iMoveToJob)
import Ganeti.HTools.GlobalN1 (allocGlobalN1, redundant)
import qualified Ganeti.HTools.Instance as Instance
import qualified Ganeti.HTools.Nic as Nic
import qualified Ganeti.HTools.Node as Node
import qualified Ganeti.HTools.Group as Group
import Ganeti.HTools.Types
import Ganeti.Compat
import Ganeti.Utils
import Ganeti.Types (EvacMode(..))

-- * Types

-- | Allocation details for an instance, specifying
-- required number of nodes, and
-- an optional group (name) to allocate to
data AllocDetails = AllocDetails Int (Maybe String)
                    deriving (Show)

-- | Allocation results, as used in 'iterateAlloc' and 'tieredAlloc'.
type AllocResult = (FailStats, Node.List, Instance.List,
                    [Instance.Instance], [CStats])

-- | Type alias for easier handling.
type GenericAllocSolutionList a =
  [(Instance.Instance, GenericAllocSolution a)]
type AllocSolutionList = GenericAllocSolutionList Score

-- | A type denoting the valid allocation mode/pairs.
--
-- For a one-node allocation, this will be a @Left ['Ndx']@, whereas
-- for a two-node allocation, this will be a @Right [('Ndx',
-- ['Ndx'])]@. In the latter case, the list is basically an
-- association list, grouped by primary node and holding the potential
-- secondary nodes in the sub-list.
type AllocNodes = Either [Ndx] [(Ndx, [Ndx])]

-- | The complete state for the balancing solution.
data Table = Table Node.List Instance.List Score [Placement]
             deriving (Show)

-- | Cluster statistics data type.
data CStats = CStats
  { csFmem :: Integer -- ^ Cluster free mem
  , csFdsk :: Integer -- ^ Cluster free disk
  , csFspn :: Integer -- ^ Cluster free spindles
  , csAmem :: Integer -- ^ Cluster allocatable mem
  , csAdsk :: Integer -- ^ Cluster allocatable disk
  , csAcpu :: Integer -- ^ Cluster allocatable cpus
  , csMmem :: Integer -- ^ Max node allocatable mem
  , csMdsk :: Integer -- ^ Max node allocatable disk
  , csMcpu :: Integer -- ^ Max node allocatable cpu
  , csImem :: Integer -- ^ Instance used mem
  , csIdsk :: Integer -- ^ Instance used disk
  , csIspn :: Integer -- ^ Instance used spindles
  , csIcpu :: Integer -- ^ Instance used cpu
  , csTmem :: Double  -- ^ Cluster total mem
  , csTdsk :: Double  -- ^ Cluster total disk
  , csTspn :: Double  -- ^ Cluster total spindles
  , csTcpu :: Double  -- ^ Cluster total cpus
  , csVcpu :: Integer -- ^ Cluster total virtual cpus
  , csNcpu :: Double  -- ^ Equivalent to 'csIcpu' but in terms of
                      -- physical CPUs, i.e. normalised used phys CPUs
  , csXmem :: Integer -- ^ Unnacounted for mem
  , csNmem :: Integer -- ^ Node own memory
  , csScore :: Score  -- ^ The cluster score
  , csNinst :: Int    -- ^ The total number of instances
  } deriving (Show)

-- | A simple type for allocation functions.
type AllocMethod =  Node.List           -- ^ Node list
                 -> Instance.List       -- ^ Instance list
                 -> Maybe Int           -- ^ Optional allocation limit
                 -> Instance.Instance   -- ^ Instance spec for allocation
                 -> AllocNodes          -- ^ Which nodes we should allocate on
                 -> [Instance.Instance] -- ^ Allocated instances
                 -> [CStats]            -- ^ Running cluster stats
                 -> Result AllocResult  -- ^ Allocation result

-- * Utility functions

-- | Verifies the N+1 status and return the affected nodes.
verifyN1 :: [Node.Node] -> [Node.Node]
verifyN1 = filter Node.failN1

{-| Computes the pair of bad nodes and instances.

The bad node list is computed via a simple 'verifyN1' check, and the
bad instance list is the list of primary and secondary instances of
those nodes.

-}
computeBadItems :: Node.List -> Instance.List ->
                   ([Node.Node], [Instance.Instance])
computeBadItems nl il =
  let bad_nodes = verifyN1 $ getOnline nl
      bad_instances = map (`Container.find` il) .
                      sort . nub $
                      concatMap (\ n -> Node.sList n ++ Node.pList n) bad_nodes
  in
    (bad_nodes, bad_instances)

-- | Zero-initializer for the CStats type.
emptyCStats :: CStats
emptyCStats = CStats 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

-- | Update stats with data from a new node.
updateCStats :: CStats -> Node.Node -> CStats
updateCStats cs node =
  let CStats { csFmem = x_fmem, csFdsk = x_fdsk,
               csAmem = x_amem, csAcpu = x_acpu, csAdsk = x_adsk,
               csMmem = x_mmem, csMdsk = x_mdsk, csMcpu = x_mcpu,
               csImem = x_imem, csIdsk = x_idsk, csIcpu = x_icpu,
               csTmem = x_tmem, csTdsk = x_tdsk, csTcpu = x_tcpu,
               csVcpu = x_vcpu, csNcpu = x_ncpu,
               csXmem = x_xmem, csNmem = x_nmem, csNinst = x_ninst,
               csFspn = x_fspn, csIspn = x_ispn, csTspn = x_tspn
             }
        = cs
      inc_amem = Node.fMem node - Node.rMem node
      inc_amem' = if inc_amem > 0 then inc_amem else 0
      inc_adsk = Node.availDisk node
      inc_imem = truncate (Node.tMem node) - Node.nMem node
                 - Node.xMem node - Node.fMem node
      inc_icpu = Node.uCpu node
      inc_idsk = truncate (Node.tDsk node) - Node.fDsk node
      inc_ispn = Node.tSpindles node - Node.fSpindles node
      inc_vcpu = Node.hiCpu node
      inc_acpu = Node.availCpu node
      inc_ncpu = fromIntegral (Node.uCpu node) /
                 iPolicyVcpuRatio (Node.iPolicy node)
  in cs { csFmem = x_fmem + fromIntegral (Node.fMem node)
        , csFdsk = x_fdsk + fromIntegral (Node.fDsk node)
        , csFspn = x_fspn + fromIntegral (Node.fSpindles node)
        , csAmem = x_amem + fromIntegral inc_amem'
        , csAdsk = x_adsk + fromIntegral inc_adsk
        , csAcpu = x_acpu + fromIntegral inc_acpu
        , csMmem = max x_mmem (fromIntegral inc_amem')
        , csMdsk = max x_mdsk (fromIntegral inc_adsk)
        , csMcpu = max x_mcpu (fromIntegral inc_acpu)
        , csImem = x_imem + fromIntegral inc_imem
        , csIdsk = x_idsk + fromIntegral inc_idsk
        , csIspn = x_ispn + fromIntegral inc_ispn
        , csIcpu = x_icpu + fromIntegral inc_icpu
        , csTmem = x_tmem + Node.tMem node
        , csTdsk = x_tdsk + Node.tDsk node
        , csTspn = x_tspn + fromIntegral (Node.tSpindles node)
        , csTcpu = x_tcpu + Node.tCpu node
        , csVcpu = x_vcpu + fromIntegral inc_vcpu
        , csNcpu = x_ncpu + inc_ncpu
        , csXmem = x_xmem + fromIntegral (Node.xMem node)
        , csNmem = x_nmem + fromIntegral (Node.nMem node)
        , csNinst = x_ninst + length (Node.pList node)
        }

-- | Compute the total free disk and memory in the cluster.
totalResources :: Node.List -> CStats
totalResources nl =
  let cs = foldl' updateCStats emptyCStats . Container.elems $ nl
  in cs { csScore = compCV nl }

-- | Compute the delta between two cluster state.
--
-- This is used when doing allocations, to understand better the
-- available cluster resources. The return value is a triple of the
-- current used values, the delta that was still allocated, and what
-- was left unallocated.
computeAllocationDelta :: CStats -> CStats -> AllocStats
computeAllocationDelta cini cfin =
  let CStats {csImem = i_imem, csIdsk = i_idsk, csIcpu = i_icpu,
              csNcpu = i_ncpu, csIspn = i_ispn } = cini
      CStats {csImem = f_imem, csIdsk = f_idsk, csIcpu = f_icpu,
              csTmem = t_mem, csTdsk = t_dsk, csVcpu = f_vcpu,
              csNcpu = f_ncpu, csTcpu = f_tcpu,
              csIspn = f_ispn, csTspn = t_spn } = cfin
      rini = AllocInfo { allocInfoVCpus = fromIntegral i_icpu
                       , allocInfoNCpus = i_ncpu
                       , allocInfoMem   = fromIntegral i_imem
                       , allocInfoDisk  = fromIntegral i_idsk
                       , allocInfoSpn   = fromIntegral i_ispn
                       }
      rfin = AllocInfo { allocInfoVCpus = fromIntegral (f_icpu - i_icpu)
                       , allocInfoNCpus = f_ncpu - i_ncpu
                       , allocInfoMem   = fromIntegral (f_imem - i_imem)
                       , allocInfoDisk  = fromIntegral (f_idsk - i_idsk)
                       , allocInfoSpn   = fromIntegral (f_ispn - i_ispn)
                       }
      runa = AllocInfo { allocInfoVCpus = fromIntegral (f_vcpu - f_icpu)
                       , allocInfoNCpus = f_tcpu - f_ncpu
                       , allocInfoMem   = truncate t_mem - fromIntegral f_imem
                       , allocInfoDisk  = truncate t_dsk - fromIntegral f_idsk
                       , allocInfoSpn   = truncate t_spn - fromIntegral f_ispn
                       }
  in (rini, rfin, runa)

-- | Compute online nodes from a 'Node.List'.
getOnline :: Node.List -> [Node.Node]
getOnline = filter (not . Node.offline) . Container.elems

-- * Balancing functions

-- | Compute best table. Note that the ordering of the arguments is important.
compareTables :: Table -> Table -> Table
compareTables a@(Table _ _ a_cv _) b@(Table _ _ b_cv _ ) =
  if a_cv > b_cv then b else a

-- | Tries to perform an instance move and returns the best table
-- between the original one and the new one.
checkSingleStep :: Bool -- ^ Whether to unconditionally ignore soft errors
                -> Table -- ^ The original table
                -> Instance.Instance -- ^ The instance to move
                -> Table -- ^ The current best table
                -> IMove -- ^ The move to apply
                -> Table -- ^ The final best table
checkSingleStep force ini_tbl target cur_tbl move =
  let Table ini_nl ini_il _ ini_plc = ini_tbl
      tmp_resu = applyMoveEx force ini_nl target move
  in case tmp_resu of
       Bad _ -> cur_tbl
       Ok (upd_nl, new_inst, pri_idx, sec_idx) ->
         let tgt_idx = Instance.idx target
             upd_cvar = compCV upd_nl
             upd_il = Container.add tgt_idx new_inst ini_il
             upd_plc = (tgt_idx, pri_idx, sec_idx, move, upd_cvar):ini_plc
             upd_tbl = Table upd_nl upd_il upd_cvar upd_plc
         in compareTables cur_tbl upd_tbl

-- | Generate all possible migration moves of an instance given some
-- additional parameters
migrationMoves :: MirrorType -- ^ The mirroring type of the instance
               -> Bool       -- ^ Whether the secondary node is active
               -> [Ndx]      -- ^ Target node candidate list
               -> [IMove]    -- ^ List of valid result moves
migrationMoves MirrorNone _ _ = []
migrationMoves MirrorInternal False _ = []
migrationMoves MirrorInternal True  _ = [Failover]
migrationMoves MirrorExternal _ nodes_idx = map FailoverToAny nodes_idx

-- | Generate all possible disk moves (complex instance moves consist of disk
-- moves and maybe migrations) of an instance given some additional parameters
diskMoves :: MirrorType   -- ^ The mirroring type of the instance
          -> Bool         -- ^ Whether the secondary node is a valid new node
          -> Bool         -- ^ Whether we can change the primary node
          -> (Bool, Bool) -- ^ Whether migration is restricted and whether
                          -- the instance primary is offline
          -> [Ndx]        -- ^ Target node candidates list
          -> [IMove]      -- ^ List of valid result moves
diskMoves MirrorNone _ _ _ _ = []
diskMoves MirrorExternal _ _ _ _ = []
diskMoves MirrorInternal valid_sec inst_moves restr nodes_idx =
  concatMap (intMirrSingleDiskMove valid_sec inst_moves restr) nodes_idx
  where
    intMirrSingleDiskMove _ False _ tdx =
      [ReplaceSecondary tdx]

    intMirrSingleDiskMove _ _ (True, False) tdx =
      [ReplaceSecondary tdx]

    intMirrSingleDiskMove True True (False, _) tdx =
      [ ReplaceSecondary tdx
      , ReplaceAndFailover tdx
      , ReplacePrimary tdx
      , FailoverAndReplace tdx
      ]

    intMirrSingleDiskMove True True (True, True) tdx =
      [ ReplaceSecondary tdx
      , ReplaceAndFailover tdx
      , FailoverAndReplace tdx
      ]

    intMirrSingleDiskMove False True _ tdx =
      [ ReplaceSecondary tdx
      , ReplaceAndFailover tdx
      ]


-- | Compute the best move for a given instance.
checkInstanceMove ::  AlgorithmOptions -- ^ Algorithmic options for balancing
                  -> [Ndx]             -- ^ Allowed target node indices
                  -> Table             -- ^ Original table
                  -> Instance.Instance -- ^ Instance to move
                  -> (Table, Table)    -- ^ Pair of best new tables:
                                       -- migrations only and with disk moves
checkInstanceMove opts nodes_idx ini_tbl@(Table nl _ _ _) target =
  let force = algIgnoreSoftErrors opts
      inst_moves = algInstanceMoves opts
      rest_mig = algRestrictedMigration opts
      opdx = Instance.pNode target
      osdx = Instance.sNode target
      bad_nodes = [opdx, osdx]
      nodes = filter (`notElem` bad_nodes) nodes_idx
      mir_type = Instance.mirrorType target
      use_secondary = elem osdx nodes_idx && inst_moves
      primary_drained = Node.offline
                        . flip Container.find nl
                        $ Instance.pNode target

      migrations = migrationMoves mir_type use_secondary nodes
      disk_moves = diskMoves mir_type use_secondary inst_moves
                   (rest_mig, primary_drained) nodes

      -- iterate over the possible nodes and migrations for this instance
      best_migr_tbl =
        if inst_moves
          then foldl' (checkSingleStep force ini_tbl target) ini_tbl migrations
          else ini_tbl
      -- iterate over the possible moves for this instance
      best_tbl =
        foldl' (checkSingleStep force ini_tbl target) best_migr_tbl disk_moves
  in (best_migr_tbl, best_tbl)

-- | Compute the best next move.
checkMove :: AlgorithmOptions       -- ^ Algorithmic options for balancing
             -> [Ndx]               -- ^ Allowed target node indices
             -> Table               -- ^ The current solution
             -> [Instance.Instance] -- ^ List of instances still to move
             -> Table               -- ^ The new solution
checkMove opts nodes_idx ini_tbl@(Table _ _ ini_cv _) victims =
  let disk_moves = algDiskMoves opts
      disk_moves_f = algDiskMovesFactor opts
      -- we're using rwhnf from the Control.Parallel.Strategies
      -- package; we don't need to use rnf as that would force too
      -- much evaluation in single-threaded cases, and in
      -- multi-threaded case the weak head normal form is enough to
      -- spark the evaluation
      table_pairs = parMap rwhnf (checkInstanceMove opts nodes_idx ini_tbl)
                    victims

      -- iterate over all instances, computing the best move
      best_migr_tbl@(Table _ _ best_migr_cv _) =
        foldl' compareTables ini_tbl $ map fst table_pairs
      best_tbl@(Table _ _ best_cv _) =
        foldl' compareTables ini_tbl $ map snd table_pairs
  in if not disk_moves
     || ini_cv - best_cv <= (ini_cv - best_migr_cv) * disk_moves_f
       then best_migr_tbl
       else best_tbl -- best including disk moves

-- | Check if we are allowed to go deeper in the balancing.
doNextBalance :: Table     -- ^ The starting table
              -> Int       -- ^ Remaining length
              -> Score     -- ^ Score at which to stop
              -> Bool      -- ^ True if we can continue
doNextBalance ini_tbl max_rounds min_score =
  let Table _ _ ini_cv ini_plc = ini_tbl
      ini_plc_len = length ini_plc
  in (max_rounds < 0 || ini_plc_len < max_rounds) && ini_cv > min_score

-- | Run a balance move.
tryBalance :: AlgorithmOptions  -- ^ Algorithmic options for balancing
              -> Table          -- ^ The starting table
              -> Maybe Table    -- ^ The resulting table and commands
tryBalance opts ini_tbl =
    let evac_mode = algEvacMode opts
        mg_limit = algMinGainLimit opts
        min_gain = algMinGain opts
        Table ini_nl ini_il ini_cv _ = ini_tbl
        all_inst = Container.elems ini_il
        all_nodes = Container.elems ini_nl
        (offline_nodes, online_nodes) = partition Node.offline all_nodes
        all_inst' = if evac_mode
                      then let bad_nodes = map Node.idx offline_nodes
                           in filter (any (`elem` bad_nodes) .
                                          Instance.allNodes) all_inst
                      else all_inst
        reloc_inst = filter (\i -> Instance.movable i &&
                                   Instance.autoBalance i) all_inst'
        node_idx = map Node.idx online_nodes
        allowed_node = maybe (const True) (flip Set.member)
                         $ algAllowedNodes opts
        good_nidx = filter allowed_node node_idx
        allowed_inst = liftA2 (&&) (allowed_node . Instance.pNode)
                         (liftA2 (||) allowed_node (< 0) . Instance.sNode)
        good_reloc_inst = filter allowed_inst reloc_inst
        fin_tbl = checkMove opts good_nidx ini_tbl good_reloc_inst
        (Table _ _ fin_cv _) = fin_tbl
    in
      if fin_cv < ini_cv && (ini_cv > mg_limit || ini_cv - fin_cv >= min_gain)
      then Just fin_tbl -- this round made success, return the new table
      else Nothing

-- * Allocation functions

-- | Generate the valid node allocation singles or pairs for a new instance.
genAllocNodes :: AlgorithmOptions  -- ^ algorithmic options to honor
              -> Group.List        -- ^ Group list
              -> Node.List         -- ^ The node map
              -> Int               -- ^ The number of nodes required
              -> Bool              -- ^ Whether to drop or not
                                   -- unallocable nodes
              -> Result AllocNodes -- ^ The (monadic) result
genAllocNodes opts gl nl count drop_unalloc =
  let filter_fn = if drop_unalloc
                    then filter (Group.isAllocable .
                                 flip Container.find gl . Node.group)
                    else id
      restrict_fn = maybe id (\ns -> filter (flip elem ns . Node.name))
                    $ algRestrictToNodes opts
      all_nodes = restrict_fn . filter_fn $ getOnline nl
      all_pairs = [(Node.idx p,
                    [Node.idx s | s <- all_nodes,
                                       Node.idx p /= Node.idx s,
                                       Node.group p == Node.group s]) |
                   p <- all_nodes]
  in case count of
       1 -> Ok (Left (map Node.idx all_nodes))
       2 -> Ok (Right (filter (not . null . snd) all_pairs))
       _ -> Bad "Unsupported number of nodes, only one or two  supported"

-- | Try to allocate an instance on the cluster.
tryAlloc :: (Monad m) =>
            AlgorithmOptions
         -> Node.List         -- ^ The node list
         -> Instance.List     -- ^ The instance list
         -> Instance.Instance -- ^ The instance to allocate
         -> AllocNodes        -- ^ The allocation targets
         -> m AllocSolution   -- ^ Possible solution list
tryAlloc _ _  _ _    (Right []) = fail "Not enough online nodes"
tryAlloc opts nl il inst (Right ok_pairs) =
  let cstat = compClusterStatistics $ Container.elems nl
      n1pred = if algCapacity opts
                 then allocGlobalN1 opts nl il
                 else const True
      psols = parMap rwhnf (\(p, ss) ->
                              collectionToSolution FailN1 n1pred $
                              foldl (\cstate ->
                                      concatAllocCollections cstate
                                      . allocateOnPair opts cstat nl inst p)
                              emptyAllocCollection ss) ok_pairs
      sols = foldl' sumAllocs emptyAllocSolution psols
  in return $ annotateSolution sols

tryAlloc _ _  _ _    (Left []) = fail "No online nodes"
tryAlloc opts nl il inst (Left all_nodes) =
  let sols = foldl (\cstate ->
                       concatAllocCollections cstate
                       . allocateOnSingle opts nl inst
                   ) emptyAllocCollection all_nodes
      n1pred = if algCapacity opts
                 then allocGlobalN1 opts nl il
                 else const True
  in return . annotateSolution
       $ collectionToSolution FailN1 n1pred sols

-- | From a list of possibly bad and possibly empty solutions, filter
-- only the groups with a valid result. Note that the result will be
-- reversed compared to the original list.
filterMGResults :: [(Group.Group, Result (GenericAllocSolution a))]
                -> [(Group.Group, GenericAllocSolution a)]
filterMGResults = foldl' fn []
  where unallocable = not . Group.isAllocable
        fn accu (grp, rasol) =
          case rasol of
            Bad _ -> accu
            Ok sol | isNothing (asSolution sol) -> accu
                   | unallocable grp -> accu
                   | otherwise -> (grp, sol):accu

-- | Sort multigroup results based on policy and score.
sortMGResults :: Ord a
              => [(Group.Group, GenericAllocSolution a)]
              -> [(Group.Group, GenericAllocSolution a)]
sortMGResults sols =
  let extractScore (_, _, _, x) = x
      solScore (grp, sol) = (Group.allocPolicy grp,
                             (extractScore . fromJust . asSolution) sol)
  in sortBy (comparing solScore) sols

-- | Determines if a group is connected to the networks required by the
-- | instance.
hasRequiredNetworks :: Group.Group -> Instance.Instance -> Bool
hasRequiredNetworks ng = all hasNetwork . Instance.nics
  where hasNetwork = maybe True (`elem` Group.networks ng) . Nic.network

-- | Removes node groups which can't accommodate the instance
filterValidGroups :: [(Group.Group, (Node.List, Instance.List))]
                  -> Instance.Instance
                  -> ([(Group.Group, (Node.List, Instance.List))], [String])
filterValidGroups [] _ = ([], [])
filterValidGroups (ng:ngs) inst =
  let (valid_ngs, msgs) = filterValidGroups ngs inst
  in if hasRequiredNetworks (fst ng) inst
      then (ng:valid_ngs, msgs)
      else (valid_ngs,
            ("group " ++ Group.name (fst ng) ++
             " is not connected to a network required by instance " ++
             Instance.name inst):msgs)

-- | Finds an allocation solution for an instance on a group
findAllocation :: AlgorithmOptions
               -> Group.List           -- ^ The group list
               -> Node.List            -- ^ The node list
               -> Instance.List        -- ^ The instance list
               -> Gdx                  -- ^ The group to allocate to
               -> Instance.Instance    -- ^ The instance to allocate
               -> Int                  -- ^ Required number of nodes
               -> Result (AllocSolution, [String])
findAllocation opts mggl mgnl mgil gdx inst cnt = do
  let belongsTo nl' nidx = nidx `elem` map Node.idx (Container.elems nl')
      nl = Container.filter ((== gdx) . Node.group) mgnl
      il = Container.filter (belongsTo nl . Instance.pNode) mgil
      group' = Container.find gdx mggl
  unless (hasRequiredNetworks group' inst) . failError
         $ "The group " ++ Group.name group' ++ " is not connected to\
           \ a network required by instance " ++ Instance.name inst
  solution <- genAllocNodes opts mggl nl cnt False >>= tryAlloc opts nl il inst
  return (solution, solutionDescription (group', return solution))

-- | Finds the best group for an instance on a multi-group cluster.
--
-- Only solutions in @preferred@ and @last_resort@ groups will be
-- accepted as valid, and additionally if the allowed groups parameter
-- is not null then allocation will only be run for those group
-- indices.
findBestAllocGroup :: AlgorithmOptions
                   -> Group.List           -- ^ The group list
                   -> Node.List            -- ^ The node list
                   -> Instance.List        -- ^ The instance list
                   -> Maybe [Gdx]          -- ^ The allowed groups
                   -> Instance.Instance    -- ^ The instance to allocate
                   -> Int                  -- ^ Required number of nodes
                   -> Result (Group.Group, AllocSolution, [String])
findBestAllocGroup opts mggl mgnl mgil allowed_gdxs inst cnt =
  let groups_by_idx = splitCluster mgnl mgil
      groups = map (\(gid, d) -> (Container.find gid mggl, d)) groups_by_idx
      groups' = maybe groups
                (\gs -> filter ((`elem` gs) . Group.idx . fst) groups)
                allowed_gdxs
      (groups'', filter_group_msgs) = filterValidGroups groups' inst
      sols = map (\(gr, (nl, _)) ->
                   (gr, genAllocNodes opts mggl nl cnt False >>=
                        tryAlloc opts mgnl mgil inst))
             groups''::[(Group.Group, Result AllocSolution)]
      all_msgs = filter_group_msgs ++ concatMap solutionDescription sols
      goodSols = filterMGResults sols
      sortedSols = sortMGResults goodSols
  in case sortedSols of
       [] -> Bad $ if null groups'
                     then "no groups for evacuation: allowed groups was " ++
                          show allowed_gdxs ++ ", all groups: " ++
                          show (map fst groups)
                     else intercalate ", " all_msgs
       (final_group, final_sol):_ -> return (final_group, final_sol, all_msgs)

-- | Try to allocate an instance on a multi-group cluster.
tryMGAlloc :: AlgorithmOptions
           -> Group.List           -- ^ The group list
           -> Node.List            -- ^ The node list
           -> Instance.List        -- ^ The instance list
           -> Instance.Instance    -- ^ The instance to allocate
           -> Int                  -- ^ Required number of nodes
           -> Result AllocSolution -- ^ Possible solution list
tryMGAlloc opts mggl mgnl mgil inst cnt = do
  (best_group, solution, all_msgs) <-
      findBestAllocGroup opts mggl mgnl mgil Nothing inst cnt
  let group_name = Group.name best_group
      selmsg = "Selected group: " ++ group_name
  return $ solution { asLog = selmsg:all_msgs }

-- | Try to allocate an instance to a group.
tryGroupAlloc :: AlgorithmOptions
              -> Group.List           -- ^ The group list
              -> Node.List            -- ^ The node list
              -> Instance.List        -- ^ The instance list
              -> String               -- ^ The allocation group (name)
              -> Instance.Instance    -- ^ The instance to allocate
              -> Int                  -- ^ Required number of nodes
              -> Result AllocSolution -- ^ Solution
tryGroupAlloc opts mggl mgnl ngil gn inst cnt = do
  gdx <- Group.idx <$> Container.findByName mggl gn
  (solution, msgs) <- findAllocation opts mggl mgnl ngil gdx inst cnt
  return $ solution { asLog = msgs }


-- | Try to allocate a list of instances on a multi-group cluster.
allocList :: AlgorithmOptions
          -> Group.List                                -- ^ The group list
          -> Node.List                                 -- ^ The node list
          -> Instance.List                             -- ^ The instance list
          -> [(Instance.Instance, AllocDetails)]       -- ^ The instance to
                                                       --   allocate
          -> AllocSolutionList                         -- ^ Possible solution
                                                       --   list
          -> Result (Node.List, Instance.List,
                     AllocSolutionList)                -- ^ The final solution
                                                       --   list
allocList _ _  nl il [] result = Ok (nl, il, result)
allocList opts gl nl il ((xi, AllocDetails xicnt mgn):xies) result = do
  ares <- case mgn of
    Nothing -> tryMGAlloc opts gl nl il xi xicnt
    Just gn -> tryGroupAlloc opts gl nl il gn xi xicnt
  let sol = asSolution ares
      nl' = extractNl nl il sol
      il' = updateIl il sol
  allocList opts gl nl' il' xies ((xi, ares):result)



-- | Change-group IAllocator mode main function.
--
-- This is very similar to 'tryNodeEvac', the only difference is that
-- we don't choose as target group the current instance group, but
-- instead:
--
--   1. at the start of the function, we compute which are the target
--   groups; either no groups were passed in, in which case we choose
--   all groups out of which we don't evacuate instance, or there were
--   some groups passed, in which case we use those
--
--   2. for each instance, we use 'findBestAllocGroup' to choose the
--   best group to hold the instance, and then we do what
--   'tryNodeEvac' does, except for this group instead of the current
--   instance group.
--
-- Note that the correct behaviour of this function relies on the
-- function 'nodeEvacInstance' to be able to do correctly both
-- intra-group and inter-group moves when passed the 'ChangeAll' mode.
tryChangeGroup :: AlgorithmOptions
               -> Group.List    -- ^ The cluster groups
               -> Node.List     -- ^ The node list (cluster-wide)
               -> Instance.List -- ^ Instance list (cluster-wide)
               -> [Gdx]         -- ^ Target groups; if empty, any
                                -- groups not being evacuated
               -> [Idx]         -- ^ List of instance (indices) to be evacuated
               -> Result (Node.List, Instance.List, EvacSolution)
tryChangeGroup opts gl ini_nl ini_il gdxs idxs =
  let evac_gdxs = nub $ map (instancePriGroup ini_nl .
                             flip Container.find ini_il) idxs
      target_gdxs = (if null gdxs
                       then Container.keys gl
                       else gdxs) \\ evac_gdxs
      offline = map Node.idx . filter Node.offline $ Container.elems ini_nl
      excl_ndx = foldl' (flip IntSet.insert) IntSet.empty offline
      group_ndx = map (\(gdx, (nl, _)) -> (gdx, map Node.idx
                                           (Container.elems nl))) $
                  splitCluster ini_nl ini_il
      (fin_nl, fin_il, esol) =
        foldl' (\state@(nl, il, _) inst ->
                  let solution = do
                        let ncnt = Instance.requiredNodes $
                                   Instance.diskTemplate inst
                        (grp, _, _) <- findBestAllocGroup opts gl nl il
                                       (Just target_gdxs) inst ncnt
                        let gdx = Group.idx grp
                        av_nodes <- availableGroupNodes group_ndx
                                    excl_ndx gdx
                        nodeEvacInstance defaultOptions
                                         nl il ChangeAll inst gdx av_nodes
                  in updateEvacSolution state (Instance.idx inst) solution
               )
        (ini_nl, ini_il, emptyEvacSolution)
        (map (`Container.find` ini_il) idxs)
  in return (fin_nl, fin_il, reverseEvacSolution esol)

-- | Standard-sized allocation method.
--
-- This places instances of the same size on the cluster until we're
-- out of space. The result will be a list of identically-sized
-- instances.
iterateAllocSmallStep :: AlgorithmOptions -> AllocMethod
iterateAllocSmallStep opts nl il limit newinst allocnodes ixes cstats =
  let depth = length ixes
      newname = printf "new-%d" depth::String
      newidx = Container.size il
      newi2 = Instance.setIdx (Instance.setName newinst newname) newidx
      newlimit = fmap (flip (-) 1) limit
      opts' = if Instance.diskTemplate newi2 == DTDrbd8
                then opts { algCapacity = False }
                else opts
  in case tryAlloc opts' nl il newi2 allocnodes of
       Bad s -> Bad s
       Ok (AllocSolution { asFailures = errs, asSolution = sols3 }) ->
         let newsol = Ok (collapseFailures errs, nl, il, ixes, cstats) in
         case sols3 of
           Nothing -> newsol
           Just (xnl, xi, _, _) ->
             if limit == Just 0
               then newsol
               else iterateAllocSmallStep opts xnl (Container.add newidx xi il)
                      newlimit newinst allocnodes (xi:ixes)
                      (totalResources xnl:cstats)

-- | Guess a number of machines worth trying to put on the cluster in one step.
-- The goal is to guess a number close to the actual capacity of the cluster but
-- preferrably not bigger, unless it is quite small (as we don't want to do
-- big steps smaller than 10).
guessBigstepSize :: Node.List -> Instance.Instance -> Int
guessBigstepSize nl inst =
  let nodes = Container.elems nl
      totalUnusedMemory = sum $ map Node.fMem nodes
      reserved = round . maximum $ map Node.tMem nodes
      capacity = (totalUnusedMemory - reserved) `div` Instance.mem inst
      -- however, at every node we might lose almost an instance if it just
      -- doesn't fit by a tiny margin
      guess = capacity - Container.size nl
  in if guess < 20 then 20 else guess

-- | A speed-up version of `iterateAllocSmallStep`.
--
-- This function returns precisely the same result as `iterateAllocSmallStep`.
-- However the computation is speed up by the following heuristic: allocate
-- a group of instances iteratively without considering global N+1 redundancy;
-- if the result of this is globally N+1 redundant, then everything was OK
-- inbetween and we can continue from there. Only if that fails, do a
-- step-by-step iterative allocation.
-- In order to further speed up the computation while keeping it robust, we
-- first try (if the first argument is True) a number of steps guessed from
-- the node capacity, then, if that failed, a fixed step size and only as last
-- restort step-by-step iterative allocation.
iterateAlloc' :: Bool -> AlgorithmOptions -> AllocMethod
iterateAlloc' tryHugestep opts nl il limit newinst allocnodes ixes cstats =
  if not $ algCapacity opts
    then iterateAllocSmallStep opts nl il limit newinst allocnodes ixes cstats
    else let bigstepsize = if tryHugestep
                             then guessBigstepSize nl newinst
                             else 10
             (limit', newlimit) = maybe (Just bigstepsize, Nothing)
                                    (Just . min bigstepsize
                                     &&& Just . max 0 . flip (-) bigstepsize)
                                    limit
             opts' = opts { algCapacity = False }
         in case iterateAllocSmallStep opts' nl il limit'
                                       newinst allocnodes ixes cstats of
            Bad s -> Bad s
            Ok res@(_, nl', il', ixes', cstats') | redundant opts nl' il' ->
              if newlimit == Just 0 || length ixes' == length ixes
                then return res
                else iterateAlloc' tryHugestep opts nl' il' newlimit newinst
                                   allocnodes ixes' cstats'
            _ -> if tryHugestep
                   then iterateAlloc' False opts nl il limit newinst allocnodes
                                      ixes cstats
                   else iterateAllocSmallStep opts nl il limit newinst
                                              allocnodes ixes cstats

-- | A speed-up version of `iterateAllocSmallStep`.
iterateAlloc :: AlgorithmOptions -> AllocMethod
iterateAlloc = iterateAlloc' True

-- | Predicate whether shrinking a single resource can lead to a valid
-- allocation.
sufficesShrinking :: (Instance.Instance -> AllocSolution) -> Instance.Instance
                     -> FailMode  -> Maybe Instance.Instance
sufficesShrinking allocFn inst fm =
  case dropWhile (isNothing . asSolution . fst)
       . takeWhile (liftA2 (||) (elem fm . asFailures . fst)
                                (isJust . asSolution . fst))
       . map (allocFn &&& id) $
       iterateOk (`Instance.shrinkByType` fm) inst
  of x:_ -> Just . snd $ x
     _ -> Nothing

-- | For a failure determine the underlying resource that most likely
-- causes this kind of failure. In particular, N+1 violations are most
-- likely caused by lack of memory.
underlyingCause :: FailMode -> FailMode
underlyingCause FailN1 = FailMem
underlyingCause x = x

-- | Shrink a resource of an instance until the failure statistics for
-- this resource changes. Note that it might no be possible to allocate
-- an instance at this size; nevertheless there might be a need to change
-- the resource to shrink on, e.g., if the current instance is too big on
-- two resources.
doShrink :: (Instance.Instance -> AllocSolution) -> Instance.Instance
         -> FailMode -> Maybe Instance.Instance
doShrink allocFn inst fm =
  let physRes = underlyingCause fm
      getCount = runListHead 0 snd . filter ((==) physRes . fst)
                 . collapseFailures . map underlyingCause . asFailures
      initialStat = getCount $ allocFn inst
      hasChanged = ((/=) initialStat . getCount . fst)
      -- as the list of possible shrinks can be quite long, and, moreover,
      -- has some cost of computing it, our heuristics is to look into it
      -- only for a limited range; only once the list is shorter, we do
      -- binary search.
      lookAhead = 50
      heuristics xs = if null (drop lookAhead xs)
                        then length xs `div` 2
                        else lookAhead
  in fmap snd
     . monotoneFind heuristics hasChanged
     . map (allocFn &&& id)
     $ iterateOk (`Instance.shrinkByType` physRes) inst

-- | Tiered allocation method.
--
-- This places instances on the cluster, and decreases the spec until
-- we can allocate again. The result will be a list of decreasing
-- instance specs.
tieredAlloc :: AlgorithmOptions -> AllocMethod
tieredAlloc opts nl il limit newinst allocnodes ixes cstats =
  case iterateAlloc opts nl il limit newinst allocnodes ixes cstats of
    Bad s -> Bad s
    Ok (errs, nl', il', ixes', cstats') ->
      let newsol = Ok (errs, nl', il', ixes', cstats')
          ixes_cnt = length ixes'
          (stop, newlimit) = case limit of
                               Nothing -> (False, Nothing)
                               Just n -> (n <= ixes_cnt,
                                            Just (n - ixes_cnt))
          sortedErrs = nub . map (underlyingCause . fst)
                        $ sortBy (flip $ comparing snd) errs
          allocFn = fromMaybe emptyAllocSolution
                      . flip (tryAlloc opts nl' il') allocnodes
          suffShrink = sufficesShrinking allocFn newinst
          bigSteps = filter isJust . map suffShrink $ drop 1 sortedErrs
          progress (Ok (_, _, _, newil', _)) (Ok (_, _, _, newil, _)) =
            length newil' > length newil
          progress _ _ = False
      in if stop then newsol else
           let newsol' = case map (doShrink allocFn newinst) sortedErrs of
                 Just newinst' : _ -> tieredAlloc opts nl' il' newlimit
                                        newinst' allocnodes ixes' cstats'
                 _ -> newsol
           in if progress newsol' newsol then newsol' else
                case bigSteps of
                  Just newinst':_ -> tieredAlloc opts nl' il' newlimit
                                     newinst' allocnodes ixes' cstats'
                  _ -> newsol

-- * Formatting functions

-- | Given the original and final nodes, computes the relocation description.
computeMoves :: Instance.Instance -- ^ The instance to be moved
             -> String -- ^ The instance name
             -> IMove  -- ^ The move being performed
             -> String -- ^ New primary
             -> String -- ^ New secondary
             -> (String, [String])
                -- ^ Tuple of moves and commands list; moves is containing
                -- either @/f/@ for failover or @/r:name/@ for replace
                -- secondary, while the command list holds gnt-instance
                -- commands (without that prefix), e.g \"@failover instance1@\"
computeMoves i inam mv c d =
  case mv of
    Failover -> ("f", [mig])
    FailoverToAny _ -> (printf "fa:%s" c, [mig_any])
    FailoverAndReplace _ -> (printf "f r:%s" d, [mig, rep d])
    ReplaceSecondary _ -> (printf "r:%s" d, [rep d])
    ReplaceAndFailover _ -> (printf "r:%s f" c, [rep c, mig])
    ReplacePrimary _ -> (printf "f r:%s f" c, [mig, rep c, mig])
  where morf = if Instance.isRunning i then "migrate" else "failover"
        mig = printf "%s -f %s" morf inam::String
        mig_any = printf "%s -f -n %s %s" morf c inam::String
        rep n = printf "replace-disks -n %s %s" n inam::String

-- | Converts a placement to string format.
printSolutionLine :: Node.List     -- ^ The node list
                  -> Instance.List -- ^ The instance list
                  -> Int           -- ^ Maximum node name length
                  -> Int           -- ^ Maximum instance name length
                  -> Placement     -- ^ The current placement
                  -> Int           -- ^ The index of the placement in
                                   -- the solution
                  -> (String, [String])
printSolutionLine nl il nmlen imlen plc pos =
  let pmlen = (2*nmlen + 1)
      (i, p, s, mv, c) = plc
      old_sec = Instance.sNode inst
      inst = Container.find i il
      inam = Instance.alias inst
      npri = Node.alias $ Container.find p nl
      nsec = Node.alias $ Container.find s nl
      opri = Node.alias $ Container.find (Instance.pNode inst) nl
      osec = Node.alias $ Container.find old_sec nl
      (moves, cmds) =  computeMoves inst inam mv npri nsec
      -- FIXME: this should check instead/also the disk template
      ostr = if old_sec == Node.noSecondary
               then printf "%s" opri::String
               else printf "%s:%s" opri osec::String
      nstr = if s == Node.noSecondary
               then printf "%s" npri::String
               else printf "%s:%s" npri nsec::String
  in (printf "  %3d. %-*s %-*s => %-*s %12.8f a=%s"
      pos imlen inam pmlen ostr pmlen nstr c moves,
      cmds)

-- | Return the instance and involved nodes in an instance move.
--
-- Note that the output list length can vary, and is not required nor
-- guaranteed to be of any specific length.
involvedNodes :: Instance.List -- ^ Instance list, used for retrieving
                               -- the instance from its index; note
                               -- that this /must/ be the original
                               -- instance list, so that we can
                               -- retrieve the old nodes
              -> Placement     -- ^ The placement we're investigating,
                               -- containing the new nodes and
                               -- instance index
              -> [Ndx]         -- ^ Resulting list of node indices
involvedNodes il plc =
  let (i, np, ns, _, _) = plc
      inst = Container.find i il
  in nub . filter (>= 0) $ [np, ns] ++ Instance.allNodes inst

-- | From two adjacent cluster tables get the list of moves that transitions
-- from to the other
getMoves :: (Table, Table) -> [MoveJob]
getMoves (Table _ initial_il _ initial_plc, Table final_nl _ _ final_plc) =
  let
    plctoMoves (plc@(idx, p, s, mv, _)) =
      let inst = Container.find idx initial_il
          inst_name = Instance.name inst
          affected = involvedNodes initial_il plc
          np = Node.alias $ Container.find p final_nl
          ns = Node.alias $ Container.find s final_nl
          (_, cmds) = computeMoves inst inst_name mv np ns
      in (affected, idx, mv, cmds)
  in map plctoMoves . reverse . drop (length initial_plc) $ reverse final_plc
             
-- | Inner function for splitJobs, that either appends the next job to
-- the current jobset, or starts a new jobset.
mergeJobs :: ([JobSet], [Ndx]) -> MoveJob -> ([JobSet], [Ndx])
mergeJobs ([], _) n@(ndx, _, _, _) = ([[n]], ndx)
mergeJobs (cjs@(j:js), nbuf) n@(ndx, _, _, _)
  | null (ndx `intersect` nbuf) = ((n:j):js, ndx ++ nbuf)
  | otherwise = ([n]:cjs, ndx)

-- | Break a list of moves into independent groups. Note that this
-- will reverse the order of jobs.
splitJobs :: [MoveJob] -> [JobSet]
splitJobs = fst . foldl mergeJobs ([], [])

-- | Given a list of commands, prefix them with @gnt-instance@ and
-- also beautify the display a little.
formatJob :: Int -> Int -> (Int, MoveJob) -> [String]
formatJob jsn jsl (sn, (_, _, _, cmds)) =
  let out =
        printf "  echo job %d/%d" jsn sn:
        printf "  check":
        map ("  gnt-instance " ++) cmds
  in if sn == 1
       then ["", printf "echo jobset %d, %d jobs" jsn jsl] ++ out
       else out

-- | Given a list of commands, prefix them with @gnt-instance@ and
-- also beautify the display a little.
formatCmds :: [JobSet] -> String
formatCmds =
  unlines .
  concatMap (\(jsn, js) -> concatMap (formatJob jsn (length js))
                           (zip [1..] js)) .
  zip [1..]

-- | Print the node list.
printNodes :: Node.List -> [String] -> String
printNodes nl fs =
  let fields = case fs of
                 [] -> Node.defaultFields
                 "+":rest -> Node.defaultFields ++ rest
                 _ -> fs
      snl = sortBy (comparing Node.idx) (Container.elems nl)
      (header, isnum) = unzip $ map Node.showHeader fields
  in printTable "" header (map (Node.list fields) snl) isnum

-- | Print the instance list.
printInsts :: Node.List -> Instance.List -> String
printInsts nl il =
  let sil = sortBy (comparing Instance.idx) (Container.elems il)
      helper inst = [ if Instance.isRunning inst then "R" else " "
                    , Instance.name inst
                    , Container.nameOf nl (Instance.pNode inst)
                    , let sdx = Instance.sNode inst
                      in if sdx == Node.noSecondary
                           then  ""
                           else Container.nameOf nl sdx
                    , if Instance.autoBalance inst then "Y" else "N"
                    , printf "%3d" $ Instance.vcpus inst
                    , printf "%5d" $ Instance.mem inst
                    , printf "%5d" $ Instance.dsk inst `div` 1024
                    , printf "%5.3f" lC
                    , printf "%5.3f" lM
                    , printf "%5.3f" lD
                    , printf "%5.3f" lN
                    ]
          where DynUtil lC lM lD lN = Instance.util inst
      header = [ "F", "Name", "Pri_node", "Sec_node", "Auto_bal"
               , "vcpu", "mem" , "dsk", "lCpu", "lMem", "lDsk", "lNet" ]
      isnum = False:False:False:False:False:repeat True
  in printTable "" header (map helper sil) isnum


-- * Node group functions

-- | Computes the group of an instance.
instanceGroup :: Node.List -> Instance.Instance -> Result Gdx
instanceGroup nl i =
  let sidx = Instance.sNode i
      pnode = Container.find (Instance.pNode i) nl
      snode = if sidx == Node.noSecondary
              then pnode
              else Container.find sidx nl
      pgroup = Node.group pnode
      sgroup = Node.group snode
  in if pgroup /= sgroup
       then fail ("Instance placed accross two node groups, primary " ++
                  show pgroup ++ ", secondary " ++ show sgroup)
       else return pgroup

-- | Compute the list of badly allocated instances (split across node
-- groups).
findSplitInstances :: Node.List -> Instance.List -> [Instance.Instance]
findSplitInstances nl =
  filter (not . isOk . instanceGroup nl) . Container.elems