diff --git a/htest/Test/Ganeti/Utils.hs b/htest/Test/Ganeti/Utils.hs
index 98ea53dd18461cbaa246945f5278bcfb062e155a..35d7a5575c3762614b7244f11a668e32ff9ac5d5 100644
--- a/htest/Test/Ganeti/Utils.hs
+++ b/htest/Test/Ganeti/Utils.hs
@@ -29,7 +29,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 module Test.Ganeti.Utils (testUtils) where
 
 import Test.QuickCheck
+import Test.HUnit
 
+import Data.List
 import qualified Text.JSON as J
 
 import Test.Ganeti.TestHelper
@@ -119,6 +121,85 @@ prop_parseUnit (NonNegative n) =
         n_gb = n_mb * 1000
         n_tb = n_gb * 1000
 
+{-# ANN case_niceSort_static "HLint: ignore Use camelCase" #-}
+
+case_niceSort_static :: Assertion
+case_niceSort_static = do
+  assertEqual "empty list" [] $ niceSort []
+  assertEqual "punctuation" [",", "."] $ niceSort [",", "."]
+  assertEqual "decimal numbers" ["0.1", "0.2"] $ niceSort ["0.1", "0.2"]
+  assertEqual "various numbers" ["0,099", "0.1", "0.2", "0;099"] $
+              niceSort ["0;099", "0,099", "0.1", "0.2"]
+
+  assertEqual "simple concat" ["0000", "a0", "a1", "a2", "a20", "a99",
+                               "b00", "b10", "b70"] $
+    niceSort ["a0", "a1", "a99", "a20", "a2", "b10", "b70", "b00", "0000"]
+
+  assertEqual "ranges" ["A", "Z", "a0-0", "a0-4", "a1-0", "a9-1", "a09-2",
+                      "a20-3", "a99-3", "a99-10", "b"] $
+    niceSort ["a0-0", "a1-0", "a99-10", "a20-3", "a0-4", "a99-3", "a09-2",
+              "Z", "a9-1", "A", "b"]
+
+  assertEqual "large"
+    ["3jTwJPtrXOY22bwL2YoW", "Eegah9ei", "KOt7vn1dWXi",
+     "KVQqLPDjcPjf8T3oyzjcOsfkb", "WvNJd91OoXvLzdEiEXa6",
+     "Z8Ljf1Pf5eBfNg171wJR", "a07h8feON165N67PIE", "bH4Q7aCu3PUPjK3JtH",
+     "cPRi0lM7HLnSuWA2G9", "guKJkXnkULealVC8CyF1xefym",
+     "pqF8dkU5B1cMnyZuREaSOADYx", "uHXAyYYftCSG1o7qcCqe",
+     "xij88brTulHYAv8IEOyU", "xpIUJeVT1Rp"] $
+    niceSort ["Eegah9ei", "xij88brTulHYAv8IEOyU", "3jTwJPtrXOY22bwL2YoW",
+             "Z8Ljf1Pf5eBfNg171wJR", "WvNJd91OoXvLzdEiEXa6",
+             "uHXAyYYftCSG1o7qcCqe", "xpIUJeVT1Rp", "KOt7vn1dWXi",
+             "a07h8feON165N67PIE", "bH4Q7aCu3PUPjK3JtH",
+             "cPRi0lM7HLnSuWA2G9", "KVQqLPDjcPjf8T3oyzjcOsfkb",
+             "guKJkXnkULealVC8CyF1xefym", "pqF8dkU5B1cMnyZuREaSOADYx"]
+
+-- | Tests single-string behaviour of 'niceSort'. Last test is special
+-- in the sense that /0/ is before any other non-empty string (except
+-- itself, etc.).
+prop_niceSort_single :: Property
+prop_niceSort_single =
+  forAll getName $ \name ->
+  conjoin
+  [ printTestCase "single string" $ [name] ==? niceSort [name]
+  , printTestCase "single plus empty" $ ["", name] ==? niceSort [name, ""]
+  , printTestCase "single plus 0-digit" $ ["0", name] ==? niceSort [name, "0"]
+  ]
+
+-- | Tests some generic 'niceSort' properties. Note that the last test
+-- must add a non-digit prefix; a digit one might change ordering.
+prop_niceSort_generic :: Property
+prop_niceSort_generic =
+  forAll (resize 20 arbitrary) $ \names ->
+  let n_sorted = niceSort names in
+  conjoin [ printTestCase "length" $ length names ==? length n_sorted
+          , printTestCase "same strings" $ sort names ==? sort n_sorted
+          , printTestCase "idempotence" $ n_sorted ==? niceSort n_sorted
+          , printTestCase "static prefix" $ n_sorted ==?
+              map tail (niceSort $ map (" "++) names)
+          ]
+
+-- | Tests that niceSorting numbers is identical to actual sorting
+-- them (in numeric form).
+prop_niceSort_numbers :: Property
+prop_niceSort_numbers =
+  forAll (listOf (arbitrary::Gen (NonNegative Int))) $ \numbers ->
+  map show (sort numbers) ==? niceSort (map show numbers)
+
+-- | Tests that 'niceSort' and 'niceSortKey' are equivalent.
+prop_niceSortKey_equiv :: Property
+prop_niceSortKey_equiv =
+  forAll (resize 20 arbitrary) $ \names ->
+  forAll (vectorOf (length names) (arbitrary::Gen Int)) $ \numbers ->
+  let n_sorted = niceSort names in
+  conjoin
+  [ printTestCase "key id" $ n_sorted ==? niceSortKey id names
+  , printTestCase "key rev" $ niceSort (map reverse names) ==?
+                              map reverse (niceSortKey reverse names)
+  , printTestCase "key snd" $ n_sorted ==? map snd (niceSortKey snd $
+                                                    zip numbers names)
+  ]
+
 -- | Test list for the Utils module.
 testSuite "Utils"
             [ 'prop_commaJoinSplit
@@ -129,4 +210,9 @@ testSuite "Utils"
             , 'prop_select_undefd
             , 'prop_select_undefv
             , 'prop_parseUnit
+            , 'case_niceSort_static
+            , 'prop_niceSort_single
+            , 'prop_niceSort_generic
+            , 'prop_niceSort_numbers
+            , 'prop_niceSortKey_equiv
             ]
diff --git a/htools/Ganeti/Utils.hs b/htools/Ganeti/Utils.hs
index 708755e70a4789bd410a1fac2a41b5bd194643cf..8c1b0ce72c80bea96c60f255d095ed5cccc29045 100644
--- a/htools/Ganeti/Utils.hs
+++ b/htools/Ganeti/Utils.hs
@@ -37,13 +37,16 @@ module Ganeti.Utils
   , printTable
   , parseUnit
   , plural
+  , niceSort
+  , niceSortKey
   , exitIfBad
   , exitErr
   , exitWhen
   , exitUnless
   ) where
 
-import Data.Char (toUpper, isAlphaNum)
+import Data.Char (toUpper, isAlphaNum, isDigit)
+import Data.Function (on)
 import Data.List
 
 import Debug.Trace
@@ -230,3 +233,43 @@ exitWhen False _  = return ()
 -- if true, the opposite of 'exitWhen'.
 exitUnless :: Bool -> String -> IO ()
 exitUnless cond = exitWhen (not cond)
+
+-- | Helper for 'niceSort'. Computes the key element for a given string.
+extractKey :: [Either Integer String]  -- ^ Current (partial) key, reversed
+           -> String                   -- ^ Remaining string
+           -> ([Either Integer String], String)
+extractKey ek [] = (reverse ek, [])
+extractKey ek xs@(x:_) =
+  let (span_fn, conv_fn) = if isDigit x
+                             then (isDigit, Left . read)
+                             else (not . isDigit, Right)
+      (k, rest) = span span_fn xs
+  in extractKey (conv_fn k:ek) rest
+
+{-| Sort a list of strings based on digit and non-digit groupings.
+
+Given a list of names @['a1', 'a10', 'a11', 'a2']@ this function
+will sort the list in the logical order @['a1', 'a2', 'a10', 'a11']@.
+
+The sort algorithm breaks each name in groups of either only-digits or
+no-digits, and sorts based on each group.
+
+Internally, this is not implemented via regexes (like the Python
+version), but via actual splitting of the string in sequences of
+either digits or everything else, and converting the digit sequences
+in /Left Integer/ and the non-digit ones in /Right String/, at which
+point sorting becomes trivial due to the built-in 'Either' ordering;
+we only need one extra step of dropping the key at the end.
+
+-}
+niceSort :: [String] -> [String]
+niceSort = map snd . sort . map (\s -> (fst $ extractKey [] s, s))
+
+-- | Key-version of 'niceSort'. We use 'sortBy' and @compare `on` fst@
+-- since we don't want to add an ordering constraint on the /a/ type,
+-- hence the need to only compare the first element of the /(key, a)/
+-- tuple.
+niceSortKey :: (a -> String) -> [a] -> [a]
+niceSortKey keyfn =
+  map snd . sortBy (compare `on` fst) .
+  map (\s -> (fst . extractKey [] $ keyfn s, s))