Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / Loader.hs @ a7e1fd89

History | View | Annotate | Download (13.2 kB)

1
{-| Generic data loader.
2

    
3
This module holds the common code for parsing the input data after it
4
has been loaded from external sources.
5

    
6
-}
7

    
8
{-
9

    
10
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
11

    
12
This program is free software; you can redistribute it and/or modify
13
it under the terms of the GNU General Public License as published by
14
the Free Software Foundation; either version 2 of the License, or
15
(at your option) any later version.
16

    
17
This program is distributed in the hope that it will be useful, but
18
WITHOUT ANY WARRANTY; without even the implied warranty of
19
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
General Public License for more details.
21

    
22
You should have received a copy of the GNU General Public License
23
along with this program; if not, write to the Free Software
24
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
02110-1301, USA.
26

    
27
-}
28

    
29
module Ganeti.HTools.Loader
30
  ( mergeData
31
  , checkData
32
  , assignIndices
33
  , lookupName
34
  , goodLookupResult
35
  , lookupNode
36
  , lookupInstance
37
  , lookupGroup
38
  , commonSuffix
39
  , RqType(..)
40
  , Request(..)
41
  , ClusterData(..)
42
  , emptyCluster
43
  , compareNameComponent
44
  , prefixMatch
45
  , LookupResult(..)
46
  , MatchPriority(..)
47
  ) where
48

    
49
import Data.List
50
import Data.Function
51
import qualified Data.Map as M
52
import Text.Printf (printf)
53

    
54
import qualified Ganeti.HTools.Container as Container
55
import qualified Ganeti.HTools.Instance as Instance
56
import qualified Ganeti.HTools.Node as Node
57
import qualified Ganeti.HTools.Group as Group
58
import qualified Ganeti.HTools.Cluster as Cluster
59

    
60
import Ganeti.HTools.Types
61
import Ganeti.HTools.Utils
62

    
63
-- * Constants
64

    
65
-- | The exclusion tag prefix.
66
exTagsPrefix :: String
67
exTagsPrefix = "htools:iextags:"
68

    
69
-- * Types
70

    
71
{-| The iallocator request type.
72

    
73
This type denotes what request we got from Ganeti and also holds
74
request-specific fields.
75

    
76
-}
77
data RqType
78
  = Allocate Instance.Instance Int -- ^ A new instance allocation
79
  | Relocate Idx Int [Ndx]         -- ^ Choose a new secondary node
80
  | NodeEvacuate [Idx] EvacMode    -- ^ node-evacuate mode
81
  | ChangeGroup [Gdx] [Idx]        -- ^ Multi-relocate mode
82
    deriving (Show, Read)
83

    
84
-- | A complete request, as received from Ganeti.
85
data Request = Request RqType ClusterData
86
               deriving (Show, Read)
87

    
88
-- | The cluster state.
89
data ClusterData = ClusterData
90
  { cdGroups    :: Group.List    -- ^ The node group list
91
  , cdNodes     :: Node.List     -- ^ The node list
92
  , cdInstances :: Instance.List -- ^ The instance list
93
  , cdTags      :: [String]      -- ^ The cluster tags
94
  , cdIPolicy   :: IPolicy       -- ^ The cluster instance policy
95
  } deriving (Show, Read, Eq)
96

    
97
-- | The priority of a match in a lookup result.
98
data MatchPriority = ExactMatch
99
                   | MultipleMatch
100
                   | PartialMatch
101
                   | FailMatch
102
                   deriving (Show, Read, Enum, Eq, Ord)
103

    
104
-- | The result of a name lookup in a list.
105
data LookupResult = LookupResult
106
  { lrMatchPriority :: MatchPriority -- ^ The result type
107
  -- | Matching value (for ExactMatch, PartialMatch), Lookup string otherwise
108
  , lrContent :: String
109
  } deriving (Show, Read)
110

    
111
-- | Lookup results have an absolute preference ordering.
112
instance Eq LookupResult where
113
  (==) = (==) `on` lrMatchPriority
114

    
115
instance Ord LookupResult where
116
  compare = compare `on` lrMatchPriority
117

    
118
-- | An empty cluster.
119
emptyCluster :: ClusterData
120
emptyCluster = ClusterData Container.empty Container.empty Container.empty []
121
                 defIPolicy
122

    
123
-- * Functions
124

    
125
-- | Lookups a node into an assoc list.
126
lookupNode :: (Monad m) => NameAssoc -> String -> String -> m Ndx
127
lookupNode ktn inst node =
128
  case M.lookup node ktn of
129
    Nothing -> fail $ "Unknown node '" ++ node ++ "' for instance " ++ inst
130
    Just idx -> return idx
131

    
132
-- | Lookups an instance into an assoc list.
133
lookupInstance :: (Monad m) => NameAssoc -> String -> m Idx
134
lookupInstance kti inst =
135
  case M.lookup inst kti of
136
    Nothing -> fail $ "Unknown instance '" ++ inst ++ "'"
137
    Just idx -> return idx
138

    
139
-- | Lookups a group into an assoc list.
140
lookupGroup :: (Monad m) => NameAssoc -> String -> String -> m Gdx
141
lookupGroup ktg nname gname =
142
  case M.lookup gname ktg of
143
    Nothing -> fail $ "Unknown group '" ++ gname ++ "' for node " ++ nname
144
    Just idx -> return idx
145

    
146
-- | Check for prefix matches in names.
147
-- Implemented in Ganeti core utils.text.MatchNameComponent
148
-- as the regexp r"^%s(\..*)?$" % re.escape(key)
149
prefixMatch :: String  -- ^ Lookup
150
            -> String  -- ^ Full name
151
            -> Bool    -- ^ Whether there is a prefix match
152
prefixMatch = isPrefixOf . (++ ".")
153

    
154
-- | Is the lookup priority a "good" one?
155
goodMatchPriority :: MatchPriority -> Bool
156
goodMatchPriority ExactMatch = True
157
goodMatchPriority PartialMatch = True
158
goodMatchPriority _ = False
159

    
160
-- | Is the lookup result an actual match?
161
goodLookupResult :: LookupResult -> Bool
162
goodLookupResult = goodMatchPriority . lrMatchPriority
163

    
164
-- | Compares a canonical name and a lookup string.
165
compareNameComponent :: String        -- ^ Canonical (target) name
166
                     -> String        -- ^ Partial (lookup) name
167
                     -> LookupResult  -- ^ Result of the lookup
168
compareNameComponent cnl lkp =
169
  select (LookupResult FailMatch lkp)
170
  [ (cnl == lkp          , LookupResult ExactMatch cnl)
171
  , (prefixMatch lkp cnl , LookupResult PartialMatch cnl)
172
  ]
173

    
174
-- | Lookup a string and choose the best result.
175
chooseLookupResult :: String       -- ^ Lookup key
176
                   -> String       -- ^ String to compare to the lookup key
177
                   -> LookupResult -- ^ Previous result
178
                   -> LookupResult -- ^ New result
179
chooseLookupResult lkp cstr old =
180
  -- default: use class order to pick the minimum result
181
  select (min new old)
182
  -- special cases:
183
  -- short circuit if the new result is an exact match
184
  [ (lrMatchPriority new == ExactMatch, new)
185
  -- if both are partial matches generate a multiple match
186
  , (partial2, LookupResult MultipleMatch lkp)
187
  ] where new = compareNameComponent cstr lkp
188
          partial2 = all ((PartialMatch==) . lrMatchPriority) [old, new]
189

    
190
-- | Find the canonical name for a lookup string in a list of names.
191
lookupName :: [String]      -- ^ List of keys
192
           -> String        -- ^ Lookup string
193
           -> LookupResult  -- ^ Result of the lookup
194
lookupName l s = foldr (chooseLookupResult s)
195
                       (LookupResult FailMatch s) l
196

    
197
-- | Given a list of elements (and their names), assign indices to them.
198
assignIndices :: (Element a) =>
199
                 [(String, a)]
200
              -> (NameAssoc, Container.Container a)
201
assignIndices nodes =
202
  let (na, idx_node) =
203
          unzip . map (\ (idx, (k, v)) -> ((k, idx), (idx, setIdx v idx)))
204
          . zip [0..] $ nodes
205
  in (M.fromList na, Container.fromList idx_node)
206

    
207
-- | For each instance, add its index to its primary and secondary nodes.
208
fixNodes :: Node.List
209
         -> Instance.Instance
210
         -> Node.List
211
fixNodes accu inst =
212
  let pdx = Instance.pNode inst
213
      sdx = Instance.sNode inst
214
      pold = Container.find pdx accu
215
      pnew = Node.setPri pold inst
216
      ac2 = Container.add pdx pnew accu
217
  in if sdx /= Node.noSecondary
218
       then let sold = Container.find sdx accu
219
                snew = Node.setSec sold inst
220
            in Container.add sdx snew ac2
221
       else ac2
222

    
223
-- | Set the node's policy to its group one. Note that this requires
224
-- the group to exist (should have been checked before), otherwise it
225
-- will abort with a runtime error.
226
setNodePolicy :: Group.List -> Node.Node -> Node.Node
227
setNodePolicy gl node =
228
  let grp = Container.find (Node.group node) gl
229
      gpol = Group.iPolicy grp
230
  in Node.setPolicy gpol node
231

    
232
-- | Remove non-selected tags from the exclusion list.
233
filterExTags :: [String] -> Instance.Instance -> Instance.Instance
234
filterExTags tl inst =
235
  let old_tags = Instance.tags inst
236
      new_tags = filter (\tag -> any (`isPrefixOf` tag) tl) old_tags
237
  in inst { Instance.tags = new_tags }
238

    
239
-- | Update the movable attribute.
240
updateMovable :: [String]           -- ^ Selected instances (if not empty)
241
              -> [String]           -- ^ Excluded instances
242
              -> Instance.Instance  -- ^ Target Instance
243
              -> Instance.Instance  -- ^ Target Instance with updated attribute
244
updateMovable selinsts exinsts inst =
245
  if Instance.name inst `elem` exinsts ||
246
     not (null selinsts || Instance.name inst `elem` selinsts)
247
    then Instance.setMovable inst False
248
    else inst
249

    
250
-- | Disables moves for instances with a split group.
251
disableSplitMoves :: Node.List -> Instance.Instance -> Instance.Instance
252
disableSplitMoves nl inst =
253
  if not . isOk . Cluster.instanceGroup nl $ inst
254
    then Instance.setMovable inst False
255
    else inst
256

    
257
-- | Compute the longest common suffix of a list of strings that
258
-- starts with a dot.
259
longestDomain :: [String] -> String
260
longestDomain [] = ""
261
longestDomain (x:xs) =
262
  foldr (\ suffix accu -> if all (isSuffixOf suffix) xs
263
                            then suffix
264
                            else accu)
265
          "" $ filter (isPrefixOf ".") (tails x)
266

    
267
-- | Extracts the exclusion tags from the cluster configuration.
268
extractExTags :: [String] -> [String]
269
extractExTags =
270
  map (drop (length exTagsPrefix)) .
271
  filter (isPrefixOf exTagsPrefix)
272

    
273
-- | Extracts the common suffix from node\/instance names.
274
commonSuffix :: Node.List -> Instance.List -> String
275
commonSuffix nl il =
276
  let node_names = map Node.name $ Container.elems nl
277
      inst_names = map Instance.name $ Container.elems il
278
  in longestDomain (node_names ++ inst_names)
279

    
280
-- | Initializer function that loads the data from a node and instance
281
-- list and massages it into the correct format.
282
mergeData :: [(String, DynUtil)]  -- ^ Instance utilisation data
283
          -> [String]             -- ^ Exclusion tags
284
          -> [String]             -- ^ Selected instances (if not empty)
285
          -> [String]             -- ^ Excluded instances
286
          -> ClusterData          -- ^ Data from backends
287
          -> Result ClusterData   -- ^ Fixed cluster data
288
mergeData um extags selinsts exinsts cdata@(ClusterData gl nl il2 tags _) =
289
  let il = Container.elems il2
290
      il3 = foldl' (\im (name, n_util) ->
291
                        case Container.findByName im name of
292
                          Nothing -> im -- skipping unknown instance
293
                          Just inst ->
294
                              let new_i = inst { Instance.util = n_util }
295
                              in Container.add (Instance.idx inst) new_i im
296
                   ) il2 um
297
      allextags = extags ++ extractExTags tags
298
      inst_names = map Instance.name il
299
      selinst_lkp = map (lookupName inst_names) selinsts
300
      exinst_lkp = map (lookupName inst_names) exinsts
301
      lkp_unknown = filter (not . goodLookupResult) (selinst_lkp ++ exinst_lkp)
302
      selinst_names = map lrContent selinst_lkp
303
      exinst_names = map lrContent exinst_lkp
304
      node_names = map Node.name (Container.elems nl)
305
      common_suffix = longestDomain (node_names ++ inst_names)
306
      il4 = Container.map (computeAlias common_suffix .
307
                           filterExTags allextags .
308
                           updateMovable selinst_names exinst_names) il3
309
      nl2 = foldl' fixNodes nl (Container.elems il4)
310
      nl3 = Container.map (setNodePolicy gl .
311
                           computeAlias common_suffix .
312
                           (`Node.buildPeers` il4)) nl2
313
      il5 = Container.map (disableSplitMoves nl3) il4
314
  in if' (null lkp_unknown)
315
         (Ok cdata { cdNodes = nl3, cdInstances = il5 })
316
         (Bad $ "Unknown instance(s): " ++ show(map lrContent lkp_unknown))
317

    
318
-- | Checks the cluster data for consistency.
319
checkData :: Node.List -> Instance.List
320
          -> ([String], Node.List)
321
checkData nl il =
322
    Container.mapAccum
323
        (\ msgs node ->
324
             let nname = Node.name node
325
                 nilst = map (`Container.find` il) (Node.pList node)
326
                 dilst = filter Instance.instanceDown nilst
327
                 adj_mem = sum . map Instance.mem $ dilst
328
                 delta_mem = truncate (Node.tMem node)
329
                             - Node.nMem node
330
                             - Node.fMem node
331
                             - nodeImem node il
332
                             + adj_mem
333
                 delta_dsk = truncate (Node.tDsk node)
334
                             - Node.fDsk node
335
                             - nodeIdsk node il
336
                 newn = Node.setFmem (Node.setXmem node delta_mem)
337
                        (Node.fMem node - adj_mem)
338
                 umsg1 =
339
                   if delta_mem > 512 || delta_dsk > 1024
340
                      then printf "node %s is missing %d MB ram \
341
                                  \and %d GB disk"
342
                                  nname delta_mem (delta_dsk `div` 1024):msgs
343
                      else msgs
344
             in (umsg1, newn)
345
        ) [] nl
346

    
347
-- | Compute the amount of memory used by primary instances on a node.
348
nodeImem :: Node.Node -> Instance.List -> Int
349
nodeImem node il =
350
  let rfind = flip Container.find il
351
      il' = map rfind $ Node.pList node
352
      oil' = filter Instance.notOffline il'
353
  in sum . map Instance.mem $ oil'
354

    
355

    
356
-- | Compute the amount of disk used by instances on a node (either primary
357
-- or secondary).
358
nodeIdsk :: Node.Node -> Instance.List -> Int
359
nodeIdsk node il =
360
  let rfind = flip Container.find il
361
  in sum . map (Instance.dsk . rfind)
362
       $ Node.pList node ++ Node.sList node