diff --git a/doc/hooks.rst b/doc/hooks.rst
index be4c730b5b0d4e927cdc998b575847b07736f902..956ad303616537b29634a25338f0b7bb0ab808cd 100644
--- a/doc/hooks.rst
+++ b/doc/hooks.rst
@@ -214,6 +214,69 @@ Evacuates a node group.
 :pre-execution: master node and all nodes in the group
 :post-execution: master node and all nodes in the group
 
+Network operations
+~~~~~~~~~~~~~~~~~~
+
+OP_NETWORK_ADD
+++++++++++++++
+
+Adds a network to the cluster.
+
+:directory: network-add
+:env. vars: NETWORK_NAME, NETWORK_SUBNET, NETWORK_GATEWAY, NETWORK_SUBNET6,
+            NETWORK_GATEWAY6, NETWORK_TYPE, NETWORK_MAC_PREFIX, NETWORK_TAGS
+:pre-execution: master node
+:post-execution: master node
+
+OP_NETWORK_REMOVE
++++++++++++++++++
+
+Removes a network from the cluster.
+
+:directory: network-remove
+:env. vars: NETWORK_NAME
+:pre-execution: master node
+:post-execution: master node
+
+OP_NETWORK_CONNECT
+++++++++++++++++++
+
+Connects a network to a nodegroup.
+
+:directory: network-connect
+:env. vars: GROUP_NAME, NETWORK_NAME,
+            GROUP_NETWORK_MODE, GROUP_NETWORK_LINK,
+            NETWORK_SUBNET, NETWORK_GATEWAY, NETWORK_SUBNET6,
+            NETWORK_GATEWAY6, NETWORK_TYPE, NETWORK_MAC_PREFIX, NETWORK_TAGS
+:pre-execution: nodegroup nodes
+:post-execution: nodegroup nodes
+
+
+OP_NETWORK_DISCONNECT
++++++++++++++++++++++
+
+Disconnects a network from a nodegroup.
+
+:directory: network-disconnect
+:env. vars: GROUP_NAME, NETWORK_NAME,
+            GROUP_NETWORK_MODE, GROUP_NETWORK_LINK,
+            NETWORK_SUBNET, NETWORK_GATEWAY, NETWORK_SUBNET6,
+            NETWORK_GATEWAY6, NETWORK_TYPE, NETWORK_MAC_PREFIX, NETWORK_TAGS
+:pre-execution: nodegroup nodes
+:post-execution: nodegroup nodes
+
+
+OP_NETWORK_SET_PARAMS
++++++++++++++++++++++
+
+Modifies a network.
+
+:directory: network-modify
+:env. vars: NETWORK_NAME, NETWORK_SUBNET, NETWORK_GATEWAY, NETWORK_SUBNET6,
+            NETWORK_GATEWAY6, NETWORK_TYPE, NETWORK_MAC_PREFIX, NETWORK_TAGS
+:pre-execution: master node
+:post-execution: master node
+
 
 Instance operations
 ~~~~~~~~~~~~~~~~~~~
diff --git a/doc/rapi.rst b/doc/rapi.rst
index 179edfd7fd1d26129a6049107a0cc2baed257ac0..e8c391cfa7722ed5ef198025f9e6cbc4f0ace1ac 100644
--- a/doc/rapi.rst
+++ b/doc/rapi.rst
@@ -646,6 +646,207 @@ to URI like::
 It supports the ``dry-run`` argument.
 
 
+``/2/networks``
++++++++++++++++
+
+The networks resource.
+
+It supports the following commands: ``GET``, ``POST``.
+
+``GET``
+~~~~~~~
+
+Returns a list of all existing networks.
+
+Example::
+
+    [
+      {
+        "name": "network1",
+        "uri": "\/2\/networks\/network1"
+      },
+      {
+        "name": "network2",
+        "uri": "\/2\/networks\/network2"
+      }
+    ]
+
+If the optional bool *bulk* argument is provided and set to a true value
+(i.e ``?bulk=1``), the output contains detailed information about networks
+as a list.
+
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.NET_FIELDS))`.
+
+Example::
+
+    [
+      {
+        'external_reservations': '10.0.0.0, 10.0.0.1, 10.0.0.15',
+        'free_count': 13,
+        'gateway': '10.0.0.1',
+        'gateway6': None,
+        'group_list': ['default(bridged, prv0)'],
+        'inst_list': [],
+        'mac_prefix': None,
+        'map': 'XX.............X',
+        'name': 'nat',
+        'network': '10.0.0.0/28',
+        'network6': None,
+        'network_type': 'private',
+        'reserved_count': 3,
+        'tags': ['nfdhcpd']
+      },
+    ]
+
+``POST``
+~~~~~~~~
+
+Creates a network.
+
+If the optional bool *dry-run* argument is provided, the job will not be
+actually executed, only the pre-execution checks will be done.
+
+Returns: a job ID that can be used later for polling.
+
+Body parameters:
+
+.. opcode_params:: OP_NETWORK_ADD
+
+Job result:
+
+.. opcode_result:: OP_NETWORK_ADD
+
+
+``/2/networks/[network_name]``
+++++++++++++++++++++++++++++++
+
+Returns information about a network.
+
+It supports the following commands: ``GET``, ``DELETE``.
+
+``GET``
+~~~~~~~
+
+Returns information about a network, similar to the bulk output from
+the network list.
+
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.NET_FIELDS))`.
+
+``DELETE``
+~~~~~~~~~~
+
+Deletes a network.
+
+It supports the ``dry-run`` argument.
+
+Job result:
+
+.. opcode_result:: OP_NETWORK_REMOVE
+
+
+``/2/networks/[network_name]/modify``
++++++++++++++++++++++++++++++++++++++
+
+Modifies the parameters of a network.
+
+Supports the following commands: ``PUT``.
+
+``PUT``
+~~~~~~~
+
+Returns a job ID.
+
+Body parameters:
+
+.. opcode_params:: OP_NETWORK_SET_PARAMS
+
+Job result:
+
+.. opcode_result:: OP_NETWORK_SET_PARAMS
+
+
+``/2/networks/[network_name]/connect``
+++++++++++++++++++++++++++++++++++++++
+
+Connects a network to a nodegroup.
+
+Supports the following commands: ``PUT``.
+
+``PUT``
+~~~~~~~
+
+Returns a job ID. It supports the ``dry-run`` arguments.
+
+Body parameters:
+
+.. opcode_params:: OP_NETWORK_CONNECT
+
+Job result:
+
+.. opcode_result:: OP_NETWORK_CONNECT
+
+
+``/2/networks/[network_name]/disconnect``
++++++++++++++++++++++++++++++++++++++++++
+
+Disonnects a network from a nodegroup.
+
+Supports the following commands: ``PUT``.
+
+``PUT``
+~~~~~~~
+
+Returns a job ID. It supports the ``dry-run`` arguments.
+
+Body parameters:
+
+.. opcode_params:: OP_NETWORK_DISCONNECT
+
+Job result:
+
+.. opcode_result:: OP_NETWORK_DISCONNECT
+
+
+``/2/networks/[network_name]/tags``
++++++++++++++++++++++++++++++++++++
+
+Manages per-network tags.
+
+Supports the following commands: ``GET``, ``PUT``, ``DELETE``.
+
+``GET``
+~~~~~~~
+
+Returns a list of tags.
+
+Example::
+
+    ["tag1", "tag2", "tag3"]
+
+``PUT``
+~~~~~~~
+
+Add a set of tags.
+
+The request as a list of strings should be ``PUT`` to this URI. The
+result will be a job id.
+
+It supports the ``dry-run`` argument.
+
+
+``DELETE``
+~~~~~~~~~~
+
+Delete a tag.
+
+In order to delete a set of tags, the DELETE request should be addressed
+to URI like::
+
+    /tags?tag=[tag]&tag=[tag]
+
+It supports the ``dry-run`` argument.
+
+
 ``/2/instances-multi-alloc``
 ++++++++++++++++++++++++++++
 
diff --git a/lib/network.py b/lib/network.py
index 747904083805f29b0b2fcbe22ce197e620a3e255..96ed654ad7ea942b95b510ac94f0a480d2bcaf82 100644
--- a/lib/network.py
+++ b/lib/network.py
@@ -115,7 +115,8 @@ class AddressPool(object):
     assert self.net.family == 4
     assert len(self.reservations) == self._GetSize()
     assert len(self.ext_reservations) == self._GetSize()
-    assert not (self.reservations & self.ext_reservations).any()
+    all_res = self.reservations & self.ext_reservations
+    assert not all_res.any()
 
     if self.gateway is not None:
       assert self.net.family == self.gateway.version
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 9436935c7116285c8a22628605fd229d0a84e848..707d3389d2c006b95ed80739d6e12f6d3206ba3c 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -2011,15 +2011,22 @@ class OpNetworkAdd(OpCode):
   OP_PARAMS = [
     _PNetworkName,
     _PNetworkType,
-    ("network", None, ht.TAnd(ht.TString ,_CheckCIDRNetNotation), None),
-    ("gateway", None, ht.TOr(ht.TNone, _CheckCIDRAddrNotation), None),
-    ("network6", None, ht.TOr(ht.TNone, _CheckCIDR6NetNotation), None),
-    ("gateway6", None, ht.TOr(ht.TNone, _CheckCIDR6AddrNotation), None),
-    ("mac_prefix", None, ht.TMaybeString, None),
+    ("network", None, ht.TAnd(ht.TString ,_CheckCIDRNetNotation),
+     "IPv4 Subnet"),
+    ("gateway", None, ht.TOr(ht.TNone, _CheckCIDRAddrNotation),
+     "IPv4 Gateway"),
+    ("network6", None, ht.TOr(ht.TNone, _CheckCIDR6NetNotation),
+     "IPv6 Subnet"),
+    ("gateway6", None, ht.TOr(ht.TNone, _CheckCIDR6AddrNotation),
+     "IPv6 Gateway"),
+    ("mac_prefix", None, ht.TMaybeString,
+     "Mac prefix that overrides cluster one"),
     ("add_reserved_ips", None,
-     ht.TOr(ht.TNone, ht.TListOf(_CheckCIDRAddrNotation)), None),
+     ht.TOr(ht.TNone, ht.TListOf(_CheckCIDRAddrNotation)),
+     "Which IPs to reserve"),
     ("tags", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), "Network tags"),
     ]
+  OP_RESULT = ht.TNone
 
 class OpNetworkRemove(OpCode):
   """Remove an existing network from the cluster.
@@ -2031,6 +2038,7 @@ class OpNetworkRemove(OpCode):
     _PNetworkName,
     _PForce,
     ]
+  OP_RESULT = ht.TNone
 
 class OpNetworkSetParams(OpCode):
   """Modify Network's parameters except for IPv4 subnet"""
@@ -2038,15 +2046,22 @@ class OpNetworkSetParams(OpCode):
   OP_PARAMS = [
     _PNetworkName,
     _PNetworkType,
-    ("gateway", None, ht.TOr(ht.TNone, _CheckCIDRAddrNotation), None),
-    ("network6", None, ht.TOr(ht.TNone, _CheckCIDR6NetNotation), None),
-    ("gateway6", None, ht.TOr(ht.TNone, _CheckCIDR6AddrNotation), None),
-    ("mac_prefix", None, ht.TMaybeString, None),
+    ("gateway", None, ht.TOr(ht.TNone, _CheckCIDRAddrNotation),
+     "IPv4 Gateway"),
+    ("network6", None, ht.TOr(ht.TNone, _CheckCIDR6NetNotation),
+     "IPv6 Subnet"),
+    ("gateway6", None, ht.TOr(ht.TNone, _CheckCIDR6AddrNotation),
+     "IPv6 Gateway"),
+    ("mac_prefix", None, ht.TMaybeString,
+     "Mac prefix that overrides cluster one"),
     ("add_reserved_ips", None,
-     ht.TOr(ht.TNone, ht.TListOf(_CheckCIDRAddrNotation)), None),
+     ht.TOr(ht.TNone, ht.TListOf(_CheckCIDRAddrNotation)),
+     "Which external IPs to reserve"),
     ("remove_reserved_ips", None,
-     ht.TOr(ht.TNone, ht.TListOf(_CheckCIDRAddrNotation)), None),
+     ht.TOr(ht.TNone, ht.TListOf(_CheckCIDRAddrNotation)),
+     "Which external IPs to release"),
     ]
+  OP_RESULT = ht.TNone
 
 class OpNetworkConnect(OpCode):
   """Connect a Network to a specific Nodegroup with the defined netparams
@@ -2060,10 +2075,11 @@ class OpNetworkConnect(OpCode):
   OP_PARAMS = [
     _PGroupName,
     _PNetworkName,
-    ("network_mode", None, ht.TString, None),
-    ("network_link", None, ht.TString, None),
-    ("conflicts_check", True, ht.TBool, "Check for conflicting IPs"),
+    ("network_mode", None, ht.TString, "Connectivity mode"),
+    ("network_link", None, ht.TString, "Connectivity link"),
+    ("conflicts_check", True, ht.TBool, "Whether to check for conflicting IPs"),
     ]
+  OP_RESULT = ht.TNone
 
 class OpNetworkDisconnect(OpCode):
   """Disconnect a Network from a Nodegroup. Produce errors if NICs are
@@ -2074,8 +2090,9 @@ class OpNetworkDisconnect(OpCode):
   OP_PARAMS = [
     _PGroupName,
     _PNetworkName,
-    ("conflicts_check", True, ht.TBool, "Check for conflicting IPs"),
+    ("conflicts_check", True, ht.TBool, "Whether to check for conflicting IPs"),
     ]
+  OP_RESULT = ht.TNone
 
 class OpNetworkQuery(OpCode):
   """Compute the list of networks."""
@@ -2084,6 +2101,7 @@ class OpNetworkQuery(OpCode):
     ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Empty list to query all groups, group names otherwise"),
     ]
+  OP_RESULT = ht.TNone
 
 
 def _GetOpList():
diff --git a/lib/ovf.py b/lib/ovf.py
index dc581331cca87a03776e5ca8a418c97d86c11684..557b3a3126fcab43a0882f47c161ddf14845b8cc 100644
--- a/lib/ovf.py
+++ b/lib/ovf.py
@@ -760,7 +760,7 @@ class OVFWriter(object):
       SubElementText(nic, "gnt:MACAddress", network["mac"])
       SubElementText(nic, "gnt:IPAddress", network["ip"])
       SubElementText(nic, "gnt:Link", network["link"])
-      SubElementText(nic, "gnt:Network", network["network"])
+      SubElementText(nic, "gnt:Net", network["network"])
 
   def SaveVirtualSystemData(self, name, vcpus, memory):
     """Convert virtual system information to OVF sections.
@@ -1665,7 +1665,8 @@ class OVFExporter(Converter):
     counter = 0
     while True:
       data_link = \
-        self.config_parser.get(constants.INISECT_INS, "nic%s_network" % counter)
+        self.config_parser.get(constants.INISECT_INS,
+                               "nic%s_link" % counter)
       if data_link is None:
         break
       results.append({
@@ -1675,9 +1676,9 @@ class OVFExporter(Converter):
                                       "nic%s_mac" % counter),
         "ip": self.config_parser.get(constants.INISECT_INS,
                                      "nic%s_ip" % counter),
-        "link": self.config_parser.get(constants.INISECT_INS,
-                                       "nic%s_link" % counter),
-        "network": data_link,
+        "network": self.config_parser.get(constants.INISECT_INS,
+                                          "nic%s_network" % counter),
+        "link": data_link,
       })
       if results[counter]["mode"] not in constants.NIC_VALID_MODES:
         raise errors.OpPrereqError("Network mode %s not recognized"
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index a1d729fcd20a7e3a342b5e273e4dc9cc3857197c..ca2a965a499899aeb1ad95d414466a10fec12baf 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -1761,7 +1761,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
     return self._SendRequest(HTTP_POST, "/%s/networks" % GANETI_RAPI_VERSION,
                              query, body)
 
-  def ConnectNetwork(self, network_name, group_name, mode, link):
+  def ConnectNetwork(self, network_name, group_name, mode, link, dry_run=False):
     """Connects a Network to a NodeGroup with the given netparams
 
     """
@@ -1771,22 +1771,44 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       "network_link": link
       }
 
+    query = []
+    _AppendDryRunIf(query, dry_run)
+
     return self._SendRequest(HTTP_PUT,
                              ("/%s/networks/%s/connect" %
-                             (GANETI_RAPI_VERSION, network_name)), None, body)
+                             (GANETI_RAPI_VERSION, network_name)), query, body)
 
-  def DisconnectNetwork(self, network_name, group_name):
+  def DisconnectNetwork(self, network_name, group_name, dry_run=False):
     """Connects a Network to a NodeGroup with the given netparams
 
     """
     body = {
       "group_name": group_name
       }
+
+    query = []
+    _AppendDryRunIf(query, dry_run)
+
     return self._SendRequest(HTTP_PUT,
                              ("/%s/networks/%s/disconnect" %
-                             (GANETI_RAPI_VERSION, network_name)), None, body)
+                             (GANETI_RAPI_VERSION, network_name)), query, body)
 
 
+  def ModifyNetwork(self, network, **kwargs):
+    """Modifies a network.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type network: string
+    @param network: Network name
+    @rtype: string
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/networks/%s/modify" %
+                              (GANETI_RAPI_VERSION, network)), None, kwargs)
+
   def DeleteNetwork(self, network, dry_run=False):
     """Deletes a network.
 
@@ -1806,6 +1828,62 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                              ("/%s/networks/%s" %
                               (GANETI_RAPI_VERSION, network)), query, None)
 
+  def GetNetworkTags(self, network):
+    """Gets tags for a network.
+
+    @type network: string
+    @param network: Node group whose tags to return
+
+    @rtype: list of strings
+    @return: tags for the network
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/networks/%s/tags" %
+                              (GANETI_RAPI_VERSION, network)), None, None)
+
+  def AddNetworkTags(self, network, tags, dry_run=False):
+    """Adds tags to a network.
+
+    @type network: str
+    @param network: network to add tags to
+    @type tags: list of string
+    @param tags: tags to add to the network
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: string
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    _AppendDryRunIf(query, dry_run)
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/networks/%s/tags" %
+                              (GANETI_RAPI_VERSION, network)), query, None)
+
+  def DeleteNetworkTags(self, network, tags, dry_run=False):
+    """Deletes tags from a network.
+
+    @type network: str
+    @param network: network to delete tags from
+    @type tags: list of string
+    @param tags: tags to delete
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+    @rtype: string
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    _AppendDryRunIf(query, dry_run)
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/networks/%s/tags" %
+                              (GANETI_RAPI_VERSION, network)), query, None)
+
+
   def GetGroups(self, bulk=False):
     """Gets all node groups in the cluster.
 
diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py
index 56a234620c7e0364702318d4628fbee9c670b16a..0192e7992d70ed7d24b8ffe6632168c7db95ed59 100644
--- a/lib/rapi/connector.py
+++ b/lib/rapi/connector.py
@@ -175,6 +175,10 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
       rlib2.R_2_networks_name_connect,
     re.compile(r"^/2/networks/(%s)/disconnect$" % network_name_pattern):
       rlib2.R_2_networks_name_disconnect,
+    re.compile(r"^/2/networks/(%s)/modify$" % network_name_pattern):
+      rlib2.R_2_networks_name_modify,
+    re.compile(r"^/2/networks/(%s)/tags$" % network_name_pattern):
+      rlib2.R_2_networks_name_tags,
 
     "/2/groups": rlib2.R_2_groups,
     re.compile(r"^/2/groups/(%s)$" % group_name_pattern):
diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py
index 57fe6313cea3e0def71387f3ae1ad85ecdc2060d..a18e12d75e5edc8c7ee769aee15924fa4c17b78a 100644
--- a/lib/rapi/rlib2.py
+++ b/lib/rapi/rlib2.py
@@ -688,7 +688,7 @@ class R_2_networks(baserlib.OpcodeResource):
 
 
 class R_2_networks_name(baserlib.OpcodeResource):
-  """/2/network/[network_name] resource.
+  """/2/networks/[network_name] resource.
 
   """
   DELETE_OPCODE = opcodes.OpNetworkRemove
@@ -718,7 +718,7 @@ class R_2_networks_name(baserlib.OpcodeResource):
       })
 
 class R_2_networks_name_connect(baserlib.OpcodeResource):
-  """/2/network/[network_name]/connect.
+  """/2/networks/[network_name]/connect resource.
 
   """
   PUT_OPCODE = opcodes.OpNetworkConnect
@@ -730,10 +730,11 @@ class R_2_networks_name_connect(baserlib.OpcodeResource):
     assert self.items
     return (self.request_body, {
       "network_name": self.items[0],
+      "dry_run": self.dryRun(),
       })
 
 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
-  """/2/network/[network_name]/disconnect.
+  """/2/networks/[network_name]/disconnect resource.
 
   """
   PUT_OPCODE = opcodes.OpNetworkDisconnect
@@ -741,12 +742,29 @@ class R_2_networks_name_disconnect(baserlib.OpcodeResource):
   def GetPutOpInput(self):
     """Changes some parameters of node group.
 
+    """
+    assert self.items
+    return (self.request_body, {
+      "network_name": self.items[0],
+      "dry_run": self.dryRun(),
+      })
+
+class R_2_networks_name_modify(baserlib.OpcodeResource):
+  """/2/networks/[network_name]/modify resource.
+
+  """
+  PUT_OPCODE = opcodes.OpNetworkSetParams
+
+  def GetPutOpInput(self):
+    """Changes some parameters of network.
+
     """
     assert self.items
     return (self.request_body, {
       "network_name": self.items[0],
       })
 
+
 class R_2_groups(baserlib.OpcodeResource):
   """/2/groups resource.
 
@@ -1546,6 +1564,14 @@ class R_2_groups_name_tags(_R_Tags):
   """
   TAG_LEVEL = constants.TAG_NODEGROUP
 
+class R_2_networks_name_tags(_R_Tags):
+  """ /2/networks/[network_name]/tags resource.
+
+  Manages per-network tags.
+
+  """
+  TAG_LEVEL = constants.TAG_NETWORK
+
 
 class R_2_tags(_R_Tags):
   """ /2/tags resource.
diff --git a/man/gnt-network.rst b/man/gnt-network.rst
index 92640442f7e61257c5f550b0a49a02251acec852..6f75807a61bd325978240094d7cc61e92a785436 100644
--- a/man/gnt-network.rst
+++ b/man/gnt-network.rst
@@ -158,6 +158,45 @@ RENAME
 
 Renames a given network from *oldname* to *newname*. NOT implemeted yet
 
+TAGS
+~~~
+
+ADD-TAGS
+^^^^^^^^
+
+**add-tags** [\--from *file*] {*networkname*} {*tag*...}
+
+Add tags to the given network. If any of the tags contains invalid
+characters, the entire operation will abort.
+
+If the ``--from`` option is given, the list of tags will be extended
+with the contents of that file (each line becomes a tag). In this case,
+there is not need to pass tags on the command line (if you do, both
+sources will be used). A file name of ``-`` will be interpreted as
+stdin.
+
+LIST-TAGS
+^^^^^^^^^
+
+**list-tags** {*networkname*}
+
+List the tags of the given network.
+
+REMOVE-TAGS
+^^^^^^^^^^^
+
+**remove-tags** [\--from *file*] {*networkname*} {*tag*...}
+
+Remove tags from the given network. If any of the tags are not
+existing on the network, the entire operation will abort.
+
+If the ``--from`` option is given, the list of tags to be removed will
+be extended with the contents of that file (each line becomes a tag). In
+this case, there is not need to pass tags on the command line (if you
+do, tags from both sources will be removed). A file name of ``-`` will
+be interpreted as stdin.
+
+
 INFO
 ~~~~
 
diff --git a/test/cfgupgrade_unittest.py b/test/cfgupgrade_unittest.py
index 4d38b26756f83c56c77bd91facd095b10d2a2281..edfd11e35560a0fe19fea5db0f8f7ae560358b44 100755
--- a/test/cfgupgrade_unittest.py
+++ b/test/cfgupgrade_unittest.py
@@ -93,6 +93,7 @@ class TestCfgupgrade(unittest.TestCase):
       "version": constants.CONFIG_VERSION,
       "cluster": {},
       "instances": {},
+      "nodegroups": {},
       }))
 
     hostname = netutils.GetHostname().name
@@ -108,6 +109,7 @@ class TestCfgupgrade(unittest.TestCase):
       "version": constants.CONFIG_VERSION,
       "cluster": {},
       "instances": {},
+      "nodegroups": {},
       }))
 
     utils.WriteFile(self.ss_master_node_path,
@@ -124,6 +126,7 @@ class TestCfgupgrade(unittest.TestCase):
         "config_version": 0,
         },
       "instances": {},
+      "nodegroups": {},
       }
     utils.WriteFile(self.config_path, data=serializer.DumpJson(cfg))
     self.assertRaises(Exception, _RunUpgrade, self.tmpdir, False, True)
@@ -148,6 +151,7 @@ class TestCfgupgrade(unittest.TestCase):
       "version": from_version,
       "cluster": cluster,
       "instances": {},
+      "nodegroups": {},
       }
     self._CreateValidConfigDir()
     utils.WriteFile(self.config_path, data=serializer.DumpJson(cfg))
diff --git a/test/data/ovfdata/config.ini b/test/data/ovfdata/config.ini
index 7d0c0f581877bc51fc8762cd03d3299e6f0d922a..d5a0586ce80b966338b4a35bf7377d57a725e10e 100644
--- a/test/data/ovfdata/config.ini
+++ b/test/data/ovfdata/config.ini
@@ -8,6 +8,7 @@ nic0_mac = aa:00:00:d8:2c:1e
 nic_count = 1
 nic0_link = br0
 nic0_ip = None
+nic0_network = test
 disk0_ivname = disk/0
 disk0_size = 0
 
diff --git a/test/docs_unittest.py b/test/docs_unittest.py
index 048c0fa8bc0bfb8ed42cbcc757ec28081e4ac2eb..1fa24f5514d6119b1b5ea5e2b201367067a2bfe6 100755
--- a/test/docs_unittest.py
+++ b/test/docs_unittest.py
@@ -186,11 +186,13 @@ class TestRapiDocs(unittest.TestCase):
     node_name = re.escape("[node_name]")
     instance_name = re.escape("[instance_name]")
     group_name = re.escape("[group_name]")
+    network_name = re.escape("[network_name]")
     job_id = re.escape("[job_id]")
     disk_index = re.escape("[disk_index]")
     query_res = re.escape("[resource]")
 
-    resources = connector.GetHandlers(node_name, instance_name, group_name,
+    resources = connector.GetHandlers(node_name, instance_name,
+                                      group_name, network_name,
                                       job_id, disk_index, query_res)
 
     handler_dups = utils.FindDuplicates(resources.values())
@@ -202,6 +204,7 @@ class TestRapiDocs(unittest.TestCase):
       re.compile(node_name): "node1examplecom",
       re.compile(instance_name): "inst1examplecom",
       re.compile(group_name): "group4440",
+      re.compile(network_name): "network5550",
       re.compile(job_id): "9409",
       re.compile(disk_index): "123",
       re.compile(query_res): "lock",
diff --git a/test/ganeti.locking_unittest.py b/test/ganeti.locking_unittest.py
index 6b4ad8d7149618f3f3b735fdf28510f018ef5210..85fa0fc66251016660a7332ea98153f7b71ff771 100755
--- a/test/ganeti.locking_unittest.py
+++ b/test/ganeti.locking_unittest.py
@@ -1762,8 +1762,9 @@ class TestGanetiLockManager(_ThreadedTestCase):
     self.nodes=['n1', 'n2']
     self.nodegroups=['g1', 'g2']
     self.instances=['i1', 'i2', 'i3']
+    self.networks=['net1', 'net2', 'net3']
     self.GL = locking.GanetiLockManager(self.nodes, self.nodegroups,
-                                        self.instances)
+                                        self.instances, self.networks)
 
   def tearDown(self):
     # Don't try this at home...
@@ -1778,7 +1779,7 @@ class TestGanetiLockManager(_ThreadedTestCase):
       self.assertEqual(i, locking.LEVELS[i])
 
   def testDoubleGLFails(self):
-    self.assertRaises(AssertionError, locking.GanetiLockManager, [], [], [])
+    self.assertRaises(AssertionError, locking.GanetiLockManager, [], [], [], [])
 
   def testLockNames(self):
     self.assertEqual(self.GL._names(locking.LEVEL_CLUSTER), set(['BGL']))
@@ -1787,31 +1788,44 @@ class TestGanetiLockManager(_ThreadedTestCase):
                      set(self.nodegroups))
     self.assertEqual(self.GL._names(locking.LEVEL_INSTANCE),
                      set(self.instances))
+    self.assertEqual(self.GL._names(locking.LEVEL_NETWORK),
+                     set(self.networks))
 
   def testInitAndResources(self):
     locking.GanetiLockManager._instance = None
-    self.GL = locking.GanetiLockManager([], [], [])
+    self.GL = locking.GanetiLockManager([], [], [], [])
     self.assertEqual(self.GL._names(locking.LEVEL_CLUSTER), set(['BGL']))
     self.assertEqual(self.GL._names(locking.LEVEL_NODE), set())
     self.assertEqual(self.GL._names(locking.LEVEL_NODEGROUP), set())
     self.assertEqual(self.GL._names(locking.LEVEL_INSTANCE), set())
+    self.assertEqual(self.GL._names(locking.LEVEL_NETWORK), set())
 
     locking.GanetiLockManager._instance = None
-    self.GL = locking.GanetiLockManager(self.nodes, self.nodegroups, [])
+    self.GL = locking.GanetiLockManager(self.nodes, self.nodegroups, [], [])
     self.assertEqual(self.GL._names(locking.LEVEL_CLUSTER), set(['BGL']))
     self.assertEqual(self.GL._names(locking.LEVEL_NODE), set(self.nodes))
     self.assertEqual(self.GL._names(locking.LEVEL_NODEGROUP),
                                     set(self.nodegroups))
     self.assertEqual(self.GL._names(locking.LEVEL_INSTANCE), set())
+    self.assertEqual(self.GL._names(locking.LEVEL_NETWORK), set())
 
     locking.GanetiLockManager._instance = None
-    self.GL = locking.GanetiLockManager([], [], self.instances)
+    self.GL = locking.GanetiLockManager([], [], self.instances, [])
     self.assertEqual(self.GL._names(locking.LEVEL_CLUSTER), set(['BGL']))
     self.assertEqual(self.GL._names(locking.LEVEL_NODE), set())
     self.assertEqual(self.GL._names(locking.LEVEL_NODEGROUP), set())
     self.assertEqual(self.GL._names(locking.LEVEL_INSTANCE),
                      set(self.instances))
 
+    locking.GanetiLockManager._instance = None
+    self.GL = locking.GanetiLockManager([], [], [], self.networks)
+    self.assertEqual(self.GL._names(locking.LEVEL_CLUSTER), set(['BGL']))
+    self.assertEqual(self.GL._names(locking.LEVEL_NODE), set())
+    self.assertEqual(self.GL._names(locking.LEVEL_NODEGROUP), set())
+    self.assertEqual(self.GL._names(locking.LEVEL_INSTANCE), set())
+    self.assertEqual(self.GL._names(locking.LEVEL_NETWORK),
+                     set(self.networks))
+
   def testAcquireRelease(self):
     self.GL.acquire(locking.LEVEL_CLUSTER, ['BGL'], shared=1)
     self.assertEquals(self.GL.list_owned(locking.LEVEL_CLUSTER), set(['BGL']))
diff --git a/test/ganeti.ovf_unittest.py b/test/ganeti.ovf_unittest.py
old mode 100644
new mode 100755
index fa9197c0524ee620eb55df61b497e98574cb2c49..da7e92d8afe310c13e807eda9b36466c72a02280
--- a/test/ganeti.ovf_unittest.py
+++ b/test/ganeti.ovf_unittest.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2011 Google Inc.
+# Copyright (C) 2011, 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
@@ -59,6 +59,7 @@ GANETI_NETWORKS = {
   "nic0_ip": "none",
   "nic0_mac": "aa:00:00:d8:2c:1e",
   "nic0_link": "xen-br0",
+  "nic0_network": "auto",
 }
 GANETI_HYPERVISOR = {
   "hypervisor_name": "xen-pvm",
@@ -91,6 +92,7 @@ VIRTUALBOX_NETWORKS = {
   "nic0_ip": "none",
   "nic0_link": "auto",
   "nic0_mac": "auto",
+  "nic0_network": "auto",
 }
 VIRTUALBOX_HYPERVISOR = {"hypervisor_name": "auto"}
 VIRTUALBOX_OS = {"os_name": None}
@@ -130,6 +132,7 @@ CMDARGS_NETWORKS = {
   "nic0_ip": "none",
   "nic0_mac": "auto",
   "nic_count": "1",
+  "nic0_network": "auto",
 }
 CMDARGS_HYPERVISOR = {
   "hypervisor_name": "xen-pvm"
@@ -207,7 +210,8 @@ EXP_DISKS_LIST = [
   },
 ]
 EXP_NETWORKS_LIST = [
-  {"mac": "aa:00:00:d8:2c:1e", "ip":"None", "link":"br0","mode":"routed"},
+  {"mac": "aa:00:00:d8:2c:1e", "ip":"None", "link":"br0",
+   "mode":"routed", "network": "test"},
 ]
 EXP_PARTIAL_GANETI_DICT = {
   "hypervisor": {"name": "xen-kvm"},
@@ -263,8 +267,8 @@ EXPORT_GANETI_INCOMPLETE = ("<gnt:GanetiSection><gnt:Version>0</gnt:Version>"
                             "Nic ovf:name=\"routed0\"><gnt:Mode>routed</gnt:"
                             "Mode><gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:"
                             "MACAddress><gnt:IPAddress>None</gnt:IPAddress>"
-                            "<gnt:Link>br0</gnt:Link></gnt:Nic></gnt:Network>"
-                            "</gnt:GanetiSection>")
+                            "<gnt:Link>br0</gnt:Link><gnt:Net>test</gnt:Net>"
+                            "</gnt:Nic></gnt:Network></gnt:GanetiSection>")
 EXPORT_GANETI = ("<gnt:GanetiSection><gnt:Version>0</gnt:Version><gnt:"
                  "AutoBalance>False</gnt:AutoBalance><gnt:OperatingSystem>"
                  "<gnt:Name>lenny-image</gnt:Name><gnt:Parameters /></gnt:"
@@ -274,7 +278,8 @@ EXPORT_GANETI = ("<gnt:GanetiSection><gnt:Version>0</gnt:Version><gnt:"
                  "Hypervisor><gnt:Network><gnt:Nic ovf:name=\"routed0\"><gnt:"
                  "Mode>routed</gnt:Mode><gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:"
                  "MACAddress><gnt:IPAddress>None</gnt:IPAddress><gnt:Link>br0"
-                 "</gnt:Link></gnt:Nic></gnt:Network></gnt:GanetiSection>")
+                 "</gnt:Link><gnt:Net>test</gnt:Net></gnt:Nic></gnt:Network>"
+                 "</gnt:GanetiSection>")
 EXPORT_SYSTEM = ("<References><File ovf:compression=\"gzip\" ovf:href=\"new_"
                  "disk.cow.gz\" ovf:id=\"file0\" ovf:size=\"203\" /><File ovf:"
                  "href=\"new_disk.cow\" ovf:id=\"file1\" ovf:size=\"15\" />"
diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py
index da8409393df9af86558ef75975677c821b37221a..e5b243682638a29c5f9710d4c2d3a2501c94d71f 100755
--- a/test/ganeti.rapi.client_unittest.py
+++ b/test/ganeti.rapi.client_unittest.py
@@ -1115,6 +1115,93 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertDryRun()
     self.assertUseForce()
 
+  def testGetNetworksBulk(self):
+    networks = [{"name": "network1",
+               "uri": "/2/networks/network1",
+               "network": "192.168.0.0/24",
+               },
+              {"name": "network2",
+               "uri": "/2/networks/network2",
+               "network": "192.168.0.0/24",
+               },
+              ]
+    self.rapi.AddResponse(serializer.DumpJson(networks))
+
+    self.assertEqual(networks, self.client.GetNetworks(bulk=True))
+    self.assertHandler(rlib2.R_2_networks)
+    self.assertBulk()
+
+  def testGetNetwork(self):
+    network = {"ctime": None,
+               "name": "network1",
+               }
+    self.rapi.AddResponse(serializer.DumpJson(network))
+    self.assertEqual({"ctime": None, "name": "network1"},
+                     self.client.GetNetwork("network1"))
+    self.assertHandler(rlib2.R_2_networks_name)
+    self.assertItems(["network1"])
+
+  def testCreateNetwork(self):
+    self.rapi.AddResponse("12345")
+    job_id = self.client.CreateNetwork("newnetwork", network="192.168.0.0/24",
+                                       dry_run=True)
+    self.assertEqual(job_id, 12345)
+    self.assertHandler(rlib2.R_2_networks)
+    self.assertDryRun()
+
+  def testModifyNetwork(self):
+    self.rapi.AddResponse("12346")
+    job_id = self.client.ModifyNetwork("mynetwork", gateway="192.168.0.10",
+                                     dry_run=True)
+    self.assertEqual(job_id, 12346)
+    self.assertHandler(rlib2.R_2_networks_name_modify)
+
+  def testDeleteNetwork(self):
+    self.rapi.AddResponse("12347")
+    job_id = self.client.DeleteNetwork("newnetwork", dry_run=True)
+    self.assertEqual(job_id, 12347)
+    self.assertHandler(rlib2.R_2_networks_name)
+    self.assertDryRun()
+
+  def testConnectNetwork(self):
+    self.rapi.AddResponse("12348")
+    job_id = self.client.ConnectNetwork("mynetwork", "default",
+                                        "bridged", "br0", dry_run=True)
+    self.assertEqual(job_id, 12348)
+    self.assertHandler(rlib2.R_2_networks_name_connect)
+    self.assertDryRun()
+
+  def testDisconnectNetwork(self):
+    self.rapi.AddResponse("12349")
+    job_id = self.client.DisconnectNetwork("mynetwork", "default", dry_run=True)
+    self.assertEqual(job_id, 12349)
+    self.assertHandler(rlib2.R_2_networks_name_disconnect)
+    self.assertDryRun()
+
+  def testGetNetworkTags(self):
+    self.rapi.AddResponse("[]")
+    self.assertEqual([], self.client.GetNetworkTags("fooNetwork"))
+    self.assertHandler(rlib2.R_2_networks_name_tags)
+    self.assertItems(["fooNetwork"])
+
+  def testAddNetworkTags(self):
+    self.rapi.AddResponse("1234")
+    self.assertEqual(1234,
+        self.client.AddNetworkTags("fooNetwork", ["awesome"], dry_run=True))
+    self.assertHandler(rlib2.R_2_networks_name_tags)
+    self.assertItems(["fooNetwork"])
+    self.assertDryRun()
+    self.assertQuery("tag", ["awesome"])
+
+  def testDeleteNetworkTags(self):
+    self.rapi.AddResponse("25826")
+    self.assertEqual(25826, self.client.DeleteNetworkTags("foo", ["awesome"],
+                                                          dry_run=True))
+    self.assertHandler(rlib2.R_2_networks_name_tags)
+    self.assertItems(["foo"])
+    self.assertDryRun()
+    self.assertQuery("tag", ["awesome"])
+
   def testModifyInstance(self):
     self.rapi.AddResponse("23681")
     job_id = self.client.ModifyInstance("inst7210", os_name="linux")
diff --git a/tools/cfgupgrade b/tools/cfgupgrade
index ac182212815aea5f6539d50bff12276da55830ad..a33558af651422c58f7e45dfbe78f9d76efbf040 100755
--- a/tools/cfgupgrade
+++ b/tools/cfgupgrade
@@ -104,7 +104,6 @@ def UpgradeNetworks(config_data):
 
 
 def UpgradeGroups(config_data):
-  nicparams = config_data["cluster"]["nicparams"]["default"]
   for group in config_data["nodegroups"].values():
     networks = group.get("networks", None)
     if not networks: