diff --git a/Ganeti/HTools/IAlloc.hs b/Ganeti/HTools/IAlloc.hs
index b5391f6e90da6ff7a09a173b83fc36fd94992df6..a494cd7b10320714914418b046a7aa63f71ded63 100644
--- a/Ganeti/HTools/IAlloc.hs
+++ b/Ganeti/HTools/IAlloc.hs
@@ -9,12 +9,12 @@ module Ganeti.HTools.IAlloc
     ) where
 
 import Data.Either ()
-import Data.Maybe
+--import Data.Maybe
 import Control.Monad
 import Text.JSON (JSObject, JSValue(JSBool, JSString, JSArray),
                   makeObj, encodeStrict, decodeStrict,
                   fromJSObject, toJSString)
-import Text.Printf (printf)
+--import Text.Printf (printf)
 import Ganeti.HTools.Utils
 import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.HTools.Instance as Instance
@@ -27,9 +27,10 @@ data RqType
 data Request
     = RqAlloc String String String
     | RqReloc String String String
+    deriving (Show)
 
-parseInstance :: String -> JSObject JSValue -> Result String
-parseInstance n a =
+parseBaseInstance :: String -> JSObject JSValue -> Result String
+parseBaseInstance n a =
     let name = Ok n
         disk = case getIntElement "disk_usage" a of
                  Bad _ -> let all_d = getListElement "disks" a >>= asObjectList
@@ -40,14 +41,21 @@ parseInstance n a =
                               szf = liftM sum sze
                            in szf
                  x@(Ok _) -> x
-        nodes = getListElement "nodes" a
-        pnode = liftM head nodes >>= readEitherString
-        snode = liftM (head . tail) nodes >>= readEitherString
         mem = getIntElement "memory" a
         running = Ok "running" --getStringElement "status" a
     in
       name |+ (show `liftM` mem) |+
-              (show `liftM` disk) |+ running |+ pnode |+ snode
+              (show `liftM` disk) |+ running
+
+parseInstance :: String -> JSObject JSValue -> Result String
+parseInstance n a = do
+    base <- parseBaseInstance n a
+    let
+        nodes = getListElement "nodes" a
+        pnode = liftM head nodes >>= readEitherString
+        snode = liftM (head . tail) nodes >>= readEitherString
+    return base |+ pnode |+ snode
+
 
 parseNode :: String -> JSObject JSValue -> Result String
 parseNode n a =
@@ -74,14 +82,14 @@ parseData :: String -> Result Request
 parseData body =
     do
       decoded <- fromJResult $ decodeStrict body
-      let obj = decoded -- decoded `combineEithers` fromJSObject
-        -- request parser
+      let obj = decoded
+      -- request parser
       request <- getObjectElement "request" obj
       rname <- getStringElement "name" request
       rtype <-  getStringElement "type" request >>= validateRequest
-      inew <- (\x -> if x == Allocate then parseInstance rname request
+      inew <- (\x -> if x == Allocate then parseBaseInstance rname request
                      else Ok "") rtype
-      -- existing intstance parsing
+      -- existing instance parsing
       ilist <- getObjectElement "instances" obj
       let idata = fromJSObject ilist
       iobj <- (sequence . map (\(x,y) -> asJSObject y >>= parseInstance x))
@@ -99,7 +107,6 @@ parseData body =
                       Relocate -> RqReloc rnam nl il)
                  rtype nlines ilines inew rname
 
-
 formatResponse :: Bool -> String -> [String] -> String
 formatResponse success info nodes =
     let
diff --git a/Ganeti/HTools/Utils.hs b/Ganeti/HTools/Utils.hs
index f782118477244cc7bcd1a76b7f2826323e049b6a..d26e7c7d97638321fd1761a2d2d5b3df96046f28 100644
--- a/Ganeti/HTools/Utils.hs
+++ b/Ganeti/HTools/Utils.hs
@@ -123,7 +123,7 @@ loadJSArray s = fromJResult $ J.decodeStrict s
 fromObj :: (J.JSON a, Monad m) => String -> J.JSObject J.JSValue -> m a
 fromObj k o =
     case lookup k (J.fromJSObject o) of
-      Nothing -> fail $ printf "key '%s' not found" k
+      Nothing -> fail $ printf "key '%s' not found in %s" k (show o)
       Just val -> fromJResult $ J.readJSON val
 
 getStringElement :: (Monad m) => String -> J.JSObject J.JSValue -> m String
diff --git a/Makefile b/Makefile
index d5558883328d30182ef44b52d4ed0f2b4df2aaa5..c681119c8aa0b898dac9b4e61791397ece0d29e5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-HPROGS = hbal hn1 hscan
+HPROGS = hbal hn1 hscan hail
 HSRCS := $(wildcard Ganeti/HTools/*.hs)
 HDDIR = apidoc
 
diff --git a/hail.hs b/hail.hs
new file mode 100644
index 0000000000000000000000000000000000000000..69ada441bff4e62d11a5da4f0b510ef1dd35aa0d
--- /dev/null
+++ b/hail.hs
@@ -0,0 +1,307 @@
+{-| Solver for N+1 cluster errors
+
+-}
+
+module Main (main) where
+
+import Data.List
+import Data.Function
+import Data.Maybe (isJust, fromJust, fromMaybe)
+import Monad
+import System
+import System.IO
+import System.Console.GetOpt
+import qualified System
+
+import Text.Printf (printf)
+
+import qualified Ganeti.HTools.Container as Container
+import qualified Ganeti.HTools.Cluster as Cluster
+import qualified Ganeti.HTools.Node as Node
+import qualified Ganeti.HTools.CLI as CLI
+import Ganeti.HTools.IAlloc
+import Ganeti.HTools.Utils
+
+-- | Command line options structure.
+data Options = Options
+    { optShowNodes :: Bool           -- ^ Whether to show node status
+    , optShowCmds  :: Maybe FilePath -- ^ Whether to show the command list
+    , optOneline   :: Bool           -- ^ Switch output to a single line
+    , optNodef     :: FilePath       -- ^ Path to the nodes file
+    , optNodeSet   :: Bool           -- ^ The nodes have been set by options
+    , optInstf     :: FilePath       -- ^ Path to the instances file
+    , optInstSet   :: Bool           -- ^ The insts have been set by options
+    , optMaxLength :: Int            -- ^ Stop after this many steps
+    , optMaster    :: String         -- ^ Collect data from RAPI
+    , optVerbose   :: Int            -- ^ Verbosity level
+    , optOffline   :: [String]       -- ^ Names of offline nodes
+    , optMinScore  :: Cluster.Score  -- ^ The minimum score we aim for
+    , optShowVer   :: Bool           -- ^ Just show the program version
+    , optShowHelp  :: Bool           -- ^ Just show the help
+    } deriving Show
+
+-- | Default values for the command line options.
+defaultOptions :: Options
+defaultOptions  = Options
+ { optShowNodes = False
+ , optShowCmds  = Nothing
+ , optOneline   = False
+ , optNodef     = "nodes"
+ , optNodeSet   = False
+ , optInstf     = "instances"
+ , optInstSet   = False
+ , optMaxLength = -1
+ , optMaster    = ""
+ , optVerbose   = 1
+ , optOffline   = []
+ , optMinScore  = 1e-9
+ , optShowVer   = False
+ , optShowHelp  = False
+ }
+
+-- | Options list and functions
+options :: [OptDescr (Options -> Options)]
+options =
+    [ Option ['p']     ["print-nodes"]
+      (NoArg (\ opts -> opts { optShowNodes = True }))
+      "print the final node list"
+    , Option ['C']     ["print-commands"]
+      (OptArg ((\ f opts -> opts { optShowCmds = Just f }) . fromMaybe "-")
+                  "FILE")
+      "print the ganeti command list for reaching the solution,\
+      \if an argument is passed then write the commands to a file named\
+      \ as such"
+    , Option ['o']     ["oneline"]
+      (NoArg (\ opts -> opts { optOneline = True }))
+      "print the ganeti command list for reaching the solution"
+    , Option ['n']     ["nodes"]
+      (ReqArg (\ f opts -> opts { optNodef = f, optNodeSet = True }) "FILE")
+      "the node list FILE"
+    , Option ['i']     ["instances"]
+      (ReqArg (\ f opts -> opts { optInstf =  f, optInstSet = True }) "FILE")
+      "the instance list FILE"
+    , Option ['m']     ["master"]
+      (ReqArg (\ m opts -> opts { optMaster = m }) "ADDRESS")
+      "collect data via RAPI at the given ADDRESS"
+    , Option ['l']     ["max-length"]
+      (ReqArg (\ i opts -> opts { optMaxLength =  (read i)::Int }) "N")
+      "cap the solution at this many moves (useful for very unbalanced \
+      \clusters)"
+    , Option ['v']     ["verbose"]
+      (NoArg (\ opts -> opts { optVerbose = (optVerbose opts) + 1 }))
+      "increase the verbosity level"
+    , Option ['q']     ["quiet"]
+      (NoArg (\ opts -> opts { optVerbose = (optVerbose opts) - 1 }))
+      "decrease the verbosity level"
+    , Option ['O']     ["offline"]
+      (ReqArg (\ n opts -> opts { optOffline = n:optOffline opts }) "NODE")
+      " set node as offline"
+    , Option ['e']     ["min-score"]
+      (ReqArg (\ e opts -> opts { optMinScore = read e }) "EPSILON")
+      " mininum score to aim for"
+    , Option ['V']     ["version"]
+      (NoArg (\ opts -> opts { optShowVer = True}))
+      "show the version of the program"
+    , Option ['h']     ["help"]
+      (NoArg (\ opts -> opts { optShowHelp = True}))
+      "show help"
+    ]
+
+{- | Start computing the solution at the given depth and recurse until
+we find a valid solution or we exceed the maximum depth.
+
+-}
+iterateDepth :: Cluster.Table    -- ^ The starting table
+             -> Int              -- ^ Remaining length
+             -> Cluster.NameList -- ^ Node idx to name list
+             -> Cluster.NameList -- ^ Inst idx to name list
+             -> Int              -- ^ Max node name len
+             -> Int              -- ^ Max instance name len
+             -> [[String]]       -- ^ Current command list
+             -> Bool             -- ^ Wheter to be silent
+             -> Cluster.Score    -- ^ Score at which to stop
+             -> IO (Cluster.Table, [[String]]) -- ^ The resulting table and
+                                               -- commands
+iterateDepth ini_tbl max_rounds ktn kti nmlen imlen
+             cmd_strs oneline min_score =
+    let Cluster.Table ini_nl ini_il ini_cv ini_plc = ini_tbl
+        all_inst = Container.elems ini_il
+        node_idx = map Node.idx . filter (not . Node.offline) $
+                   Container.elems ini_nl
+        fin_tbl = Cluster.checkMove node_idx ini_tbl all_inst
+        (Cluster.Table _ _ fin_cv fin_plc) = fin_tbl
+        ini_plc_len = length ini_plc
+        fin_plc_len = length fin_plc
+        allowed_next = (max_rounds < 0 || length fin_plc < max_rounds)
+    in
+      do
+        let
+            (sol_line, cmds) = Cluster.printSolutionLine ini_il ktn kti
+                               nmlen imlen (head fin_plc) fin_plc_len
+            upd_cmd_strs = cmds:cmd_strs
+        unless (oneline || fin_plc_len == ini_plc_len) $ do
+          putStrLn sol_line
+          hFlush stdout
+        (if fin_cv < ini_cv then -- this round made success, try deeper
+             if allowed_next && fin_cv > min_score
+             then iterateDepth fin_tbl max_rounds ktn kti
+                  nmlen imlen upd_cmd_strs oneline min_score
+             -- don't go deeper, but return the better solution
+             else return (fin_tbl, upd_cmd_strs)
+         else
+             return (ini_tbl, cmd_strs))
+
+-- | Formats the solution for the oneline display
+formatOneline :: Double -> Int -> Double -> String
+formatOneline ini_cv plc_len fin_cv =
+    printf "%.8f %d %.8f %8.3f" ini_cv plc_len fin_cv
+               (if fin_cv == 0 then 1 else (ini_cv / fin_cv))
+
+-- | Main function.
+main :: IO ()
+main = do
+  cmd_args <- System.getArgs
+  (opts, args) <- CLI.parseOpts cmd_args "hail" options
+                  defaultOptions optShowHelp
+
+  when (optShowVer opts) $ do
+         putStr $ CLI.showVersion "hbal"
+         exitWith ExitSuccess
+
+  when (null args) $ do
+         hPutStrLn stderr "Error: this program needs an input file."
+         exitWith $ ExitFailure 1
+
+  let input_file = head args
+  input_data <- readFile input_file
+
+  request <- case (parseData input_data) of
+               Bad err -> do
+                 putStrLn $ "Error: " ++ err
+                 exitWith $ ExitFailure 1
+               Ok rq -> return rq
+
+  putStrLn $ show request
+  exitWith ExitSuccess
+{-
+  (loaded_nl, il, csf, ktn, kti) <- liftM2 Cluster.loadData node_data inst_data
+  let (fix_msgs, fixed_nl) = Cluster.checkData loaded_nl il ktn kti
+
+  unless (null fix_msgs || verbose == 0) $ do
+         putStrLn "Warning: cluster has inconsistent data:"
+         putStrLn . unlines . map (\s -> printf "  - %s" s) $ fix_msgs
+
+  let offline_names = optOffline opts
+      all_names = snd . unzip $ ktn
+      offline_wrong = filter (\n -> not $ elem n all_names) offline_names
+      offline_indices = fst . unzip .
+                        filter (\(_, n) -> elem n offline_names) $ ktn
+
+  when (length offline_wrong > 0) $ do
+         printf "Wrong node name(s) set as offline: %s\n"
+                (commaJoin offline_wrong)
+         exitWith $ ExitFailure 1
+
+  let nl = Container.map (\n -> if elem (Node.idx n) offline_indices
+                                then Node.setOffline n True
+                                else n) fixed_nl
+
+  when (Container.size il == 0) $ do
+         (if oneline then
+              putStrLn $ formatOneline 0 0 0
+          else
+              printf "Cluster is empty, exiting.\n")
+         exitWith ExitSuccess
+
+
+  unless oneline $ printf "Loaded %d nodes, %d instances\n"
+             (Container.size nl)
+             (Container.size il)
+
+  when (length csf > 0 && not oneline && verbose > 1) $ do
+         printf "Note: Stripping common suffix of '%s' from names\n" csf
+
+  let (bad_nodes, bad_instances) = Cluster.computeBadItems nl il
+  unless (oneline || verbose == 0) $ printf
+             "Initial check done: %d bad nodes, %d bad instances.\n"
+             (length bad_nodes) (length bad_instances)
+
+  when (length bad_nodes > 0) $ do
+         putStrLn "Cluster is not N+1 happy, continuing but no guarantee \
+                  \that the cluster will end N+1 happy."
+
+  when (optShowNodes opts) $
+       do
+         putStrLn "Initial cluster status:"
+         putStrLn $ Cluster.printNodes ktn nl
+
+  let ini_cv = Cluster.compCV nl
+      ini_tbl = Cluster.Table nl il ini_cv []
+      min_cv = optMinScore opts
+
+  when (ini_cv < min_cv) $ do
+         (if oneline then
+              putStrLn $ formatOneline ini_cv 0 ini_cv
+          else printf "Cluster is already well balanced (initial score %.6g,\n\
+                      \minimum score %.6g).\nNothing to do, exiting\n"
+                      ini_cv min_cv)
+         exitWith ExitSuccess
+
+  unless oneline (if verbose > 2 then
+                      printf "Initial coefficients: overall %.8f, %s\n"
+                      ini_cv (Cluster.printStats nl)
+                  else
+                      printf "Initial score: %.8f\n" ini_cv)
+
+  unless oneline $ putStrLn "Trying to minimize the CV..."
+  let mlen_fn = maximum . (map length) . snd . unzip
+      imlen = mlen_fn kti
+      nmlen = mlen_fn ktn
+
+  (fin_tbl, cmd_strs) <- iterateDepth ini_tbl (optMaxLength opts)
+                         ktn kti nmlen imlen [] oneline min_cv
+  let (Cluster.Table fin_nl _ fin_cv fin_plc) = fin_tbl
+      ord_plc = reverse fin_plc
+      sol_msg = if null fin_plc
+                then printf "No solution found\n"
+                else (if verbose > 2
+                      then printf "Final coefficients:   overall %.8f, %s\n"
+                           fin_cv (Cluster.printStats fin_nl)
+                      else printf "Cluster score improved from %.8f to %.8f\n"
+                           ini_cv fin_cv
+                     )
+
+  unless oneline $ putStr sol_msg
+
+  unless (oneline || verbose == 0) $
+         printf "Solution length=%d\n" (length ord_plc)
+
+  let cmd_data = Cluster.formatCmds . reverse $ cmd_strs
+
+  when (isJust $ optShowCmds opts) $
+       do
+         let out_path = fromJust $ optShowCmds opts
+         putStrLn ""
+         (if out_path == "-" then
+              printf "Commands to run to reach the above solution:\n%s"
+                     (unlines . map ("  " ++) .
+                      filter (/= "check") .
+                      lines $ cmd_data)
+          else do
+            writeFile out_path (CLI.shTemplate ++ cmd_data)
+            printf "The commands have been written to file '%s'\n" out_path)
+
+  when (optShowNodes opts) $
+       do
+         let (orig_mem, orig_disk) = Cluster.totalResources nl
+             (final_mem, final_disk) = Cluster.totalResources fin_nl
+         putStrLn ""
+         putStrLn "Final cluster status:"
+         putStrLn $ Cluster.printNodes ktn fin_nl
+         when (verbose > 3) $
+              do
+                printf "Original: mem=%d disk=%d\n" orig_mem orig_disk
+                printf "Final:    mem=%d disk=%d\n" final_mem final_disk
+  when oneline $
+         putStrLn $ formatOneline ini_cv (length ord_plc) fin_cv
+-}