Revision 525bfb36
b/htools/Ganeti/HTools/CLI.hs | ||
---|---|---|
1 | 1 |
{-| Implementation of command-line functions. |
2 | 2 |
|
3 |
This module holds the common cli-related functions for the binaries,
|
|
4 |
separated into this module since Utils.hs is used in many other places
|
|
5 |
and this is more IO oriented. |
|
3 |
This module holds the common command-line related functions for the
|
|
4 |
binaries, separated into this module since "Ganeti.HTools.Utils" is
|
|
5 |
used in many other places and this is more IO oriented.
|
|
6 | 6 |
|
7 | 7 |
-} |
8 | 8 |
|
... | ... | |
89 | 89 |
import Ganeti.HTools.Types |
90 | 90 |
import Ganeti.HTools.Utils |
91 | 91 |
|
92 |
-- | The default value for the luxi socket |
|
92 |
-- * Constants |
|
93 |
|
|
94 |
-- | The default value for the luxi socket. |
|
95 |
-- |
|
96 |
-- This is re-exported from the "Ganeti.Constants" module. |
|
93 | 97 |
defaultLuxiSocket :: FilePath |
94 | 98 |
defaultLuxiSocket = C.masterSocket |
95 | 99 |
|
100 |
-- * Data types |
|
101 |
|
|
96 | 102 |
-- | Command line options structure. |
97 | 103 |
data Options = Options |
98 | 104 |
{ optDataFile :: Maybe FilePath -- ^ Path to the cluster data file |
... | ... | |
170 | 176 |
, optVerbose = 1 |
171 | 177 |
} |
172 | 178 |
|
173 |
-- | Abrreviation for the option type |
|
179 |
-- | Abrreviation for the option type.
|
|
174 | 180 |
type OptType = OptDescr (Options -> Result Options) |
175 | 181 |
|
182 |
-- * Command line options |
|
183 |
|
|
176 | 184 |
oDataFile :: OptType |
177 | 185 |
oDataFile = Option "t" ["text-data"] |
178 | 186 |
(ReqArg (\ f o -> Ok o { optDataFile = Just f }) "FILE") |
... | ... | |
394 | 402 |
(NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 })) |
395 | 403 |
"increase the verbosity level" |
396 | 404 |
|
397 |
-- | Usage info |
|
405 |
-- * Functions |
|
406 |
|
|
407 |
-- | Usage info. |
|
398 | 408 |
usageHelp :: String -> [OptType] -> String |
399 | 409 |
usageHelp progname = |
400 | 410 |
usageInfo (printf "%s %s\nUsage: %s [OPTION...]" |
401 | 411 |
progname Version.version progname) |
402 | 412 |
|
403 |
-- | Command line parser, using the 'options' structure.
|
|
413 |
-- | Command line parser, using the 'Options' structure.
|
|
404 | 414 |
parseOpts :: [String] -- ^ The command line arguments |
405 | 415 |
-> String -- ^ The program name |
406 | 416 |
-> [OptType] -- ^ The supported command line options |
b/htools/Ganeti/HTools/Cluster.hs | ||
---|---|---|
1 | 1 |
{-| Implementation of cluster-wide logic. |
2 | 2 |
|
3 | 3 |
This module holds all pure cluster-logic; I\/O related functionality |
4 |
goes into the "Main" module for the individual binaries.
|
|
4 |
goes into the /Main/ module for the individual binaries.
|
|
5 | 5 |
|
6 | 6 |
-} |
7 | 7 |
|
... | ... | |
106 | 106 |
|
107 | 107 |
|
108 | 108 |
-- | A type denoting the valid allocation mode/pairs. |
109 |
-- |
|
109 | 110 |
-- For a one-node allocation, this will be a @Left ['Node.Node']@, |
110 | 111 |
-- whereas for a two-node allocation, this will be a @Right |
111 | 112 |
-- [('Node.Node', 'Node.Node')]@. |
112 | 113 |
type AllocNodes = Either [Ndx] [(Ndx, Ndx)] |
113 | 114 |
|
114 |
-- | The empty solution we start with when computing allocations |
|
115 |
-- | The empty solution we start with when computing allocations.
|
|
115 | 116 |
emptySolution :: AllocSolution |
116 | 117 |
emptySolution = AllocSolution { asFailures = [], asAllocs = 0 |
117 | 118 |
, asSolutions = [], asLog = [] } |
118 | 119 |
|
119 |
-- | The complete state for the balancing solution |
|
120 |
-- | The complete state for the balancing solution.
|
|
120 | 121 |
data Table = Table Node.List Instance.List Score [Placement] |
121 | 122 |
deriving (Show, Read) |
122 | 123 |
|
... | ... | |
144 | 145 |
} |
145 | 146 |
deriving (Show, Read) |
146 | 147 |
|
147 |
-- | Currently used, possibly to allocate, unallocable |
|
148 |
-- | Currently used, possibly to allocate, unallocable.
|
|
148 | 149 |
type AllocStats = (RSpec, RSpec, RSpec) |
149 | 150 |
|
150 | 151 |
-- * Utility functions |
... | ... | |
170 | 171 |
in |
171 | 172 |
(bad_nodes, bad_instances) |
172 | 173 |
|
173 |
-- | Zero-initializer for the CStats type |
|
174 |
-- | Zero-initializer for the CStats type.
|
|
174 | 175 |
emptyCStats :: CStats |
175 | 176 |
emptyCStats = CStats 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 |
176 | 177 |
|
177 |
-- | Update stats with data from a new node |
|
178 |
-- | Update stats with data from a new node.
|
|
178 | 179 |
updateCStats :: CStats -> Node.Node -> CStats |
179 | 180 |
updateCStats cs node = |
180 | 181 |
let CStats { csFmem = x_fmem, csFdsk = x_fdsk, |
... | ... | |
243 | 244 |
(truncate t_dsk - fromIntegral f_idsk) |
244 | 245 |
in (rini, rfin, runa) |
245 | 246 |
|
246 |
-- | The names and weights of the individual elements in the CV list |
|
247 |
-- | The names and weights of the individual elements in the CV list.
|
|
247 | 248 |
detailedCVInfo :: [(Double, String)] |
248 | 249 |
detailedCVInfo = [ (1, "free_mem_cv") |
249 | 250 |
, (1, "free_disk_cv") |
... | ... | |
311 | 312 |
compCV :: Node.List -> Double |
312 | 313 |
compCV = sum . zipWith (*) detailedCVWeights . compDetailedCV |
313 | 314 |
|
314 |
-- | Compute online nodes from a Node.List
|
|
315 |
-- | Compute online nodes from a 'Node.List'.
|
|
315 | 316 |
getOnline :: Node.List -> [Node.Node] |
316 | 317 |
getOnline = filter (not . Node.offline) . Container.elems |
317 | 318 |
|
318 |
-- * hbal functions
|
|
319 |
-- * Balancing functions
|
|
319 | 320 |
|
320 | 321 |
-- | Compute best table. Note that the ordering of the arguments is important. |
321 | 322 |
compareTables :: Table -> Table -> Table |
... | ... | |
534 | 535 |
then ini_tbl -- no advancement |
535 | 536 |
else best_tbl |
536 | 537 |
|
537 |
-- | Check if we are allowed to go deeper in the balancing |
|
538 |
-- | Check if we are allowed to go deeper in the balancing.
|
|
538 | 539 |
doNextBalance :: Table -- ^ The starting table |
539 | 540 |
-> Int -- ^ Remaining length |
540 | 541 |
-> Score -- ^ Score at which to stop |
... | ... | |
544 | 545 |
ini_plc_len = length ini_plc |
545 | 546 |
in (max_rounds < 0 || ini_plc_len < max_rounds) && ini_cv > min_score |
546 | 547 |
|
547 |
-- | Run a balance move |
|
548 |
-- | Run a balance move.
|
|
548 | 549 |
tryBalance :: Table -- ^ The starting table |
549 | 550 |
-> Bool -- ^ Allow disk moves |
550 | 551 |
-> Bool -- ^ Allow instance moves |
... | ... | |
574 | 575 |
|
575 | 576 |
-- * Allocation functions |
576 | 577 |
|
577 |
-- | Build failure stats out of a list of failures |
|
578 |
-- | Build failure stats out of a list of failures.
|
|
578 | 579 |
collapseFailures :: [FailMode] -> FailStats |
579 | 580 |
collapseFailures flst = |
580 | 581 |
map (\k -> (k, length $ filter (k ==) flst)) [minBound..maxBound] |
581 | 582 |
|
582 | 583 |
-- | Update current Allocation solution and failure stats with new |
583 |
-- elements |
|
584 |
-- elements.
|
|
584 | 585 |
concatAllocs :: AllocSolution -> OpResult Node.AllocElement -> AllocSolution |
585 | 586 |
concatAllocs as (OpFail reason) = as { asFailures = reason : asFailures as } |
586 | 587 |
|
... | ... | |
611 | 612 |
sumAllocs (AllocSolution af aa as al) (AllocSolution bf ba bs bl) = |
612 | 613 |
AllocSolution (af ++ bf) (aa + ba) (as ++ bs) (al ++ bl) |
613 | 614 |
|
614 |
-- | Given a solution, generates a reasonable description for it |
|
615 |
-- | Given a solution, generates a reasonable description for it.
|
|
615 | 616 |
describeSolution :: AllocSolution -> String |
616 | 617 |
describeSolution as = |
617 | 618 |
let fcnt = asFailures as |
... | ... | |
629 | 630 |
" for node(s) %s") cv (asAllocs as) (length fcnt) freasons |
630 | 631 |
(intercalate "/" . map Node.name $ nodes) |
631 | 632 |
|
632 |
-- | Annotates a solution with the appropriate string |
|
633 |
-- | Annotates a solution with the appropriate string.
|
|
633 | 634 |
annotateSolution :: AllocSolution -> AllocSolution |
634 | 635 |
annotateSolution as = as { asLog = describeSolution as : asLog as } |
635 | 636 |
|
... | ... | |
678 | 679 |
then fail "No online nodes" |
679 | 680 |
else return $ annotateSolution sols |
680 | 681 |
|
681 |
-- | Given a group/result, describe it as a nice (list of) messages |
|
682 |
-- | Given a group/result, describe it as a nice (list of) messages.
|
|
682 | 683 |
solutionDescription :: Group.List -> (Gdx, Result AllocSolution) -> [String] |
683 | 684 |
solutionDescription gl (groupId, result) = |
684 | 685 |
case result of |
... | ... | |
690 | 691 |
|
691 | 692 |
-- | From a list of possibly bad and possibly empty solutions, filter |
692 | 693 |
-- only the groups with a valid result. Note that the result will be |
693 |
-- reversed compared to the original list |
|
694 |
-- reversed compared to the original list.
|
|
694 | 695 |
filterMGResults :: Group.List |
695 | 696 |
-> [(Gdx, Result AllocSolution)] |
696 | 697 |
-> [(Gdx, AllocSolution)] |
... | ... | |
703 | 704 |
| unallocable gdx -> accu |
704 | 705 |
| otherwise -> (gdx, sol):accu |
705 | 706 |
|
706 |
-- | Sort multigroup results based on policy and score |
|
707 |
-- | Sort multigroup results based on policy and score.
|
|
707 | 708 |
sortMGResults :: Group.List |
708 | 709 |
-> [(Gdx, AllocSolution)] |
709 | 710 |
-> [(Gdx, AllocSolution)] |
... | ... | |
782 | 783 |
Just v -> return v |
783 | 784 |
tryReloc nl il xid ncount ex_ndx |
784 | 785 |
|
785 |
-- | Change an instance's secondary node |
|
786 |
-- | Change an instance's secondary node.
|
|
786 | 787 |
evacInstance :: (Monad m) => |
787 | 788 |
[Ndx] -- ^ Excluded nodes |
788 | 789 |
-> Instance.List -- ^ The current instance list |
... | ... | |
854 | 855 |
let sol = foldl' sumAllocs emptySolution results |
855 | 856 |
return $ annotateSolution sol |
856 | 857 |
|
857 |
-- | Recursively place instances on the cluster until we're out of space |
|
858 |
-- | Recursively place instances on the cluster until we're out of space.
|
|
858 | 859 |
iterateAlloc :: Node.List |
859 | 860 |
-> Instance.List |
860 | 861 |
-> Instance.Instance |
... | ... | |
879 | 880 |
_ -> Bad "Internal error: multiple solutions for single\ |
880 | 881 |
\ allocation" |
881 | 882 |
|
882 |
-- | The core of the tiered allocation mode |
|
883 |
-- | The core of the tiered allocation mode.
|
|
883 | 884 |
tieredAlloc :: Node.List |
884 | 885 |
-> Instance.List |
885 | 886 |
-> Instance.Instance |
... | ... | |
1072 | 1073 |
|
1073 | 1074 |
-- * Node group functions |
1074 | 1075 |
|
1075 |
-- | Computes the group of an instance |
|
1076 |
-- | Computes the group of an instance.
|
|
1076 | 1077 |
instanceGroup :: Node.List -> Instance.Instance -> Result Gdx |
1077 | 1078 |
instanceGroup nl i = |
1078 | 1079 |
let sidx = Instance.sNode i |
... | ... | |
1087 | 1088 |
show pgroup ++ ", secondary " ++ show sgroup) |
1088 | 1089 |
else return pgroup |
1089 | 1090 |
|
1090 |
-- | Computes the group of an instance per the primary node |
|
1091 |
-- | Computes the group of an instance per the primary node.
|
|
1091 | 1092 |
instancePriGroup :: Node.List -> Instance.Instance -> Gdx |
1092 | 1093 |
instancePriGroup nl i = |
1093 | 1094 |
let pnode = Container.find (Instance.pNode i) nl |
1094 | 1095 |
in Node.group pnode |
1095 | 1096 |
|
1096 | 1097 |
-- | Compute the list of badly allocated instances (split across node |
1097 |
-- groups) |
|
1098 |
-- groups).
|
|
1098 | 1099 |
findSplitInstances :: Node.List -> Instance.List -> [Instance.Instance] |
1099 | 1100 |
findSplitInstances nl = |
1100 | 1101 |
filter (not . isOk . instanceGroup nl) . Container.elems |
1101 | 1102 |
|
1102 |
-- | Splits a cluster into the component node groups |
|
1103 |
-- | Splits a cluster into the component node groups.
|
|
1103 | 1104 |
splitCluster :: Node.List -> Instance.List -> |
1104 | 1105 |
[(Gdx, (Node.List, Instance.List))] |
1105 | 1106 |
splitCluster nl il = |
b/htools/Ganeti/HTools/ExtLoader.hs | ||
---|---|---|
1 |
{-| External data loader |
|
1 |
{-| External data loader.
|
|
2 | 2 |
|
3 | 3 |
This module holds the external data loading, and thus is the only one |
4 | 4 |
depending (via the specialized Text\/Rapi\/Luxi modules) on the actual |
... | ... | |
51 | 51 |
import Ganeti.HTools.CLI |
52 | 52 |
import Ganeti.HTools.Utils (sepSplit, tryRead) |
53 | 53 |
|
54 |
-- | Error beautifier |
|
54 |
-- | Error beautifier.
|
|
55 | 55 |
wrapIO :: IO (Result a) -> IO (Result a) |
56 | 56 |
wrapIO = flip catch (return . Bad . show) |
57 | 57 |
|
b/htools/Ganeti/HTools/IAlloc.hs | ||
---|---|---|
60 | 60 |
let running = "running" |
61 | 61 |
return (n, Instance.create n mem disk vcpus running tags True 0 0) |
62 | 62 |
|
63 |
-- | Parses an instance as found in the cluster instance listg.
|
|
63 |
-- | Parses an instance as found in the cluster instance list. |
|
64 | 64 |
parseInstance :: NameAssoc -- ^ The node name-to-index association list |
65 | 65 |
-> String -- ^ The name of the instance |
66 | 66 |
-> JSRecord -- ^ The JSON object |
b/htools/Ganeti/HTools/Instance.hs | ||
---|---|---|
49 | 49 |
|
50 | 50 |
-- * Type declarations |
51 | 51 |
|
52 |
-- | The instance type |
|
52 |
-- | The instance type.
|
|
53 | 53 |
data Instance = Instance |
54 | 54 |
{ name :: String -- ^ The instance name |
55 | 55 |
, alias :: String -- ^ The shortened name |
... | ... | |
74 | 74 |
setIdx = setIdx |
75 | 75 |
allNames n = [name n, alias n] |
76 | 76 |
|
77 |
-- | Running instance states.
|
|
77 |
-- | Constant holding the running instance states.
|
|
78 | 78 |
runningStates :: [String] |
79 | 79 |
runningStates = [C.inststRunning, C.inststErrorup] |
80 | 80 |
|
b/htools/Ganeti/HTools/Loader.hs | ||
---|---|---|
1 |
{-| Generic data loader |
|
1 |
{-| Generic data loader.
|
|
2 | 2 |
|
3 | 3 |
This module holds the common code for parsing the input data after it |
4 | 4 |
has been loaded from external sources. |
... | ... | |
54 | 54 |
|
55 | 55 |
-- * Constants |
56 | 56 |
|
57 |
-- | The exclusion tag prefix |
|
57 |
-- | The exclusion tag prefix.
|
|
58 | 58 |
exTagsPrefix :: String |
59 | 59 |
exTagsPrefix = "htools:iextags:" |
60 | 60 |
|
... | ... | |
147 | 147 |
in Container.add sdx snew ac2 |
148 | 148 |
else ac2 |
149 | 149 |
|
150 |
-- | Remove non-selected tags from the exclusion list |
|
150 |
-- | Remove non-selected tags from the exclusion list.
|
|
151 | 151 |
filterExTags :: [String] -> Instance.Instance -> Instance.Instance |
152 | 152 |
filterExTags tl inst = |
153 | 153 |
let old_tags = Instance.tags inst |
... | ... | |
155 | 155 |
old_tags |
156 | 156 |
in inst { Instance.tags = new_tags } |
157 | 157 |
|
158 |
-- | Update the movable attribute |
|
158 |
-- | Update the movable attribute.
|
|
159 | 159 |
updateMovable :: [String] -- ^ Selected instances (if not empty) |
160 | 160 |
-> [String] -- ^ Excluded instances |
161 | 161 |
-> Instance.Instance -- ^ Target Instance |
... | ... | |
168 | 168 |
else inst |
169 | 169 |
|
170 | 170 |
-- | Compute the longest common suffix of a list of strings that |
171 |
-- | starts with a dot.
|
|
171 |
-- starts with a dot. |
|
172 | 172 |
longestDomain :: [String] -> String |
173 | 173 |
longestDomain [] = "" |
174 | 174 |
longestDomain (x:xs) = |
... | ... | |
177 | 177 |
else accu) |
178 | 178 |
"" $ filter (isPrefixOf ".") (tails x) |
179 | 179 |
|
180 |
-- | Extracts the exclusion tags from the cluster configuration |
|
180 |
-- | Extracts the exclusion tags from the cluster configuration.
|
|
181 | 181 |
extractExTags :: [String] -> [String] |
182 | 182 |
extractExTags = |
183 | 183 |
map (drop (length exTagsPrefix)) . |
184 | 184 |
filter (isPrefixOf exTagsPrefix) |
185 | 185 |
|
186 |
-- | Extracts the common suffix from node\/instance names |
|
186 |
-- | Extracts the common suffix from node\/instance names.
|
|
187 | 187 |
commonSuffix :: Node.List -> Instance.List -> String |
188 | 188 |
commonSuffix nl il = |
189 | 189 |
let node_names = map Node.name $ Container.elems nl |
b/htools/Ganeti/HTools/Luxi.hs | ||
---|---|---|
187 | 187 |
|
188 | 188 |
-- * Main loader functionality |
189 | 189 |
|
190 |
-- | Builds the cluster data from an URL.
|
|
190 |
-- | Builds the cluster data by querying a given socket name.
|
|
191 | 191 |
readData :: String -- ^ Unix socket to use as source |
192 | 192 |
-> IO (Result JSValue, Result JSValue, Result JSValue, Result JSValue) |
193 | 193 |
readData master = |
... | ... | |
202 | 202 |
return (groups, nodes, instances, cinfo) |
203 | 203 |
) |
204 | 204 |
|
205 |
-- | Converts the output of 'readData' into the internal cluster |
|
206 |
-- representation. |
|
205 | 207 |
parseData :: (Result JSValue, Result JSValue, Result JSValue, Result JSValue) |
206 | 208 |
-> Result ClusterData |
207 | 209 |
parseData (groups, nodes, instances, cinfo) = do |
... | ... | |
214 | 216 |
ctags <- cinfo >>= getClusterTags |
215 | 217 |
return (ClusterData group_idx node_idx inst_idx ctags) |
216 | 218 |
|
217 |
-- | Top level function for data loading |
|
219 |
-- | Top level function for data loading.
|
|
218 | 220 |
loadData :: String -- ^ Unix socket to use as source |
219 | 221 |
-> IO (Result ClusterData) |
220 | 222 |
loadData = fmap parseData . readData |
b/htools/Ganeti/HTools/Node.hs | ||
---|---|---|
85 | 85 |
|
86 | 86 |
-- * Type declarations |
87 | 87 |
|
88 |
-- | The tag map type |
|
88 |
-- | The tag map type.
|
|
89 | 89 |
type TagMap = Map.Map String Int |
90 | 90 |
|
91 | 91 |
-- | The node type. |
... | ... | |
140 | 140 |
type List = Container.Container Node |
141 | 141 |
|
142 | 142 |
-- | A simple name for an allocation element (here just for logistic |
143 |
-- reasons) |
|
143 |
-- reasons).
|
|
144 | 144 |
type AllocElement = (List, Instance.Instance, [Node], T.Score) |
145 | 145 |
|
146 | 146 |
-- | Constant node index for a non-moveable instance. |
... | ... | |
149 | 149 |
|
150 | 150 |
-- * Helper functions |
151 | 151 |
|
152 |
-- | Add a tag to a tagmap |
|
152 |
-- | Add a tag to a tagmap.
|
|
153 | 153 |
addTag :: TagMap -> String -> TagMap |
154 | 154 |
addTag t s = Map.insertWith (+) s 1 t |
155 | 155 |
|
156 |
-- | Add multiple tags |
|
156 |
-- | Add multiple tags.
|
|
157 | 157 |
addTags :: TagMap -> [String] -> TagMap |
158 | 158 |
addTags = foldl' addTag |
159 | 159 |
|
160 |
-- | Adjust or delete a tag from a tagmap |
|
160 |
-- | Adjust or delete a tag from a tagmap.
|
|
161 | 161 |
delTag :: TagMap -> String -> TagMap |
162 | 162 |
delTag t s = Map.update (\v -> if v > 1 |
163 | 163 |
then Just (v-1) |
164 | 164 |
else Nothing) |
165 | 165 |
s t |
166 | 166 |
|
167 |
-- | Remove multiple tags |
|
167 |
-- | Remove multiple tags.
|
|
168 | 168 |
delTags :: TagMap -> [String] -> TagMap |
169 | 169 |
delTags = foldl' delTag |
170 | 170 |
|
171 |
-- | Check if we can add a list of tags to a tagmap |
|
171 |
-- | Check if we can add a list of tags to a tagmap.
|
|
172 | 172 |
rejectAddTags :: TagMap -> [String] -> Bool |
173 | 173 |
rejectAddTags t = any (`Map.member` t) |
174 | 174 |
|
... | ... | |
221 | 221 |
, group = group_init |
222 | 222 |
} |
223 | 223 |
|
224 |
-- | Conversion formula from mDsk\/tDsk to loDsk |
|
224 |
-- | Conversion formula from mDsk\/tDsk to loDsk.
|
|
225 | 225 |
mDskToloDsk :: Double -> Double -> Int |
226 | 226 |
mDskToloDsk mval tdsk = floor (mval * tdsk) |
227 | 227 |
|
228 |
-- | Conversion formula from mCpu\/tCpu to hiCpu |
|
228 |
-- | Conversion formula from mCpu\/tCpu to hiCpu.
|
|
229 | 229 |
mCpuTohiCpu :: Double -> Double -> Int |
230 | 230 |
mCpuTohiCpu mval tcpu = floor (mval * tcpu) |
231 | 231 |
|
... | ... | |
249 | 249 |
setXmem :: Node -> Int -> Node |
250 | 250 |
setXmem t val = t { xMem = val } |
251 | 251 |
|
252 |
-- | Sets the max disk usage ratio |
|
252 |
-- | Sets the max disk usage ratio.
|
|
253 | 253 |
setMdsk :: Node -> Double -> Node |
254 | 254 |
setMdsk t val = t { mDsk = val, loDsk = mDskToloDsk val (tDsk t) } |
255 | 255 |
|
256 |
-- | Sets the max cpu usage ratio |
|
256 |
-- | Sets the max cpu usage ratio.
|
|
257 | 257 |
setMcpu :: Node -> Double -> Node |
258 | 258 |
setMcpu t val = t { mCpu = val, hiCpu = mCpuTohiCpu val (tCpu t) } |
259 | 259 |
|
... | ... | |
435 | 435 |
|
436 | 436 |
-- * Stats functions |
437 | 437 |
|
438 |
-- | Computes the amount of available disk on a given node |
|
438 |
-- | Computes the amount of available disk on a given node.
|
|
439 | 439 |
availDisk :: Node -> Int |
440 | 440 |
availDisk t = |
441 | 441 |
let _f = fDsk t |
... | ... | |
444 | 444 |
then 0 |
445 | 445 |
else _f - _l |
446 | 446 |
|
447 |
-- | Computes the amount of used disk on a given node |
|
447 |
-- | Computes the amount of used disk on a given node.
|
|
448 | 448 |
iDsk :: Node -> Int |
449 | 449 |
iDsk t = truncate (tDsk t) - fDsk t |
450 | 450 |
|
451 |
-- | Computes the amount of available memory on a given node |
|
451 |
-- | Computes the amount of available memory on a given node.
|
|
452 | 452 |
availMem :: Node -> Int |
453 | 453 |
availMem t = |
454 | 454 |
let _f = fMem t |
... | ... | |
457 | 457 |
then 0 |
458 | 458 |
else _f - _l |
459 | 459 |
|
460 |
-- | Computes the amount of available memory on a given node |
|
460 |
-- | Computes the amount of available memory on a given node.
|
|
461 | 461 |
availCpu :: Node -> Int |
462 | 462 |
availCpu t = |
463 | 463 |
let _u = uCpu t |
... | ... | |
472 | 472 |
|
473 | 473 |
-- * Display functions |
474 | 474 |
|
475 |
showField :: Node -> String -> String |
|
475 |
-- | Return a field for a given node. |
|
476 |
showField :: Node -- ^ Node which we're querying |
|
477 |
-> String -- ^ Field name |
|
478 |
-> String -- ^ Field value as string |
|
476 | 479 |
showField t field = |
477 | 480 |
case field of |
478 | 481 |
"idx" -> printf "%4d" $ idx t |
... | ... | |
512 | 515 |
T.DynUtil { T.cpuWeight = uC, T.memWeight = uM, |
513 | 516 |
T.dskWeight = uD, T.netWeight = uN } = utilLoad t |
514 | 517 |
|
515 |
-- | Returns the header and numeric propery of a field |
|
518 |
-- | Returns the header and numeric propery of a field.
|
|
516 | 519 |
showHeader :: String -> (String, Bool) |
517 | 520 |
showHeader field = |
518 | 521 |
case field of |
... | ... | |
552 | 555 |
list fields t = map (showField t) fields |
553 | 556 |
|
554 | 557 |
|
558 |
-- | Constant holding the fields we're displaying by default. |
|
555 | 559 |
defaultFields :: [String] |
556 | 560 |
defaultFields = |
557 | 561 |
[ "status", "name", "tmem", "nmem", "imem", "xmem", "fmem" |
... | ... | |
560 | 564 |
, "cload", "mload", "dload", "nload" ] |
561 | 565 |
|
562 | 566 |
-- | Split a list of nodes into a list of (node group UUID, list of |
563 |
-- associated nodes) |
|
567 |
-- associated nodes).
|
|
564 | 568 |
computeGroups :: [Node] -> [(T.Gdx, [Node])] |
565 | 569 |
computeGroups nodes = |
566 | 570 |
let nodes' = sortBy (comparing group) nodes |
b/htools/Ganeti/HTools/PeerMap.hs | ||
---|---|---|
1 |
{-| |
|
2 |
Module abstracting the peer map implementation. |
|
1 |
{-| Module abstracting the peer map implementation. |
|
3 | 2 |
|
4 | 3 |
This is abstracted separately since the speed of peermap updates can |
5 | 4 |
be a significant part of the total runtime, and as such changing the |
... | ... | |
46 | 45 |
|
47 | 46 |
import Ganeti.HTools.Types |
48 | 47 |
|
48 |
-- * Type definitions |
|
49 |
|
|
50 |
-- | Our key type. |
|
49 | 51 |
type Key = Ndx |
52 |
|
|
53 |
-- | Our element type. |
|
54 |
|
|
50 | 55 |
type Elem = Int |
56 |
|
|
57 |
-- | The definition of a peer map. |
|
51 | 58 |
type PeerMap = [(Key, Elem)] |
52 | 59 |
|
53 | 60 |
-- * Initialization functions |
... | ... | |
67 | 74 |
Nothing -> insertBy pmCompare (k, v) lst |
68 | 75 |
Just o -> insertBy pmCompare (k, fn o v) (remove k lst) |
69 | 76 |
|
70 |
-- | Create a PeerMap from an association list, with possible duplicates |
|
77 |
-- | Create a PeerMap from an association list, with possible duplicates.
|
|
71 | 78 |
accumArray :: (Elem -> Elem -> Elem) -- ^ function used to merge the elements |
72 | 79 |
-> [(Key, Elem)] -- ^ source data |
73 | 80 |
-> PeerMap -- ^ results |
... | ... | |
76 | 83 |
|
77 | 84 |
-- * Basic operations |
78 | 85 |
|
79 |
-- | Returns either the value for a key or zero if not found |
|
86 |
-- | Returns either the value for a key or zero if not found.
|
|
80 | 87 |
find :: Key -> PeerMap -> Elem |
81 | 88 |
find k = fromMaybe 0 . lookup k |
82 | 89 |
|
83 |
-- | Add an element to a peermap, overwriting the previous value |
|
90 |
-- | Add an element to a peermap, overwriting the previous value.
|
|
84 | 91 |
add :: Key -> Elem -> PeerMap -> PeerMap |
85 | 92 |
add = addWith (flip const) |
86 | 93 |
|
87 |
-- | Remove an element from a peermap |
|
94 |
-- | Remove an element from a peermap.
|
|
88 | 95 |
remove :: Key -> PeerMap -> PeerMap |
89 | 96 |
remove _ [] = [] |
90 | 97 |
remove k ((x@(x', _)):xs) = if k == x' |
b/htools/Ganeti/HTools/QC.hs | ||
---|---|---|
1 |
{-| Unittests for ganeti-htools |
|
1 |
{-| Unittests for ganeti-htools.
|
|
2 | 2 |
|
3 | 3 |
-} |
4 | 4 |
|
... | ... | |
70 | 70 |
|
71 | 71 |
-- * Constants |
72 | 72 |
|
73 |
-- | Maximum memory (1TiB, somewhat random value) |
|
73 |
-- | Maximum memory (1TiB, somewhat random value).
|
|
74 | 74 |
maxMem :: Int |
75 | 75 |
maxMem = 1024 * 1024 |
76 | 76 |
|
77 |
-- | Maximum disk (8TiB, somewhat random value) |
|
77 |
-- | Maximum disk (8TiB, somewhat random value).
|
|
78 | 78 |
maxDsk :: Int |
79 | 79 |
maxDsk = 1024 * 1024 * 8 |
80 | 80 |
|
81 |
-- | Max CPUs (1024, somewhat random value) |
|
81 |
-- | Max CPUs (1024, somewhat random value).
|
|
82 | 82 |
maxCpu :: Int |
83 | 83 |
maxCpu = 1024 |
84 | 84 |
|
... | ... | |
95 | 95 |
|
96 | 96 |
-- * Helper functions |
97 | 97 |
|
98 |
-- | Simple checker for whether OpResult is fail or pass |
|
98 |
-- | Simple checker for whether OpResult is fail or pass.
|
|
99 | 99 |
isFailure :: Types.OpResult a -> Bool |
100 | 100 |
isFailure (Types.OpFail _) = True |
101 | 101 |
isFailure _ = False |
102 | 102 |
|
103 |
-- | Update an instance to be smaller than a node |
|
103 |
-- | Update an instance to be smaller than a node.
|
|
104 | 104 |
setInstanceSmallerThanNode node inst = |
105 | 105 |
inst { Instance.mem = Node.availMem node `div` 2 |
106 | 106 |
, Instance.dsk = Node.availDisk node `div` 2 |
107 | 107 |
, Instance.vcpus = Node.availCpu node `div` 2 |
108 | 108 |
} |
109 | 109 |
|
110 |
-- | Create an instance given its spec |
|
110 |
-- | Create an instance given its spec.
|
|
111 | 111 |
createInstance mem dsk vcpus = |
112 | 112 |
Instance.create "inst-unnamed" mem dsk vcpus "running" [] True (-1) (-1) |
113 | 113 |
|
114 |
-- | Create a small cluster by repeating a node spec |
|
114 |
-- | Create a small cluster by repeating a node spec.
|
|
115 | 115 |
makeSmallCluster :: Node.Node -> Int -> Node.List |
116 | 116 |
makeSmallCluster node count = |
117 | 117 |
let fn = Node.buildPeers node Container.empty |
... | ... | |
119 | 119 |
(_, nlst) = Loader.assignIndices namelst |
120 | 120 |
in nlst |
121 | 121 |
|
122 |
-- | Checks if a node is "big" enough |
|
122 |
-- | Checks if a node is "big" enough.
|
|
123 | 123 |
isNodeBig :: Node.Node -> Int -> Bool |
124 | 124 |
isNodeBig node size = Node.availDisk node > size * Types.unitDsk |
125 | 125 |
&& Node.availMem node > size * Types.unitMem |
... | ... | |
129 | 129 |
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0 |
130 | 130 |
|
131 | 131 |
-- | Assigns a new fresh instance to a cluster; this is not |
132 |
-- allocation, so no resource checks are done |
|
132 |
-- allocation, so no resource checks are done.
|
|
133 | 133 |
assignInstance :: Node.List -> Instance.List -> Instance.Instance -> |
134 | 134 |
Types.Idx -> Types.Idx -> |
135 | 135 |
(Node.List, Instance.List) |
... | ... | |
149 | 149 |
|
150 | 150 |
-- * Arbitrary instances |
151 | 151 |
|
152 |
-- | Defines a DNS name. |
|
152 | 153 |
newtype DNSChar = DNSChar { dnsGetChar::Char } |
154 |
|
|
153 | 155 |
instance Arbitrary DNSChar where |
154 | 156 |
arbitrary = do |
155 | 157 |
x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-") |
... | ... | |
189 | 191 |
vcpus <- choose (0, maxCpu) |
190 | 192 |
return $ Instance.create name mem dsk vcpus run_st [] True pn sn |
191 | 193 |
|
192 |
genNode :: Maybe Int -> Maybe Int -> Gen Node.Node |
|
194 |
-- | Generas an arbitrary node based on sizing information. |
|
195 |
genNode :: Maybe Int -- ^ Minimum node size in terms of units |
|
196 |
-> Maybe Int -- ^ Maximum node size (when Nothing, bounded |
|
197 |
-- just by the max... constants) |
|
198 |
-> Gen Node.Node |
|
193 | 199 |
genNode min_multiplier max_multiplier = do |
194 | 200 |
let (base_mem, base_dsk, base_cpu) = |
195 | 201 |
case min_multiplier of |
... | ... | |
253 | 259 |
instance Arbitrary Jobs.JobStatus where |
254 | 260 |
arbitrary = elements [minBound..maxBound] |
255 | 261 |
|
262 |
newtype SmallRatio = SmallRatio Double deriving Show |
|
263 |
instance Arbitrary SmallRatio where |
|
264 |
arbitrary = do |
|
265 |
v <- choose (0, 1) |
|
266 |
return $ SmallRatio v |
|
267 |
|
|
256 | 268 |
-- * Actual tests |
257 | 269 |
|
258 |
-- If the list is not just an empty element, and if the elements do |
|
259 |
-- not contain commas, then join+split should be idepotent |
|
270 |
-- ** Utils tests |
|
271 |
|
|
272 |
-- | If the list is not just an empty element, and if the elements do |
|
273 |
-- not contain commas, then join+split should be idempotent. |
|
260 | 274 |
prop_Utils_commaJoinSplit = |
261 | 275 |
forAll (arbitrary `suchThat` |
262 | 276 |
(\l -> l /= [""] && all (not . elem ',') l )) $ \lst -> |
263 | 277 |
Utils.sepSplit ',' (Utils.commaJoin lst) == lst |
264 | 278 |
|
265 |
-- Split and join should always be idempotent
|
|
279 |
-- | Split and join should always be idempotent.
|
|
266 | 280 |
prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s |
267 | 281 |
|
268 | 282 |
-- | fromObjWithDefault, we test using the Maybe monad and an integer |
269 |
-- value |
|
283 |
-- value.
|
|
270 | 284 |
prop_Utils_fromObjWithDefault def_value random_key = |
271 | 285 |
-- a missing key will be returned with the default |
272 | 286 |
Utils.fromObjWithDefault [] random_key def_value == Just def_value && |
... | ... | |
275 | 289 |
random_key (def_value+1) == Just def_value |
276 | 290 |
where _types = def_value :: Integer |
277 | 291 |
|
292 |
-- | Test list for the Utils module. |
|
278 | 293 |
testUtils = |
279 | 294 |
[ run prop_Utils_commaJoinSplit |
280 | 295 |
, run prop_Utils_commaSplitJoin |
281 | 296 |
, run prop_Utils_fromObjWithDefault |
282 | 297 |
] |
283 | 298 |
|
284 |
-- | Make sure add is idempotent |
|
299 |
-- ** PeerMap tests |
|
300 |
|
|
301 |
-- | Make sure add is idempotent. |
|
285 | 302 |
prop_PeerMap_addIdempotent pmap key em = |
286 | 303 |
fn puniq == fn (fn puniq) |
287 | 304 |
where _types = (pmap::PeerMap.PeerMap, |
... | ... | |
289 | 306 |
fn = PeerMap.add key em |
290 | 307 |
puniq = PeerMap.accumArray const pmap |
291 | 308 |
|
292 |
-- | Make sure remove is idempotent |
|
309 |
-- | Make sure remove is idempotent.
|
|
293 | 310 |
prop_PeerMap_removeIdempotent pmap key = |
294 | 311 |
fn puniq == fn (fn puniq) |
295 | 312 |
where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key) |
296 | 313 |
fn = PeerMap.remove key |
297 | 314 |
puniq = PeerMap.accumArray const pmap |
298 | 315 |
|
299 |
-- | Make sure a missing item returns 0 |
|
316 |
-- | Make sure a missing item returns 0.
|
|
300 | 317 |
prop_PeerMap_findMissing pmap key = |
301 | 318 |
PeerMap.find key (PeerMap.remove key puniq) == 0 |
302 | 319 |
where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key) |
303 | 320 |
puniq = PeerMap.accumArray const pmap |
304 | 321 |
|
305 |
-- | Make sure an added item is found |
|
322 |
-- | Make sure an added item is found.
|
|
306 | 323 |
prop_PeerMap_addFind pmap key em = |
307 | 324 |
PeerMap.find key (PeerMap.add key em puniq) == em |
308 | 325 |
where _types = (pmap::PeerMap.PeerMap, |
309 | 326 |
key::PeerMap.Key, em::PeerMap.Elem) |
310 | 327 |
puniq = PeerMap.accumArray const pmap |
311 | 328 |
|
312 |
-- | Manual check that maxElem returns the maximum indeed, or 0 for null |
|
329 |
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
|
|
313 | 330 |
prop_PeerMap_maxElem pmap = |
314 | 331 |
PeerMap.maxElem puniq == if null puniq then 0 |
315 | 332 |
else (maximum . snd . unzip) puniq |
316 | 333 |
where _types = pmap::PeerMap.PeerMap |
317 | 334 |
puniq = PeerMap.accumArray const pmap |
318 | 335 |
|
336 |
-- | List of tests for the PeerMap module. |
|
319 | 337 |
testPeerMap = |
320 | 338 |
[ run prop_PeerMap_addIdempotent |
321 | 339 |
, run prop_PeerMap_removeIdempotent |
... | ... | |
324 | 342 |
, run prop_PeerMap_findMissing |
325 | 343 |
] |
326 | 344 |
|
327 |
-- Container tests |
|
345 |
-- ** Container tests
|
|
328 | 346 |
|
329 | 347 |
prop_Container_addTwo cdata i1 i2 = |
330 | 348 |
fn i1 i2 cont == fn i2 i1 cont && |
... | ... | |
339 | 357 |
fnode = head (Container.elems nl) |
340 | 358 |
in Container.nameOf nl (Node.idx fnode) == Node.name fnode |
341 | 359 |
|
342 |
-- We test that in a cluster, given a random node, we can find it by |
|
360 |
-- | We test that in a cluster, given a random node, we can find it by
|
|
343 | 361 |
-- its name and alias, as long as all names and aliases are unique, |
344 |
-- and that we fail to find a non-existing name |
|
362 |
-- and that we fail to find a non-existing name.
|
|
345 | 363 |
prop_Container_findByName node othername = |
346 | 364 |
forAll (choose (1, 20)) $ \ cnt -> |
347 | 365 |
forAll (choose (0, cnt - 1)) $ \ fidx -> |
... | ... | |
367 | 385 |
, run prop_Container_findByName |
368 | 386 |
] |
369 | 387 |
|
388 |
-- ** Instance tests |
|
389 |
|
|
370 | 390 |
-- Simple instance tests, we only have setter/getters |
371 | 391 |
|
372 | 392 |
prop_Instance_creat inst = |
... | ... | |
471 | 491 |
, run prop_Instance_setMovable |
472 | 492 |
] |
473 | 493 |
|
494 |
-- ** Text backend tests |
|
495 |
|
|
474 | 496 |
-- Instance text loader tests |
475 | 497 |
|
476 | 498 |
prop_Text_Load_Instance name mem dsk vcpus status |
... | ... | |
565 | 587 |
, run prop_Text_NodeLSIdempotent |
566 | 588 |
] |
567 | 589 |
|
568 |
-- Node tests |
|
590 |
-- ** Node tests
|
|
569 | 591 |
|
570 | 592 |
prop_Node_setAlias node name = |
571 | 593 |
Node.name newnode == Node.name node && |
... | ... | |
585 | 607 |
Node.mCpu newnode == mc |
586 | 608 |
where newnode = Node.setMcpu node mc |
587 | 609 |
|
588 |
-- | Check that an instance add with too high memory or disk will be rejected |
|
610 |
-- | Check that an instance add with too high memory or disk will be |
|
611 |
-- rejected. |
|
589 | 612 |
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node && |
590 | 613 |
not (Node.failN1 node) |
591 | 614 |
==> |
... | ... | |
615 | 638 |
inst' = setInstanceSmallerThanNode node inst |
616 | 639 |
inst'' = inst' { Instance.vcpus = Node.availCpu node + extra } |
617 | 640 |
|
618 |
-- | Check that an instance add with too high memory or disk will be rejected |
|
641 |
-- | Check that an instance add with too high memory or disk will be |
|
642 |
-- rejected. |
|
619 | 643 |
prop_Node_addSec node inst pdx = |
620 | 644 |
(Instance.mem inst >= (Node.fMem node - Node.rMem node) || |
621 | 645 |
Instance.dsk inst >= Node.fDsk node) && |
... | ... | |
623 | 647 |
==> isFailure (Node.addSec node inst pdx) |
624 | 648 |
where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int) |
625 | 649 |
|
626 |
-- | Checks for memory reservation changes |
|
650 |
-- | Checks for memory reservation changes.
|
|
627 | 651 |
prop_Node_rMem inst = |
628 | 652 |
forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node -> |
629 | 653 |
-- ab = auto_balance, nb = non-auto_balance |
... | ... | |
655 | 679 |
x -> printTestCase ("Failed to add/remove instances: " ++ show x) |
656 | 680 |
False |
657 | 681 |
|
658 |
newtype SmallRatio = SmallRatio Double deriving Show |
|
659 |
instance Arbitrary SmallRatio where |
|
660 |
arbitrary = do |
|
661 |
v <- choose (0, 1) |
|
662 |
return $ SmallRatio v |
|
663 |
|
|
664 |
-- | Check mdsk setting |
|
682 |
-- | Check mdsk setting. |
|
665 | 683 |
prop_Node_setMdsk node mx = |
666 | 684 |
Node.loDsk node' >= 0 && |
667 | 685 |
fromIntegral (Node.loDsk node') <= Node.tDsk node && |
... | ... | |
715 | 733 |
] |
716 | 734 |
|
717 | 735 |
|
718 |
-- Cluster tests |
|
736 |
-- ** Cluster tests
|
|
719 | 737 |
|
720 |
-- | Check that the cluster score is close to zero for a homogeneous cluster |
|
738 |
-- | Check that the cluster score is close to zero for a homogeneous |
|
739 |
-- cluster. |
|
721 | 740 |
prop_Score_Zero node = |
722 | 741 |
forAll (choose (1, 1024)) $ \count -> |
723 | 742 |
(not (Node.offline node) && not (Node.failN1 node) && (count > 0) && |
... | ... | |
730 | 749 |
-- this should be much lower than the default score in CLI.hs |
731 | 750 |
in score <= 1e-12 |
732 | 751 |
|
733 |
-- | Check that cluster stats are sane |
|
752 |
-- | Check that cluster stats are sane.
|
|
734 | 753 |
prop_CStats_sane node = |
735 | 754 |
forAll (choose (1, 1024)) $ \count -> |
736 | 755 |
(not (Node.offline node) && not (Node.failN1 node) && |
... | ... | |
743 | 762 |
Cluster.csAdsk cstats <= Cluster.csFdsk cstats |
744 | 763 |
|
745 | 764 |
-- | Check that one instance is allocated correctly, without |
746 |
-- rebalances needed |
|
765 |
-- rebalances needed.
|
|
747 | 766 |
prop_ClusterAlloc_sane node inst = |
748 | 767 |
forAll (choose (5, 20)) $ \count -> |
749 | 768 |
not (Node.offline node) |
... | ... | |
768 | 787 |
|
769 | 788 |
-- | Checks that on a 2-5 node cluster, we can allocate a random |
770 | 789 |
-- instance spec via tiered allocation (whatever the original instance |
771 |
-- spec), on either one or two nodes |
|
790 |
-- spec), on either one or two nodes.
|
|
772 | 791 |
prop_ClusterCanTieredAlloc node inst = |
773 | 792 |
forAll (choose (2, 5)) $ \count -> |
774 | 793 |
forAll (choose (1, 2)) $ \rqnodes -> |
... | ... | |
787 | 806 |
length ixes == length cstats |
788 | 807 |
|
789 | 808 |
-- | Checks that on a 4-8 node cluster, once we allocate an instance, |
790 |
-- we can also evacuate it |
|
809 |
-- we can also evacuate it.
|
|
791 | 810 |
prop_ClusterAllocEvac node inst = |
792 | 811 |
forAll (choose (4, 8)) $ \count -> |
793 | 812 |
not (Node.offline node) |
... | ... | |
812 | 831 |
_ -> False |
813 | 832 |
|
814 | 833 |
-- | Check that allocating multiple instances on a cluster, then |
815 |
-- adding an empty node, results in a valid rebalance |
|
834 |
-- adding an empty node, results in a valid rebalance.
|
|
816 | 835 |
prop_ClusterAllocBalance = |
817 | 836 |
forAll (genNode (Just 5) (Just 128)) $ \node -> |
818 | 837 |
forAll (choose (3, 5)) $ \count -> |
... | ... | |
831 | 850 |
tbl = Cluster.Table ynl il' cv [] |
832 | 851 |
in canBalance tbl True True False |
833 | 852 |
|
834 |
-- | Checks consistency |
|
853 |
-- | Checks consistency.
|
|
835 | 854 |
prop_ClusterCheckConsistency node inst = |
836 | 855 |
let nl = makeSmallCluster node 3 |
837 | 856 |
[node1, node2, node3] = Container.elems nl |
... | ... | |
845 | 864 |
null (ccheck [(0, inst2)]) && |
846 | 865 |
(not . null $ ccheck [(0, inst3)]) |
847 | 866 |
|
848 |
-- For now, we only test that we don't lose instances during the split
|
|
867 |
-- | For now, we only test that we don't lose instances during the split.
|
|
849 | 868 |
prop_ClusterSplitCluster node inst = |
850 | 869 |
forAll (choose (0, 100)) $ \icnt -> |
851 | 870 |
let nl = makeSmallCluster node 2 |
... | ... | |
867 | 886 |
, run prop_ClusterSplitCluster |
868 | 887 |
] |
869 | 888 |
|
870 |
-- | Check that opcode serialization is idempotent
|
|
889 |
-- ** OpCodes tests
|
|
871 | 890 |
|
891 |
-- | Check that opcode serialization is idempotent. |
|
872 | 892 |
prop_OpCodes_serialization op = |
873 | 893 |
case J.readJSON (J.showJSON op) of |
874 | 894 |
J.Error _ -> False |
... | ... | |
879 | 899 |
[ run prop_OpCodes_serialization |
880 | 900 |
] |
881 | 901 |
|
882 |
-- | Check that (queued) job\/opcode status serialization is idempotent |
|
902 |
-- ** Jobs tests |
|
903 |
|
|
904 |
-- | Check that (queued) job\/opcode status serialization is idempotent. |
|
883 | 905 |
prop_OpStatus_serialization os = |
884 | 906 |
case J.readJSON (J.showJSON os) of |
885 | 907 |
J.Error _ -> False |
... | ... | |
897 | 919 |
, run prop_JobStatus_serialization |
898 | 920 |
] |
899 | 921 |
|
900 |
-- | Loader tests
|
|
922 |
-- ** Loader tests
|
|
901 | 923 |
|
902 | 924 |
prop_Loader_lookupNode ktn inst node = |
903 | 925 |
Loader.lookupNode nl inst node == Data.Map.lookup node nl |
... | ... | |
915 | 937 |
else True) |
916 | 938 |
where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes) |
917 | 939 |
|
918 |
|
|
919 | 940 |
-- | Checks that the number of primary instances recorded on the nodes |
920 |
-- is zero |
|
941 |
-- is zero.
|
|
921 | 942 |
prop_Loader_mergeData ns = |
922 | 943 |
let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns |
923 | 944 |
in case Loader.mergeData [] [] [] [] |
b/htools/Ganeti/HTools/Rapi.hs | ||
---|---|---|
57 | 57 |
|
58 | 58 |
#else |
59 | 59 |
|
60 |
-- | The curl options we use |
|
60 |
-- | The curl options we use.
|
|
61 | 61 |
curlOpts :: [CurlOption] |
62 | 62 |
curlOpts = [ CurlSSLVerifyPeer False |
63 | 63 |
, CurlSSLVerifyHost 0 |
... | ... | |
97 | 97 |
getGroups body = loadJSArray "Parsing group data" body >>= |
98 | 98 |
mapM (parseGroup . fromJSObject) |
99 | 99 |
|
100 |
-- | Generates a fake group list. |
|
100 | 101 |
getFakeGroups :: Result [(String, Group.Group)] |
101 | 102 |
getFakeGroups = |
102 | 103 |
return [(defaultGroupID, |
... | ... | |
173 | 174 |
tags_body <- getUrl $ printf "%s/2/tags" url |
174 | 175 |
return (group_body, node_body, inst_body, tags_body) |
175 | 176 |
|
176 |
-- | Builds the cluster data from the raw Rapi content |
|
177 |
-- | Builds the cluster data from the raw Rapi content.
|
|
177 | 178 |
parseData :: (Result String, Result String, Result String, Result String) |
178 | 179 |
-> Result ClusterData |
179 | 180 |
parseData (group_body, node_body, inst_body, tags_body) = do |
... | ... | |
191 | 192 |
tags_data <- tags_body >>= (fromJResult "Parsing tags data" . decodeStrict) |
192 | 193 |
return (ClusterData group_idx node_idx inst_idx tags_data) |
193 | 194 |
|
194 |
-- | Top level function for data loading |
|
195 |
-- | Top level function for data loading.
|
|
195 | 196 |
loadData :: String -- ^ Cluster or URL to use as source |
196 | 197 |
-> IO (Result ClusterData) |
197 | 198 |
loadData = fmap parseData . readData |
b/htools/Ganeti/HTools/Simu.hs | ||
---|---|---|
1 |
{-| Parsing data from a simulated description of the cluster |
|
1 |
{-| Parsing data from a simulated description of the cluster.
|
|
2 | 2 |
|
3 | 3 |
This module holds the code for parsing a cluster description. |
4 | 4 |
|
b/htools/Ganeti/HTools/Text.hs | ||
---|---|---|
1 |
{-| Parsing data from text-files |
|
1 |
{-| Parsing data from text-files.
|
|
2 | 2 |
|
3 | 3 |
This module holds the code for loading the cluster state from text |
4 |
files, as produced by gnt-node and gnt-instance list.
|
|
4 |
files, as produced by @gnt-node@ and @gnt-instance@ @list@ command.
|
|
5 | 5 |
|
6 | 6 |
-} |
7 | 7 |
|
... | ... | |
51 | 51 |
import qualified Ganeti.HTools.Node as Node |
52 | 52 |
import qualified Ganeti.HTools.Instance as Instance |
53 | 53 |
|
54 |
-- | Serialize a single group |
|
54 |
-- * Serialisation functions |
|
55 |
|
|
56 |
-- | Serialize a single group. |
|
55 | 57 |
serializeGroup :: Group.Group -> String |
56 | 58 |
serializeGroup grp = |
57 | 59 |
printf "%s|%s|%s" (Group.name grp) (Group.uuid grp) |
58 | 60 |
(apolToString (Group.allocPolicy grp)) |
59 | 61 |
|
60 |
-- | Generate group file data from a group list |
|
62 |
-- | Generate group file data from a group list.
|
|
61 | 63 |
serializeGroups :: Group.List -> String |
62 | 64 |
serializeGroups = unlines . map serializeGroup . Container.elems |
63 | 65 |
|
64 |
-- | Serialize a single node |
|
65 |
serializeNode :: Group.List -> Node.Node -> String |
|
66 |
-- | Serialize a single node. |
|
67 |
serializeNode :: Group.List -- ^ The list of groups (needed for group uuid) |
|
68 |
-> Node.Node -- ^ The node to be serialised |
|
69 |
-> String |
|
66 | 70 |
serializeNode gl node = |
67 | 71 |
printf "%s|%.0f|%d|%d|%.0f|%d|%.0f|%c|%s" (Node.name node) |
68 | 72 |
(Node.tMem node) (Node.nMem node) (Node.fMem node) |
... | ... | |
71 | 75 |
(Group.uuid grp) |
72 | 76 |
where grp = Container.find (Node.group node) gl |
73 | 77 |
|
74 |
-- | Generate node file data from node objects |
|
78 |
-- | Generate node file data from node objects.
|
|
75 | 79 |
serializeNodes :: Group.List -> Node.List -> String |
76 | 80 |
serializeNodes gl = unlines . map (serializeNode gl) . Container.elems |
77 | 81 |
|
78 |
-- | Serialize a single instance |
|
79 |
serializeInstance :: Node.List -> Instance.Instance -> String |
|
82 |
-- | Serialize a single instance. |
|
83 |
serializeInstance :: Node.List -- ^ The node list (needed for |
|
84 |
-- node names) |
|
85 |
-> Instance.Instance -- ^ The instance to be serialised |
|
86 |
-> String |
|
80 | 87 |
serializeInstance nl inst = |
81 | 88 |
let |
82 | 89 |
iname = Instance.name inst |
... | ... | |
92 | 99 |
(if Instance.auto_balance inst then "Y" else "N") |
93 | 100 |
pnode snode (intercalate "," (Instance.tags inst)) |
94 | 101 |
|
95 |
-- | Generate instance file data from instance objects |
|
102 |
-- | Generate instance file data from instance objects.
|
|
96 | 103 |
serializeInstances :: Node.List -> Instance.List -> String |
97 | 104 |
serializeInstances nl = |
98 | 105 |
unlines . map (serializeInstance nl) . Container.elems |
99 | 106 |
|
100 |
-- | Generate complete cluster data from node and instance lists |
|
107 |
-- | Generate complete cluster data from node and instance lists.
|
|
101 | 108 |
serializeCluster :: ClusterData -> String |
102 | 109 |
serializeCluster (ClusterData gl nl il ctags) = |
103 | 110 |
let gdata = serializeGroups gl |
... | ... | |
106 | 113 |
-- note: not using 'unlines' as that adds too many newlines |
107 | 114 |
in intercalate "\n" [gdata, ndata, idata, unlines ctags] |
108 | 115 |
|
116 |
-- * Parsing functions |
|
117 |
|
|
109 | 118 |
-- | Load a group from a field list. |
110 |
loadGroup :: (Monad m) => [String] -> m (String, Group.Group) |
|
119 |
loadGroup :: (Monad m) => [String] |
|
120 |
-> m (String, Group.Group) -- ^ The result, a tuple of group |
|
121 |
-- UUID and group object |
|
111 | 122 |
loadGroup [name, gid, apol] = do |
112 | 123 |
xapol <- apolFromString apol |
113 | 124 |
return (gid, Group.create name gid xapol) |
... | ... | |
115 | 126 |
loadGroup s = fail $ "Invalid/incomplete group data: '" ++ show s ++ "'" |
116 | 127 |
|
117 | 128 |
-- | Load a node from a field list. |
118 |
loadNode :: (Monad m) => NameAssoc -> [String] -> m (String, Node.Node) |
|
129 |
loadNode :: (Monad m) => |
|
130 |
NameAssoc -- ^ Association list with current groups |
|
131 |
-> [String] -- ^ Input data as a list of fields |
|
132 |
-> m (String, Node.Node) -- ^ The result, a tuple o node name |
|
133 |
-- and node object |
|
119 | 134 |
loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu] = do |
120 | 135 |
gdx <- lookupGroup ktg name gu |
121 | 136 |
new_node <- |
... | ... | |
134 | 149 |
|
135 | 150 |
-- | Load an instance from a field list. |
136 | 151 |
loadInst :: (Monad m) => |
137 |
NameAssoc -> [String] -> m (String, Instance.Instance) |
|
152 |
NameAssoc -- ^ Association list with |
|
153 |
-- the current nodes |
|
154 |
-> [String] -- ^ Input data as a list of |
|
155 |
-- fields |
|
156 |
-> m (String, Instance.Instance) -- ^ The result, a tuple of |
|
157 |
-- instance name and the |
|
158 |
-- instance object |
|
138 | 159 |
loadInst ktn [name, mem, dsk, vcpus, status, auto_bal, pnode, snode, tags] = do |
139 | 160 |
pidx <- lookupNode ktn name pnode |
140 | 161 |
sidx <- (if null snode then return Node.noSecondary |
... | ... | |
161 | 182 |
-- @gnt-instance list@ and @gnt-node list@ to a list of objects using |
162 | 183 |
-- a supplied conversion function. |
163 | 184 |
loadTabular :: (Monad m, Element a) => |
164 |
[String] -> ([String] -> m (String, a)) |
|
165 |
-> m (NameAssoc, Container.Container a) |
|
185 |
[String] -- ^ Input data, as a list of lines |
|
186 |
-> ([String] -> m (String, a)) -- ^ Conversion function |
|
187 |
-> m ( NameAssoc |
|
188 |
, Container.Container a ) -- ^ A tuple of an |
|
189 |
-- association list (name |
|
190 |
-- to object) and a set as |
|
191 |
-- used in |
|
192 |
-- "Ganeti.HTools.Container" |
|
193 |
|
|
166 | 194 |
loadTabular lines_data convert_fn = do |
167 | 195 |
let rows = map (sepSplit '|') lines_data |
168 | 196 |
kerows <- mapM convert_fn rows |
169 | 197 |
return $ assignIndices kerows |
170 | 198 |
|
171 | 199 |
-- | Load the cluser data from disk. |
172 |
readData :: String -- ^ Path to the text file |
|
173 |
-> IO String |
|
200 |
-- |
|
201 |
-- This is an alias to 'readFile' just for consistency with the other |
|
202 |
-- modules. |
|
203 |
readData :: String -- ^ Path to the text file |
|
204 |
-> IO String -- ^ Contents of the file |
|
174 | 205 |
readData = readFile |
175 | 206 |
|
176 | 207 |
-- | Builds the cluster data from text input. |
... | ... | |
192 | 223 |
{- the tags are simply line-based, no processing needed -} |
193 | 224 |
return (ClusterData gl nl il ctags) |
194 | 225 |
|
195 |
-- | Top level function for data loading |
|
226 |
-- | Top level function for data loading.
|
|
196 | 227 |
loadData :: String -- ^ Path to the text file |
197 | 228 |
-> IO (Result ClusterData) |
198 | 229 |
loadData = fmap parseData . readData |
b/htools/Ganeti/HTools/Types.hs | ||
---|---|---|
102 | 102 |
-- allocations |
103 | 103 |
deriving (Show, Read, Eq, Ord) |
104 | 104 |
|
105 |
-- | Convert a string to an alloc policy |
|
105 |
-- | Convert a string to an alloc policy.
|
|
106 | 106 |
apolFromString :: (Monad m) => String -> m AllocPolicy |
107 | 107 |
apolFromString s = |
108 | 108 |
case () of |
... | ... | |
111 | 111 |
| s == C.allocPolicyUnallocable -> return AllocUnallocable |
112 | 112 |
| otherwise -> fail $ "Invalid alloc policy mode: " ++ s |
113 | 113 |
|
114 |
-- | Convert an alloc policy to the Ganeti string equivalent |
|
114 |
-- | Convert an alloc policy to the Ganeti string equivalent.
|
|
115 | 115 |
apolToString :: AllocPolicy -> String |
116 | 116 |
apolToString AllocPreferred = C.allocPolicyPreferred |
117 | 117 |
apolToString AllocLastResort = C.allocPolicyLastResort |
... | ... | |
140 | 140 |
, netWeight :: Weight -- ^ Standardised network usage |
141 | 141 |
} deriving (Show, Read, Eq) |
142 | 142 |
|
143 |
-- | Initial empty utilisation |
|
143 |
-- | Initial empty utilisation.
|
|
144 | 144 |
zeroUtil :: DynUtil |
145 | 145 |
zeroUtil = DynUtil { cpuWeight = 0, memWeight = 0 |
146 | 146 |
, dskWeight = 0, netWeight = 0 } |
147 | 147 |
|
148 |
-- | Base utilisation (used when no actual utilisation data is |
|
149 |
-- supplied). |
|
148 | 150 |
baseUtil :: DynUtil |
149 | 151 |
baseUtil = DynUtil { cpuWeight = 1, memWeight = 1 |
150 | 152 |
, dskWeight = 1, netWeight = 1 } |
151 | 153 |
|
154 |
-- | Sum two utilisation records. |
|
152 | 155 |
addUtil :: DynUtil -> DynUtil -> DynUtil |
153 | 156 |
addUtil (DynUtil a1 a2 a3 a4) (DynUtil b1 b2 b3 b4) = |
154 | 157 |
DynUtil (a1+b1) (a2+b2) (a3+b3) (a4+b4) |
155 | 158 |
|
159 |
-- | Substracts one utilisation record from another. |
|
156 | 160 |
subUtil :: DynUtil -> DynUtil -> DynUtil |
157 | 161 |
subUtil (DynUtil a1 a2 a3 a4) (DynUtil b1 b2 b3 b4) = |
158 | 162 |
DynUtil (a1-b1) (a2-b2) (a3-b3) (a4-b4) |
... | ... | |
162 | 166 |
-- performed and the score of the cluster after the move. |
163 | 167 |
type Placement = (Idx, Ndx, Ndx, IMove, Score) |
164 | 168 |
|
165 |
-- | An instance move definition |
|
169 |
-- | An instance move definition.
|
|
166 | 170 |
data IMove = Failover -- ^ Failover the instance (f) |
167 | 171 |
| ReplacePrimary Ndx -- ^ Replace primary (f, r:np, f) |
168 | 172 |
| ReplaceSecondary Ndx -- ^ Replace secondary (r:ns) |
... | ... | |
171 | 175 |
deriving (Show, Read) |
172 | 176 |
|
173 | 177 |
-- | Formatted solution output for one move (involved nodes and |
174 |
-- commands |
|
178 |
-- commands.
|
|
175 | 179 |
type MoveJob = ([Ndx], Idx, IMove, [String]) |
176 | 180 |
|
177 |
-- | Unknown field in table output |
|
181 |
-- | Unknown field in table output.
|
|
178 | 182 |
unknownField :: String |
179 | 183 |
unknownField = "<unknown field>" |
180 | 184 |
|
181 |
-- | A list of command elements |
|
185 |
-- | A list of command elements.
|
|
182 | 186 |
type JobSet = [MoveJob] |
183 | 187 |
|
184 | 188 |
-- | Connection timeout (when using non-file methods). |
... | ... | |
211 | 215 |
|
212 | 216 |
{-| |
213 | 217 |
|
214 |
This is similar to the JSON library Result type - *very* similar, but
|
|
218 |
This is similar to the JSON library Result type - /very/ similar, but
|
|
215 | 219 |
we want to use it in multiple places, so we abstract it into a |
216 | 220 |
mini-library here |
217 | 221 |
|
... | ... | |
227 | 231 |
return = Ok |
228 | 232 |
fail = Bad |
229 | 233 |
|
230 |
-- | Simple checker for whether Result is OK
|
|
234 |
-- | Simple checker for whether a 'Result' is OK.
|
|
231 | 235 |
isOk :: Result a -> Bool |
232 | 236 |
isOk (Ok _) = True |
233 | 237 |
isOk _ = False |
234 | 238 |
|
235 |
-- | Simple checker for whether Result is a failure
|
|
239 |
-- | Simple checker for whether a 'Result' is a failure.
|
|
236 | 240 |
isBad :: Result a -> Bool |
237 | 241 |
isBad = not . isOk |
238 | 242 |
|
239 |
-- | Reason for an operation's falure |
|
243 |
-- | Reason for an operation's falure.
|
|
240 | 244 |
data FailMode = FailMem -- ^ Failed due to not enough RAM |
241 | 245 |
| FailDisk -- ^ Failed due to not enough disk |
242 | 246 |
| FailCPU -- ^ Failed due to not enough CPU capacity |
... | ... | |
244 | 248 |
| FailTags -- ^ Failed due to tag exclusion |
245 | 249 |
deriving (Eq, Enum, Bounded, Show, Read) |
246 | 250 |
|
247 |
-- | List with failure statistics |
|
251 |
-- | List with failure statistics.
|
|
248 | 252 |
type FailStats = [(FailMode, Int)] |
249 | 253 |
|
250 |
-- | Either-like data-type customized for our failure modes |
|
254 |
-- | Either-like data-type customized for our failure modes.
|
|
251 | 255 |
data OpResult a = OpFail FailMode -- ^ Failed operation |
252 | 256 |
| OpGood a -- ^ Success operation |
253 | 257 |
deriving (Show, Read) |
... | ... | |
268 | 272 |
-- | Updates the alias of the element |
269 | 273 |
setAlias :: a -> String -> a |
270 | 274 |
-- | Compute the alias by stripping a given suffix (domain) from |
271 |
-- | the name
|
|
275 |
-- the name |
|
272 | 276 |
computeAlias :: String -> a -> a |
273 | 277 |
computeAlias dom e = setAlias e alias |
274 | 278 |
where alias = take (length name - length dom) name |
b/htools/Ganeti/HTools/Utils.hs | ||
---|---|---|
1 |
{-| Utility functions -} |
|
1 |
{-| Utility functions. -}
|
|
2 | 2 |
|
3 | 3 |
{- |
4 | 4 |
|
... | ... | |
62 | 62 |
debug :: Show a => a -> a |
63 | 63 |
debug x = trace (show x) x |
64 | 64 |
|
65 |
-- | Displays a modified form of the second parameter before returning it |
|
65 |
-- | Displays a modified form of the second parameter before returning |
|
66 |
-- it. |
|
66 | 67 |
debugFn :: Show b => (a -> b) -> a -> a |
67 | 68 |
debugFn fn x = debug (fn x) `seq` x |
68 | 69 |
|
69 |
-- | Show the first parameter before returning the second one |
|
70 |
-- | Show the first parameter before returning the second one.
|
|
70 | 71 |
debugXy :: Show a => a -> b -> b |
71 | 72 |
debugXy a b = debug a `seq` b |
72 | 73 |
|
73 |
-- * Miscelaneous |
|
74 |
-- * Miscellaneous
|
|
74 | 75 |
|
75 | 76 |
-- | Comma-join a string list. |
76 | 77 |
commaJoin :: [String] -> String |
... | ... | |
91 | 92 |
-- Simple and slow statistical functions, please replace with better |
92 | 93 |
-- versions |
93 | 94 |
|
94 |
-- | Standard deviation function |
|
95 |
-- | Standard deviation function.
|
|
95 | 96 |
stdDev :: [Double] -> Double |
96 | 97 |
stdDev lst = |
97 | 98 |
-- first, calculate the list length and sum lst in a single step, |
... | ... | |
107 | 108 |
|
108 | 109 |
-- * JSON-related functions |
109 | 110 |
|
110 |
-- | A type alias for the list-based representation of J.JSObject |
|
111 |
-- | A type alias for the list-based representation of J.JSObject.
|
|
111 | 112 |
type JSRecord = [(String, J.JSValue)] |
112 | 113 |
|
113 | 114 |
-- | Converts a JSON Result into a monadic value. |
... | ... | |
153 | 154 |
JSRecord -> String -> a -> m a |
154 | 155 |
fromObjWithDefault o k d = liftM (fromMaybe d) $ maybeFromObj o k |
155 | 156 |
|
156 |
-- | Reads a JValue, that originated from an object key |
|
157 |
-- | Reads a JValue, that originated from an object key.
|
|
157 | 158 |
fromKeyValue :: (J.JSON a, Monad m) |
158 | 159 |
=> String -- ^ The key name |
159 | 160 |
-> J.JSValue -- ^ The value to read |
... | ... | |
161 | 162 |
fromKeyValue k val = |
162 | 163 |
fromJResult (printf "key '%s', value '%s'" k (show val)) (J.readJSON val) |
163 | 164 |
|
164 |
-- | Annotate a Result with an ownership information |
|
165 |
-- | Annotate a Result with an ownership information.
|
|
165 | 166 |
annotateResult :: String -> Result a -> Result a |
166 | 167 |
annotateResult owner (Bad s) = Bad $ owner ++ ": " ++ s |
167 | 168 |
annotateResult _ v = v |
168 | 169 |
|
169 | 170 |
-- | Try to extract a key from a object with better error reporting |
170 |
-- than fromObj |
|
171 |
-- than fromObj.
|
|
171 | 172 |
tryFromObj :: (J.JSON a) => |
172 | 173 |
String -- ^ Textual "owner" in error messages |
173 | 174 |
-> JSRecord -- ^ The object array |
... | ... | |
194 | 195 |
|
195 | 196 |
-- * Parsing utility functions |
196 | 197 |
|
197 |
-- | Parse results from readsPrec |
|
198 |
-- | Parse results from readsPrec.
|
|
198 | 199 |
parseChoices :: (Monad m, Read a) => String -> String -> [(a, String)] -> m a |
199 | 200 |
parseChoices _ _ ((v, ""):[]) = return v |
200 | 201 |
parseChoices name s ((_, e):[]) = |
... | ... | |
206 | 207 |
tryRead :: (Monad m, Read a) => String -> String -> m a |
207 | 208 |
tryRead name s = parseChoices name s $ reads s |
208 | 209 |
|
209 |
-- | Format a table of strings to maintain consistent length |
|
210 |
-- | Format a table of strings to maintain consistent length.
|
|
210 | 211 |
formatTable :: [[String]] -> [Bool] -> [[String]] |
211 | 212 |
formatTable vals numpos = |
212 | 213 |
let vtrans = transpose vals -- transpose, so that we work on rows |
... | ... | |
225 | 226 |
) (zip3 vtrans numpos mlens) |
226 | 227 |
in transpose expnd |
227 | 228 |
|
228 |
-- | Default group UUID (just a string, not a real UUID) |
|
229 |
-- | Default group UUID (just a string, not a real UUID).
|
|
229 | 230 |
defaultGroupID :: GroupID |
230 | 231 |
defaultGroupID = "00000000-0000-0000-0000-000000000000" |
b/htools/Ganeti/Jobs.hs | ||
---|---|---|
33 | 33 |
|
34 | 34 |
import qualified Ganeti.Constants as C |
35 | 35 |
|
36 |
-- | Our ADT for the OpCode status at runtime (while in a job). |
|
36 | 37 |
data OpStatus = OP_STATUS_QUEUED |
37 | 38 |
| OP_STATUS_WAITLOCK |
38 | 39 |
| OP_STATUS_CANCELING |
b/htools/Ganeti/Luxi.hs | ||
---|---|---|
161 | 161 |
writeIORef (rbuf s) nbuf |
162 | 162 |
return msg |
163 | 163 |
|
164 |
-- | Compute the serialized form of a Luxi operation |
|
164 |
-- | Compute the serialized form of a Luxi operation.
|
|
165 | 165 |
opToArgs :: LuxiOp -> JSValue |
Also available in: Unified diff