Commit 10ef6b4e authored by Iustin Pop's avatar Iustin Pop
Browse files

Change the Node.group attribute



Currently, the Node.group attribute is the UUID of the group, as until
recently Ganeti didn't export the node group properties. Since it does
so now, we make the following changes (again apologies for a big
patch):

- we change the group attribute to be an index, similar to the way an
  Instance.pnode and snode attributes point to the parent node(s)
- on load, we read the group.uuid attribute and we use that to lookup
  the actual group index, from previously-loaded groups info
- this means that we now first read groups, then read nodes using the
  group info, and then read instances using the node info

This patch leaves a few functions showing the group index (ugly since
it's htools internal), will be converted later.
Signed-off-by: default avatarIustin Pop <iustin@google.com>
Reviewed-by: default avatarBalazs Lecz <leczb@google.com>
parent a679e9dc
......@@ -625,16 +625,16 @@ tryAlloc _ _ _ reqn = fail $ "Unsupported number of allocation \
"), only two supported"
-- | Given a group/result, describe it as a nice (list of) messages
solutionDescription :: (GroupID, Result AllocSolution) -> [String]
solutionDescription :: (Gdx, Result AllocSolution) -> [String]
solutionDescription (groupId, result) =
case result of
Ok solution -> map (printf "Group %s: %s" groupId) (asLog solution)
Bad message -> [printf "Group %s: error %s" groupId message]
Ok solution -> map (printf "Group %d: %s" groupId) (asLog solution)
Bad message -> [printf "Group %d: error %s" groupId message]
-- | From a list of possibly bad and possibly empty solutions, filter
-- only the groups with a valid result
filterMGResults :: [(GroupID, Result AllocSolution)] ->
[(GroupID, AllocSolution)]
filterMGResults :: [(Gdx, Result AllocSolution)] ->
[(Gdx, AllocSolution)]
filterMGResults =
filter (not . null . asSolutions . snd) .
map (\(y, Ok x) -> (y, x)) .
......@@ -651,7 +651,7 @@ tryMGAlloc mgnl mgil inst cnt =
-- TODO: currently we consider all groups preferred
sols = map (\(gid, (nl, il)) ->
(gid, tryAlloc nl il inst cnt)) groups::
[(GroupID, Result AllocSolution)]
[(Gdx, Result AllocSolution)]
all_msgs = concatMap solutionDescription sols
goodSols = filterMGResults sols
extractScore = \(_, _, _, x) -> x
......@@ -660,7 +660,7 @@ tryMGAlloc mgnl mgil inst cnt =
in if null sortedSols
then Bad $ intercalate ", " all_msgs
else let (final_group, final_sol) = head sortedSols
selmsg = "Selected group: " ++ final_group
selmsg = "Selected group: " ++ show final_group
in Ok $ final_sol { asLog = selmsg:all_msgs }
-- | Try to relocate an instance on the cluster.
......@@ -925,19 +925,19 @@ iMoveToJob nl il idx move =
FailoverAndReplace ns -> [ opF, opR ns ]
-- | Computes the group of an instance
instanceGroup :: Node.List -> Instance.Instance -> Result GroupID
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
puuid = Node.group pnode
suuid = Node.group snode
in if puuid /= suuid
then fail ("Instance placed accross two node groups, primary " ++ puuid ++
", secondary " ++ suuid)
else return puuid
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)
......@@ -947,7 +947,7 @@ findSplitInstances nl il =
-- | Splits a cluster into the component node groups
splitCluster :: Node.List -> Instance.List ->
[(GroupID, (Node.List, Instance.List))]
[(Gdx, (Node.List, Instance.List))]
splitCluster nl il =
let ngroups = Node.computeGroups (Container.elems nl)
in map (\(guuid, nodes) ->
......
......@@ -75,15 +75,17 @@ parseInstance ktn n a = do
return (n, Instance.setBoth (snd base) pidx sidx)
-- | Parses a node as found in the cluster node list.
parseNode :: String -- ^ The node's name
parseNode :: NameAssoc -- ^ The group association
-> String -- ^ The node's name
-> [(String, JSValue)] -- ^ The JSON object
-> Result (String, Node.Node)
parseNode n a = do
parseNode ktg n a = do
offline <- fromObj "offline" a
drained <- fromObj "drained" a
guuid <- fromObj "group" a
gidx <- lookupGroup ktg n guuid
node <- (if offline || drained
then return $ Node.create n 0 0 0 0 0 0 True guuid
then return $ Node.create n 0 0 0 0 0 0 True gidx
else do
mtotal <- fromObj "total_memory" a
mnode <- fromObj "reserved_memory" a
......@@ -92,7 +94,7 @@ parseNode n a = do
dfree <- fromObj "free_disk" a
ctotal <- fromObj "total_cpus" a
return $ Node.create n mtotal mnode mfree
dtotal dfree ctotal False guuid)
dtotal dfree ctotal False gidx)
return (n, node)
-- | Parses a group as found in the cluster group list.
......@@ -114,10 +116,11 @@ parseData body = do
-- existing group parsing
glist <- liftM fromJSObject (fromObj "nodegroups" obj)
gobj <- mapM (\(x, y) -> asJSObject y >>= parseGroup x . fromJSObject) glist
let (_, gl) = assignIndices gobj
let (ktg, gl) = assignIndices gobj
-- existing node parsing
nlist <- liftM fromJSObject (fromObj "nodes" obj)
nobj <- mapM (\(x,y) -> asJSObject y >>= parseNode x . fromJSObject) nlist
nobj <- mapM (\(x,y) ->
asJSObject y >>= parseNode ktg x . fromJSObject) nlist
let (ktn, nl) = assignIndices nobj
-- existing instance parsing
ilist <- fromObj "instances" obj
......
......@@ -120,12 +120,12 @@ parseInstance ktn (JSArray [ name, disk, mem, vcpus
parseInstance _ v = fail ("Invalid instance query result: " ++ show v)
-- | Parse a node list in JSON format.
getNodes :: JSValue -> Result [(String, Node.Node)]
getNodes arr = toArray arr >>= mapM parseNode
getNodes :: NameAssoc -> JSValue -> Result [(String, Node.Node)]
getNodes ktg arr = toArray arr >>= mapM (parseNode ktg)
-- | Construct a node from a JSON object.
parseNode :: JSValue -> Result (String, Node.Node)
parseNode (JSArray [ name, mtotal, mnode, mfree, dtotal, dfree
parseNode :: NameAssoc -> JSValue -> Result (String, Node.Node)
parseNode ktg (JSArray [ name, mtotal, mnode, mfree, dtotal, dfree
, ctotal, offline, drained, vm_capable, g_uuid ])
= do
xname <- annotateResult "Parsing new node" (fromJVal name)
......@@ -133,9 +133,9 @@ parseNode (JSArray [ name, mtotal, mnode, mfree, dtotal, dfree
xoffline <- convert offline
xdrained <- convert drained
xvm_capable <- convert vm_capable
xguuid <- convert g_uuid
xgdx <- convert g_uuid >>= lookupGroup ktg xname
node <- (if xoffline || xdrained || not xvm_capable
then return $ Node.create xname 0 0 0 0 0 0 True xguuid
then return $ Node.create xname 0 0 0 0 0 0 True xgdx
else do
xmtotal <- convert mtotal
xmnode <- convert mnode
......@@ -144,10 +144,10 @@ parseNode (JSArray [ name, mtotal, mnode, mfree, dtotal, dfree
xdfree <- convert dfree
xctotal <- convert ctotal
return $ Node.create xname xmtotal xmnode xmfree
xdtotal xdfree xctotal False xguuid)
xdtotal xdfree xctotal False xgdx)
return (xname, node)
parseNode v = fail ("Invalid node query result: " ++ show v)
parseNode _ v = fail ("Invalid node query result: " ++ show v)
getClusterTags :: JSValue -> Result [String]
getClusterTags v = do
......@@ -188,8 +188,8 @@ parseData :: (Result JSValue, Result JSValue, Result JSValue, Result JSValue)
-> Result (Group.List, Node.List, Instance.List, [String])
parseData (groups, nodes, instances, cinfo) = do
group_data <- groups >>= getGroups
let (_, group_idx) = assignIndices group_data
node_data <- nodes >>= getNodes
let (group_names, group_idx) = assignIndices group_data
node_data <- nodes >>= getNodes group_names
let (node_names, node_idx) = assignIndices node_data
inst_data <- instances >>= getInstances node_names
let (_, inst_idx) = assignIndices inst_data
......
......@@ -121,7 +121,7 @@ data Node = Node
, utilPool :: T.DynUtil -- ^ Total utilisation capacity
, utilLoad :: T.DynUtil -- ^ Sum of instance utilisation
, pTags :: TagMap -- ^ Map of primary instance tags and their count
, group :: T.GroupID -- ^ The node's group (as UUID)
, group :: T.Gdx -- ^ The node's group (index)
} deriving (Show, Eq)
instance T.Element Node where
......@@ -185,7 +185,7 @@ conflictingPrimaries (Node { pTags = t }) = Foldable.sum t - Map.size t
-- The index and the peers maps are empty, and will be need to be
-- update later via the 'setIdx' and 'buildPeers' functions.
create :: String -> Double -> Int -> Int -> Double
-> Int -> Double -> Bool -> String -> Node
-> Int -> Double -> Bool -> T.Gdx -> Node
create name_init mem_t_init mem_n_init mem_f_init
dsk_t_init dsk_f_init cpu_t_init offline_init group_init =
Node { name = name_init
......@@ -487,7 +487,6 @@ showField t field =
"ptags" -> intercalate "," . map (\(k, v) -> printf "%s=%d" k v) .
Map.toList $ pTags t
"peermap" -> show $ peers t
"group.uuid" -> group t
_ -> T.unknownField
where
T.DynUtil { T.cpuWeight = uC, T.memWeight = uM,
......@@ -526,7 +525,7 @@ showHeader field =
"nload" -> ("lNet", True)
"ptags" -> ("PrimaryTags", False)
"peermap" -> ("PeerMap", False)
"group.uuid" -> ("GroupUUID", False)
-- TODO: add node fields (group.uuid, group)
_ -> (T.unknownField, False)
-- | String converter for the node list functionality.
......@@ -543,7 +542,7 @@ defaultFields =
-- | Split a list of nodes into a list of (node group UUID, list of
-- associated nodes)
computeGroups :: [Node] -> [(T.GroupID, [Node])]
computeGroups :: [Node] -> [(T.Gdx, [Node])]
computeGroups nodes =
let nodes' = sortBy (comparing group) nodes
nodes'' = groupBy (\a b -> group a == group b) nodes'
......
......@@ -56,6 +56,7 @@ import qualified Ganeti.HTools.Instance as Instance
import qualified Ganeti.HTools.Loader as Loader
import qualified Ganeti.HTools.Luxi
import qualified Ganeti.HTools.Node as Node
import qualified Ganeti.HTools.Group as Group
import qualified Ganeti.HTools.PeerMap as PeerMap
import qualified Ganeti.HTools.Rapi
import qualified Ganeti.HTools.Simu
......@@ -78,6 +79,17 @@ maxDsk = 1024 * 1024 * 8
maxCpu :: Int
maxCpu = 1024
defGroup :: Group.Group
defGroup = flip Group.setIdx 0 $
Group.create "default" Utils.defaultGroupID
Types.AllocPreferred
defGroupList :: Group.List
defGroupList = Container.fromAssocList [(Group.idx defGroup, defGroup)]
defGroupAssoc :: Data.Map.Map String Types.Gdx
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
-- * Helper functions
-- | Simple checker for whether OpResult is fail or pass
......@@ -187,7 +199,7 @@ instance Arbitrary Node.Node where
offl <- arbitrary
let n = Node.create name (fromIntegral mem_t) mem_n mem_f
(fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl
Utils.defaultGroupID
0
n' = Node.buildPeers n Container.empty
return n'
......@@ -483,7 +495,9 @@ prop_Text_Load_Node name tm nm fm td fd tc fo =
then "Y"
else "N"
any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
in case Text.loadNode [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s] of
gid = Group.uuid defGroup
in case Text.loadNode defGroupAssoc
[name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
Nothing -> False
Just (name', node) ->
if fo || any_broken
......@@ -498,11 +512,11 @@ prop_Text_Load_Node name tm nm fm td fd tc fo =
Node.tCpu node == fromIntegral tc
prop_Text_Load_NodeFail fields =
length fields /= 8 ==> isNothing $ Text.loadNode fields
length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
prop_Text_NodeLSIdempotent node =
(Text.loadNode .
Utils.sepSplit '|' . Text.serializeNode) n ==
(Text.loadNode defGroupAssoc.
Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
Just (Node.name n, n)
-- override failN1 to what loadNode returns by default
where n = node { Node.failN1 = True, Node.offline = False }
......@@ -750,7 +764,7 @@ prop_ClusterAllocBalance node =
prop_ClusterCheckConsistency node inst =
let nl = makeSmallCluster node 3
[node1, node2, node3] = Container.elems nl
node3' = node3 { Node.group = "other-uuid" }
node3' = node3 { Node.group = 1 }
nl' = Container.add (Node.idx node3') node3' nl
inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
......
......@@ -71,9 +71,9 @@ getInstances ktn body =
mapM (parseInstance ktn . fromJSObject)
-- | Parse a node list in JSON format.
getNodes :: String -> Result [(String, Node.Node)]
getNodes body = loadJSArray "Parsing node data" body >>=
mapM (parseNode . fromJSObject)
getNodes :: NameAssoc -> String -> Result [(String, Node.Node)]
getNodes ktg body = loadJSArray "Parsing node data" body >>=
mapM (parseNode ktg . fromJSObject)
-- | Parse a group list in JSON format.
getGroups :: String -> Result [(String, Group.Group)]
......@@ -105,13 +105,13 @@ parseInstance ktn a = do
return (name, inst)
-- | Construct a node from a JSON object.
parseNode :: [(String, JSValue)] -> Result (String, Node.Node)
parseNode a = do
parseNode :: NameAssoc -> [(String, JSValue)] -> Result (String, Node.Node)
parseNode ktg a = do
name <- tryFromObj "Parsing new node" a "name"
let extract s = tryFromObj ("Node '" ++ name ++ "'") a s
offline <- extract "offline"
drained <- extract "drained"
guuid <- extract "group.uuid"
guuid <- extract "group.uuid" >>= lookupGroup ktg name
node <- (if offline || drained
then return $ Node.create name 0 0 0 0 0 0 True guuid
else do
......@@ -149,8 +149,8 @@ parseData :: (Result String, Result String, Result String, Result String)
-> Result (Group.List, Node.List, Instance.List, [String])
parseData (group_body, node_body, inst_body, tags_body) = do
group_data <- group_body >>= getGroups
let (_, group_idx) = assignIndices group_data
node_data <- node_body >>= getNodes
let (group_names, group_idx) = assignIndices group_data
node_data <- node_body >>= getNodes group_names
let (node_names, node_idx) = assignIndices node_data
inst_data <- inst_body >>= getInstances node_names
let (_, inst_idx) = assignIndices inst_data
......
......@@ -62,7 +62,7 @@ parseData ndata = do
let n = Node.create (printf "node%03d" idx)
(fromIntegral mem) 0 mem
(fromIntegral disk) disk
(fromIntegral cpu) False defaultGroupID
(fromIntegral cpu) False 0
in (idx, Node.setIdx n idx)
) [1..cnt]
return (Container.fromAssocList [(0, Group.setIdx defgroup 0)],
......
......@@ -52,17 +52,18 @@ import qualified Ganeti.HTools.Node as Node
import qualified Ganeti.HTools.Instance as Instance
-- | Serialize a single node
serializeNode :: Node.Node -> String
serializeNode node =
serializeNode :: Group.List -> Node.Node -> String
serializeNode gl node =
printf "%s|%.0f|%d|%d|%.0f|%d|%.0f|%c|%s" (Node.name node)
(Node.tMem node) (Node.nMem node) (Node.fMem node)
(Node.tDsk node) (Node.fDsk node) (Node.tCpu node)
(if Node.offline node then 'Y' else 'N')
(Node.group node)
(Group.uuid grp)
where grp = Container.find (Node.group node) gl
-- | Generate node file data from node objects
serializeNodes :: Node.List -> String
serializeNodes = unlines . map serializeNode . Container.elems
serializeNodes :: Group.List -> Node.List -> String
serializeNodes gl = unlines . map (serializeNode gl) . Container.elems
-- | Serialize a single instance
serializeInstance :: Node.List -> Instance.Instance -> String
......@@ -86,9 +87,9 @@ serializeInstances nl =
unlines . map (serializeInstance nl) . Container.elems
-- | Generate complete cluster data from node and instance lists
serializeCluster :: Node.List -> Instance.List -> String
serializeCluster nl il =
let ndata = serializeNodes nl
serializeCluster :: Group.List -> Node.List -> Instance.List -> String
serializeCluster gl nl il =
let ndata = serializeNodes gl nl
idata = serializeInstances nl il
in ndata ++ ['\n'] ++ idata
......@@ -100,14 +101,12 @@ loadGroup [name, gid] =
loadGroup s = fail $ "Invalid/incomplete group data: '" ++ show s ++ "'"
-- | Load a node from a field list.
loadNode :: (Monad m) => [String] -> m (String, Node.Node)
-- compatibility wrapper for old text files
loadNode [name, tm, nm, fm, td, fd, tc, fo] =
loadNode [name, tm, nm, fm, td, fd, tc, fo, defaultGroupID]
loadNode [name, tm, nm, fm, td, fd, tc, fo, gu] = do
loadNode :: (Monad m) => NameAssoc -> [String] -> m (String, Node.Node)
loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu] = do
gdx <- lookupGroup ktg name gu
new_node <-
if any (== "?") [tm,nm,fm,td,fd,tc] || fo == "Y" then
return $ Node.create name 0 0 0 0 0 0 True gu
return $ Node.create name 0 0 0 0 0 0 True gdx
else do
vtm <- tryRead name tm
vnm <- tryRead name nm
......@@ -115,9 +114,9 @@ loadNode [name, tm, nm, fm, td, fd, tc, fo, gu] = do
vtd <- tryRead name td
vfd <- tryRead name fd
vtc <- tryRead name tc
return $ Node.create name vtm vnm vfm vtd vfd vtc False gu
return $ Node.create name vtm vnm vfm vtd vfd vtc False gdx
return (name, new_node)
loadNode s = fail $ "Invalid/incomplete node data: '" ++ show s ++ "'"
loadNode _ s = fail $ "Invalid/incomplete node data: '" ++ show s ++ "'"
-- | Load an instance from a field list.
loadInst :: (Monad m) =>
......@@ -168,9 +167,9 @@ parseData fdata = do
[] -> Bad "Invalid format of the input file (no instance data)"
_:xs -> Ok xs
{- group file: name uuid -}
(_, gl) <- loadTabular glines loadGroup
(ktg, gl) <- loadTabular glines loadGroup
{- node file: name t_mem n_mem f_mem t_disk f_disk -}
(ktn, nl) <- loadTabular nfixed loadNode
(ktn, nl) <- loadTabular nfixed (loadNode ktg)
{- instance file: name mem disk status pnode snode -}
(_, il) <- loadTabular ifixed (loadInst ktn)
return (gl, nl, il, [])
......
......@@ -41,6 +41,7 @@ import Text.Printf (printf, hPrintf)
import qualified Ganeti.HTools.Container as Container
import qualified Ganeti.HTools.Cluster as Cluster
import qualified Ganeti.HTools.Group as Group
import qualified Ganeti.HTools.Node as Node
import qualified Ganeti.HTools.Instance as Instance
......@@ -226,7 +227,7 @@ main = do
verbose = optVerbose opts
shownodes = optShowNodes opts
(_, fixed_nl, ilf, ctags) <- loadExternalData opts
(gl, fixed_nl, ilf, ctags) <- loadExternalData opts
let offline_names = optOffline opts
all_nodes = Container.elems fixed_nl
......@@ -270,7 +271,7 @@ main = do
let ngroups = Cluster.splitCluster nlf ilf
when (length ngroups > 1 && isNothing (optGroup opts)) $ do
hPutStrLn stderr "Found multiple node groups:"
mapM_ (hPutStrLn stderr . (" " ++) . fst ) ngroups
mapM_ (hPutStrLn stderr . (" " ++) . show . fst ) ngroups
hPutStrLn stderr "Aborting."
exitWith $ ExitFailure 1
......@@ -278,22 +279,32 @@ main = do
(Container.size nlf)
(Container.size ilf)
(guuid, (nl, il)) <- case optGroup opts of
Nothing -> return $ head ngroups
Just g -> case lookup g ngroups of
(gname, (nl, il)) <- case optGroup opts of
Nothing -> do
let (gidx, cdata) = head ngroups
grp = Container.find gidx gl
return (Group.name grp, cdata)
Just g -> case Container.findByName gl g of
Nothing -> do
hPutStrLn stderr $ "Node group " ++ g ++
" not found. Node group list is:"
mapM_ (hPutStrLn stderr . (" " ++) . fst ) ngroups
mapM_ (hPutStrLn stderr . (" " ++) . Group.name ) (Container.elems gl)
hPutStrLn stderr "Aborting."
exitWith $ ExitFailure 1
Just cdata -> return (g, cdata)
Just grp ->
case lookup (Group.idx grp) ngroups of
Nothing -> do
-- TODO: while this is unlikely to happen, log here the
-- actual group data to help debugging
hPutStrLn stderr $ "Internal failure, missing group idx"
exitWith $ ExitFailure 1
Just cdata -> return (Group.name grp, cdata)
unless oneline $ printf "Group size %d nodes, %d instances\n"
(Container.size nl)
(Container.size il)
putStrLn $ "Selected node group: " ++ guuid
putStrLn $ "Selected node group: " ++ gname
when (length csf > 0 && not oneline && verbose > 1) $
printf "Note: Stripping common suffix of '%s' from names\n" csf
......@@ -379,7 +390,7 @@ main = do
when (isJust $ optSaveCluster opts) $
do
let out_path = fromJust $ optSaveCluster opts
adata = serializeCluster fin_nl fin_il
adata = serializeCluster gl fin_nl fin_il
writeFile out_path adata
printf "The cluster state has been written to file '%s'\n" out_path
......
......@@ -95,7 +95,7 @@ processData :: Result (Group.List, Node.List, Instance.List, [String])
processData input_data = do
(gl, nl, il, _) <- input_data >>= Loader.mergeData [] [] []
let (_, fix_nl) = Loader.checkData nl il
adata = serializeCluster nl il
adata = serializeCluster gl nl il
return (gl, fix_nl, il, adata)
-- | Writes cluster data out
......
......@@ -207,7 +207,7 @@ main = do
ispec = optISpec opts
shownodes = optShowNodes opts
(_, fixed_nl, il, _) <- loadExternalData opts
(gl, fixed_nl, il, _) <- loadExternalData opts
printKeys $ map (\(a, fn) -> ("SPEC_" ++ a, fn ispec)) specData
printKeys [ ("SPEC_RQN", printf "%d" (optINodes opts)) ]
......@@ -312,7 +312,7 @@ main = do
when (isJust $ optSaveCluster opts) $
do
let out_path = (fromJust $ optSaveCluster opts) <.> "tiered"
adata = serializeCluster trl_nl trl_il
adata = serializeCluster gl trl_nl trl_il
writeFile out_path adata
hPrintf stderr "The cluster state after tiered allocation\
\ has been written to file '%s'\n"
......@@ -346,7 +346,7 @@ main = do
when (isJust $ optSaveCluster opts) $
do
let out_path = (fromJust $ optSaveCluster opts) <.> "alloc"
adata = serializeCluster fin_nl fin_il
adata = serializeCluster gl fin_nl fin_il
writeFile out_path adata
hPrintf stderr "The cluster state after standard allocation\
\ has been written to file '%s'\n"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment