{-# LANGUAGE ViewPatterns #-}

{-| Implementation of the Ganeti configuration database.

-}

{-

Copyright (C) 2011, 2012 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.Config
    ( LinkIpMap
    , NdParamObject(..)
    , loadConfig
    , saveConfig
    , getNodeInstances
    , getNodeRole
    , getNodeNdParams
    , getDefaultNicLink
    , getDefaultHypervisor
    , getInstancesIpByLink
    , getMasterNodes
    , getMasterCandidates
    , getMasterOrCandidates
    , getMasterNetworkParameters
    , getOnlineNodes
    , getNode
    , getNodeByUuid
    , getInstance
    , getDisk
    , getFilterRule
    , getGroup
    , getGroupNdParams
    , getGroupIpolicy
    , getGroupDiskParams
    , getGroupNodes
    , getGroupInstances
    , getGroupOfNode
    , getInstPrimaryNode
    , getInstMinorsForNode
    , getInstAllNodes
    , getInstDisks
    , getDrbdMinorsForDisk
    , getDrbdMinorsForInstance
    , getFilledHvStateParams
    , getFilledInstHvParams
    , getFilledInstBeParams
    , getFilledInstOsParams
    , getNetwork
    , MAC
    , getAllMACs
    , getAllDrbdSecrets
    , NodeLVsMap
    , getInstanceLVsByNode
    , getAllLVs
    , buildLinkIpInstnameMap
    , instNodes
    , instName
    ) where

import Prelude ()
import Ganeti.Prelude

import Control.Arrow ((&&&))
import Control.Monad (liftM)
import qualified Data.ByteString as BS
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.Foldable as F
import Data.List (foldl', nub)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Monoid
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Text.JSON as J
import System.IO

import Ganeti.BasicTypes
import qualified Ganeti.Constants as C
import qualified Ganeti.ConstantUtils as CU
import Ganeti.Errors
import Ganeti.JSON (fromJResult, fromContainer, GenericContainer(..))
import Ganeti.Objects
import Ganeti.Types
import qualified Ganeti.Utils.MultiMap as MM

-- | Type alias for the link and ip map.
type LinkIpMap = M.Map String (M.Map String String)

-- * Operations on the whole configuration

-- | Reads the config file.
readConfig :: FilePath -> IO (Result String)
readConfig = runResultT . liftIO . readFile

-- | Parses the configuration file.
parseConfig :: String -> Result ConfigData
parseConfig = fromJResult "parsing configuration" . J.decodeStrict

-- | Encodes the configuration file.
encodeConfig :: ConfigData -> String
encodeConfig = J.encodeStrict

-- | Wrapper over 'readConfig' and 'parseConfig'.
loadConfig :: FilePath -> IO (Result ConfigData)
loadConfig = fmap (>>= parseConfig) . readConfig

-- | Wrapper over 'hPutStr' and 'encodeConfig'.
saveConfig :: Handle -> ConfigData -> IO ()
saveConfig fh = hPutStr fh . encodeConfig

-- * Query functions

-- | Annotate Nothing as missing parameter and apply the given
-- transformation otherwise
withMissingParam :: String -> (a -> ErrorResult b) -> Maybe a -> ErrorResult b
withMissingParam = maybe . Bad . ParameterError

-- | Computes the nodes covered by a disk.
computeDiskNodes :: Disk -> S.Set String
computeDiskNodes dsk =
  case diskLogicalId dsk of
    Just (LIDDrbd8 nodeA nodeB _ _ _ _) -> S.fromList [nodeA, nodeB]
    _ -> S.empty

-- | Computes all disk-related nodes of an instance. For non-DRBD,
-- this will be empty, for DRBD it will contain both the primary and
-- the secondaries.
instDiskNodes :: ConfigData -> Instance -> S.Set String
instDiskNodes cfg inst =
  case getInstDisks cfg inst of
    Ok disks -> S.unions $ map computeDiskNodes disks
    Bad _ -> S.empty

-- | Computes all nodes of an instance.
instNodes :: ConfigData -> Instance -> S.Set String
instNodes cfg inst = maybe id S.insert (instPrimaryNode inst)
                      $ instDiskNodes cfg inst

-- | Computes the secondary node UUID for a DRBD disk
computeDiskSecondaryNode :: Disk -> String -> Maybe String
computeDiskSecondaryNode dsk primary =
  case diskLogicalId dsk of
    Just (LIDDrbd8 a b _ _ _ _) -> Just $ if primary == a then b else a
    _ -> Nothing

-- | Get instances of a given node.
-- The node is specified through its UUID.
-- The secondary calculation is expensive and frequently called, so optimise
-- this to allocate fewer temporary values
getNodeInstances :: ConfigData -> String -> ([Instance], [Instance])
getNodeInstances cfg nname =
    let all_insts = M.elems . fromContainer . configInstances $ cfg
        all_disks = fromContainer . configDisks $ cfg

        pri_inst = filter ((== Just nname) . instPrimaryNode) all_insts

        find_disk :: String -> Maybe Disk
        find_disk d_uuid = M.lookup (UTF8.fromString d_uuid) all_disks

        inst_disks :: [(Instance, [Disk])]
        inst_disks = [(i, mapMaybe find_disk $ instDisks i) | i <- all_insts]

        sec_insts :: [Instance]
        sec_insts = [inst |
          (inst, disks) <- inst_disks,
          s_uuid <- mapMaybe (\d -> (instPrimaryNode inst) >>= (computeDiskSecondaryNode d)) disks,
          s_uuid == nname]
    in (pri_inst, sec_insts)

-- | Computes the role of a node.
getNodeRole :: ConfigData -> Node -> NodeRole
getNodeRole cfg node
  | uuidOf node == clusterMasterNode (configCluster cfg) = NRMaster
  | nodeMasterCandidate node = NRCandidate
  | nodeDrained node = NRDrained
  | nodeOffline node = NROffline
  | otherwise = NRRegular

-- | Get the list of the master nodes (usually one).
getMasterNodes :: ConfigData -> [Node]
getMasterNodes cfg =
  filter ((==) NRMaster . getNodeRole cfg) . F.toList . configNodes $ cfg

-- | Get the list of master candidates, /not including/ the master itself.
getMasterCandidates :: ConfigData -> [Node]
getMasterCandidates cfg =
  filter ((==) NRCandidate . getNodeRole cfg) . F.toList . configNodes $ cfg

-- | Get the list of master candidates, /including/ the master.
getMasterOrCandidates :: ConfigData -> [Node]
getMasterOrCandidates cfg =
  let isMC r = (r == NRCandidate) || (r == NRMaster)
  in filter (isMC . getNodeRole cfg) . F.toList . configNodes $ cfg

-- | Get the network parameters for the master IP address.
getMasterNetworkParameters :: ConfigData -> MasterNetworkParameters
getMasterNetworkParameters cfg =
  let cluster = configCluster cfg
  in MasterNetworkParameters
      { masterNetworkParametersUuid = clusterMasterNode cluster
      , masterNetworkParametersIp = clusterMasterIp cluster
      , masterNetworkParametersNetmask = clusterMasterNetmask cluster
      , masterNetworkParametersNetdev = clusterMasterNetdev cluster
      , masterNetworkParametersIpFamily = clusterPrimaryIpFamily cluster
      }

-- | Get the list of online nodes.
getOnlineNodes :: ConfigData -> [Node]
getOnlineNodes = filter (not . nodeOffline) . F.toList . configNodes

-- | Returns the default cluster link.
getDefaultNicLink :: ConfigData -> String
getDefaultNicLink =
  let ppDefault = UTF8.fromString C.ppDefault
  in nicpLink . (M.! ppDefault) . fromContainer
     . clusterNicparams . configCluster

-- | Returns the default cluster hypervisor.
getDefaultHypervisor :: ConfigData -> Hypervisor
getDefaultHypervisor cfg =
  case clusterEnabledHypervisors $ configCluster cfg of
    -- FIXME: this case shouldn't happen (configuration broken), but
    -- for now we handle it here because we're not authoritative for
    -- the config
    []  -> XenPvm
    x:_ -> x

-- | Returns instances of a given link.
getInstancesIpByLink :: LinkIpMap -> String -> [String]
getInstancesIpByLink linkipmap link =
  M.keys $ M.findWithDefault M.empty link linkipmap

-- | Generic lookup function that converts from a possible abbreviated
-- name to a full name.
getItem :: String -> String -> M.Map String a -> ErrorResult a
getItem kind name allitems = do
  let lresult = lookupName (M.keys allitems) name
      err msg = Bad $ OpPrereqError (kind ++ " name " ++ name ++ " " ++ msg)
                        ECodeNoEnt
  fullname <- case lrMatchPriority lresult of
                PartialMatch -> Ok $ lrContent lresult
                ExactMatch -> Ok $ lrContent lresult
                MultipleMatch -> err "has multiple matches"
                FailMatch -> err "not found"
  maybe (err "not found after successfull match?!") Ok $
        M.lookup fullname allitems

-- | Simple lookup function, insisting on exact matches and using
-- byte strings.
getItem' :: String -> String -> M.Map BS.ByteString a -> ErrorResult a
getItem' kind name allitems =
  let name' = UTF8.fromString name
      err = Bad $ OpPrereqError (kind ++ " uuid " ++ name ++ " not found")
                    ECodeNoEnt
  in maybe err Ok $ M.lookup name' allitems

-- | Looks up a node by uuid.
getNodeByUuid :: ConfigData -> String -> ErrorResult Node
getNodeByUuid cfg uuid =
  let nodes = fromContainer (configNodes cfg)
  in getItem' "Node" uuid nodes

-- | Looks up a node by name matching.
getNodeByPartialName :: ConfigData -> String -> ErrorResult Node
getNodeByPartialName cfg name =
  let nodes = fromContainer (configNodes cfg)
      by_name = M.mapKeys (nodeName . (M.!) nodes) nodes
  in getItem "Node" name by_name

-- | Looks up a node by name or uuid.
getNode :: ConfigData -> String -> ErrorResult Node
getNode cfg name =
  case getNodeByUuid cfg name of
    -- if not found by uuid, we need to look it up by name
    x@(Ok _) -> x
    Bad _ -> getNodeByPartialName cfg name

-- | Looks up an instance by uuid.
getInstanceByUuid :: ConfigData -> String -> ErrorResult Instance
getInstanceByUuid cfg uuid =
  let instances = fromContainer (configInstances cfg)
  in getItem' "Instance" uuid instances

-- | Looks up an instance by approximate name.
getInstanceByPartialName :: ConfigData -> String -> ErrorResult Instance
getInstanceByPartialName cfg name =
  let instances = fromContainer (configInstances cfg)
      by_name = M.delete ""
                  . M.mapKeys (fromMaybe "" . instName . (M.!) instances)
                  $ instances
  in getItem "Instance" name by_name

-- | Looks up an instance by exact name match.
getInstanceByExactName :: ConfigData -> String -> ErrorResult Instance
getInstanceByExactName cfg name =
  let instances = M.elems . fromContainer . configInstances $ cfg
      matching = F.find (maybe False (== name) . instName) instances
  in case matching of
      Just inst -> Ok inst
      Nothing   -> Bad $ OpPrereqError
                   ("Instance name " ++ name ++ " not found")
                   ECodeNoEnt

-- | Looks up an instance by partial name or uuid.
getInstance :: ConfigData -> String -> ErrorResult Instance
getInstance cfg name =
  case getInstanceByUuid cfg name of
    x@(Ok _) -> x
    Bad _ -> getInstanceByPartialName cfg name

-- | Looks up a disk by uuid.
getDisk :: ConfigData -> String -> ErrorResult Disk
getDisk cfg name =
  let disks = fromContainer (configDisks cfg)
  in getItem' "Disk" name disks

-- | Looks up a filter by uuid.
getFilterRule :: ConfigData -> String -> ErrorResult FilterRule
getFilterRule cfg name =
  let filters = fromContainer (configFilters cfg)
  in getItem' "Filter" name filters

-- | Looks up a node group by name or uuid.
getGroup :: ConfigData -> String -> ErrorResult NodeGroup
getGroup cfg name =
  let groups = fromContainer (configNodegroups cfg)
  in case getItem' "NodeGroup" name groups of
       -- if not found by uuid, we need to look it up by name, slow
       Ok grp -> Ok grp
       Bad _ -> let by_name = M.mapKeys
                              (groupName . (M.!) groups) groups
                in getItem "NodeGroup" name by_name

-- | Computes a node group's node params.
getGroupNdParams :: ConfigData -> NodeGroup -> FilledNDParams
getGroupNdParams cfg ng =
  fillParams (clusterNdparams $ configCluster cfg) (groupNdparams ng)

-- | Computes a node group's ipolicy.
getGroupIpolicy :: ConfigData -> NodeGroup -> FilledIPolicy
getGroupIpolicy cfg ng =
  fillParams (clusterIpolicy $ configCluster cfg) (groupIpolicy ng)

-- | Computes a group\'s (merged) disk params.
getGroupDiskParams :: ConfigData -> NodeGroup -> GroupDiskParams
getGroupDiskParams cfg ng =
  GenericContainer $
  fillDict (fromContainer . clusterDiskparams $ configCluster cfg)
           (fromContainer $ groupDiskparams ng) []

-- | Get nodes of a given node group.
getGroupNodes :: ConfigData -> String -> [Node]
getGroupNodes cfg gname =
  let all_nodes = M.elems . fromContainer . configNodes $ cfg in
  filter ((==gname) . nodeGroup) all_nodes

-- | Get (primary, secondary) instances of a given node group.
getGroupInstances :: ConfigData -> String -> ([Instance], [Instance])
getGroupInstances cfg gname =
  let gnodes = map uuidOf (getGroupNodes cfg gname)
      ginsts = map (getNodeInstances cfg) gnodes in
  (concatMap fst ginsts, concatMap snd ginsts)

-- | default FilledHvStateParams.
defaultHvStateParams :: FilledHvStateParams
defaultHvStateParams = FilledHvStateParams
  { hvstateCpuNode  = CU.hvstDefaultCpuNode
  , hvstateCpuTotal = CU.hvstDefaultCpuTotal
  , hvstateMemHv    = CU.hvstDefaultMemoryHv
  , hvstateMemNode  = CU.hvstDefaultMemoryNode
  , hvstateMemTotal = CU.hvstDefaultMemoryTotal
  }

-- | Retrieves the node's static hypervisor state parameters, missing values
-- filled with group's parameters, missing group parameters are filled
-- with cluster's parameters. Currently, returns hvstate parameters only for
-- the default hypervisor.
getFilledHvStateParams :: ConfigData -> Node -> FilledHvState
getFilledHvStateParams cfg n =
  let cluster_hv_state =
        fromContainer . clusterHvStateStatic $ configCluster cfg
      def_hv = getDefaultHypervisor cfg
      cluster_fv = fromMaybe defaultHvStateParams $ M.lookup def_hv
                                                    cluster_hv_state
      group_fv = case getGroupOfNode cfg n >>=
                      M.lookup def_hv . fromContainer . groupHvStateStatic of
                   Just pv -> fillParams cluster_fv pv
                   Nothing -> cluster_fv
      node_fv = case M.lookup def_hv . fromContainer $ nodeHvStateStatic n of
                      Just pv -> fillParams group_fv pv
                      Nothing -> group_fv
  in GenericContainer $ M.fromList [(def_hv, node_fv)]

-- | Retrieves the instance hypervisor params, missing values filled with
-- cluster defaults.
getFilledInstHvParams :: [String] -> ConfigData -> Instance -> HvParams
getFilledInstHvParams globals cfg inst =
  -- First get the defaults of the parent
  let maybeHvName = instHypervisor inst
      hvParamMap = fromContainer . clusterHvparams $ configCluster cfg
      parentHvParams =
        maybe M.empty fromContainer (maybeHvName >>= flip M.lookup hvParamMap)
  -- Then the os defaults for the given hypervisor
      maybeOsName = UTF8.fromString <$> instOs inst
      osParamMap = fromContainer . clusterOsHvp $ configCluster cfg
      osHvParamMap =
        maybe M.empty (maybe M.empty fromContainer . flip M.lookup osParamMap)
          maybeOsName
      osHvParams =
        maybe M.empty (maybe M.empty fromContainer . flip M.lookup osHvParamMap)
          maybeHvName
  -- Then the child
      childHvParams = fromContainer . instHvparams $ inst
  -- Helper function
      fillFn con val = fillDict con val $ fmap UTF8.fromString globals
  in GenericContainer $ fillFn (fillFn parentHvParams osHvParams) childHvParams

-- | Retrieves the instance backend params, missing values filled with cluster
-- defaults.
getFilledInstBeParams :: ConfigData -> Instance -> ErrorResult FilledBeParams
getFilledInstBeParams cfg inst = do
  let beParamMap = fromContainer . clusterBeparams . configCluster $ cfg
  parentParams <- getItem' "FilledBeParams" C.ppDefault beParamMap
  return $ fillParams parentParams (instBeparams inst)

-- | Retrieves the instance os params, missing values filled with cluster
-- defaults. This does NOT include private and secret parameters.
getFilledInstOsParams :: ConfigData -> Instance -> OsParams
getFilledInstOsParams cfg inst =
  let maybeOsLookupName = liftM (takeWhile (/= '+')) (instOs inst)
      osParamMap = fromContainer . clusterOsparams $ configCluster cfg
      childOsParams = instOsparams inst
  in case withMissingParam "Instance without OS"
            (flip (getItem' "OsParams") osParamMap)
            maybeOsLookupName of
       Ok parentOsParams -> GenericContainer $
                              fillDict (fromContainer parentOsParams)
                                       (fromContainer childOsParams) []
       Bad _             -> childOsParams

-- | Looks up an instance's primary node.
getInstPrimaryNode :: ConfigData -> String -> ErrorResult Node
getInstPrimaryNode cfg name =
  getInstanceByExactName cfg name
  >>= withMissingParam "Instance without primary node" return . instPrimaryNode
  >>= getNodeByUuid cfg

-- | Retrieves all nodes hosting a DRBD disk
getDrbdDiskNodes :: ConfigData -> Disk -> [Node]
getDrbdDiskNodes cfg disk =
  let retrieved = case diskLogicalId disk of
                    Just (LIDDrbd8 nodeA nodeB _ _ _ _) ->
                      justOk [getNodeByUuid cfg nodeA, getNodeByUuid cfg nodeB]
                    _ -> []
  in retrieved ++ concatMap (getDrbdDiskNodes cfg) (diskChildren disk)

-- | Retrieves all the nodes of the instance.
--
-- As instances not using DRBD can be sent as a parameter as well,
-- the primary node has to be appended to the results.
getInstAllNodes :: ConfigData -> String -> ErrorResult [Node]
getInstAllNodes cfg name = do
  inst <- getInstanceByExactName cfg name
  inst_disks <- getInstDisks cfg inst
  let disk_nodes = concatMap (getDrbdDiskNodes cfg) inst_disks
  pNode <- getInstPrimaryNode cfg name
  return . nub $ pNode:disk_nodes

-- | Get disks for a given instance object.
getInstDisks :: ConfigData -> Instance -> ErrorResult [Disk]
getInstDisks cfg = mapM (getDisk cfg) . instDisks

-- | Collects a value for all DRBD disks
collectFromDrbdDisks
  :: (Monoid a)
  => (String -> String -> Int -> Int -> Int -> Private DRBDSecret -> a)
  -- ^ NodeA, NodeB, Port, MinorA, MinorB, Secret
  -> Disk -> a
collectFromDrbdDisks f = col
  where
    col (diskLogicalId &&& diskChildren ->
           (Just (LIDDrbd8 nA nB port mA mB secret), ch)) =
             f nA nB port mA mB secret <> F.foldMap col ch
    col d = F.foldMap col (diskChildren d)

-- | Returns the DRBD secrets of a given 'Disk'
getDrbdSecretsForDisk :: Disk -> [DRBDSecret]
getDrbdSecretsForDisk = collectFromDrbdDisks
                          (\_ _ _ _ _ (Private secret) -> [secret])

-- | Returns the DRBD minors of a given 'Disk'
getDrbdMinorsForDisk :: Disk -> [(Int, String)]
getDrbdMinorsForDisk =
  collectFromDrbdDisks (\nA nB _ mnA mnB _ -> [(mnA, nA), (mnB, nB)])

-- | Filters DRBD minors for a given node.
getDrbdMinorsForNode :: String -> Disk -> [(Int, String)]
getDrbdMinorsForNode node disk =
  let child_minors = concatMap (getDrbdMinorsForNode node) (diskChildren disk)
      this_minors =
        case diskLogicalId disk of
          Just (LIDDrbd8 nodeA nodeB _ minorA minorB _)
            | nodeA == node -> [(minorA, nodeB)]
            | nodeB == node -> [(minorB, nodeA)]
          _ -> []
  in this_minors ++ child_minors

-- | Returns the DRBD minors of a given instance
getDrbdMinorsForInstance :: ConfigData -> Instance
                         -> ErrorResult [(Int, String)]
getDrbdMinorsForInstance cfg =
  liftM (concatMap getDrbdMinorsForDisk) . getInstDisks cfg

-- | String for primary role.
rolePrimary :: String
rolePrimary = "primary"

-- | String for secondary role.
roleSecondary :: String
roleSecondary = "secondary"

-- | Gets the list of DRBD minors for an instance that are related to
-- a given node.
getInstMinorsForNode :: ConfigData
                     -> String -- ^ The UUID of a node.
                     -> Instance
                     -> [(String, Int, String, String, String, String)]
getInstMinorsForNode cfg node inst =
  let nrole = if Just node == instPrimaryNode inst
                then rolePrimary
                else roleSecondary
      iname = fromMaybe "" $ instName inst
      inst_disks = case getInstDisks cfg inst of
                     Ok disks -> disks
                     Bad _ -> []
  -- FIXME: the disk/ build there is hack-ish; unify this in a
  -- separate place, or reuse the iv_name (but that is deprecated on
  -- the Python side)
  in concatMap (\(idx, dsk) ->
            [(node, minor, iname, "disk/" ++ show idx, nrole, peer)
               | (minor, peer) <- getDrbdMinorsForNode node dsk]) .
     zip [(0::Int)..] $ inst_disks

-- | Builds link -> ip -> instname map.
-- For instances without a name, we insert the uuid instead.
--
-- TODO: improve this by splitting it into multiple independent functions:
--
-- * abstract the \"fetch instance with filled params\" functionality
--
-- * abstsract the [instance] -> [(nic, instance_name)] part
--
-- * etc.
buildLinkIpInstnameMap :: ConfigData -> LinkIpMap
buildLinkIpInstnameMap cfg =
  let cluster = configCluster cfg
      instances = M.elems . fromContainer . configInstances $ cfg
      defparams = (M.!) (fromContainer $ clusterNicparams cluster)
                    $ UTF8.fromString C.ppDefault
      nics = concatMap (\i -> [(fromMaybe (uuidOf i) $ instName i, nic)
                                | nic <- instNics i])
             instances
  in foldl' (\accum (iname, nic) ->
               let pparams = nicNicparams nic
                   fparams = fillParams defparams pparams
                   link = nicpLink fparams
               in case nicIp nic of
                    Nothing -> accum
                    Just ip -> let oldipmap = M.findWithDefault M.empty
                                              link accum
                                   newipmap = M.insert ip iname oldipmap
                               in M.insert link newipmap accum
            ) M.empty nics


-- | Returns a node's group, with optional failure if we can't find it
-- (configuration corrupt).
getGroupOfNode :: ConfigData -> Node -> Maybe NodeGroup
getGroupOfNode cfg node =
  M.lookup (UTF8.fromString $ nodeGroup node)
    (fromContainer . configNodegroups $ cfg)

-- | Returns a node's ndparams, filled.
getNodeNdParams :: ConfigData -> Node -> Maybe FilledNDParams
getNodeNdParams cfg node = do
  group <- getGroupOfNode cfg node
  let gparams = getGroupNdParams cfg group
  return $ fillParams gparams (nodeNdparams node)

-- * Network

-- | Looks up a network. If looking up by uuid fails, we look up
-- by name.
getNetwork :: ConfigData -> String -> ErrorResult Network
getNetwork cfg name =
  let networks = fromContainer (configNetworks cfg)
  in case getItem' "Network" name networks of
       Ok net -> Ok net
       Bad _ -> let by_name = M.mapKeys
                              (fromNonEmpty . networkName . (M.!) networks)
                              networks
                in getItem "Network" name by_name

-- ** MACs

type MAC = String

-- | Returns all MAC addresses used in the cluster.
getAllMACs :: ConfigData -> [MAC]
getAllMACs = F.foldMap (map nicMac . instNics) . configInstances

-- ** DRBD secrets

getAllDrbdSecrets :: ConfigData -> [DRBDSecret]
getAllDrbdSecrets = F.foldMap getDrbdSecretsForDisk . configDisks

-- ** LVs

-- | A map from node UUIDs to
--
-- FIXME: After adding designated types for UUIDs,
-- use them to replace 'String' here.
type NodeLVsMap = MM.MultiMap String LogicalVolume

getInstanceLVsByNode :: ConfigData -> Instance -> ErrorResult NodeLVsMap
getInstanceLVsByNode cd inst =
    withMissingParam "Instance without Primary Node"
      (\i -> return $ MM.fromList . lvsByNode i)
      (instPrimaryNode inst)
    <*> getInstDisks cd inst
  where
    lvsByNode :: String -> [Disk] -> [(String, LogicalVolume)]
    lvsByNode node = concatMap (lvsByNode1 node)
    lvsByNode1 :: String -> Disk -> [(String, LogicalVolume)]
    lvsByNode1 _ (diskLogicalId &&& diskChildren
                   -> (Just (LIDDrbd8 nA nB _ _ _ _), ch)) =
                         lvsByNode nA ch ++ lvsByNode nB ch
    lvsByNode1 node (diskLogicalId -> (Just (LIDPlain lv))) =
                         [(node, lv)]
    lvsByNode1 node (diskChildren -> ch) = lvsByNode node ch

getAllLVs :: ConfigData -> ErrorResult (S.Set LogicalVolume)
getAllLVs cd = mconcat <$> mapM (liftM MM.values . getInstanceLVsByNode cd)
                                (F.toList $ configInstances cd)

-- * ND params

-- | Type class denoting objects which have node parameters.
class NdParamObject a where
  getNdParamsOf :: ConfigData -> a -> Maybe FilledNDParams

instance NdParamObject Node where
  getNdParamsOf = getNodeNdParams

instance NdParamObject NodeGroup where
  getNdParamsOf cfg = Just . getGroupNdParams cfg

instance NdParamObject Cluster where
  getNdParamsOf _ = Just . clusterNdparams
