diff --git a/Makefile.am b/Makefile.am
index 865df3ebec0ef62f2f01bd7041eb6d0d36013eaf..6f621a999465152921f45b8570eda86603544feb 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -172,6 +172,7 @@ client_PYTHON = \
 	lib/client/gnt_instance.py \
 	lib/client/gnt_job.py \
 	lib/client/gnt_node.py \
+	lib/client/gnt_network.py \
 	lib/client/gnt_os.py
 
 hypervisor_PYTHON = \
@@ -318,6 +319,7 @@ gnt_scripts = \
 	scripts/gnt-group \
 	scripts/gnt-instance \
 	scripts/gnt-job \
+	scripts/gnt-network \
 	scripts/gnt-node \
 	scripts/gnt-os
 
@@ -333,6 +335,7 @@ PYTHON_BOOTSTRAP = \
 	scripts/gnt-group \
 	scripts/gnt-instance \
 	scripts/gnt-job \
+	scripts/gnt-network \
 	scripts/gnt-node \
 	scripts/gnt-os
 
@@ -441,6 +444,7 @@ man_MANS = \
 	man/gnt-cluster.8 \
 	man/gnt-debug.8 \
 	man/gnt-group.8 \
+	man/gnt-network.8 \
 	man/gnt-instance.8 \
 	man/gnt-job.8 \
 	man/gnt-node.8 \
diff --git a/lib/cli.py b/lib/cli.py
index 3c889c9684a5443156a38867ec3be7e8cadafc1f..9d304055bec855346df8f2d89e06f88d52b00998 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -79,6 +79,7 @@ __all__ = [
   "FILESTORE_DRIVER_OPT",
   "FORCE_OPT",
   "FORCE_VARIANT_OPT",
+  "GATEWAY_OPT",
   "GLOBAL_FILEDIR_OPT",
   "HID_OS_OPT",
   "GLOBAL_SHARED_FILEDIR_OPT",
@@ -101,6 +102,7 @@ __all__ = [
   "MC_OPT",
   "MIGRATION_MODE_OPT",
   "NET_OPT",
+  "NETWORK_OPT",
   "NEW_CLUSTER_CERT_OPT",
   "NEW_CLUSTER_DOMAIN_SECRET_OPT",
   "NEW_CONFD_HMAC_KEY_OPT",
@@ -143,6 +145,7 @@ __all__ = [
   "REBOOT_TYPE_OPT",
   "REMOVE_INSTANCE_OPT",
   "REMOVE_UIDS_OPT",
+  "RESERVED_IPS_OPT",
   "RESERVED_LVS_OPT",
   "ROMAN_OPT",
   "SECONDARY_IP_OPT",
@@ -195,11 +198,13 @@ __all__ = [
   "ARGS_MANY_INSTANCES",
   "ARGS_MANY_NODES",
   "ARGS_MANY_GROUPS",
+  "ARGS_MANY_NETWORKS",
   "ARGS_NONE",
   "ARGS_ONE_INSTANCE",
   "ARGS_ONE_NODE",
   "ARGS_ONE_GROUP",
   "ARGS_ONE_OS",
+  "ARGS_ONE_NETWORK",
   "ArgChoice",
   "ArgCommand",
   "ArgFile",
@@ -207,6 +212,7 @@ __all__ = [
   "ArgHost",
   "ArgInstance",
   "ArgJobId",
+  "ArgNetwork",
   "ArgNode",
   "ArgOs",
   "ArgSuggest",
@@ -217,6 +223,7 @@ __all__ = [
   "OPT_COMPL_ONE_INSTANCE",
   "OPT_COMPL_ONE_NODE",
   "OPT_COMPL_ONE_NODEGROUP",
+  "OPT_COMPL_ONE_NETWORK",
   "OPT_COMPL_ONE_OS",
   "OPT_COMPL_NIC_PARAMS",
   "OPT_COMPL_DISK_PARAMS",
@@ -302,6 +309,11 @@ class ArgNode(_Argument):
   """
 
 
+class ArgNetwork(_Argument):
+  """Network argument.
+
+  """
+
 class ArgGroup(_Argument):
   """Node group argument.
 
@@ -340,9 +352,11 @@ class ArgOs(_Argument):
 
 ARGS_NONE = []
 ARGS_MANY_INSTANCES = [ArgInstance()]
+ARGS_MANY_NETWORKS = [ArgNetwork()]
 ARGS_MANY_NODES = [ArgNode()]
 ARGS_MANY_GROUPS = [ArgGroup()]
 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
+ARGS_ONE_NETWORK = [ArgNetwork(min=1, max=1)]
 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
 # TODO
 ARGS_ONE_GROUP = [ArgGroup(min=1, max=1)]
@@ -559,9 +573,10 @@ def check_bool(option, opt, value): # pylint: disable-msg=W0613
  OPT_COMPL_ONE_IALLOCATOR,
  OPT_COMPL_INST_ADD_NODES,
  OPT_COMPL_ONE_NODEGROUP,
+ OPT_COMPL_ONE_NETWORK,
  OPT_COMPL_NIC_PARAMS,
  OPT_COMPL_DISK_PARAMS,
- OPT_COMPL_BACKEND_PARAMS) = range(100, 110)
+ OPT_COMPL_BACKEND_PARAMS) = range(100, 111)
 
 OPT_COMPL_ALL = frozenset([
   OPT_COMPL_MANY_NODES,
@@ -571,6 +586,7 @@ OPT_COMPL_ALL = frozenset([
   OPT_COMPL_ONE_IALLOCATOR,
   OPT_COMPL_INST_ADD_NODES,
   OPT_COMPL_ONE_NODEGROUP,
+  OPT_COMPL_ONE_NETWORK,
   OPT_COMPL_NIC_PARAMS,
   OPT_COMPL_DISK_PARAMS,
   OPT_COMPL_BACKEND_PARAMS,
@@ -1185,6 +1201,18 @@ NODE_POWERED_OPT = cli_option("--node-powered", default=None,
                               dest="node_powered",
                               help="Specify if the SoR for node is powered")
 
+NETWORK_OPT = cli_option("--network",
+                         action="store", default=None, dest="network",
+                         help="IP network in CIDR notation")
+
+GATEWAY_OPT = cli_option("--gateway",
+                         action="store", default=None, dest="gateway",
+                         help="IP address of the router (gateway)")
+
+RESERVED_IPS_OPT = cli_option("--reserved-ips",
+                              action="store", default=None, dest="reserved_ips",
+                              help="Comma-delimited list of reserved IPs")
+
 
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT]
diff --git a/lib/client/gnt_network.py b/lib/client/gnt_network.py
new file mode 100644
index 0000000000000000000000000000000000000000..65a3d02e414624f0d2a338188ac744bea653b0db
--- /dev/null
+++ b/lib/client/gnt_network.py
@@ -0,0 +1,274 @@
+#
+#
+
+# Copyright (C) 2011 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.
+
+"""IP pool related commands"""
+
+# pylint: disable-msg=W0401,W0614
+# W0401: Wildcard import ganeti.cli
+# W0614: Unused import %s from wildcard import (since we need cli)
+
+from ganeti.cli import *
+from ganeti import constants
+from ganeti import opcodes
+from ganeti import utils
+from textwrap import wrap
+
+
+#: default list of fields for L{ListNetworks}
+_LIST_DEF_FIELDS = ["name", "network", "gateway", "group_cnt", "group_links"]
+
+
+def AddNetwork(opts, args):
+  """Add a network to the cluster.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: a list of length 1 with the link name the pool to create
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  (network_name, ) = args
+  if opts.reserved_ips is not None:
+    if opts.reserved_ips == "":
+      opts.reserved_ips = []
+    else:
+      opts.reserved_ips = utils.UnescapeAndSplit(opts.reserved_ips, sep=",")
+
+  op = opcodes.OpNetworkAdd(network_name=network_name,
+                            gateway=opts.gateway,
+                            network=opts.network,
+                            reserved_ips=opts.reserved_ips)
+  SubmitOpCode(op, opts=opts)
+
+
+def MapNetwork(opts, args):
+  """Map a network to a node group.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: a list of length 3 with the link name the pool to create
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  op = opcodes.OpGroupSetParams(group_name=args[1],
+                                network=[constants.DDM_ADD,
+                                         args[0], args[2]])
+  SubmitOpCode(op, opts=opts)
+
+
+def UnmapNetwork(opts, args):
+  """Unmap a network from a node group.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: a list of length 3 with the link name the pool to create
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  op = opcodes.OpGroupSetParams(group_name=args[1],
+                                network=[constants.DDM_REMOVE,
+                                         args[0], None])
+  SubmitOpCode(op, opts=opts)
+
+def ListNetworks(opts, args):
+  """List Ip pools and their properties.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: networks to list, or empty for all
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  desired_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
+  fmtoverride = {
+    "group_links": (",".join, False),
+    "inst_list": (",".join, False),
+  }
+
+  return GenericList(constants.QR_NETWORK, desired_fields, args, None,
+                     opts.separator, not opts.no_headers,
+                     verbose=opts.verbose, format_override=fmtoverride)
+
+
+def ListNetworkFields(opts, args):
+  """List network fields.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: fields to list, or empty for all
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  return GenericListFields(constants.QR_NETWORK, args, opts.separator,
+                           not opts.no_headers)
+
+
+def ShowNetworkConfig(opts, args):
+  """Show network information.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should either be an empty list, in which case
+      we show information about all nodes, or should contain
+      a list of networks (names or UUIDs) to be queried for information
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  cl = GetClient()
+  result = cl.QueryNetworks(fields=["name", "network", "gateway",
+                                    "free_count", "reserved_count",
+                                    "map", "group_links", "inst_list"],
+                            names=args, use_locking=False)
+
+  for (name, network, gateway, free_count, reserved_count,
+       map, group_links, instances) in result:
+    size = free_count + reserved_count
+    ToStdout("Network name: %s", name)
+    ToStdout("  subnet: %s", network)
+    ToStdout("  gateway: %s", gateway)
+    ToStdout("  size: %d", size)
+    ToStdout("  free: %d (%.2f%%)", free_count,
+             100 * float(free_count)/float(size))
+    ToStdout("  usage map:")
+    idx = 0
+    for line in wrap(map, width=64):
+      ToStdout("     %s %s %d", str(idx).rjust(3), line.ljust(64), idx + 63)
+      idx += 64
+    ToStdout("         (X) used    (.) free")
+
+    if group_links:
+      ToStdout("  connected to node groups:")
+      for conn in group_links:
+        group, link = conn.split(":")
+        ToStdout("    %s (link %s)", group, link)
+    else:
+      ToStdout("  not connected to any node group")
+
+    if instances:
+      ToStdout("  used by %d instances:", len(instances))
+      for inst in instances:
+        ToStdout("    %s", inst)
+    else:
+      ToStdOut("  not used by any instances")
+
+
+def SetNetworkParams(opts, args):
+  """Modifies an IP address pool's parameters.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should contain only one element, the node group name
+
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  all_changes = {
+    "gateway": opts.gateway,
+    "reserved_ips": opts.reserved_ips,
+  }
+
+  if all_changes.values().count(None) == len(all_changes):
+    ToStderr("Please give at least one of the parameters.")
+    return 1
+
+  op = opcodes.OpNetworkSetParams(network_name=args[0],
+                                  # pylint: disable-msg=W0142
+                                  **all_changes)
+  result = SubmitOrSend(op, opts)
+
+  if result:
+    ToStdout("Modified ip pool %s", args[0])
+    for param, data in result:
+      ToStdout(" - %-5s -> %s", param, data)
+
+  return 0
+
+
+def RemoveNetwork(opts, args):
+  """Remove an IP address pool from the cluster.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: a list of length 1 with the id of the IP address pool to remove
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  (network_name,) = args
+  op = opcodes.OpNetworkRemove(network_name=network_name, force=opts.force)
+  SubmitOpCode(op, opts=opts)
+
+
+commands = {
+  "add": (
+    AddNetwork, ARGS_ONE_NETWORK,
+    [DRY_RUN_OPT, NETWORK_OPT, GATEWAY_OPT, RESERVED_IPS_OPT],
+    "<network_name>", "Add a new IP network to the cluster"),
+  "list": (
+    ListNetworks, ARGS_MANY_NETWORKS,
+    [NOHDR_OPT, SEP_OPT, FIELDS_OPT, VERBOSE_OPT],
+    "[<network_id>...]",
+    "Lists the IP networks in the cluster. The available fields can be shown"
+    " using the \"list-fields\" command (see the man page for details)."
+    " The default list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
+  "list-fields": (
+    ListNetworkFields, [ArgUnknown()], [NOHDR_OPT, SEP_OPT], "[fields...]",
+    "Lists all available fields for networks"),
+  #
+  #"list-maps"
+  #
+  #"info"
+  "info": (
+    ShowNetworkConfig, ARGS_MANY_NETWORKS, [],
+    "[<network_name>...]", "Show information about the network(s)"),
+  #
+  #"modify": (
+  #  SetNetworkParams, ARGS_ONE_NETWORK,
+  #  [DRY_RUN_OPT, SUBMIT_OPT, RESERVED_IPS_OPT, NETWORK_OPT, GATEWAY_OPT],
+  #  "<network_name>", "Alters the parameters of a network"),
+  "connect": (
+    MapNetwork,
+    [ArgNetwork(min=1, max=1), ArgGroup(min=1, max=1),
+     ArgUnknown(min=1, max=1)],
+    [],
+    "<network_name> <node_group> <link_name>",
+    "Map a given network to a link of a specified node group"),
+  "disconnect": (
+    UnmapNetwork,
+    [ArgNetwork(min=1, max=1), ArgGroup(min=1, max=1)],
+    [],
+    "<network_name> <node_group>",
+    "Unmap a given network from a specified node group"),
+  #"remove": (
+  #  RemoveNetwork, ARGS_ONE_NETWORK, [FORCE_OPT, DRY_RUN_OPT],
+  #  "[--dry-run] <network_id>",
+  #  "Remove an (empty) network from the cluster"),
+}
+
+
+def Main():
+  return GenericMain(commands)
diff --git a/man/gnt-network.rst b/man/gnt-network.rst
new file mode 100644
index 0000000000000000000000000000000000000000..831ccbde1814bcf678b79921b5b894bac4cfd249
--- /dev/null
+++ b/man/gnt-network.rst
@@ -0,0 +1,154 @@
+gnt-network(8) Ganeti | Version @GANETI_VERSION@
+================================================
+
+Name
+----
+
+gnt-network - Ganeti network administration
+
+Synopsis
+--------
+
+**gnt-network** {command} [arguments...]
+
+DESCRIPTION
+-----------
+
+The **gnt-network** command is used for network definition administration
+in the Ganeti system.
+
+COMMANDS
+--------
+
+ADD
+~~~
+
+| **add**
+| [--network=*NETWORK*]
+| [--gateway=*GATEWAY*]
+| {*network*}
+
+Creates a new network with the given name. The network will be unused
+initially. To connect it to a node group, use ``gnt-network connect``.
+
+The ``--network`` option allows you to specify the network in a CIDR notation.
+
+The ``--gateway`` option allows you to specify the default gateway for this
+network.
+
+MODIFY
+~~~~~~
+
+| **modify**
+| [--node-parameters=*NDPARAMS*]
+| [--alloc-policy=*POLICY*]
+| {*group*}
+
+Modifies some parameters from the node group.
+
+The ``--node-parameters`` and ``--alloc-policy`` optiosn are documented
+in the **add** command above.
+
+REMOVE
+~~~~~~
+
+| **remove** {*group*}
+
+Deletes the indicated node group, which must be empty. There must always be at
+least one group, so the last group cannot be removed.
+
+LIST
+~~~~
+
+| **list** [--no-headers] [--separator=*SEPARATOR*] [-v]
+| [-o *[+]FIELD,...*] [network...]
+
+Lists all existing node groups in the cluster.
+
+The ``--no-headers`` option will skip the initial header line. The
+``--separator`` option takes an argument which denotes what will be
+used between the output fields. Both these options are to help
+scripting.
+
+The ``-v`` option activates verbose mode, which changes the display of
+special field states (see **ganeti(7)**).
+
+The ``-o`` option takes a comma-separated list of output fields.
+If the value of the option starts with the character ``+``, the new
+fields will be added to the default list. This allows to quickly
+see the default list plus a few other fields, instead of retyping
+the entire list of fields.
+
+The available fields and their meaning are:
+
+name
+    the group name
+
+uuid
+    the group's UUID
+
+node_cnt
+    the number of nodes in the node group
+
+node_list
+    the list of nodes that belong to this group
+
+pinst_cnt
+    the number of primary instances in the group (i.e., the number of
+    primary instances nodes in this group have)
+
+pinst_list
+    the list of primary instances in the group
+
+alloc_policy
+    the current allocation policy for the group
+
+ctime
+    the creation time of the group; note that this field contains spaces
+    and as such it's harder to parse
+
+    if this attribute is not present (e.g. when upgrading from older
+    versions), then "N/A" will be shown instead
+
+mtime
+    the last modification time of the group; note that this field
+    contains spaces and as such it's harder to parse
+
+serial_no
+    the so called 'serial number' of the group; this is a numeric field
+    that is incremented each time the node is modified, and it can be
+    used to detect modifications
+
+If no group names are given, then all groups are included. Otherwise,
+only the named groups will be listed.
+
+LIST-FIELDS
+~~~~~~~~~~~
+
+**list-fields** [field...]
+
+List available fields for node groups.
+
+RENAME
+~~~~~~
+
+| **rename** {*oldname*} {*newname*}
+
+Renames a given group from *oldname* to *newname*.
+
+INFO
+~~~~
+
+| **info** [network...]
+
+Displays information about a given network.
+
+CONNECT
+~~~~~~~
+| **connect** {*network*} {*group*} {*link*}
+
+Connect a network to a given nodegroup's link.
+
+DISCONNECT
+~~~~~~~~~~
+| **disconnect** {*network*} {*group*}