commit 2522e4897582c3b7af41ffd3914bce9071b72e10
Author: Christos Stavrakakis <cstavr@grnet.gr>
Date:   Wed Aug 7 10:50:28 2013 +0300

    cyclades: extend VM info with 'port_forwarding'
    
    Extend servers info API response with 'SNF:port_forwarding' attribute,
    describing port fowarding rules (DNAT) that are applied to vms. The
    description of such rules is done via the new CYCLADES_PORT_FORWARDING
    setting. This setting can be either a dictionary mapping to ports to
    tuples of (host, port), or a callable object that must return such a
    tuple.   The caller will pass to the callable the following positional
    arguments, in the following order:
    * server_id: The ID of the VM in the DB
    * ip_address: The IPv4 address of the public VM NIC
    * fqdn: The FQDN of the VM
    * user: The UUID of the owner of the VM

diff --git a/Changelog b/Changelog
index fe2d4a1..f99ac35 100644
--- a/Changelog
+++ b/Changelog
@@ -41,8 +41,12 @@ Cyclades
 * Compute quotas for CPU and memory of running vms.
 * Obsolete PUBLIC_USE_POOL setting, since Cyclades manages IP pool for all
   type of networks.
-* Extend servers info API respones with 'SNF:fqdn' attribute, and introduce
+* Extend servers info API response with 'SNF:fqdn' attribute, and introduce
   CYCLADES_SERVERS_FQDN to set the template for servers FDQN.
+* Extend servers info API response with 'SNF:port_forwarding' attribute,
+  describing port fowarding rules (DNAT) that are applied to vms. The
+  description of such rules is done via the new CYCLADES_PORT_FORWARDING
+  setting.
 
 Pithos
 ------
diff --git a/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf b/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf
index 35a38f3..298294f 100644
--- a/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf
+++ b/snf-cyclades-app/conf/20-snf-cyclades-app-api.conf
@@ -112,3 +112,24 @@
 ## the id of the VM. If set to 'None' the first public IPv4 or IPv6 address
 ## of the VM will be used.
 #CYCLADES_SERVERS_FQDN = 'snf-%(id)s.vm.example.synnefo.org'
+#
+## Description of applied port forwarding rules (DNAT) for Cyclades VMs. This
+## setting contains a mapping from the port of each VM to a tuple contaning the
+## destination IP/hostname and the new port: (host, port). Instead of a tuple a
+## python callable object may be used which must return such a tuple. The caller
+## will pass to the callable the following positional arguments, in the
+## following order:
+## * server_id: The ID of the VM in the DB
+## * ip_address: The IPv4 address of the public VM NIC
+## * fqdn: The FQDN of the VM
+## * user: The UUID of the owner of the VM
+##
+## Here is an example describing the mapping of the SSH port of all VMs to
+## the external address 'gate.example.synnefo.org' and port 60000+server_id.
+## e.g. iptables -t nat -A prerouting -d gate.example.synnefo.org \
+## --dport (61000 # $(VM_ID)) -j DNAT --to-destination $(VM_IP):22
+##CYCLADES_PORT_FORWARDING = {
+##    22: lambda ip_address, server_id, fqdn, user:
+##               ("gate.example.synnefo.org", 61000 + server_id),
+##}
+#CYCLADES_PORT_FORWARDING = {}
diff --git a/snf-cyclades-app/synnefo/api/servers.py b/snf-cyclades-app/synnefo/api/servers.py
index 19fdedc..bad71e0 100644
--- a/snf-cyclades-app/synnefo/api/servers.py
+++ b/snf-cyclades-app/synnefo/api/servers.py
@@ -170,7 +170,9 @@ def vm_to_dict(vm, detail=False):
         d["config_drive"] = ""
         d["accessIPv4"] = ""
         d["accessIPv6"] = ""
-        d["SNF:fqdn"] = get_server_fqdn(vm)
+        fqdn = get_server_fqdn(vm)
+        d["SNF:fqdn"] = fqdn
+        d["SNF:port_forwarding"] = get_server_port_forwarding(vm, fqdn)
 
     return d
 
@@ -196,6 +198,45 @@ def get_server_fqdn(vm):
         raise faults.InternalServerError(msg)
 
 
+def get_server_port_forwarding(vm, fqdn):
+    """Create API 'port_forwarding' attribute from corresponding setting.
+
+    Create the 'port_forwarding' API vm attribute based on the corresponding
+    setting (CYCLADES_PORT_FORWARDING), which can be either a tuple
+    of the form (host, port) or a callable object returning such tuple. In
+    case of callable object, must be called with the following arguments:
+    * ip_address
+    * server_id
+    * fqdn
+    * owner UUID
+
+    """
+    port_forwarding = {}
+    for dport, to_dest in settings.CYCLADES_PORT_FORWARDING.items():
+        if hasattr(to_dest, "__call__"):
+            public_nics = vm.nics.filter(network__public=True, state="ACTIVE")\
+                                 .exclude(ipv4=None).order_by('index')
+            if public_nics:
+                vm_ipv4 = public_nics[0].ipv4
+            else:
+                vm_ipv4 = None
+            to_dest = to_dest(vm_ipv4, vm.id, fqdn, vm.userid)
+        msg = ("Invalid setting: CYCLADES_PORT_FOWARDING."
+               " Value must be a tuple of two elements (host, port).")
+        if to_dest is None:
+            continue
+        if not isinstance(to_dest, tuple) or len(to_dest) != 2:
+                raise faults.InternalServerError(msg)
+        else:
+            try:
+                host, port = to_dest
+            except (TypeError, ValueError):
+                raise faults.InternalServerError(msg)
+
+        port_forwarding[dport] = {"host": host, "port": str(port)}
+    return port_forwarding
+
+
 def diagnostics_to_dict(diagnostics):
     """
     Extract api data from diagnostics QuerySet.
diff --git a/snf-cyclades-app/synnefo/api/tests/servers.py b/snf-cyclades-app/synnefo/api/tests/servers.py
index 13d05f7..f1f8b97 100644
--- a/snf-cyclades-app/synnefo/api/tests/servers.py
+++ b/snf-cyclades-app/synnefo/api/tests/servers.py
@@ -202,6 +202,41 @@ class ServerAPITest(ComputeAPITest):
             server = json.loads(response.content)['server']
             self.assertEqual(server["SNF:fqdn"], nic.ipv4)
 
+    def test_server_port_forwarding(self):
+        vm = mfactory.VirtualMachineFactory()
+        ports = {
+            22: ("foo", 61000),
+            80: lambda ip, id, fqdn, user: ("bar", 61001)}
+        with override_settings(settings,
+                               CYCLADES_PORT_FORWARDING=ports):
+            response = self.myget("servers/%d" % vm.id, vm.userid)
+            server = json.loads(response.content)['server']
+            self.assertEqual(server["SNF:port_forwarding"],
+                             {"22": {"host": "foo", "port": "61000"},
+                              "80": {"host": "bar", "port": "61001"}})
+
+        def _port_from_ip(ip, base):
+            fields = ip.split('.', 4)
+            return (base + 256*int(fields[2]) + int(fields[3]))
+
+        ports = {
+            22: lambda ip, id, fqdn, user:
+            ip and ("gate", _port_from_ip(ip, 10000)) or None}
+        with override_settings(settings,
+                               CYCLADES_PORT_FORWARDING=ports):
+            response = self.myget("servers/%d" % vm.id, vm.userid)
+            server = json.loads(response.content)['server']
+            self.assertEqual(server["SNF:port_forwarding"], {})
+
+        mfactory.NetworkInterfaceFactory(machine=vm, ipv4="192.168.2.2",
+                                         network__public=True)
+        with override_settings(settings,
+                               CYCLADES_PORT_FORWARDING=ports):
+            response = self.myget("servers/%d" % vm.id, vm.userid)
+            server = json.loads(response.content)['server']
+            self.assertEqual(server["SNF:port_forwarding"],
+                             {"22": {"host": "gate", "port": "10514"}})
+
     def test_server_building_nics(self):
         db_vm = self.vm2
         user = self.vm2.userid
diff --git a/snf-cyclades-app/synnefo/app_settings/default/api.py b/snf-cyclades-app/synnefo/app_settings/default/api.py
index a4fd2c3..bb2bd88 100644
--- a/snf-cyclades-app/synnefo/app_settings/default/api.py
+++ b/snf-cyclades-app/synnefo/app_settings/default/api.py
@@ -112,3 +112,24 @@ CYCLADES_PROXY_USER_SERVICES = True
 # the id of the VM. If set to 'None' the first public IPv4 or IPv6 address
 # of the VM will be used.
 CYCLADES_SERVERS_FQDN = 'snf-%(id)s.vm.example.synnefo.org'
+
+# Description of applied port forwarding rules (DNAT) for Cyclades VMs. This
+# setting contains a mapping from the port of each VM to a tuple contaning the
+# destination IP/hostname and the new port: (host, port). Instead of a tuple a
+# python callable object may be used which must return such a tuple. The caller
+# will pass to the callable the following positional arguments, in the
+# following order:
+# * server_id: The ID of the VM in the DB
+# * ip_address: The IPv4 address of the public VM NIC
+# * fqdn: The FQDN of the VM
+# * user: The UUID of the owner of the VM
+#
+# Here is an example describing the mapping of the SSH port of all VMs to
+# the external address 'gate.example.synnefo.org' and port 60000+server_id.
+# e.g. iptables -t nat -A prerouting -d gate.example.synnefo.org \
+# --dport (61000 + $(VM_ID)) -j DNAT --to-destination $(VM_IP):22
+#CYCLADES_PORT_FORWARDING = {
+#    22: lambda ip_address, server_id, fqdn, user:
+#               ("gate.example.synnefo.org", 61000 + server_id),
+#}
+CYCLADES_PORT_FORWARDING = {}
