commit 225cea18fa1aa04a54bdd2248a773439f7de99d1
Author: Christos Stavrakakis <cstavr@grnet.gr>
Date:   Thu Mar 14 11:16:56 2013 +0200

    Move common code for mngmt commands to webproject
    
    Move cyclades functions that are useful for all managament commands, like
    pprint_table or parse_filters, to snf-webproject.

diff --git a/snf-cyclades-app/synnefo/api/management/commands/network-create.py b/snf-cyclades-app/synnefo/api/management/commands/network-create.py
index 94e942b..76eff55 100644
--- a/snf-cyclades-app/synnefo/api/management/commands/network-create.py
+++ b/snf-cyclades-app/synnefo/api/management/commands/network-create.py
@@ -35,7 +35,7 @@ from optparse import make_option
 
 from django.core.management.base import BaseCommand, CommandError
 from synnefo.management.common import validate_network_info, get_backend
-from synnefo.management.common import pprint_table
+from synnefo.webproject.management.util import pprint_table
 
 from synnefo.db.models import Network
 from synnefo.logic.backend import create_network
diff --git a/snf-cyclades-app/synnefo/api/management/commands/server-show.py b/snf-cyclades-app/synnefo/api/management/commands/server-show.py
index e56c79a..f6864f2 100644
--- a/snf-cyclades-app/synnefo/api/management/commands/server-show.py
+++ b/snf-cyclades-app/synnefo/api/management/commands/server-show.py
@@ -32,8 +32,8 @@
 # or implied, of GRNET S.A.
 
 from django.core.management.base import BaseCommand, CommandError
-from synnefo.management.common import (format_bool, format_date,
-                                       format_vm_state, get_vm,
+from synnefo.webproject.management.util import format_bool, format_date
+from synnefo.management.common import (format_vm_state, get_vm,
                                        get_image, UserCache)
 
 
diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py
index c98bd4c..987316e 100644
--- a/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py
+++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-add.py
@@ -38,7 +38,8 @@ from synnefo.logic.backend import (get_physical_resources,
                                    update_resources,
                                    create_network_synced,
                                    connect_network_synced)
-from synnefo.management.common import check_backend_credentials, pprint_table
+from synnefo.management.common import check_backend_credentials
+from synnefo.webproject.management.util import pprint_table
 
 
 class Command(BaseCommand):
diff --git a/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py b/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py
index a6b2e99..dd33ba6 100644
--- a/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py
+++ b/snf-cyclades-app/synnefo/logic/management/commands/backend-modify.py
@@ -33,8 +33,8 @@
 
 from optparse import make_option
 from django.core.management.base import BaseCommand, CommandError
-from synnefo.management.common import (get_backend, check_backend_credentials,
-                                       parse_bool)
+from synnefo.webproject.management.util import parse_bool
+from synnefo.management.common import (get_backend, check_backend_credentials)
 
 
 class Command(BaseCommand):
@@ -89,8 +89,8 @@ class Command(BaseCommand):
                 check_backend_credentials(backend.clustername, backend.port,
                                           backend.username, backend.password)
         if options['drained']:
-            backend.drained = parse_bool(options['drained'])
+            backend.drained = parse_bool(options['drained'], strict=True)
         if options['offline']:
-            backend.offline = parse_bool(options['offline'])
+            backend.offline = parse_bool(options['offline'], strict=True)
 
         backend.save()
diff --git a/snf-cyclades-app/synnefo/management/common.py b/snf-cyclades-app/synnefo/management/common.py
index eb90b6e..276c5ce 100644
--- a/snf-cyclades-app/synnefo/management/common.py
+++ b/snf-cyclades-app/synnefo/management/common.py
@@ -31,17 +31,10 @@
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
 
-
-import ipaddr
-from datetime import datetime
-
-from django.utils.timesince import timesince, timeuntil
-
 from django.core.management import CommandError
 from synnefo.db.models import Backend, VirtualMachine, Network, Flavor
 from synnefo.api.util import get_image as backend_get_image
 from synnefo.api.faults import ItemNotFound, BadRequest, OverLimit
-from django.core.exceptions import FieldError
 
 from synnefo.api.util import validate_network_params
 from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN,
@@ -49,35 +42,10 @@ from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN,
 from synnefo.logic.rapi import GanetiApiError, GanetiRapiClient
 from synnefo.lib import astakos
 
-from synnefo.util.text import uenc
-
 import logging
 log = logging.getLogger(__name__)
 
 
-def format_bool(b):
-    return 'YES' if b else 'NO'
-
-
-def parse_bool(string):
-    if string == "True":
-        return True
-    elif string == "False":
-        return False
-    else:
-        raise Exception("Can not parse string %s to bool" % string)
-
-
-def format_date(d):
-    if not d:
-        return ''
-
-    if d < datetime.now():
-        return timesince(d) + ' ago'
-    else:
-        return 'in ' + timeuntil(d)
-
-
 def format_vm_state(vm):
     if vm.operstate == "BUILD":
         return "BUILD(" + str(vm.buildpercentage) + "%)"
@@ -159,49 +127,6 @@ def get_flavor(flavor_id):
                            " available flavor IDs." % flavor_id)
 
 
-def filter_results(objects, filter_by):
-    filter_list = filter_by.split(",")
-    filter_dict = {}
-    exclude_dict = {}
-
-    def map_field_type(query):
-        def fix_bool(val):
-            if val.lower() in ("yes", "true", "t"):
-                return True
-            if val.lower() in ("no", "false", "f"):
-                return False
-            return val
-
-        if "!=" in query:
-            key, val = query.split("!=")
-            exclude_dict[key] = fix_bool(val)
-            return
-        OP_MAP = {
-            ">=": "__gte",
-            "=>": "__gte",
-            ">":  "__gt",
-            "<=": "__lte",
-            "=<": "__lte",
-            "<":  "__lt",
-            "=":  "",
-        }
-        for op, new_op in OP_MAP.items():
-            if op in query:
-                key, val = query.split(op)
-                filter_dict[key + new_op] = fix_bool(val)
-                return
-
-    map(lambda x: map_field_type(x), filter_list)
-
-    try:
-        objects = objects.filter(**filter_dict)
-        return objects.exclude(**exclude_dict)
-    except FieldError as e:
-        raise CommandError(e)
-    except Exception as e:
-        raise CommandError("Can not filter results: %s" % e)
-
-
 def check_backend_credentials(clustername, port, username, password):
     try:
         client = GanetiRapiClient(clustername, port, username, password)
@@ -218,38 +143,6 @@ def check_backend_credentials(clustername, port, username, password):
                            " Ganeti Cluster name: %s" % info_name)
 
 
-def pprint_table(out, table, headers=None, separator=None):
-    """Print a pretty, aligned string representation of table.
-
-    Works by finding out the max width of each column and padding to data
-    to this value.
-    """
-
-    assert(isinstance(table, (list, tuple))), "Invalid table type"
-    sep = separator if separator else "  "
-
-    if headers:
-        assert(isinstance(headers, (list, tuple))), "Invalid headers type"
-        table.insert(0, headers)
-
-    # Find out the max width of each column
-    widths = [max(map(len, col)) for col in zip(*table)]
-
-    t_length = sum(widths) + len(sep) * (len(widths) - 1)
-    if headers:
-        # pretty print the headers
-        print >> out, sep.join((str(val).rjust(width)
-                               for val, width in zip(headers, widths)))
-        print >> out, "-" * t_length
-        # remove headers
-        table = table[1:]
-
-    # print the rest table
-    for row in table:
-        print >> out, sep.join(uenc(val.rjust(width))
-                               for val, width in zip(row, widths))
-
-
 class UserCache(object):
     """uuid<->displayname user 'cache'"""
 
diff --git a/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py b/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py
index 6b38074..bb97730 100644
--- a/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py
+++ b/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py
@@ -31,7 +31,7 @@
 from django.core.management.base import BaseCommand
 from optparse import make_option
 
-from synnefo.management.common import pprint_table
+from synnefo.webproject.management.util import pprint_table
 from synnefo.plankton.backend import ImageBackend
 
 
diff --git a/snf-cyclades-app/synnefo/quotas/management/commands/cyclades-usage-verify.py b/snf-cyclades-app/synnefo/quotas/management/commands/cyclades-usage-verify.py
index d098ecf..df17f8c 100644
--- a/snf-cyclades-app/synnefo/quotas/management/commands/cyclades-usage-verify.py
+++ b/snf-cyclades-app/synnefo/quotas/management/commands/cyclades-usage-verify.py
@@ -35,7 +35,7 @@ from django.core.management.base import BaseCommand
 from optparse import make_option
 
 from synnefo.quotas.util import get_db_holdings, get_quotaholder_holdings
-from synnefo.management.common import pprint_table
+from synnefo.webproject.management.util import pprint_table
 
 
 class Command(BaseCommand):
diff --git a/snf-webproject/synnefo/webproject/management/tests.py b/snf-webproject/synnefo/webproject/management/tests.py
new file mode 100644
index 0000000..3685820
--- /dev/null
+++ b/snf-webproject/synnefo/webproject/management/tests.py
@@ -0,0 +1,28 @@
+import sys
+from synnefo.webproject.management import util
+
+# Use backported unittest functionality if Python < 2.7
+try:
+    import unittest2 as unittest
+except ImportError:
+    if sys.version_info < (2, 7):
+        raise Exception("The unittest2 package is required for Python < 2.7")
+    import unittest
+
+
+class ParseFiltersTestCase(unittest.TestCase):
+    def test_parse_empty(self):
+        res = util.parse_filters("")
+        self.assertEqual(res, ({}, {}))
+
+    def test_parse_one(self):
+        res = util.parse_filters("x=2")
+        self.assertEqual(res, ({"x": "2"}, {}))
+        res = util.parse_filters("x!=2")
+        self.assertEqual(res, ({}, {"x": "2"}))
+
+    def test_parse_many(self):
+        res = util.parse_filters("x=2,x>=3,y!=4,z<3")
+        filters = {"x": "2", "x__gte": "3", "z__lt": "3"}
+        excludes = {"y": "4"}
+        self.assertEqual(res, (filters, excludes))
diff --git a/snf-webproject/synnefo/webproject/management/util.py b/snf-webproject/synnefo/webproject/management/util.py
new file mode 100644
index 0000000..0f59871
--- /dev/null
+++ b/snf-webproject/synnefo/webproject/management/util.py
@@ -0,0 +1,153 @@
+# Copyright 2012 - 2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from datetime import datetime
+from django.utils.timesince import timesince, timeuntil
+
+from synnefo.util.text import uenc, udec
+
+
+def parse_bool(value, strict=True):
+    """Convert a string to boolen value.
+
+    If string is True, then ValueError will be raised, if the string can not be
+    converted to boolean. Otherwise the string will be returned as is.
+
+    """
+    if value.lower() in ("yes", "true", "t", "1"):
+        return True
+    if value.lower() in ("no", "false", "f", "0"):
+        return False
+
+    if strict:
+        raise ValueError("Can convert '%s' to boolean value")
+    else:
+        return value
+
+
+def format_bool(b):
+    """Convert a boolean value to YES or NO."""
+    return "YES" if b else "NO"
+
+
+def format_date(d):
+    if not d:
+        return ""
+
+    if d < datetime.now():
+        return timesince(d) + " ago"
+    else:
+        return "in " + timeuntil(d)
+
+
+def parse_filters(filter_by):
+    """Parse a string into lookup parameters for QuerySet.filter(**kwargs).
+
+    This functions converts a string of comma-separated key 'cond' val triples
+    to two dictionaries, containing lookup parameters to be used for filter
+    and exclude functions of QuerySet.
+
+    e.g. filter_by="foo>=2, baz!=4" -> ({"foo__gte": "2"}, {"baz": "4"})
+
+    """
+
+    filter_dict = {}
+    exclude_dict = {}
+
+    filter_list = filter_by.split(",")
+
+    def map_field_type(query):
+        if "!=" in query:
+            key, val = query.split("!=")
+            exclude_dict[key] = parse_bool(val, strict=False)
+            return
+
+        OP_MAP = {
+            ">=": "__gte",
+            "=>": "__gte",
+            ">":  "__gt",
+            "<=": "__lte",
+            "=<": "__lte",
+            "<":  "__lt",
+            "=":  "",
+        }
+
+        for op, new_op in OP_MAP.items():
+            if op in query:
+                key, val = query.split(op)
+                filter_dict[key + new_op] = parse_bool(val, strict=False)
+                return
+
+    map(lambda x: map_field_type(x), filter_list)
+
+    return (filter_dict, exclude_dict)
+
+
+def pprint_table(out, table, headers=None, separator=None):
+    """Print a pretty, aligned string representation of table.
+
+    Works by finding out the max width of each column and padding to data
+    to this value.
+    """
+
+    assert(isinstance(table, (list, tuple))), "Invalid table type"
+    sep = separator if separator else "  "
+
+    if headers:
+        assert(isinstance(headers, (list, tuple))), "Invalid headers type"
+        table.insert(0, headers)
+
+    def strignify(obj):
+        if isinstance(obj, (unicode, str)):
+            return udec(obj)
+        else:
+            return str(obj)
+
+    table = [map(strignify, row) for row in table]
+
+    # Find out the max width of each column
+    widths = [max(map(len, col)) for col in zip(*table)]
+
+    t_length = sum(widths) + len(sep) * (len(widths) - 1)
+    if headers:
+        # pretty print the headers
+        line = sep.join(uenc(v.rjust(w)) for v, w in zip(headers, widths))
+        out.write(line + "\n")
+        out.write("-" * t_length + "\n")
+        # remove headers
+        table = table[1:]
+
+    # print the rest table
+    for row in table:
+        line = sep.join(uenc(v.rjust(w)) for v, w in zip(row, widths))
+        out.write(line + "\n")
