diff --git a/Makefile.am b/Makefile.am
index b94c0e3ddbcf8f56959cd3d2dd6a93393339d28c..3e2eba4f7dc98003ece96c4acef94a65c0968dac 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -416,7 +416,10 @@ HS_LIB_SRCS = \
 	htools/Ganeti/OpCodes.hs \
 	htools/Ganeti/Rpc.hs \
 	htools/Ganeti/Qlang.hs \
+	htools/Ganeti/Query/Common.hs \
+	htools/Ganeti/Query/Node.hs \
 	htools/Ganeti/Query/Query.hs \
+	htools/Ganeti/Query/Types.hs \
 	htools/Ganeti/Queryd.hs \
 	htools/Ganeti/Runtime.hs \
 	htools/Ganeti/Ssconf.hs \
diff --git a/htools/Ganeti/Qlang.hs b/htools/Ganeti/Qlang.hs
index 9ff43e74534984e3db227882f881e68b1161a12e..1a7a6f77a4dac172e2a1b27c37375049a4e1f79d 100644
--- a/htools/Ganeti/Qlang.hs
+++ b/htools/Ganeti/Qlang.hs
@@ -33,10 +33,14 @@ module Ganeti.Qlang
     , QueryResult(..)
     , QueryFields(..)
     , QueryFieldsResult(..)
+    , FieldName
+    , FieldTitle
     , FieldType(..)
+    , FieldDoc
     , FieldDefinition(..)
     , ResultEntry(..)
     , ResultStatus(..)
+    , ResultValue
     , ItemType(..)
     , checkRS
     ) where
diff --git a/htools/Ganeti/Query/Common.hs b/htools/Ganeti/Query/Common.hs
new file mode 100644
index 0000000000000000000000000000000000000000..76fe9fa8d2965abbb3ad5f27bfebbbddfda07317
--- /dev/null
+++ b/htools/Ganeti/Query/Common.hs
@@ -0,0 +1,158 @@
+{-| Implementation of the Ganeti Query2 common objects.
+
+ -}
+
+{-
+
+Copyright (C) 2012 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.Query.Common
+  ( rsNoData
+  , rsNormal
+  , rsMaybe
+  , rsUnknown
+  , missingRuntime
+  , timeStampFields
+  , uuidFields
+  , serialFields
+  , tagsFields
+  , dictFieldGetter
+  , buildQFTLookup
+  , buildNdParamField
+  ) where
+
+import qualified Data.Map as Map
+import Data.Maybe (fromMaybe)
+import Text.JSON (JSON, showJSON)
+
+import qualified Ganeti.Constants as C
+import Ganeti.Config
+import Ganeti.Objects
+import Ganeti.Qlang
+import Ganeti.Query.Types
+
+-- * Generic functions
+
+-- | Conversion from 'VType' to 'FieldType'.
+vTypeToQFT :: VType -> FieldType
+vTypeToQFT VTypeString      = QFTOther
+vTypeToQFT VTypeMaybeString = QFTOther
+vTypeToQFT VTypeBool        = QFTBool
+vTypeToQFT VTypeSize        = QFTUnit
+vTypeToQFT VTypeInt         = QFTNumber
+
+-- * Result helpers
+
+-- | Helper for a result with no data.
+rsNoData :: ResultEntry
+rsNoData = ResultEntry RSNoData Nothing
+
+-- | Helper to declare a normal result.
+rsNormal :: (JSON a) => a -> ResultEntry
+rsNormal a = ResultEntry RSNormal $ Just (showJSON a)
+
+-- | Helper to declare a result from a 'Maybe' (the item might be
+-- missing, in which case we return no data). Note that there's some
+-- ambiguity here: in some cases, we mean 'RSNoData', but in other
+-- 'RSUnavail'; this is easy to solve in simple cases, but not in
+-- nested dicts.
+rsMaybe :: (JSON a) => Maybe a -> ResultEntry
+rsMaybe = maybe rsNoData rsNormal
+
+-- | Helper for unknown field result.
+rsUnknown :: ResultEntry
+rsUnknown = ResultEntry RSUnknown Nothing
+
+-- | Helper for a missing runtime parameter.
+missingRuntime :: FieldGetter a b
+missingRuntime = FieldRuntime (\_ _ -> ResultEntry RSNoData Nothing)
+
+-- * Common fields
+
+-- | The list of timestamp fields.
+timeStampFields :: (TimeStampObject a) => FieldList a b
+timeStampFields =
+  [ (FieldDefinition "ctime" "CTime" QFTTimestamp "Creation timestamp",
+     FieldSimple (rsNormal . cTimeOf))
+  , (FieldDefinition "mtime" "MTime" QFTTimestamp "Modification timestamp",
+     FieldSimple (rsNormal . mTimeOf))
+  ]
+
+-- | The list of UUID fields.
+uuidFields :: (UuidObject a) => String -> FieldList a b
+uuidFields name =
+  [ (FieldDefinition "uuid" "UUID" QFTText  (name ++ " UUID"),
+     FieldSimple (rsNormal . uuidOf)) ]
+
+-- | The list of serial number fields.
+serialFields :: (SerialNoObject a) => String -> FieldList a b
+serialFields name =
+  [ (FieldDefinition "serial_no" "SerialNo" QFTNumber
+     (name ++ " object serial number, incremented on each modification"),
+     FieldSimple (rsNormal . serialOf)) ]
+
+-- | The list of tag fields.
+tagsFields :: (TagsObject a) => FieldList a b
+tagsFields =
+  [ (FieldDefinition "tags" "Tags" QFTOther "Tags",
+     FieldSimple (rsNormal . tagsOf)) ]
+
+-- * Generic parameter functions
+
+-- | Returns a field from a (possibly missing) 'DictObject'. This is
+-- used by parameter dictionaries, usually. Note that we have two
+-- levels of maybe: the top level dict might be missing, or one key in
+-- the dictionary might be.
+dictFieldGetter :: (DictObject a) => String -> Maybe a -> ResultEntry
+dictFieldGetter k = maybe rsNoData (rsMaybe . lookup k . toDict)
+
+-- | Build an optimised lookup map from a Python _PARAMETER_TYPES
+-- association list.
+buildQFTLookup :: [(String, String)] -> Map.Map String FieldType
+buildQFTLookup =
+  Map.fromList .
+  map (\(k, v) -> (k, maybe QFTOther vTypeToQFT (vTypeFromRaw v)))
+
+-- | Ndparams optimised lookup map.
+ndParamTypes :: Map.Map String FieldType
+ndParamTypes = buildQFTLookup C.ndsParameterTypes
+
+-- | Ndparams title map.
+ndParamTitles :: Map.Map String FieldTitle
+ndParamTitles = Map.fromList C.ndsParameterTitles
+
+-- | Ndparam getter builder: given a field, it returns a FieldConfig
+-- getter, that is a function that takes the config and the object and
+-- returns the Ndparam field specified when the getter was built.
+ndParamGetter :: (NdParamObject a) =>
+                 String -- ^ The field we're building the getter for
+              -> ConfigData -> a -> ResultEntry
+ndParamGetter field config =
+  dictFieldGetter field . getNdParamsOf config
+
+-- | Builds the ndparam fields for an object.
+buildNdParamField :: (NdParamObject a) => String -> FieldData a b
+buildNdParamField field =
+  let full_name = "ndp/" ++ field
+      title = fromMaybe field $ field `Map.lookup` ndParamTitles
+      qft = fromMaybe QFTOther $ field `Map.lookup` ndParamTypes
+      desc = "The \"" ++ field ++ "\" node parameter"
+  in (FieldDefinition full_name title qft desc,
+      FieldConfig (ndParamGetter field))
diff --git a/htools/Ganeti/Query/Node.hs b/htools/Ganeti/Query/Node.hs
new file mode 100644
index 0000000000000000000000000000000000000000..77fcc882d4e44184ea08732330ebdb5aa5920859
--- /dev/null
+++ b/htools/Ganeti/Query/Node.hs
@@ -0,0 +1,163 @@
+{-| Implementation of the Ganeti Query2 node queries.
+
+ -}
+
+{-
+
+Copyright (C) 2012 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.Query.Node
+  ( NodeRuntime(..)
+  , nodeFieldsMap
+  ) where
+
+import Control.Applicative
+import Data.List
+import qualified Data.Map as Map
+
+import Ganeti.Config
+import Ganeti.Objects
+import Ganeti.Qlang
+import Ganeti.Query.Common
+import Ganeti.Query.Types
+
+-- | Stub data type until we integrate the RPC.
+data NodeRuntime = NodeRuntime
+
+-- | List of node live fields, all ignored for now (no RPC).
+nodeLiveFieldsDefs :: [(FieldName, FieldTitle, FieldType, String, FieldDoc)]
+nodeLiveFieldsDefs =
+  [ ("bootid", "BootID", QFTText, "bootid",
+     "Random UUID renewed for each system reboot, can be used\
+     \ for detecting reboots by tracking changes")
+  , ("cnodes", "CNodes", QFTNumber, "cpu_nodes",
+     "Number of NUMA domains on node (if exported by hypervisor)")
+  , ("csockets", "CSockets", QFTNumber, "cpu_sockets",
+     "Number of physical CPU sockets (if exported by hypervisor)")
+  , ("ctotal", "CTotal", QFTNumber, "cpu_total",
+     "Number of logical processors")
+  , ("dfree", "DFree", QFTUnit, "vg_free",
+     "Available disk space in volume group")
+  , ("dtotal", "DTotal", QFTUnit, "vg_size",
+     "Total disk space in volume group used for instance disk allocation")
+  , ("mfree", "MFree", QFTUnit, "memory_free",
+     "Memory available for instance allocations")
+  , ("mnode", "MNode", QFTUnit, "memory_dom0",
+     "Amount of memory used by node (dom0 for Xen)")
+  , ("mtotal", "MTotal", QFTUnit, "memory_total",
+     "Total amount of memory of physical machine")
+  ]
+
+-- | Builder for node live fields.
+nodeLiveFieldBuilder :: (FieldName, FieldTitle, FieldType, String, FieldDoc)
+                     -> FieldData Node NodeRuntime
+nodeLiveFieldBuilder (fname, ftitle, ftype, _, fdoc) =
+  (FieldDefinition fname ftitle ftype fdoc, missingRuntime)
+
+-- | The docstring for the node role. Note that we use 'reverse in
+-- order to keep the same order as Python.
+nodeRoleDoc :: String
+nodeRoleDoc =
+  "Node role; " ++
+  (intercalate ", " $
+   map (\role ->
+          "\"" ++ nodeRoleToRaw role ++ "\" for " ++ roleDescription role)
+   (reverse [minBound..maxBound]))
+
+-- | List of all node fields.
+nodeFields :: FieldList Node NodeRuntime
+nodeFields =
+  [ (FieldDefinition "drained" "Drained" QFTBool "Whether node is drained",
+     FieldSimple (rsNormal . nodeDrained))
+  , (FieldDefinition "master_candidate" "MasterC" QFTBool
+       "Whether node is a master candidate",
+     FieldSimple (rsNormal . nodeMasterCandidate))
+  , (FieldDefinition "master_capable" "MasterCapable" QFTBool
+       "Whether node can become a master candidate",
+     FieldSimple (rsNormal . nodeMasterCapable))
+  , (FieldDefinition "name" "Node" QFTText "Node name",
+     FieldSimple (rsNormal . nodeName))
+  , (FieldDefinition "offline" "Offline" QFTBool
+       "Whether node is marked offline",
+     FieldSimple (rsNormal . nodeOffline))
+  , (FieldDefinition "vm_capable" "VMCapable" QFTBool
+       "Whether node can host instances",
+     FieldSimple (rsNormal . nodeVmCapable))
+  , (FieldDefinition "pip" "PrimaryIP" QFTText "Primary IP address",
+     FieldSimple (rsNormal . nodePrimaryIp))
+  , (FieldDefinition "sip" "SecondaryIP" QFTText "Secondary IP address",
+     FieldSimple (rsNormal . nodeSecondaryIp))
+  , (FieldDefinition "master" "IsMaster" QFTBool "Whether node is master",
+     FieldConfig (\cfg node ->
+                    rsNormal (nodeName node ==
+                              clusterMasterNode (configCluster cfg))))
+  , (FieldDefinition "group" "Group" QFTText "Node group",
+     FieldConfig (\cfg node ->
+                    rsMaybe (groupName <$> getGroupOfNode cfg node)))
+  , (FieldDefinition "group.uuid" "GroupUUID" QFTText "UUID of node group",
+     FieldSimple (rsNormal . nodeGroup))
+  ,  (FieldDefinition "ndparams" "NodeParameters" QFTOther
+        "Merged node parameters",
+      FieldConfig ((rsMaybe .) . getNodeNdParams))
+  , (FieldDefinition "custom_ndparams" "CustomNodeParameters" QFTOther
+                       "Custom node parameters",
+     FieldSimple (rsNormal . nodeNdparams))
+  -- FIXME: the below could be generalised a bit, like in Python
+  , (FieldDefinition "pinst_cnt" "Pinst" QFTNumber
+       "Number of instances with this node as primary",
+     FieldConfig (\cfg ->
+                    rsNormal . length . fst . getNodeInstances cfg . nodeName))
+  , (FieldDefinition "sinst_cnt" "Sinst" QFTNumber
+       "Number of instances with this node as secondary",
+     FieldConfig (\cfg ->
+                    rsNormal . length . snd . getNodeInstances cfg . nodeName))
+  , (FieldDefinition "pinst_list" "PriInstances" QFTNumber
+       "List of instances with this node as primary",
+     FieldConfig (\cfg -> rsNormal . map instName . fst .
+                          getNodeInstances cfg . nodeName))
+  , (FieldDefinition "sinst_list" "SecInstances" QFTNumber
+       "List of instances with this node as secondary",
+     FieldConfig (\cfg -> rsNormal . map instName . snd .
+                          getNodeInstances cfg . nodeName))
+  , (FieldDefinition "role" "Role" QFTText nodeRoleDoc,
+     FieldConfig ((rsNormal .) . getNodeRole))
+  -- FIXME: the powered state is special (has an different context,
+  -- not runtime) in Python
+  , (FieldDefinition "powered" "Powered" QFTBool
+       "Whether node is thought to be powered on",
+     missingRuntime)
+  -- FIXME: the two fields below are incomplete in Python, part of the
+  -- non-implemented node resource model; they are declared just for
+  -- parity, but are not functional
+  , (FieldDefinition "hv_state" "HypervisorState" QFTOther "Hypervisor state",
+     missingRuntime)
+  , (FieldDefinition "disk_state" "DiskState" QFTOther "Disk state",
+     missingRuntime)
+  ] ++
+  map nodeLiveFieldBuilder nodeLiveFieldsDefs ++
+  map buildNdParamField allNDParamFields ++
+  timeStampFields ++
+  uuidFields "Node" ++
+  serialFields "Node" ++
+  tagsFields
+
+-- | The node fields map.
+nodeFieldsMap :: FieldMap Node NodeRuntime
+nodeFieldsMap = Map.fromList $ map (\v -> (fdefName (fst v), v)) nodeFields
diff --git a/htools/Ganeti/Query/Query.hs b/htools/Ganeti/Query/Query.hs
index 6cf7093cdf9d8a615e29445f8560ede56be517a5..3df39a3a1df7d698df248a51d1bbb894df9d86b1 100644
--- a/htools/Ganeti/Query/Query.hs
+++ b/htools/Ganeti/Query/Query.hs
@@ -27,13 +27,55 @@ module Ganeti.Query.Query
     ( query
     ) where
 
+import Data.Maybe (fromMaybe)
+import qualified Data.Map as Map
+
 import Ganeti.BasicTypes
+import Ganeti.HTools.JSON
 import Ganeti.Qlang
+import Ganeti.Query.Common
+import Ganeti.Query.Types
+import Ganeti.Query.Node
 import Ganeti.Objects
 
+-- * Helper functions
+
+-- | Builds an unknown field definition.
+mkUnknownFDef :: String -> FieldData a b
+mkUnknownFDef name =
+  ( FieldDefinition name name QFTUnknown ("Unknown field '" ++ name ++ "'")
+  , FieldUnknown )
+
+-- | Runs a field getter on the existing contexts.
+execGetter :: ConfigData -> b -> a -> FieldGetter a b -> ResultEntry
+execGetter _   _ item (FieldSimple getter)  = getter item
+execGetter cfg _ item (FieldConfig getter)  = getter cfg item
+execGetter _  rt item (FieldRuntime getter) = getter rt item
+execGetter _   _ _    FieldUnknown          = rsUnknown
+
+-- * Main query execution
+
+-- | Helper to build the list of requested fields. This transforms the
+-- list of string fields to a list of field defs and getters, with
+-- some of them possibly being unknown fields.
+getSelectedFields :: FieldMap a b  -- ^ Defined fields
+                  -> [String]      -- ^ Requested fields
+                  -> FieldList a b -- ^ Selected fields
+getSelectedFields defined =
+  map (\name -> fromMaybe (mkUnknownFDef name) $ name `Map.lookup` defined)
+
 -- | Main query execution function.
 query :: ConfigData   -- ^ The current configuration
       -> Query        -- ^ The query (item, fields, filter)
       -> IO (Result QueryResult) -- ^ Result
+
+query cfg (Query QRNode fields _) = return $ do
+  let selected = getSelectedFields nodeFieldsMap fields
+      (fdefs, fgetters) = unzip selected
+      nodes = Map.elems . fromContainer $ configNodes cfg
+      fdata = map (\node -> map (execGetter cfg NodeRuntime node) fgetters)
+              nodes
+  return QueryResult { qresFields = fdefs, qresData = fdata }
+
 query _ (Query qkind _ _) =
   return . Bad $ "Query '" ++ show qkind ++ "' not supported"
diff --git a/htools/Ganeti/Query/Types.hs b/htools/Ganeti/Query/Types.hs
new file mode 100644
index 0000000000000000000000000000000000000000..b3bfde7b367030748f47a122bcf5fc5487cb6657
--- /dev/null
+++ b/htools/Ganeti/Query/Types.hs
@@ -0,0 +1,54 @@
+{-| Implementation of the Ganeti Query2 basic types.
+
+These are types internal to the library, and for example clients that
+use the library should not need to import it.
+
+ -}
+
+{-
+
+Copyright (C) 2012 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.Query.Types where
+
+import qualified Data.Map as Map
+
+import Ganeti.Qlang
+import Ganeti.Objects
+
+-- | The type of field getters. The \"a\" type represents the type
+-- we're querying, whereas the \"b\" type represents the \'runtime\'
+-- data for that type (if any). Note that we don't support multiple
+-- runtime sources, and we always consider the entire configuration as
+-- a given (so no equivalent for Python's /*_CONFIG/ and /*_GROUP/;
+-- configuration accesses are cheap for us).
+data FieldGetter a b = FieldSimple  (a -> ResultEntry)
+                     | FieldRuntime (b -> a -> ResultEntry)
+                     | FieldConfig  (ConfigData -> a -> ResultEntry)
+                     | FieldUnknown
+
+-- | Alias for a field data (definition and getter).
+type FieldData a b = (FieldDefinition, FieldGetter a b)
+
+-- | Alias for a field data list.
+type FieldList a b = [FieldData a b]
+
+-- | Alias for field maps.
+type FieldMap a b = Map.Map String (FieldData a b)