diff --git a/htest/Test/Ganeti/OpCodes.hs b/htest/Test/Ganeti/OpCodes.hs
index b9398ecc1ebaf5cdcfd8baaebc44d8c572e7d624..e3ed1f397e13941a6baa13c643846e4d65a80ec3 100644
--- a/htest/Test/Ganeti/OpCodes.hs
+++ b/htest/Test/Ganeti/OpCodes.hs
@@ -451,6 +451,8 @@ case_py_compat_types = do
   sample_opcodes <- sample' (vectorOf num_opcodes
                              (arbitrary::Gen OpCodes.MetaOpCode))
   let opcodes = head sample_opcodes
+      with_sum = map (\o -> (OpCodes.opSummary $
+                             OpCodes.metaOpCode o, o)) opcodes
       serialized = J.encode opcodes
   -- check for non-ASCII fields, usually due to 'arbitrary :: String'
   mapM_ (\op -> when (any (not . isAscii) (J.encode op)) .
@@ -465,10 +467,12 @@ case_py_compat_types = do
                \decoded = [opcodes.OpCode.LoadOpCode(o) for o in op_data]\n\
                \for op in decoded:\n\
                \  op.Validate(True)\n\
-               \encoded = [op.__getstate__() for op in decoded]\n\
+               \encoded = [(op.Summary(), op.__getstate__())\n\
+               \           for op in decoded]\n\
                \print serializer.Dump(encoded)" serialized
      >>= checkPythonResult
-  let deserialised = J.decode py_stdout::J.Result [OpCodes.MetaOpCode]
+  let deserialised =
+        J.decode py_stdout::J.Result [(String, OpCodes.MetaOpCode)]
   decoded <- case deserialised of
                J.Ok ops -> return ops
                J.Error msg ->
@@ -477,9 +481,9 @@ case_py_compat_types = do
                  -- for proper types
                  >> fail "Unable to decode opcodes"
   HUnit.assertEqual "Mismatch in number of returned opcodes"
-    (length opcodes) (length decoded)
+    (length decoded) (length with_sum)
   mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
-        ) $ zip opcodes decoded
+        ) $ zip decoded with_sum
 
 -- | Custom HUnit test case that forks a Python process and checks
 -- correspondence between Haskell OpCodes fields and their Python
diff --git a/htools/Ganeti/OpCodes.hs b/htools/Ganeti/OpCodes.hs
index 1e8f561263fcb519fe55aba4258636b4d2cc6cd3..f0ffd00f362b12b004d63c5da32381ecf502f1b0 100644
--- a/htools/Ganeti/OpCodes.hs
+++ b/htools/Ganeti/OpCodes.hs
@@ -38,6 +38,7 @@ module Ganeti.OpCodes
   , opID
   , allOpIDs
   , allOpFields
+  , opSummary
   , CommonOpParams(..)
   , defOpParams
   , MetaOpCode(..)
@@ -45,13 +46,15 @@ module Ganeti.OpCodes
   , setOpComment
   ) where
 
+import Data.Maybe (fromMaybe)
 import Text.JSON (readJSON, showJSON, JSON, JSValue, makeObj)
 import qualified Text.JSON
 
 import Ganeti.THH
 
 import Ganeti.OpParams
-import Ganeti.Types (OpSubmitPriority(..))
+import Ganeti.Types (OpSubmitPriority(..), fromNonEmpty)
+import Ganeti.Query.Language (queryTypeOpToRaw)
 
 -- | OpCode representation.
 --
@@ -548,6 +551,70 @@ instance JSON OpCode where
   readJSON = loadOpCode
   showJSON = saveOpCode
 
+-- | Generates the summary value for an opcode.
+opSummaryVal :: OpCode -> Maybe String
+opSummaryVal OpClusterVerifyGroup { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpGroupVerifyDisks { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpClusterRename { opName = s } = Just (fromNonEmpty s)
+opSummaryVal OpQuery { opWhat = s } = Just (queryTypeOpToRaw s)
+opSummaryVal OpQueryFields { opWhat = s } = Just (queryTypeOpToRaw s)
+opSummaryVal OpNodeRemove { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpNodeAdd { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpNodeModifyStorage { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpRepairNodeStorage  { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpNodeSetParams { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpNodePowercycle { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpNodeMigrate { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpNodeEvacuate { opNodeName = s } = Just (fromNonEmpty s)
+opSummaryVal OpInstanceCreate { opInstanceName = s } = Just s
+opSummaryVal OpInstanceReinstall { opInstanceName = s } = Just s
+opSummaryVal OpInstanceRemove { opInstanceName = s } = Just s
+-- FIXME: instance rename should show both names; currently it shows none
+-- opSummaryVal OpInstanceRename { opInstanceName = s } = Just s
+opSummaryVal OpInstanceStartup { opInstanceName = s } = Just s
+opSummaryVal OpInstanceShutdown { opInstanceName = s } = Just s
+opSummaryVal OpInstanceReboot { opInstanceName = s } = Just s
+opSummaryVal OpInstanceReplaceDisks { opInstanceName = s } = Just s
+opSummaryVal OpInstanceFailover { opInstanceName = s } = Just s
+opSummaryVal OpInstanceMigrate { opInstanceName = s } = Just s
+opSummaryVal OpInstanceMove { opInstanceName = s } = Just s
+opSummaryVal OpInstanceConsole { opInstanceName = s } = Just s
+opSummaryVal OpInstanceActivateDisks { opInstanceName = s } = Just s
+opSummaryVal OpInstanceDeactivateDisks { opInstanceName = s } = Just s
+opSummaryVal OpInstanceRecreateDisks { opInstanceName = s } = Just s
+opSummaryVal OpInstanceSetParams { opInstanceName = s } = Just s
+opSummaryVal OpInstanceGrowDisk { opInstanceName = s } = Just s
+opSummaryVal OpInstanceChangeGroup { opInstanceName = s } = Just s
+opSummaryVal OpGroupAdd { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpGroupAssignNodes { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpGroupSetParams { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpGroupRemove { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpGroupEvacuate { opGroupName = s } = Just (fromNonEmpty s)
+opSummaryVal OpBackupPrepare { opInstanceName = s } = Just s
+opSummaryVal OpBackupExport { opInstanceName = s } = Just s
+opSummaryVal OpBackupRemove { opInstanceName = s } = Just s
+opSummaryVal OpTagsGet { opKind = k } =
+  Just . fromMaybe "None" $ tagNameOf k
+opSummaryVal OpTagsSearch { opTagSearchPattern = s } = Just (fromNonEmpty s)
+opSummaryVal OpTestDelay { opDelayDuration = d } = Just (show d)
+opSummaryVal OpTestAllocator { opIallocator = s } =
+  -- FIXME: Python doesn't handle None fields well, so we have behave the same
+  Just $ maybe "None" fromNonEmpty s
+opSummaryVal OpNetworkAdd { opNetworkName = s} = Just (fromNonEmpty s)
+opSummaryVal OpNetworkRemove { opNetworkName = s} = Just (fromNonEmpty s)
+opSummaryVal OpNetworkSetParams { opNetworkName = s} = Just (fromNonEmpty s)
+opSummaryVal OpNetworkConnect { opNetworkName = s} = Just (fromNonEmpty s)
+opSummaryVal OpNetworkDisconnect { opNetworkName = s} = Just (fromNonEmpty s)
+opSummaryVal _ = Nothing
+
+-- | Computes the summary of the opcode.
+opSummary :: OpCode -> String
+opSummary op =
+  case opSummaryVal op of
+    Nothing -> op_suffix
+    Just s -> op_suffix ++ "(" ++ s ++ ")"
+  where op_suffix = drop 3 $ opID op
+
 -- | Generic\/common opcode parameters.
 $(buildObject "CommonOpParams" "op"
   [ pDryRun
@@ -568,8 +635,9 @@ defOpParams =
                  }
 
 -- | The top-level opcode type.
-data MetaOpCode = MetaOpCode CommonOpParams OpCode
-                  deriving (Show, Eq)
+data MetaOpCode = MetaOpCode { metaParams :: CommonOpParams
+                             , metaOpCode :: OpCode
+                             } deriving (Show, Eq)
 
 -- | JSON serialisation for 'MetaOpCode'.
 showMeta :: MetaOpCode -> JSValue
diff --git a/htools/Ganeti/OpParams.hs b/htools/Ganeti/OpParams.hs
index e375ae596a17a846845b1b073305d8d09c63fb9a..acb47ba60e3c29d8909800d4b4a8539bbcf4c991 100644
--- a/htools/Ganeti/OpParams.hs
+++ b/htools/Ganeti/OpParams.hs
@@ -35,6 +35,7 @@ module Ganeti.OpParams
   ( TagType(..)
   , TagObject(..)
   , tagObjectFrom
+  , tagNameOf
   , decodeTagObject
   , encodeTagObject
   , ReplaceDisksMode(..)
diff --git a/htools/Ganeti/Query/Language.hs b/htools/Ganeti/Query/Language.hs
index c54f0f5a378ba4e0b2474b80b9394f0f9a3df28e..f074439981a78db391bb2061086866a3bba37521 100644
--- a/htools/Ganeti/Query/Language.hs
+++ b/htools/Ganeti/Query/Language.hs
@@ -48,6 +48,7 @@ module Ganeti.Query.Language
     , ResultValue
     , ItemType(..)
     , QueryTypeOp(..)
+    , queryTypeOpToRaw
     , QueryTypeLuxi(..)
     , checkRS
     ) where