Revision 669d7e3d
b/Ganeti/HTools/Cluster.hs | ||
---|---|---|
1 |
{-| Implementation of cluster-wide logic. |
|
2 |
|
|
3 |
This module holds all pure cluster-logic; I\/O related functionality |
|
4 |
goes into the "Main" module. |
|
5 |
|
|
6 |
-} |
|
7 |
|
|
8 |
module Ganeti.HTools.Cluster |
|
9 |
( |
|
10 |
-- * Types |
|
11 |
NodeList |
|
12 |
, InstanceList |
|
13 |
, Placement |
|
14 |
, Solution(..) |
|
15 |
, Table(..) |
|
16 |
, Removal |
|
17 |
-- * Generic functions |
|
18 |
, totalResources |
|
19 |
-- * First phase functions |
|
20 |
, computeBadItems |
|
21 |
-- * Second phase functions |
|
22 |
, computeSolution |
|
23 |
, applySolution |
|
24 |
, printSolution |
|
25 |
, printSolutionLine |
|
26 |
, formatCmds |
|
27 |
, printNodes |
|
28 |
-- * Balacing functions |
|
29 |
, checkMove |
|
30 |
, compCV |
|
31 |
, printStats |
|
32 |
-- * Loading functions |
|
33 |
, loadData |
|
34 |
) where |
|
35 |
|
|
36 |
import Data.List |
|
37 |
import Data.Maybe (isNothing, fromJust) |
|
38 |
import Text.Printf (printf) |
|
39 |
import Data.Function |
|
40 |
|
|
41 |
import qualified Ganeti.HTools.Container as Container |
|
42 |
import qualified Ganeti.HTools.Instance as Instance |
|
43 |
import qualified Ganeti.HTools.Node as Node |
|
44 |
import Ganeti.HTools.Utils |
|
45 |
|
|
46 |
type NodeList = Container.Container Node.Node |
|
47 |
type InstanceList = Container.Container Instance.Instance |
|
48 |
type Score = Double |
|
49 |
|
|
50 |
-- | The description of an instance placement. |
|
51 |
type Placement = (Int, Int, Int, Score) |
|
52 |
|
|
53 |
{- | A cluster solution described as the solution delta and the list |
|
54 |
of placements. |
|
55 |
|
|
56 |
-} |
|
57 |
data Solution = Solution Int [Placement] |
|
58 |
deriving (Eq, Ord, Show) |
|
59 |
|
|
60 |
-- | Returns the delta of a solution or -1 for Nothing |
|
61 |
solutionDelta :: Maybe Solution -> Int |
|
62 |
solutionDelta sol = case sol of |
|
63 |
Just (Solution d _) -> d |
|
64 |
_ -> -1 |
|
65 |
|
|
66 |
-- | A removal set. |
|
67 |
data Removal = Removal NodeList [Instance.Instance] |
|
68 |
|
|
69 |
-- | An instance move definition |
|
70 |
data IMove = Failover -- ^ Failover the instance (f) |
|
71 |
| ReplacePrimary Int -- ^ Replace primary (f, r:np, f) |
|
72 |
| ReplaceSecondary Int -- ^ Replace secondary (r:ns) |
|
73 |
| ReplaceAndFailover Int -- ^ Replace secondary, failover (r:np, f) |
|
74 |
| FailoverAndReplace Int -- ^ Failover, replace secondary (f, r:ns) |
|
75 |
deriving (Show) |
|
76 |
|
|
77 |
-- | The complete state for the balancing solution |
|
78 |
data Table = Table NodeList InstanceList Score [Placement] |
|
79 |
deriving (Show) |
|
80 |
|
|
81 |
-- General functions |
|
82 |
|
|
83 |
-- | Cap the removal list if needed. |
|
84 |
capRemovals :: [a] -> Int -> [a] |
|
85 |
capRemovals removals max_removals = |
|
86 |
if max_removals > 0 then |
|
87 |
take max_removals removals |
|
88 |
else |
|
89 |
removals |
|
90 |
|
|
91 |
-- | Check if the given node list fails the N+1 check. |
|
92 |
verifyN1Check :: [Node.Node] -> Bool |
|
93 |
verifyN1Check nl = any Node.failN1 nl |
|
94 |
|
|
95 |
-- | Verifies the N+1 status and return the affected nodes. |
|
96 |
verifyN1 :: [Node.Node] -> [Node.Node] |
|
97 |
verifyN1 nl = filter Node.failN1 nl |
|
98 |
|
|
99 |
{-| Add an instance and return the new node and instance maps. -} |
|
100 |
addInstance :: NodeList -> Instance.Instance -> |
|
101 |
Node.Node -> Node.Node -> Maybe NodeList |
|
102 |
addInstance nl idata pri sec = |
|
103 |
let pdx = Node.idx pri |
|
104 |
sdx = Node.idx sec |
|
105 |
in do |
|
106 |
pnode <- Node.addPri pri idata |
|
107 |
snode <- Node.addSec sec idata pdx |
|
108 |
new_nl <- return $ Container.addTwo sdx snode |
|
109 |
pdx pnode nl |
|
110 |
return new_nl |
|
111 |
|
|
112 |
-- | Remove an instance and return the new node and instance maps. |
|
113 |
removeInstance :: NodeList -> Instance.Instance -> NodeList |
|
114 |
removeInstance nl idata = |
|
115 |
let pnode = Instance.pnode idata |
|
116 |
snode = Instance.snode idata |
|
117 |
pn = Container.find pnode nl |
|
118 |
sn = Container.find snode nl |
|
119 |
new_nl = Container.addTwo |
|
120 |
pnode (Node.removePri pn idata) |
|
121 |
snode (Node.removeSec sn idata) nl in |
|
122 |
new_nl |
|
123 |
|
|
124 |
-- | Remove an instance and return the new node map. |
|
125 |
removeInstances :: NodeList -> [Instance.Instance] -> NodeList |
|
126 |
removeInstances = foldl' removeInstance |
|
127 |
|
|
128 |
-- | Compute the total free disk and memory in the cluster. |
|
129 |
totalResources :: Container.Container Node.Node -> (Int, Int) |
|
130 |
totalResources nl = |
|
131 |
foldl' |
|
132 |
(\ (mem, dsk) node -> (mem + (Node.f_mem node), |
|
133 |
dsk + (Node.f_dsk node))) |
|
134 |
(0, 0) (Container.elems nl) |
|
135 |
|
|
136 |
{- | Compute a new version of a cluster given a solution. |
|
137 |
|
|
138 |
This is not used for computing the solutions, but for applying a |
|
139 |
(known-good) solution to the original cluster for final display. |
|
140 |
|
|
141 |
It first removes the relocated instances after which it places them on |
|
142 |
their new nodes. |
|
143 |
|
|
144 |
-} |
|
145 |
applySolution :: NodeList -> InstanceList -> [Placement] -> NodeList |
|
146 |
applySolution nl il sol = |
|
147 |
let odxes = map (\ (a, b, c, _) -> (Container.find a il, |
|
148 |
Node.idx (Container.find b nl), |
|
149 |
Node.idx (Container.find c nl)) |
|
150 |
) sol |
|
151 |
idxes = (\ (x, _, _) -> x) (unzip3 odxes) |
|
152 |
nc = removeInstances nl idxes |
|
153 |
in |
|
154 |
foldl' (\ nz (a, b, c) -> |
|
155 |
let new_p = Container.find b nz |
|
156 |
new_s = Container.find c nz in |
|
157 |
fromJust (addInstance nz a new_p new_s) |
|
158 |
) nc odxes |
|
159 |
|
|
160 |
|
|
161 |
-- First phase functions |
|
162 |
|
|
163 |
{- | Given a list 1,2,3..n build a list of pairs [(1, [2..n]), (2, |
|
164 |
[3..n]), ...] |
|
165 |
|
|
166 |
-} |
|
167 |
genParts :: [a] -> Int -> [(a, [a])] |
|
168 |
genParts l count = |
|
169 |
case l of |
|
170 |
[] -> [] |
|
171 |
x:xs -> |
|
172 |
if length l < count then |
|
173 |
[] |
|
174 |
else |
|
175 |
(x, xs) : (genParts xs count) |
|
176 |
|
|
177 |
-- | Generates combinations of count items from the names list. |
|
178 |
genNames :: Int -> [b] -> [[b]] |
|
179 |
genNames count1 names1 = |
|
180 |
let aux_fn count names current = |
|
181 |
case count of |
|
182 |
0 -> [current] |
|
183 |
_ -> |
|
184 |
concatMap |
|
185 |
(\ (x, xs) -> aux_fn (count - 1) xs (x:current)) |
|
186 |
(genParts names count) |
|
187 |
in |
|
188 |
aux_fn count1 names1 [] |
|
189 |
|
|
190 |
{- | Computes the pair of bad nodes and instances. |
|
191 |
|
|
192 |
The bad node list is computed via a simple 'verifyN1' check, and the |
|
193 |
bad instance list is the list of primary and secondary instances of |
|
194 |
those nodes. |
|
195 |
|
|
196 |
-} |
|
197 |
computeBadItems :: NodeList -> InstanceList -> |
|
198 |
([Node.Node], [Instance.Instance]) |
|
199 |
computeBadItems nl il = |
|
200 |
let bad_nodes = verifyN1 $ Container.elems nl |
|
201 |
bad_instances = map (\idx -> Container.find idx il) $ |
|
202 |
sort $ nub $ concat $ |
|
203 |
map (\ n -> (Node.slist n) ++ (Node.plist n)) bad_nodes |
|
204 |
in |
|
205 |
(bad_nodes, bad_instances) |
|
206 |
|
|
207 |
|
|
208 |
{- | Checks if removal of instances results in N+1 pass. |
|
209 |
|
|
210 |
Note: the check removal cannot optimize by scanning only the affected |
|
211 |
nodes, since the cluster is known to be not healthy; only the check |
|
212 |
placement can make this shortcut. |
|
213 |
|
|
214 |
-} |
|
215 |
checkRemoval :: NodeList -> [Instance.Instance] -> Maybe Removal |
|
216 |
checkRemoval nl victims = |
|
217 |
let nx = removeInstances nl victims |
|
218 |
failN1 = verifyN1Check (Container.elems nx) |
|
219 |
in |
|
220 |
if failN1 then |
|
221 |
Nothing |
|
222 |
else |
|
223 |
Just $ Removal nx victims |
|
224 |
|
|
225 |
|
|
226 |
-- | Computes the removals list for a given depth |
|
227 |
computeRemovals :: NodeList |
|
228 |
-> [Instance.Instance] |
|
229 |
-> Int |
|
230 |
-> [Maybe Removal] |
|
231 |
computeRemovals nl bad_instances depth = |
|
232 |
map (checkRemoval nl) $ genNames depth bad_instances |
|
233 |
|
|
234 |
-- Second phase functions |
|
235 |
|
|
236 |
-- | Single-node relocation cost |
|
237 |
nodeDelta :: Int -> Int -> Int -> Int |
|
238 |
nodeDelta i p s = |
|
239 |
if i == p || i == s then |
|
240 |
0 |
|
241 |
else |
|
242 |
1 |
|
243 |
|
|
244 |
{-| Compute best solution. |
|
245 |
|
|
246 |
This function compares two solutions, choosing the minimum valid |
|
247 |
solution. |
|
248 |
-} |
|
249 |
compareSolutions :: Maybe Solution -> Maybe Solution -> Maybe Solution |
|
250 |
compareSolutions a b = case (a, b) of |
|
251 |
(Nothing, x) -> x |
|
252 |
(x, Nothing) -> x |
|
253 |
(x, y) -> min x y |
|
254 |
|
|
255 |
-- | Compute best table. Note that the ordering of the arguments is important. |
|
256 |
compareTables :: Table -> Table -> Table |
|
257 |
compareTables a@(Table _ _ a_cv _) b@(Table _ _ b_cv _ ) = |
|
258 |
if a_cv > b_cv then b else a |
|
259 |
|
|
260 |
-- | Check if a given delta is worse then an existing solution. |
|
261 |
tooHighDelta :: Maybe Solution -> Int -> Int -> Bool |
|
262 |
tooHighDelta sol new_delta max_delta = |
|
263 |
if new_delta > max_delta && max_delta >=0 then |
|
264 |
True |
|
265 |
else |
|
266 |
case sol of |
|
267 |
Nothing -> False |
|
268 |
Just (Solution old_delta _) -> old_delta <= new_delta |
|
269 |
|
|
270 |
{-| Check if placement of instances still keeps the cluster N+1 compliant. |
|
271 |
|
|
272 |
This is the workhorse of the allocation algorithm: given the |
|
273 |
current node and instance maps, the list of instances to be |
|
274 |
placed, and the current solution, this will return all possible |
|
275 |
solution by recursing until all target instances are placed. |
|
276 |
|
|
277 |
-} |
|
278 |
checkPlacement :: NodeList -- ^ The current node list |
|
279 |
-> [Instance.Instance] -- ^ List of instances still to place |
|
280 |
-> [Placement] -- ^ Partial solution until now |
|
281 |
-> Int -- ^ The delta of the partial solution |
|
282 |
-> Maybe Solution -- ^ The previous solution |
|
283 |
-> Int -- ^ Abort if the we go above this delta |
|
284 |
-> Maybe Solution -- ^ The new solution |
|
285 |
checkPlacement nl victims current current_delta prev_sol max_delta = |
|
286 |
let target = head victims |
|
287 |
opdx = Instance.pnode target |
|
288 |
osdx = Instance.snode target |
|
289 |
vtail = tail victims |
|
290 |
have_tail = (length vtail) > 0 |
|
291 |
nodes = Container.elems nl |
|
292 |
iidx = Instance.idx target |
|
293 |
in |
|
294 |
foldl' |
|
295 |
(\ accu_p pri -> |
|
296 |
let |
|
297 |
pri_idx = Node.idx pri |
|
298 |
upri_delta = current_delta + nodeDelta pri_idx opdx osdx |
|
299 |
new_pri = Node.addPri pri target |
|
300 |
fail_delta1 = tooHighDelta accu_p upri_delta max_delta |
|
301 |
in |
|
302 |
if fail_delta1 || isNothing(new_pri) then accu_p |
|
303 |
else let pri_nl = Container.add pri_idx (fromJust new_pri) nl in |
|
304 |
foldl' |
|
305 |
(\ accu sec -> |
|
306 |
let |
|
307 |
sec_idx = Node.idx sec |
|
308 |
upd_delta = upri_delta + |
|
309 |
nodeDelta sec_idx opdx osdx |
|
310 |
fail_delta2 = tooHighDelta accu upd_delta max_delta |
|
311 |
new_sec = Node.addSec sec target pri_idx |
|
312 |
in |
|
313 |
if sec_idx == pri_idx || fail_delta2 || |
|
314 |
isNothing new_sec then accu |
|
315 |
else let |
|
316 |
nx = Container.add sec_idx (fromJust new_sec) pri_nl |
|
317 |
upd_cv = compCV nx |
|
318 |
plc = (iidx, pri_idx, sec_idx, upd_cv) |
|
319 |
c2 = plc:current |
|
320 |
result = |
|
321 |
if have_tail then |
|
322 |
checkPlacement nx vtail c2 upd_delta |
|
323 |
accu max_delta |
|
324 |
else |
|
325 |
Just (Solution upd_delta c2) |
|
326 |
in compareSolutions accu result |
|
327 |
) accu_p nodes |
|
328 |
) prev_sol nodes |
|
329 |
|
|
330 |
-- | Apply a move |
|
331 |
applyMove :: NodeList -> Instance.Instance |
|
332 |
-> IMove -> (Maybe NodeList, Instance.Instance, Int, Int) |
|
333 |
-- Failover (f) |
|
334 |
applyMove nl inst Failover = |
|
335 |
let old_pdx = Instance.pnode inst |
|
336 |
old_sdx = Instance.snode inst |
|
337 |
old_p = Container.find old_pdx nl |
|
338 |
old_s = Container.find old_sdx nl |
|
339 |
int_p = Node.removePri old_p inst |
|
340 |
int_s = Node.removeSec old_s inst |
|
341 |
new_p = Node.addPri int_s inst |
|
342 |
new_s = Node.addSec int_p inst old_sdx |
|
343 |
new_nl = if isNothing(new_p) || isNothing(new_s) then Nothing |
|
344 |
else Just $ Container.addTwo old_pdx (fromJust new_s) |
|
345 |
old_sdx (fromJust new_p) nl |
|
346 |
in (new_nl, Instance.setBoth inst old_sdx old_pdx, old_sdx, old_pdx) |
|
347 |
|
|
348 |
-- Replace the primary (f:, r:np, f) |
|
349 |
applyMove nl inst (ReplacePrimary new_pdx) = |
|
350 |
let old_pdx = Instance.pnode inst |
|
351 |
old_sdx = Instance.snode inst |
|
352 |
old_p = Container.find old_pdx nl |
|
353 |
old_s = Container.find old_sdx nl |
|
354 |
tgt_n = Container.find new_pdx nl |
|
355 |
int_p = Node.removePri old_p inst |
|
356 |
int_s = Node.removeSec old_s inst |
|
357 |
new_p = Node.addPri tgt_n inst |
|
358 |
new_s = Node.addSec int_s inst new_pdx |
|
359 |
new_nl = if isNothing(new_p) || isNothing(new_s) then Nothing |
|
360 |
else Just $ Container.add new_pdx (fromJust new_p) $ |
|
361 |
Container.addTwo old_pdx int_p |
|
362 |
old_sdx (fromJust new_s) nl |
|
363 |
in (new_nl, Instance.setPri inst new_pdx, new_pdx, old_sdx) |
|
364 |
|
|
365 |
-- Replace the secondary (r:ns) |
|
366 |
applyMove nl inst (ReplaceSecondary new_sdx) = |
|
367 |
let old_pdx = Instance.pnode inst |
|
368 |
old_sdx = Instance.snode inst |
|
369 |
old_s = Container.find old_sdx nl |
|
370 |
tgt_n = Container.find new_sdx nl |
|
371 |
int_s = Node.removeSec old_s inst |
|
372 |
new_s = Node.addSec tgt_n inst old_pdx |
|
373 |
new_nl = if isNothing(new_s) then Nothing |
|
374 |
else Just $ Container.addTwo new_sdx (fromJust new_s) |
|
375 |
old_sdx int_s nl |
|
376 |
in (new_nl, Instance.setSec inst new_sdx, old_pdx, new_sdx) |
|
377 |
|
|
378 |
-- Replace the secondary and failover (r:np, f) |
|
379 |
applyMove nl inst (ReplaceAndFailover new_pdx) = |
|
380 |
let old_pdx = Instance.pnode inst |
|
381 |
old_sdx = Instance.snode inst |
|
382 |
old_p = Container.find old_pdx nl |
|
383 |
old_s = Container.find old_sdx nl |
|
384 |
tgt_n = Container.find new_pdx nl |
|
385 |
int_p = Node.removePri old_p inst |
|
386 |
int_s = Node.removeSec old_s inst |
|
387 |
new_p = Node.addPri tgt_n inst |
|
388 |
new_s = Node.addSec int_p inst new_pdx |
|
389 |
new_nl = if isNothing(new_p) || isNothing(new_s) then Nothing |
|
390 |
else Just $ Container.add new_pdx (fromJust new_p) $ |
|
391 |
Container.addTwo old_pdx (fromJust new_s) |
|
392 |
old_sdx int_s nl |
|
393 |
in (new_nl, Instance.setBoth inst new_pdx old_pdx, new_pdx, old_pdx) |
|
394 |
|
|
395 |
-- Failver and replace the secondary (f, r:ns) |
|
396 |
applyMove nl inst (FailoverAndReplace new_sdx) = |
|
397 |
let old_pdx = Instance.pnode inst |
|
398 |
old_sdx = Instance.snode inst |
|
399 |
old_p = Container.find old_pdx nl |
|
400 |
old_s = Container.find old_sdx nl |
|
401 |
tgt_n = Container.find new_sdx nl |
|
402 |
int_p = Node.removePri old_p inst |
|
403 |
int_s = Node.removeSec old_s inst |
|
404 |
new_p = Node.addPri int_s inst |
|
405 |
new_s = Node.addSec tgt_n inst old_sdx |
|
406 |
new_nl = if isNothing(new_p) || isNothing(new_s) then Nothing |
|
407 |
else Just $ Container.add new_sdx (fromJust new_s) $ |
|
408 |
Container.addTwo old_sdx (fromJust new_p) |
|
409 |
old_pdx int_p nl |
|
410 |
in (new_nl, Instance.setBoth inst old_sdx new_sdx, old_sdx, new_sdx) |
|
411 |
|
|
412 |
checkSingleStep :: Table -- ^ The original table |
|
413 |
-> Instance.Instance -- ^ The instance to move |
|
414 |
-> Table -- ^ The current best table |
|
415 |
-> IMove -- ^ The move to apply |
|
416 |
-> Table -- ^ The final best table |
|
417 |
checkSingleStep ini_tbl target cur_tbl move = |
|
418 |
let |
|
419 |
Table ini_nl ini_il _ ini_plc = ini_tbl |
|
420 |
(tmp_nl, new_inst, pri_idx, sec_idx) = applyMove ini_nl target move |
|
421 |
in |
|
422 |
if isNothing tmp_nl then cur_tbl |
|
423 |
else |
|
424 |
let tgt_idx = Instance.idx target |
|
425 |
upd_nl = fromJust tmp_nl |
|
426 |
upd_cvar = compCV upd_nl |
|
427 |
upd_il = Container.add tgt_idx new_inst ini_il |
|
428 |
upd_plc = (tgt_idx, pri_idx, sec_idx, upd_cvar):ini_plc |
|
429 |
upd_tbl = Table upd_nl upd_il upd_cvar upd_plc |
|
430 |
in |
|
431 |
compareTables cur_tbl upd_tbl |
|
432 |
|
|
433 |
checkInstanceMove :: [Int] -- Allowed target node indices |
|
434 |
-> Table -- Original table |
|
435 |
-> Instance.Instance -- Instance to move |
|
436 |
-> Table -- Best new table for this instance |
|
437 |
checkInstanceMove nodes_idx ini_tbl target = |
|
438 |
let |
|
439 |
opdx = Instance.pnode target |
|
440 |
osdx = Instance.snode target |
|
441 |
nodes = filter (\idx -> idx /= opdx && idx /= osdx) nodes_idx |
|
442 |
aft_failover = checkSingleStep ini_tbl target ini_tbl Failover |
|
443 |
all_moves = concatMap (\idx -> [ReplacePrimary idx, |
|
444 |
ReplaceSecondary idx, |
|
445 |
ReplaceAndFailover idx, |
|
446 |
FailoverAndReplace idx]) nodes |
|
447 |
in |
|
448 |
-- iterate over the possible nodes for this instance |
|
449 |
foldl' (checkSingleStep ini_tbl target) aft_failover all_moves |
|
450 |
|
|
451 |
-- | Compute the best next move. |
|
452 |
checkMove :: [Int] -- ^ Allowed target node indices |
|
453 |
-> Table -- ^ The current solution |
|
454 |
-> [Instance.Instance] -- ^ List of instances still to move |
|
455 |
-> Table -- ^ The new solution |
|
456 |
checkMove nodes_idx ini_tbl victims = |
|
457 |
let Table _ _ _ ini_plc = ini_tbl |
|
458 |
-- iterate over all instances, computing the best move |
|
459 |
best_tbl = |
|
460 |
foldl' |
|
461 |
(\ step_tbl elem -> compareTables step_tbl $ |
|
462 |
checkInstanceMove nodes_idx ini_tbl elem) |
|
463 |
ini_tbl victims |
|
464 |
Table _ _ _ best_plc = best_tbl |
|
465 |
in |
|
466 |
if length best_plc == length ini_plc then -- no advancement |
|
467 |
ini_tbl |
|
468 |
else |
|
469 |
best_tbl |
|
470 |
|
|
471 |
{- | Auxiliary function for solution computation. |
|
472 |
|
|
473 |
We write this in an explicit recursive fashion in order to control |
|
474 |
early-abort in case we have met the min delta. We can't use foldr |
|
475 |
instead of explicit recursion since we need the accumulator for the |
|
476 |
abort decision. |
|
477 |
|
|
478 |
-} |
|
479 |
advanceSolution :: [Maybe Removal] -- ^ The removal to process |
|
480 |
-> Int -- ^ Minimum delta parameter |
|
481 |
-> Int -- ^ Maximum delta parameter |
|
482 |
-> Maybe Solution -- ^ Current best solution |
|
483 |
-> Maybe Solution -- ^ New best solution |
|
484 |
advanceSolution [] _ _ sol = sol |
|
485 |
advanceSolution (Nothing:xs) m n sol = advanceSolution xs m n sol |
|
486 |
advanceSolution ((Just (Removal nx removed)):xs) min_d max_d prev_sol = |
|
487 |
let new_sol = checkPlacement nx removed [] 0 prev_sol max_d |
|
488 |
new_delta = solutionDelta $! new_sol |
|
489 |
in |
|
490 |
if new_delta >= 0 && new_delta <= min_d then |
|
491 |
new_sol |
|
492 |
else |
|
493 |
advanceSolution xs min_d max_d new_sol |
|
494 |
|
|
495 |
-- | Computes the placement solution. |
|
496 |
solutionFromRemovals :: [Maybe Removal] -- ^ The list of (possible) removals |
|
497 |
-> Int -- ^ Minimum delta parameter |
|
498 |
-> Int -- ^ Maximum delta parameter |
|
499 |
-> Maybe Solution -- ^ The best solution found |
|
500 |
solutionFromRemovals removals min_delta max_delta = |
|
501 |
advanceSolution removals min_delta max_delta Nothing |
|
502 |
|
|
503 |
{- | Computes the solution at the given depth. |
|
504 |
|
|
505 |
This is a wrapper over both computeRemovals and |
|
506 |
solutionFromRemovals. In case we have no solution, we return Nothing. |
|
507 |
|
|
508 |
-} |
|
509 |
computeSolution :: NodeList -- ^ The original node data |
|
510 |
-> [Instance.Instance] -- ^ The list of /bad/ instances |
|
511 |
-> Int -- ^ The /depth/ of removals |
|
512 |
-> Int -- ^ Maximum number of removals to process |
|
513 |
-> Int -- ^ Minimum delta parameter |
|
514 |
-> Int -- ^ Maximum delta parameter |
|
515 |
-> Maybe Solution -- ^ The best solution found (or Nothing) |
|
516 |
computeSolution nl bad_instances depth max_removals min_delta max_delta = |
|
517 |
let |
|
518 |
removals = computeRemovals nl bad_instances depth |
|
519 |
removals' = capRemovals removals max_removals |
|
520 |
in |
|
521 |
solutionFromRemovals removals' min_delta max_delta |
|
522 |
|
|
523 |
-- Solution display functions (pure) |
|
524 |
|
|
525 |
-- | Given the original and final nodes, computes the relocation description. |
|
526 |
computeMoves :: String -- ^ The instance name |
|
527 |
-> String -- ^ Original primary |
|
528 |
-> String -- ^ Original secondary |
|
529 |
-> String -- ^ New primary |
|
530 |
-> String -- ^ New secondary |
|
531 |
-> (String, [String]) |
|
532 |
-- ^ Tuple of moves and commands list; moves is containing |
|
533 |
-- either @/f/@ for failover or @/r:name/@ for replace |
|
534 |
-- secondary, while the command list holds gnt-instance |
|
535 |
-- commands (without that prefix), e.g \"@failover instance1@\" |
|
536 |
computeMoves i a b c d = |
|
537 |
if c == a then {- Same primary -} |
|
538 |
if d == b then {- Same sec??! -} |
|
539 |
("-", []) |
|
540 |
else {- Change of secondary -} |
|
541 |
(printf "r:%s" d, |
|
542 |
[printf "replace-disks -n %s %s" d i]) |
|
543 |
else |
|
544 |
if c == b then {- Failover and ... -} |
|
545 |
if d == a then {- that's all -} |
|
546 |
("f", [printf "migrate %s" i]) |
|
547 |
else |
|
548 |
(printf "f r:%s" d, |
|
549 |
[printf "migrate %s" i, |
|
550 |
printf "replace-disks -n %s %s" d i]) |
|
551 |
else |
|
552 |
if d == a then {- ... and keep primary as secondary -} |
|
553 |
(printf "r:%s f" c, |
|
554 |
[printf "replace-disks -n %s %s" c i, |
|
555 |
printf "migrate %s" i]) |
|
556 |
else |
|
557 |
if d == b then {- ... keep same secondary -} |
|
558 |
(printf "f r:%s f" c, |
|
559 |
[printf "migrate %s" i, |
|
560 |
printf "replace-disks -n %s %s" c i, |
|
561 |
printf "migrate %s" i]) |
|
562 |
|
|
563 |
else {- Nothing in common -} |
|
564 |
(printf "r:%s f r:%s" c d, |
|
565 |
[printf "replace-disks -n %s %s" c i, |
|
566 |
printf "migrate %s" i, |
|
567 |
printf "replace-disks -n %s %s" d i]) |
|
568 |
|
|
569 |
{-| Converts a placement to string format -} |
|
570 |
printSolutionLine :: InstanceList |
|
571 |
-> [(Int, String)] |
|
572 |
-> [(Int, String)] |
|
573 |
-> Int |
|
574 |
-> Int |
|
575 |
-> Placement |
|
576 |
-> Int |
|
577 |
-> (String, [String]) |
|
578 |
printSolutionLine il ktn kti nmlen imlen plc pos = |
|
579 |
let |
|
580 |
pmlen = (2*nmlen + 1) |
|
581 |
(i, p, s, c) = plc |
|
582 |
inst = Container.find i il |
|
583 |
inam = fromJust $ lookup (Instance.idx inst) kti |
|
584 |
npri = fromJust $ lookup p ktn |
|
585 |
nsec = fromJust $ lookup s ktn |
|
586 |
opri = fromJust $ lookup (Instance.pnode inst) ktn |
|
587 |
osec = fromJust $ lookup (Instance.snode inst) ktn |
|
588 |
(moves, cmds) = computeMoves inam opri osec npri nsec |
|
589 |
ostr = (printf "%s:%s" opri osec)::String |
|
590 |
nstr = (printf "%s:%s" npri nsec)::String |
|
591 |
in |
|
592 |
(printf " %3d. %-*s %-*s => %-*s %.8f a=%s" |
|
593 |
pos imlen inam pmlen ostr |
|
594 |
pmlen nstr c moves, |
|
595 |
cmds) |
|
596 |
|
|
597 |
formatCmds :: [[String]] -> String |
|
598 |
formatCmds cmd_strs = |
|
599 |
unlines $ map (" echo " ++) $ |
|
600 |
concat $ map (\(a, b) -> |
|
601 |
(printf "step %d" (a::Int)):(map ("gnt-instance " ++) b)) $ |
|
602 |
zip [1..] cmd_strs |
|
603 |
|
|
604 |
{-| Converts a solution to string format -} |
|
605 |
printSolution :: InstanceList |
|
606 |
-> [(Int, String)] |
|
607 |
-> [(Int, String)] |
|
608 |
-> [Placement] |
|
609 |
-> ([String], [[String]]) |
|
610 |
printSolution il ktn kti sol = |
|
611 |
let |
|
612 |
mlen_fn = maximum . (map length) . snd . unzip |
|
613 |
imlen = mlen_fn kti |
|
614 |
nmlen = mlen_fn ktn |
|
615 |
in |
|
616 |
unzip $ map (uncurry $ printSolutionLine il ktn kti nmlen imlen) $ |
|
617 |
zip sol [1..] |
|
618 |
|
|
619 |
-- | Print the node list. |
|
620 |
printNodes :: [(Int, String)] -> NodeList -> String |
|
621 |
printNodes ktn nl = |
|
622 |
let snl = sortBy (compare `on` Node.idx) (Container.elems nl) |
|
623 |
snl' = map (\ n -> ((fromJust $ lookup (Node.idx n) ktn), n)) snl |
|
624 |
m_name = maximum . (map length) . fst . unzip $ snl' |
|
625 |
helper = Node.list m_name |
|
626 |
header = printf "%2s %-*s %5s %5s %5s %5s %5s %3s %3s %7s %7s" |
|
627 |
"N1" m_name "Name" "t_mem" "f_mem" "r_mem" |
|
628 |
"t_dsk" "f_dsk" |
|
629 |
"pri" "sec" "p_fmem" "p_fdsk" |
|
630 |
in unlines $ (header:map (uncurry helper) snl') |
|
631 |
|
|
632 |
-- | Compute the mem and disk covariance. |
|
633 |
compDetailedCV :: NodeList -> (Double, Double, Double, Double) |
|
634 |
compDetailedCV nl = |
|
635 |
let |
|
636 |
nodes = Container.elems nl |
|
637 |
mem_l = map Node.p_mem nodes |
|
638 |
dsk_l = map Node.p_dsk nodes |
|
639 |
mem_cv = varianceCoeff mem_l |
|
640 |
dsk_cv = varianceCoeff dsk_l |
|
641 |
n1_l = length $ filter Node.failN1 nodes |
|
642 |
n1_score = (fromIntegral n1_l) / (fromIntegral $ length nodes) |
|
643 |
res_l = map Node.p_rem nodes |
|
644 |
res_cv = varianceCoeff res_l |
|
645 |
in (mem_cv, dsk_cv, n1_score, res_cv) |
|
646 |
|
|
647 |
-- | Compute the 'total' variance. |
|
648 |
compCV :: NodeList -> Double |
|
649 |
compCV nl = |
|
650 |
let (mem_cv, dsk_cv, n1_score, res_cv) = compDetailedCV nl |
|
651 |
in mem_cv + dsk_cv + n1_score + res_cv |
|
652 |
|
|
653 |
printStats :: NodeList -> String |
|
654 |
printStats nl = |
|
655 |
let (mem_cv, dsk_cv, n1_score, res_cv) = compDetailedCV nl |
|
656 |
in printf "f_mem=%.8f, r_mem=%.8f, f_dsk=%.8f, n1=%.3f" |
|
657 |
mem_cv res_cv dsk_cv n1_score |
|
658 |
|
|
659 |
-- Balancing functions |
|
660 |
|
|
661 |
-- Loading functions |
|
662 |
|
|
663 |
{- | Convert newline and delimiter-separated text. |
|
664 |
|
|
665 |
This function converts a text in tabular format as generated by |
|
666 |
@gnt-instance list@ and @gnt-node list@ to a list of objects using a |
|
667 |
supplied conversion function. |
|
668 |
|
|
669 |
-} |
|
670 |
loadTabular :: String -> ([String] -> (String, a)) |
|
671 |
-> (a -> Int -> a) -> ([(String, Int)], [(Int, a)]) |
|
672 |
loadTabular text_data convert_fn set_fn = |
|
673 |
let lines_data = lines text_data |
|
674 |
rows = map (sepSplit '|') lines_data |
|
675 |
kerows = (map convert_fn rows) |
|
676 |
idxrows = map (\ (idx, (k, v)) -> ((k, idx), (idx, set_fn v idx))) |
|
677 |
(zip [0..] kerows) |
|
678 |
in unzip idxrows |
|
679 |
|
|
680 |
-- | For each instance, add its index to its primary and secondary nodes |
|
681 |
fixNodes :: [(Int, Node.Node)] |
|
682 |
-> [(Int, Instance.Instance)] |
|
683 |
-> [(Int, Node.Node)] |
|
684 |
fixNodes nl il = |
|
685 |
foldl' (\accu (idx, inst) -> |
|
686 |
let |
|
687 |
assocEqual = (\ (i, _) (j, _) -> i == j) |
|
688 |
pdx = Instance.pnode inst |
|
689 |
sdx = Instance.snode inst |
|
690 |
pold = fromJust $ lookup pdx accu |
|
691 |
sold = fromJust $ lookup sdx accu |
|
692 |
pnew = Node.setPri pold idx |
|
693 |
snew = Node.setSec sold idx |
|
694 |
ac1 = deleteBy assocEqual (pdx, pold) accu |
|
695 |
ac2 = deleteBy assocEqual (sdx, sold) ac1 |
|
696 |
ac3 = (pdx, pnew):(sdx, snew):ac2 |
|
697 |
in ac3) nl il |
|
698 |
|
|
699 |
-- | Compute the longest common suffix of a [(Int, String)] list that |
|
700 |
-- | starts with a dot |
|
701 |
longestDomain :: [(Int, String)] -> String |
|
702 |
longestDomain [] = "" |
|
703 |
longestDomain ((_,x):xs) = |
|
704 |
let |
|
705 |
onlyStrings = snd $ unzip xs |
|
706 |
in |
|
707 |
foldr (\ suffix accu -> if all (isSuffixOf suffix) onlyStrings |
|
708 |
then suffix |
|
709 |
else accu) |
|
710 |
"" $ filter (isPrefixOf ".") (tails x) |
|
711 |
|
|
712 |
-- | Remove tails from the (Int, String) lists |
|
713 |
stripSuffix :: String -> [(Int, String)] -> [(Int, String)] |
|
714 |
stripSuffix suffix lst = |
|
715 |
let sflen = length suffix in |
|
716 |
map (\ (key, name) -> (key, take ((length name) - sflen) name)) lst |
|
717 |
|
|
718 |
{-| Initializer function that loads the data from a node and list file |
|
719 |
and massages it into the correct format. -} |
|
720 |
loadData :: String -- ^ Node data in text format |
|
721 |
-> String -- ^ Instance data in text format |
|
722 |
-> (Container.Container Node.Node, |
|
723 |
Container.Container Instance.Instance, |
|
724 |
String, [(Int, String)], [(Int, String)]) |
|
725 |
loadData ndata idata = |
|
726 |
let |
|
727 |
{- node file: name mem disk -} |
|
728 |
(ktn, nl) = loadTabular ndata |
|
729 |
(\ (i:jt:jf:kt:kf:[]) -> (i, Node.create jt jf kt kf)) |
|
730 |
Node.setIdx |
|
731 |
{- instance file: name mem disk -} |
|
732 |
(kti, il) = loadTabular idata |
|
733 |
(\ (i:j:k:l:m:[]) -> (i, |
|
734 |
Instance.create j k |
|
735 |
(fromJust $ lookup l ktn) |
|
736 |
(fromJust $ lookup m ktn))) |
|
737 |
Instance.setIdx |
|
738 |
nl2 = fixNodes nl il |
|
739 |
il3 = Container.fromAssocList il |
|
740 |
nl3 = Container.fromAssocList |
|
741 |
(map (\ (k, v) -> (k, Node.buildPeers v il3 (length nl2))) nl2) |
|
742 |
xtn = swapPairs ktn |
|
743 |
xti = swapPairs kti |
|
744 |
common_suffix = longestDomain (xti ++ xtn) |
|
745 |
stn = stripSuffix common_suffix xtn |
|
746 |
sti = stripSuffix common_suffix xti |
|
747 |
in |
|
748 |
(nl3, il3, common_suffix, stn, sti) |
b/Ganeti/HTools/Container.hs | ||
---|---|---|
1 |
{-| Module abstracting the node and instance container implementation. |
|
2 |
|
|
3 |
This is currently implemented on top of an 'IntMap', which seems to |
|
4 |
give the best performance for our workload. |
|
5 |
|
|
6 |
-} |
|
7 |
|
|
8 |
module Ganeti.HTools.Container |
|
9 |
( |
|
10 |
-- * Types |
|
11 |
Container |
|
12 |
-- * Creation |
|
13 |
, empty |
|
14 |
, fromAssocList |
|
15 |
-- * Query |
|
16 |
, size |
|
17 |
, find |
|
18 |
-- * Update |
|
19 |
, add |
|
20 |
, addTwo |
|
21 |
, remove |
|
22 |
-- * Conversion |
|
23 |
, elems |
|
24 |
, keys |
|
25 |
) where |
|
26 |
|
|
27 |
import qualified Data.IntMap as IntMap |
|
28 |
|
|
29 |
type Key = IntMap.Key |
|
30 |
type Container = IntMap.IntMap |
|
31 |
|
|
32 |
-- | Create an empty container. |
|
33 |
empty :: Container a |
|
34 |
empty = IntMap.empty |
|
35 |
|
|
36 |
-- | Returns the number of elements in the map. |
|
37 |
size :: Container a -> Int |
|
38 |
size = IntMap.size |
|
39 |
|
|
40 |
-- | Locate a key in the map (must exist). |
|
41 |
find :: Key -> Container a -> a |
|
42 |
find k c = c IntMap.! k |
|
43 |
|
|
44 |
-- | Locate a keyin the map returning a default value if not existing. |
|
45 |
findWithDefault :: a -> Key -> Container a -> a |
|
46 |
findWithDefault = IntMap.findWithDefault |
|
47 |
|
|
48 |
-- | Add or update one element to the map. |
|
49 |
add :: Key -> a -> Container a -> Container a |
|
50 |
add k v c = IntMap.insert k v c |
|
51 |
|
|
52 |
-- | Remove an element from the map. |
|
53 |
remove :: Key -> Container a -> Container a |
|
54 |
remove = IntMap.delete |
|
55 |
|
|
56 |
-- | Return the list of values in the map. |
|
57 |
elems :: Container a -> [a] |
|
58 |
elems = IntMap.elems |
|
59 |
|
|
60 |
-- | Return the list of keys in the map. |
|
61 |
keys :: Container a -> [Key] |
|
62 |
keys = IntMap.keys |
|
63 |
|
|
64 |
-- | Create a map from an association list. |
|
65 |
fromAssocList :: [(Key, a)] -> Container a |
|
66 |
fromAssocList = IntMap.fromList |
|
67 |
|
|
68 |
-- | Create a map from an association list with a combining function. |
|
69 |
fromListWith :: (a -> a -> a) -> [(Key, a)] -> Container a |
|
70 |
fromListWith = IntMap.fromListWith |
|
71 |
|
|
72 |
-- | Fold over the values of the map. |
|
73 |
fold :: (a -> b -> b) -> b -> Container a -> b |
|
74 |
fold = IntMap.fold |
|
75 |
|
|
76 |
-- | Add or update two elements of the map. |
|
77 |
addTwo :: Key -> a -> Key -> a -> Container a -> Container a |
|
78 |
addTwo k1 v1 k2 v2 c = add k1 v1 $ add k2 v2 c |
b/Ganeti/HTools/Instance.hs | ||
---|---|---|
1 |
{-| Module describing an instance. |
|
2 |
|
|
3 |
The instance data type holds very few fields, the algorithm |
|
4 |
intelligence is in the "Node" and "Cluster" modules. |
|
5 |
|
|
6 |
-} |
|
7 |
module Ganeti.HTools.Instance where |
|
8 |
|
|
9 |
data Instance = Instance { mem :: Int -- ^ memory of the instance |
|
10 |
, dsk :: Int -- ^ disk size of instance |
|
11 |
, pnode :: Int -- ^ original primary node |
|
12 |
, snode :: Int -- ^ original secondary node |
|
13 |
, idx :: Int -- ^ internal index for book-keeping |
|
14 |
} deriving (Show) |
|
15 |
|
|
16 |
create :: String -> String -> Int -> Int -> Instance |
|
17 |
create mem_init dsk_init pn sn = Instance { |
|
18 |
mem = read mem_init, |
|
19 |
dsk = read dsk_init, |
|
20 |
pnode = pn, |
|
21 |
snode = sn, |
|
22 |
idx = -1 |
|
23 |
} |
|
24 |
|
|
25 |
-- | Changes the primary node of the instance. |
|
26 |
setPri :: Instance -- ^ the original instance |
|
27 |
-> Int -- ^ the new primary node |
|
28 |
-> Instance -- ^ the modified instance |
|
29 |
setPri t p = t { pnode = p } |
|
30 |
|
|
31 |
-- | Changes the secondary node of the instance. |
|
32 |
setSec :: Instance -- ^ the original instance |
|
33 |
-> Int -- ^ the new secondary node |
|
34 |
-> Instance -- ^ the modified instance |
|
35 |
setSec t s = t { snode = s } |
|
36 |
|
|
37 |
-- | Changes both nodes of the instance. |
|
38 |
setBoth :: Instance -- ^ the original instance |
|
39 |
-> Int -- ^ new primary node index |
|
40 |
-> Int -- ^ new secondary node index |
|
41 |
-> Instance -- ^ the modified instance |
|
42 |
setBoth t p s = t { pnode = p, snode = s } |
|
43 |
|
|
44 |
-- | Changes the index. |
|
45 |
-- This is used only during the building of the data structures. |
|
46 |
setIdx :: Instance -- ^ the original instance |
|
47 |
-> Int -- ^ new index |
|
48 |
-> Instance -- ^ the modified instance |
|
49 |
setIdx t i = t { idx = i } |
b/Ganeti/HTools/Node.hs | ||
---|---|---|
1 |
{-| Module describing a node. |
|
2 |
|
|
3 |
All updates are functional (copy-based) and return a new node with |
|
4 |
updated value. |
|
5 |
-} |
|
6 |
|
|
7 |
module Ganeti.HTools.Node |
|
8 |
( |
|
9 |
Node(failN1, idx, f_mem, f_dsk, p_mem, p_dsk, slist, plist, p_rem) |
|
10 |
-- * Constructor |
|
11 |
, create |
|
12 |
-- ** Finalization after data loading |
|
13 |
, buildPeers |
|
14 |
, setIdx |
|
15 |
-- * Instance (re)location |
|
16 |
, removePri |
|
17 |
, removeSec |
|
18 |
, addPri |
|
19 |
, addSec |
|
20 |
, setPri |
|
21 |
, setSec |
|
22 |
-- * Formatting |
|
23 |
, list |
|
24 |
) where |
|
25 |
|
|
26 |
import Data.List |
|
27 |
import Text.Printf (printf) |
|
28 |
|
|
29 |
import qualified Ganeti.HTools.Container as Container |
|
30 |
import qualified Ganeti.HTools.Instance as Instance |
|
31 |
import qualified Ganeti.HTools.PeerMap as PeerMap |
|
32 |
|
|
33 |
import Ganeti.HTools.Utils |
|
34 |
|
|
35 |
data Node = Node { t_mem :: Double -- ^ total memory (Mib) |
|
36 |
, f_mem :: Int -- ^ free memory (MiB) |
|
37 |
, t_dsk :: Double -- ^ total disk space (MiB) |
|
38 |
, f_dsk :: Int -- ^ free disk space (MiB) |
|
39 |
, plist :: [Int] -- ^ list of primary instance indices |
|
40 |
, slist :: [Int] -- ^ list of secondary instance indices |
|
41 |
, idx :: Int -- ^ internal index for book-keeping |
|
42 |
, peers:: PeerMap.PeerMap -- ^ primary node to instance |
|
43 |
-- mapping |
|
44 |
, failN1:: Bool -- ^ whether the node has failed n1 |
|
45 |
, r_mem :: Int -- ^ maximum memory needed for |
|
46 |
-- failover by primaries of this node |
|
47 |
, p_mem :: Double |
|
48 |
, p_dsk :: Double |
|
49 |
, p_rem :: Double |
|
50 |
} deriving (Show) |
|
51 |
|
|
52 |
{- | Create a new node. |
|
53 |
|
|
54 |
The index and the peers maps are empty, and will be need to be update |
|
55 |
later via the 'setIdx' and 'buildPeers' functions. |
|
56 |
|
|
57 |
-} |
|
58 |
create :: String -> String -> String -> String -> Node |
|
59 |
create mem_t_init mem_f_init dsk_t_init dsk_f_init = |
|
60 |
let mem_t = read mem_t_init |
|
61 |
mem_f = read mem_f_init |
|
62 |
dsk_t = read dsk_t_init |
|
63 |
dsk_f = read dsk_f_init |
|
64 |
in |
|
65 |
Node |
|
66 |
{ |
|
67 |
t_mem = read mem_t_init, |
|
68 |
f_mem = read mem_f_init, |
|
69 |
t_dsk = read dsk_t_init, |
|
70 |
f_dsk = read dsk_f_init, |
|
71 |
plist = [], |
|
72 |
slist = [], |
|
73 |
failN1 = True, |
|
74 |
idx = -1, |
|
75 |
peers = PeerMap.empty, |
|
76 |
r_mem = 0, |
|
77 |
p_mem = (fromIntegral mem_f) / (fromIntegral mem_t), |
|
78 |
p_dsk = (fromIntegral dsk_f) / (fromIntegral dsk_t), |
|
79 |
p_rem = 0 |
|
80 |
} |
|
81 |
|
|
82 |
-- | Changes the index. |
|
83 |
-- This is used only during the building of the data structures. |
|
84 |
setIdx :: Node -> Int -> Node |
|
85 |
setIdx t i = t {idx = i} |
|
86 |
|
|
87 |
-- | Given the rmem, free memory and disk, computes the failn1 status. |
|
88 |
computeFailN1 :: Int -> Int -> Int -> Bool |
|
89 |
computeFailN1 new_rmem new_mem new_dsk = |
|
90 |
new_mem <= new_rmem || new_dsk <= 0 |
|
91 |
|
|
92 |
-- | Given the new free memory and disk, fail if any of them is below zero. |
|
93 |
failHealth :: Int -> Int -> Bool |
|
94 |
failHealth new_mem new_dsk = new_mem <= 0 || new_dsk <= 0 |
|
95 |
|
|
96 |
-- | Computes the maximum reserved memory for peers from a peer map. |
|
97 |
computeMaxRes :: PeerMap.PeerMap -> PeerMap.Elem |
|
98 |
computeMaxRes new_peers = PeerMap.maxElem new_peers |
|
99 |
|
|
100 |
-- | Builds the peer map for a given node. |
|
101 |
buildPeers :: Node -> Container.Container Instance.Instance -> Int -> Node |
|
102 |
buildPeers t il num_nodes = |
|
103 |
let mdata = map |
|
104 |
(\i_idx -> let inst = Container.find i_idx il |
|
105 |
in (Instance.pnode inst, Instance.mem inst)) |
|
106 |
(slist t) |
|
107 |
pmap = PeerMap.accumArray (+) 0 (0, num_nodes - 1) mdata |
|
108 |
new_rmem = computeMaxRes pmap |
|
109 |
new_failN1 = computeFailN1 new_rmem (f_mem t) (f_dsk t) |
|
110 |
new_prem = (fromIntegral new_rmem) / (t_mem t) |
|
111 |
in t {peers=pmap, failN1 = new_failN1, r_mem = new_rmem, p_rem = new_prem} |
|
112 |
|
|
113 |
-- | Removes a primary instance. |
|
114 |
removePri :: Node -> Instance.Instance -> Node |
|
115 |
removePri t inst = |
|
116 |
let iname = Instance.idx inst |
|
117 |
new_plist = delete iname (plist t) |
|
118 |
new_mem = f_mem t + Instance.mem inst |
|
119 |
new_dsk = f_dsk t + Instance.dsk inst |
|
120 |
new_mp = (fromIntegral new_mem) / (t_mem t) |
|
121 |
new_dp = (fromIntegral new_dsk) / (t_dsk t) |
|
122 |
new_failn1 = computeFailN1 (r_mem t) new_mem new_dsk |
|
123 |
in t {plist = new_plist, f_mem = new_mem, f_dsk = new_dsk, |
|
124 |
failN1 = new_failn1, p_mem = new_mp, p_dsk = new_dp} |
|
125 |
|
|
126 |
-- | Removes a secondary instance. |
|
127 |
removeSec :: Node -> Instance.Instance -> Node |
|
128 |
removeSec t inst = |
|
129 |
let iname = Instance.idx inst |
|
130 |
pnode = Instance.pnode inst |
|
131 |
new_slist = delete iname (slist t) |
|
132 |
new_dsk = f_dsk t + Instance.dsk inst |
|
133 |
old_peers = peers t |
|
134 |
old_peem = PeerMap.find pnode old_peers |
|
135 |
new_peem = old_peem - (Instance.mem inst) |
|
136 |
new_peers = PeerMap.add pnode new_peem old_peers |
|
137 |
old_rmem = r_mem t |
|
138 |
new_rmem = if old_peem < old_rmem then |
|
139 |
old_rmem |
|
140 |
else |
|
141 |
computeMaxRes new_peers |
|
142 |
new_prem = (fromIntegral new_rmem) / (t_mem t) |
|
143 |
new_failn1 = computeFailN1 new_rmem (f_mem t) new_dsk |
|
144 |
new_dp = (fromIntegral new_dsk) / (t_dsk t) |
|
145 |
in t {slist = new_slist, f_dsk = new_dsk, peers = new_peers, |
|
146 |
failN1 = new_failn1, r_mem = new_rmem, p_dsk = new_dp, |
|
147 |
p_rem = new_prem} |
|
148 |
|
|
149 |
-- | Adds a primary instance. |
|
150 |
addPri :: Node -> Instance.Instance -> Maybe Node |
|
151 |
addPri t inst = |
|
152 |
let iname = Instance.idx inst |
|
153 |
new_mem = f_mem t - Instance.mem inst |
|
154 |
new_dsk = f_dsk t - Instance.dsk inst |
|
155 |
new_failn1 = computeFailN1 (r_mem t) new_mem new_dsk in |
|
156 |
if (failHealth new_mem new_dsk) || (new_failn1 && not (failN1 t)) then |
|
157 |
Nothing |
|
158 |
else |
|
159 |
let new_plist = iname:(plist t) |
|
160 |
new_mp = (fromIntegral new_mem) / (t_mem t) |
|
161 |
new_dp = (fromIntegral new_dsk) / (t_dsk t) |
|
162 |
in |
|
163 |
Just t {plist = new_plist, f_mem = new_mem, f_dsk = new_dsk, |
|
164 |
failN1 = new_failn1, p_mem = new_mp, p_dsk = new_dp} |
|
165 |
|
|
166 |
-- | Adds a secondary instance. |
|
167 |
addSec :: Node -> Instance.Instance -> Int -> Maybe Node |
|
168 |
addSec t inst pdx = |
|
169 |
let iname = Instance.idx inst |
|
170 |
old_peers = peers t |
|
171 |
old_mem = f_mem t |
|
172 |
new_dsk = f_dsk t - Instance.dsk inst |
|
173 |
new_peem = PeerMap.find pdx old_peers + Instance.mem inst |
|
174 |
new_peers = PeerMap.add pdx new_peem old_peers |
|
175 |
new_rmem = max (r_mem t) new_peem |
|
176 |
new_prem = (fromIntegral new_rmem) / (t_mem t) |
|
177 |
new_failn1 = computeFailN1 new_rmem old_mem new_dsk in |
|
178 |
if (failHealth old_mem new_dsk) || (new_failn1 && not (failN1 t)) then |
|
179 |
Nothing |
|
180 |
else |
|
181 |
let new_slist = iname:(slist t) |
|
182 |
new_dp = (fromIntegral new_dsk) / (t_dsk t) |
|
183 |
in |
|
184 |
Just t {slist = new_slist, f_dsk = new_dsk, |
|
185 |
peers = new_peers, failN1 = new_failn1, |
|
186 |
r_mem = new_rmem, p_dsk = new_dp, |
|
187 |
p_rem = new_prem} |
|
188 |
|
|
189 |
-- | Add a primary instance to a node without other updates |
|
190 |
setPri :: Node -> Int -> Node |
|
191 |
setPri t idx = t { plist = idx:(plist t) } |
|
192 |
|
|
193 |
-- | Add a secondary instance to a node without other updates |
|
194 |
setSec :: Node -> Int -> Node |
|
195 |
setSec t idx = t { slist = idx:(slist t) } |
|
196 |
|
|
197 |
-- | Simple converter to string. |
|
198 |
str :: Node -> String |
|
199 |
str t = |
|
200 |
printf ("Node %d (mem=%5d MiB, disk=%5.2f GiB)\n Primaries:" ++ |
|
201 |
" %s\nSecondaries: %s") |
|
202 |
(idx t) (f_mem t) ((f_dsk t) `div` 1024) |
|
203 |
(commaJoin (map show (plist t))) |
|
204 |
(commaJoin (map show (slist t))) |
|
205 |
|
|
206 |
-- | String converter for the node list functionality. |
|
207 |
list :: Int -> String -> Node -> String |
|
208 |
list mname n t = |
|
209 |
let pl = plist t |
|
210 |
sl = slist t |
|
211 |
mp = p_mem t |
|
212 |
dp = p_dsk t |
|
213 |
fn = failN1 t |
|
214 |
in |
|
215 |
printf " %c %-*s %5.0f %5d %5d %5.0f %5d %3d %3d %.5f %.5f" |
|
216 |
(if fn then '*' else ' ') |
|
217 |
mname n (t_mem t) (f_mem t) (r_mem t) |
|
218 |
((t_dsk t) / 1024) ((f_dsk t) `div` 1024) |
|
219 |
(length pl) (length sl) |
|
220 |
mp dp |
b/Ganeti/HTools/PeerMap.hs | ||
---|---|---|
1 |
{-| |
|
2 |
Module abstracting the peer map implementation. |
|
3 |
|
|
4 |
This is abstracted separately since the speed of peermap updates can |
|
5 |
be a significant part of the total runtime, and as such changing the |
|
6 |
implementation should be easy in case it's needed. |
|
7 |
|
|
8 |
-} |
|
9 |
|
|
10 |
module Ganeti.HTools.PeerMap |
|
11 |
( |
|
12 |
PeerMap, |
|
13 |
Key, |
|
14 |
Elem, |
|
15 |
empty, |
|
16 |
create, |
|
17 |
accumArray, |
|
18 |
Ganeti.HTools.PeerMap.find, |
|
19 |
add, |
|
20 |
remove, |
|
21 |
maxElem |
|
22 |
) where |
|
23 |
|
|
24 |
import Data.Maybe (fromMaybe) |
|
25 |
import Data.List |
|
26 |
import Data.Function |
|
27 |
import Data.Ord |
|
28 |
|
|
29 |
type Key = Int |
|
30 |
type Elem = Int |
|
31 |
type PeerMap = [(Key, Elem)] |
|
32 |
|
|
33 |
empty :: PeerMap |
|
34 |
empty = [] |
|
35 |
|
|
36 |
create :: Key -> PeerMap |
|
37 |
create _ = [] |
|
38 |
|
|
39 |
-- | Our reverse-compare function |
|
40 |
pmCompare :: (Key, Elem) -> (Key, Elem) -> Ordering |
|
41 |
pmCompare a b = (compare `on` snd) b a |
|
42 |
|
|
43 |
addWith :: (Elem -> Elem -> Elem) -> Key -> Elem -> PeerMap -> PeerMap |
|
44 |
addWith fn k v lst = |
|
45 |
let r = lookup k lst |
|
46 |
in |
|
47 |
case r of |
|
48 |
Nothing -> insertBy pmCompare (k, v) lst |
|
49 |
Just o -> insertBy pmCompare (k, fn o v) (remove k lst) |
|
50 |
|
|
51 |
accumArray :: (Elem -> Elem -> Elem) -> Elem -> (Key, Key) -> |
|
52 |
[(Key, Elem)] -> PeerMap |
|
53 |
accumArray fn _ _ lst = |
|
54 |
case lst of |
|
55 |
[] -> empty |
|
56 |
(k, v):xs -> addWith fn k v $ accumArray fn undefined undefined xs |
|
57 |
|
|
58 |
find :: Key -> PeerMap -> Elem |
|
59 |
find k c = fromMaybe 0 $ lookup k c |
|
60 |
|
|
61 |
add :: Key -> Elem -> PeerMap -> PeerMap |
|
62 |
add k v c = addWith (\_ n -> n) k v c |
|
63 |
|
|
64 |
remove :: Key -> PeerMap -> PeerMap |
|
65 |
remove k c = case c of |
|
66 |
[] -> [] |
|
67 |
(x@(x', _)):xs -> if k == x' then xs |
|
68 |
else x:(remove k xs) |
|
69 |
|
|
70 |
to_list :: PeerMap -> [Elem] |
|
71 |
to_list c = snd $ unzip c |
|
72 |
|
|
73 |
maxElem :: PeerMap -> Elem |
|
74 |
maxElem c = case c of |
|
75 |
[] -> 0 |
|
76 |
(_, v):_ -> v |
b/Ganeti/HTools/Rapi.hs | ||
---|---|---|
1 |
{-| Implementation of the RAPI client interface. |
|
2 |
|
|
3 |
-} |
|
4 |
|
|
5 |
module Ganeti.HTools.Rapi |
|
6 |
( |
|
7 |
getNodes |
|
8 |
, getInstances |
|
9 |
) where |
|
10 |
|
|
11 |
import Network.Curl |
|
12 |
import Network.Curl.Types () |
|
13 |
import Network.Curl.Code |
|
14 |
import Data.Either () |
|
15 |
import Data.Maybe |
|
16 |
import Control.Monad |
|
17 |
import Text.JSON |
|
18 |
import Text.Printf (printf) |
|
19 |
import Ganeti.HTools.Utils () |
|
20 |
|
|
21 |
|
|
22 |
{-- Our cheap monad-like stuff. |
|
23 |
|
|
24 |
Thi is needed since Either e a is already a monad instance somewhere |
|
25 |
in the standard libraries (Control.Monad.Error) and we don't need that |
|
26 |
entire thing. |
|
27 |
|
|
28 |
-} |
|
29 |
combine :: (Either String a) -> (a -> Either String b) -> (Either String b) |
|
30 |
combine (Left s) _ = Left s |
|
31 |
combine (Right s) f = f s |
|
32 |
|
|
33 |
ensureList :: [Either String a] -> Either String [a] |
|
34 |
ensureList lst = |
|
35 |
foldr (\elem accu -> |
|
36 |
case (elem, accu) of |
|
37 |
(Left x, _) -> Left x |
|
38 |
(_, Left x) -> Left x -- should never happen |
|
39 |
(Right e, Right a) -> Right (e:a) |
|
40 |
) |
|
41 |
(Right []) lst |
|
42 |
|
|
43 |
listHead :: Either String [a] -> Either String a |
|
44 |
listHead lst = |
|
45 |
case lst of |
|
46 |
Left x -> Left x |
|
47 |
Right (x:_) -> Right x |
|
48 |
Right [] -> Left "List empty" |
|
49 |
|
|
50 |
loadJSArray :: String -> Either String [JSObject JSValue] |
|
51 |
loadJSArray s = resultToEither $ decodeStrict s |
|
52 |
|
|
53 |
fromObj :: JSON a => String -> JSObject JSValue -> Either String a |
|
54 |
fromObj k o = |
|
55 |
case lookup k (fromJSObject o) of |
|
56 |
Nothing -> Left $ printf "key '%s' not found" k |
|
57 |
Just val -> resultToEither $ readJSON val |
|
58 |
|
|
59 |
getStringElement :: String -> JSObject JSValue -> Either String String |
|
60 |
getStringElement = fromObj |
|
61 |
|
|
62 |
getIntElement :: String -> JSObject JSValue -> Either String Int |
|
63 |
getIntElement = fromObj |
|
64 |
|
|
65 |
getListElement :: String -> JSObject JSValue |
|
66 |
-> Either String [JSValue] |
|
67 |
getListElement = fromObj |
|
68 |
|
|
69 |
readString :: JSValue -> Either String String |
|
70 |
readString v = |
|
71 |
case v of |
|
72 |
JSString s -> Right $ fromJSString s |
|
73 |
_ -> Left "Wrong JSON type" |
|
74 |
|
|
75 |
concatElems :: Either String String |
|
76 |
-> Either String String |
|
77 |
-> Either String String |
|
78 |
concatElems = apply2 (\x y -> x ++ "|" ++ y) |
|
79 |
|
|
80 |
apply1 :: (a -> b) -> Either String a -> Either String b |
|
81 |
apply1 fn a = |
|
82 |
case a of |
|
83 |
Left x -> Left x |
|
84 |
Right y -> Right $ fn y |
|
85 |
|
|
86 |
apply2 :: (a -> b -> c) |
|
87 |
-> Either String a |
|
88 |
-> Either String b |
|
89 |
-> Either String c |
|
90 |
apply2 fn a b = |
|
91 |
case (a, b) of |
|
92 |
(Right x, Right y) -> Right $ fn x y |
|
93 |
(Left x, _) -> Left x |
|
94 |
(_, Left y) -> Left y |
|
95 |
|
|
96 |
getUrl :: String -> IO (Either String String) |
|
97 |
getUrl url = do |
|
98 |
(code, body) <- curlGetString url [CurlSSLVerifyPeer False, |
|
99 |
CurlSSLVerifyHost 0] |
|
100 |
return (case code of |
|
101 |
CurlOK -> Right body |
|
102 |
_ -> Left $ printf "Curl error for '%s', error %s" |
|
103 |
url (show code)) |
|
104 |
|
|
105 |
tryRapi :: String -> String -> IO (Either String String) |
|
106 |
tryRapi url1 url2 = |
|
107 |
do |
|
108 |
body1 <- getUrl url1 |
|
109 |
(case body1 of |
|
110 |
Left _ -> getUrl url2 |
|
111 |
Right _ -> return body1) |
|
112 |
|
|
113 |
getInstances :: String -> IO (Either String String) |
|
114 |
getInstances master = |
|
115 |
let |
|
116 |
url2 = printf "https://%s:5080/2/instances?bulk=1" master |
|
117 |
url1 = printf "http://%s:5080/instances?bulk=1" master |
|
118 |
in do |
|
119 |
body <- tryRapi url1 url2 |
|
120 |
let inst = body `combine` loadJSArray `combine` (parseList parseInstance) |
|
121 |
return inst |
|
122 |
|
|
123 |
getNodes :: String -> IO (Either String String) |
|
124 |
getNodes master = |
|
125 |
let |
|
126 |
url2 = printf "https://%s:5080/2/nodes?bulk=1" master |
|
127 |
url1 = printf "http://%s:5080/nodes?bulk=1" master |
|
128 |
in do |
|
129 |
body <- tryRapi url1 url2 |
|
130 |
let inst = body `combine` loadJSArray `combine` (parseList parseNode) |
|
131 |
return inst |
|
132 |
|
|
133 |
parseList :: (JSObject JSValue -> Either String String) |
|
134 |
-> [JSObject JSValue] |
|
135 |
->Either String String |
|
136 |
parseList fn idata = |
|
137 |
let ml = ensureList $ map fn idata |
|
138 |
in ml `combine` (Right . unlines) |
|
139 |
|
|
140 |
parseInstance :: JSObject JSValue -> Either String String |
|
141 |
parseInstance a = |
|
142 |
let name = getStringElement "name" a |
|
143 |
disk = case getIntElement "disk_usage" a of |
|
144 |
Left _ -> apply2 (+) |
|
145 |
(getIntElement "sda_size" a) |
|
146 |
(getIntElement "sdb_size" a) |
|
147 |
Right x -> Right x |
|
148 |
bep = fromObj "beparams" a |
|
149 |
pnode = getStringElement "pnode" a |
|
150 |
snode = (listHead $ getListElement "snodes" a) `combine` readString |
|
151 |
mem = case bep of |
|
152 |
Left _ -> getIntElement "admin_ram" a |
|
153 |
Right o -> getIntElement "memory" o |
|
154 |
in |
|
155 |
concatElems name $ |
|
156 |
concatElems (show `apply1` mem) $ |
|
157 |
concatElems (show `apply1` disk) $ |
|
158 |
concatElems pnode snode |
|
159 |
|
|
160 |
parseNode :: JSObject JSValue -> Either String String |
|
161 |
parseNode a = |
|
162 |
let name = getStringElement "name" a |
|
163 |
mtotal = getIntElement "mtotal" a |
|
164 |
mfree = getIntElement "mfree" a |
|
165 |
dtotal = getIntElement "dtotal" a |
|
166 |
dfree = getIntElement "dfree" a |
|
167 |
in concatElems name $ |
|
168 |
concatElems (show `apply1` mtotal) $ |
|
169 |
concatElems (show `apply1` mfree) $ |
|
170 |
concatElems (show `apply1` dtotal) (show `apply1` dfree) |
b/Ganeti/HTools/Utils.hs | ||
---|---|---|
1 |
{-| Utility functions -} |
|
2 |
|
|
3 |
module Ganeti.HTools.Utils where |
|
4 |
|
|
5 |
import Data.Either |
|
6 |
import Data.List |
|
7 |
import qualified Data.Version |
|
8 |
import Monad |
|
9 |
import System |
|
10 |
import System.IO |
|
11 |
import System.Info |
|
12 |
import Text.Printf |
|
13 |
import qualified Ganeti.HTools.Version as Version |
|
14 |
|
|
15 |
import Debug.Trace |
|
16 |
|
|
17 |
-- | To be used only for debugging, breaks referential integrity. |
|
18 |
debug :: Show a => a -> a |
|
19 |
debug x = trace (show x) x |
|
20 |
|
|
21 |
-- | Check if the given argument is Left something |
|
22 |
isLeft :: Either a b -> Bool |
|
23 |
isLeft val = |
|
24 |
case val of |
|
25 |
Left _ -> True |
|
26 |
_ -> False |
|
27 |
|
|
28 |
fromLeft :: Either a b -> a |
|
29 |
fromLeft = either (\x -> x) (\_ -> undefined) |
|
30 |
|
|
31 |
fromRight :: Either a b -> b |
|
32 |
fromRight = either (\_ -> undefined) id |
|
33 |
|
|
34 |
-- | Comma-join a string list. |
|
35 |
commaJoin :: [String] -> String |
|
36 |
commaJoin = intercalate "," |
|
37 |
|
|
38 |
-- | Split a string on a separator and return an array. |
|
39 |
sepSplit :: Char -> String -> [String] |
|
40 |
sepSplit sep s |
|
41 |
| x == "" && xs == [] = [] |
|
42 |
| xs == [] = [x] |
|
43 |
| ys == [] = x:"":[] |
|
44 |
| otherwise = x:(sepSplit sep ys) |
|
45 |
where (x, xs) = break (== sep) s |
|
46 |
ys = drop 1 xs |
|
47 |
|
|
48 |
-- | Partial application of sepSplit to @'.'@ |
|
49 |
commaSplit :: String -> [String] |
|
50 |
commaSplit = sepSplit ',' |
|
51 |
|
|
52 |
-- | Swap a list of @(a, b)@ into @(b, a)@ |
|
53 |
swapPairs :: [(a, b)] -> [(b, a)] |
|
54 |
swapPairs = map (\ (a, b) -> (b, a)) |
|
55 |
|
|
56 |
-- Simple and slow statistical functions, please replace with better versions |
|
57 |
|
|
58 |
-- | Mean value of a list. |
|
59 |
meanValue :: Floating a => [a] -> a |
|
60 |
meanValue lst = (sum lst) / (fromIntegral $ length lst) |
|
61 |
|
|
62 |
-- | Standard deviation. |
|
63 |
stdDev :: Floating a => [a] -> a |
|
64 |
stdDev lst = |
|
65 |
let mv = meanValue lst |
|
66 |
square = (^ (2::Int)) -- silences "defaulting the constraint..." |
|
67 |
av = sum $ map square $ map (\e -> e - mv) lst |
|
68 |
bv = sqrt (av / (fromIntegral $ length lst)) |
|
69 |
in bv |
|
70 |
|
|
71 |
|
|
72 |
-- | Coefficient of variation. |
|
73 |
varianceCoeff :: Floating a => [a] -> a |
|
74 |
varianceCoeff lst = (stdDev lst) / (fromIntegral $ length lst) |
|
75 |
|
|
76 |
-- | Get a Right result or print the error and exit |
|
77 |
readData :: (String -> IO (Either String String)) -> String -> IO String |
|
78 |
readData fn host = do |
|
79 |
nd <- fn host |
|
80 |
when (isLeft nd) $ |
|
81 |
do |
|
82 |
putStrLn $ fromLeft nd |
|
83 |
exitWith $ ExitFailure 1 |
|
84 |
return $ fromRight nd |
|
85 |
|
|
86 |
showVersion :: String -- ^ The program name |
|
87 |
-> String -- ^ The formatted version and other information data |
|
88 |
showVersion name = |
|
89 |
printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n" |
|
90 |
name Version.version |
|
91 |
compilerName (Data.Version.showVersion compilerVersion) |
|
92 |
os arch |
b/Ganeti/HTools/Version.hs.in | ||
---|---|---|
1 |
module Ganeti.HTools.Version |
|
2 |
( |
|
3 |
version -- ^ the version of the tree |
|
4 |
) where |
|
5 |
|
|
6 |
version = "(htools) version %ver%" |
Also available in: Unified diff