content: {-| xentop CPU data collector
  
  -}
  
  {-
  
  Copyright (C) 2015 Google Inc.
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:
  
  1. Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
  
  2. Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
  
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
  -}
  
  module Ganeti.DataCollectors.XenCpuLoad
    ( dcName
    , dcVersion
    , dcFormatVersion
    , dcCategory
    , dcKind
    , dcReport
    , dcUpdate
    ) where
  
  import Control.Applicative (liftA2)
  import Control.Arrow ((***))
  import Control.Monad (liftM, when)
  import Control.Monad.IO.Class (liftIO)
  import qualified Data.Map as Map
  import Data.Maybe (mapMaybe)
  import qualified Data.Sequence as Seq
  import System.Process (readProcess)
  import qualified Text.JSON as J
  import System.Time (ClockTime, getClockTime)
  import Ganeti.Utils.Time (addToClockTime, diffClockTimes, clockTimeToUSec)
  
  import Ganeti.BasicTypes (GenericResult(..), Result, genericResult, runResultT)
  import qualified Ganeti.Constants as C
  import Ganeti.DataCollectors.Types
  import Ganeti.Utils (readMaybe)
  
  -- | The name of this data collector.
  dcName :: String
  dcName = C.dataCollectorXenCpuLoad
  
  -- | The version of this data collector.
  dcVersion :: DCVersion
  dcVersion = DCVerBuiltin
  
  -- | The version number for the data format of this data collector.
  dcFormatVersion :: Int
  dcFormatVersion = 1
  
  -- | The category of this data collector.
  dcCategory :: Maybe DCCategory
  dcCategory = Nothing
  
  -- | The kind of this data collector.
  dcKind :: DCKind
  dcKind = DCKPerf
  
  -- | Read xentop output, if this program is available.
  readXentop :: IO (Result String)
  readXentop =
    runResultT . liftIO $ readProcess C.xentopCommand ["-f", "-b", "-i", "1"] ""
  
  -- | Parse output of xentop command.
  parseXentop :: String -> Result (Map.Map String Double)
  parseXentop s = do
    let values = map words $ lines s
    case values of
      [] -> Bad "No output received"
      (name_header:_:cpu_header:_):vals -> do
        when (name_header /= "NAME" || cpu_header /= "CPU(sec)")
          $ Bad "Unexpected data format"
        return . Map.fromList
          $ mapMaybe
              (\ dom -> case dom of
                          name:_:cpu:_ -> if name /= "Domain-0"
                                            then liftM ((,) name) $ readMaybe cpu
                                            else Nothing
                          _ -> Nothing
              )
              vals
      _ -> Bad "Insufficient number of output columns"
  
  
  -- | Add a new value to a sequence of observations, taking into account
  -- counter rollovers. In case of a rollover, we drop the joining interval
  -- so that we do not have to make assumptions about the value at which is
  -- rolled over, but we do keep the right sequence, appropriately moved.
  combineWithRollover :: Seq.Seq (ClockTime, Double)
                      -> Seq.Seq (ClockTime, Double)
                      -> Seq.Seq (ClockTime, Double)
  combineWithRollover new old | Seq.null new || Seq.null old = new Seq.>< old
  combineWithRollover new old =
    let (t2, x2) = Seq.index new $ Seq.length new - 1
        (t1, x1) = Seq.index old 0
    in if x2 >= x1
         then new Seq.>< old
         else let delta_t = diffClockTimes t2 t1
                  deltax = x2 - x1
                  old' = (addToClockTime delta_t *** (+ deltax))
                         <$> Seq.drop 1 old
              in new Seq.>< old'
  
  -- | Updates the given Collector data.
  dcUpdate :: Maybe CollectorData -> IO CollectorData
  dcUpdate maybeCollector = do
    let oldData = case maybeCollector of
                    Just (InstanceCpuLoad x) -> x
                    _ -> Map.empty
    now <- getClockTime
    newResult <- liftM (>>= parseXentop) readXentop
    let newValues = Map.map (Seq.singleton . (,) now)
                    $ genericResult (const Map.empty) id newResult
        sampleSizeUSec = fromIntegral C.cpuavgloadWindowSize * 1000000
        combinedValues = Map.unionWith combineWithRollover newValues oldData
        withinRange = Map.map
                        (Seq.dropWhileR
                          ((<) sampleSizeUSec
                           . (clockTimeToUSec now -)
                           . clockTimeToUSec . fst))
                        combinedValues
        withoutOld = Map.filter
                       (liftA2 (&&) (not . Seq.null)
                        $ (>) (fromIntegral $ C.xentopAverageThreshold * 1000000)
                          . (clockTimeToUSec now -) . clockTimeToUSec
                          . fst . flip Seq.index 0)
                       withinRange
    return $ InstanceCpuLoad withoutOld
  
  -- | From a list of timestamps and cumulative CPU data, compute the
  -- average CPU activity in vCPUs.
  loadAverage :: Seq.Seq (ClockTime, Double) -> Maybe Double
  loadAverage observations = do
    when (Seq.null observations) Nothing
    let (t2, cpu2) = Seq.index observations 0
        (t1, cpu1) = Seq.index observations $ Seq.length observations - 1
        tUsec2 = clockTimeToUSec t2
        tUsec1 = clockTimeToUSec t1
    when (tUsec2 - tUsec1 < (fromIntegral C.xentopAverageThreshold * 1000000))
      Nothing
    return $ 1000000 * (cpu2 - cpu1) / fromIntegral (tUsec2 - tUsec1)
  
  -- | The data exported by the data collector, taken from the default location.
  dcReport :: Maybe CollectorData -> IO DCReport
  dcReport maybeCollector =
    let collectedData = case maybeCollector of
                          Just (InstanceCpuLoad x) -> x
                          _ -> Map.empty
        loads = Map.mapMaybe loadAverage collectedData
    in buildReport dcName dcVersion dcFormatVersion dcCategory dcKind
        . J.JSObject . J.toJSObject . Map.toAscList $ Map.map J.showJSON loads
  