commit 7fcec5d754dd5dfd4d6284df716346259638df09
Author: Stratos Psomadakis <psomas@grnet.gr>
Date:   Tue Jan 22 19:29:20 2013 +0200

    snf-stats: Initial commit

diff --git a/.gitignore b/.gitignore
index 690c99a..376cb13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,4 @@ snf-tools/synnefo_tools/version.py
 snf-quotaholder-app/quotaholder_django/version.py
 *.iml
 *.graffle
+snf-stats/synnefo_stats/version.py
diff --git a/snf-stats/COPYRIGHT b/snf-stats/COPYRIGHT
new file mode 100644
index 0000000..6f7fb94
--- /dev/null
+++ b/snf-stats/COPYRIGHT
@@ -0,0 +1,32 @@
+Copyright 2011 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.
diff --git a/snf-stats/Changelog b/snf-stats/Changelog
new file mode 100644
index 0000000..9f80f39
--- /dev/null
+++ b/snf-stats/Changelog
@@ -0,0 +1,6 @@
+Changelog
+---------
+
+v0.1.0
+*******
+	* Initial release
diff --git a/snf-stats/MANIFEST.in b/snf-stats/MANIFEST.in
new file mode 100644
index 0000000..8b42070
--- /dev/null
+++ b/snf-stats/MANIFEST.in
@@ -0,0 +1,2 @@
+include README Changelog
+include distribute_setup.py
diff --git a/snf-stats/README b/snf-stats/README
new file mode 100644
index 0000000..d88cc51
--- /dev/null
+++ b/snf-stats/README
@@ -0,0 +1,4 @@
+README
+=======
+
+snf-stats
diff --git a/snf-stats/conf/20-snf-stats-settings.conf b/snf-stats/conf/20-snf-stats-settings.conf
new file mode 100644
index 0000000..aae995d
--- /dev/null
+++ b/snf-stats/conf/20-snf-stats-settings.conf
@@ -0,0 +1,18 @@
+## -*- coding: utf-8 -*-
+##
+## Image properties
+#IMAGE_WIDTH = 210
+#WIDTH = 68
+#HEIGHT = 10
+
+## Path settings
+#RRD_PREFIX = "/var/lib/collectd/rrd/"
+#GRAPH_PREFIX = "/var/cache/snf-stats/"
+
+## Font settings
+#FONT = "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono.ttf"
+#FONT = "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf"
+
+## Bar settings
+#BAR_BORDER_COLOR = (0x5c, 0xa1, 0xc0)
+#BAR_BG_COLOR = (0xea, 0xea, 0xea)
diff --git a/snf-stats/distribute_setup.py b/snf-stats/distribute_setup.py
new file mode 100644
index 0000000..10d6684
--- /dev/null
+++ b/snf-stats/distribute_setup.py
@@ -0,0 +1,485 @@
+#!python
+"""Bootstrap distribute installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from distribute_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import os
+import sys
+import time
+import fnmatch
+import tempfile
+import tarfile
+from distutils import log
+
+try:
+    from site import USER_SITE
+except ImportError:
+    USER_SITE = None
+
+try:
+    import subprocess
+
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        return subprocess.call(args) == 0
+
+except ImportError:
+    # will be used for python 2.3
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        # quoting arguments if windows
+        if sys.platform == 'win32':
+            def quote(arg):
+                if ' ' in arg:
+                    return '"%s"' % arg
+                return arg
+            args = [quote(arg) for arg in args]
+        return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
+
+DEFAULT_VERSION = "0.6.10"
+DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
+SETUPTOOLS_FAKED_VERSION = "0.6c11"
+
+SETUPTOOLS_PKG_INFO = """\
+Metadata-Version: 1.0
+Name: setuptools
+Version: %s
+Summary: xxxx
+Home-page: xxx
+Author: xxx
+Author-email: xxx
+License: xxx
+Description: xxx
+""" % SETUPTOOLS_FAKED_VERSION
+
+
+def _install(tarball):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # installing
+        log.warn('Installing Distribute')
+        if not _python_cmd('setup.py', 'install'):
+            log.warn('Something went wrong during the installation.')
+            log.warn('See the error message above.')
+    finally:
+        os.chdir(old_wd)
+
+
+def _build_egg(egg, tarball, to_dir):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # building an egg
+        log.warn('Building a Distribute egg in %s', to_dir)
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+
+    finally:
+        os.chdir(old_wd)
+    # returning the result
+    log.warn(egg)
+    if not os.path.exists(egg):
+        raise IOError('Could not build the egg.')
+
+
+def _do_download(version, download_base, to_dir, download_delay):
+    egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
+                       % (version, sys.version_info[0], sys.version_info[1]))
+    if not os.path.exists(egg):
+        tarball = download_setuptools(version, download_base,
+                                      to_dir, download_delay)
+        _build_egg(egg, tarball, to_dir)
+    sys.path.insert(0, egg)
+    import setuptools
+    setuptools.bootstrap_install_from = egg
+
+
+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                   to_dir=os.curdir, download_delay=15, no_fake=True):
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    was_imported = 'pkg_resources' in sys.modules or \
+        'setuptools' in sys.modules
+    try:
+        try:
+            import pkg_resources
+            if not hasattr(pkg_resources, '_distribute'):
+                if not no_fake:
+                    _fake_setuptools()
+                raise ImportError
+        except ImportError:
+            return _do_download(version, download_base, to_dir, download_delay)
+        try:
+            pkg_resources.require("distribute>="+version)
+            return
+        except pkg_resources.VersionConflict:
+            e = sys.exc_info()[1]
+            if was_imported:
+                sys.stderr.write(
+                "The required version of distribute (>=%s) is not available,\n"
+                "and can't be installed while this script is running. Please\n"
+                "install a more recent version first, using\n"
+                "'easy_install -U distribute'."
+                "\n\n(Currently using %r)\n" % (version, e.args[0]))
+                sys.exit(2)
+            else:
+                del pkg_resources, sys.modules['pkg_resources']    # reload ok
+                return _do_download(version, download_base, to_dir,
+                                    download_delay)
+        except pkg_resources.DistributionNotFound:
+            return _do_download(version, download_base, to_dir,
+                                download_delay)
+    finally:
+        if not no_fake:
+            _create_fake_setuptools_pkg_info(to_dir)
+
+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                        to_dir=os.curdir, delay=15):
+    """Download distribute from a specified location and return its filename
+
+    `version` should be a valid distribute version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download
+    attempt.
+    """
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    try:
+        from urllib.request import urlopen
+    except ImportError:
+        from urllib2 import urlopen
+    tgz_name = "distribute-%s.tar.gz" % version
+    url = download_base + tgz_name
+    saveto = os.path.join(to_dir, tgz_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            log.warn("Downloading %s", url)
+            src = urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = src.read()
+            dst = open(saveto, "wb")
+            dst.write(data)
+        finally:
+            if src:
+                src.close()
+            if dst:
+                dst.close()
+    return os.path.realpath(saveto)
+
+def _no_sandbox(function):
+    def __no_sandbox(*args, **kw):
+        try:
+            from setuptools.sandbox import DirectorySandbox
+            if not hasattr(DirectorySandbox, '_old'):
+                def violation(*args):
+                    pass
+                DirectorySandbox._old = DirectorySandbox._violation
+                DirectorySandbox._violation = violation
+                patched = True
+            else:
+                patched = False
+        except ImportError:
+            patched = False
+
+        try:
+            return function(*args, **kw)
+        finally:
+            if patched:
+                DirectorySandbox._violation = DirectorySandbox._old
+                del DirectorySandbox._old
+
+    return __no_sandbox
+
+def _patch_file(path, content):
+    """Will backup the file then patch it"""
+    existing_content = open(path).read()
+    if existing_content == content:
+        # already patched
+        log.warn('Already patched.')
+        return False
+    log.warn('Patching...')
+    _rename_path(path)
+    f = open(path, 'w')
+    try:
+        f.write(content)
+    finally:
+        f.close()
+    return True
+
+_patch_file = _no_sandbox(_patch_file)
+
+def _same_content(path, content):
+    return open(path).read() == content
+
+def _rename_path(path):
+    new_name = path + '.OLD.%s' % time.time()
+    log.warn('Renaming %s into %s', path, new_name)
+    os.rename(path, new_name)
+    return new_name
+
+def _remove_flat_installation(placeholder):
+    if not os.path.isdir(placeholder):
+        log.warn('Unkown installation at %s', placeholder)
+        return False
+    found = False
+    for file in os.listdir(placeholder):
+        if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
+            found = True
+            break
+    if not found:
+        log.warn('Could not locate setuptools*.egg-info')
+        return
+
+    log.warn('Removing elements out of the way...')
+    pkg_info = os.path.join(placeholder, file)
+    if os.path.isdir(pkg_info):
+        patched = _patch_egg_dir(pkg_info)
+    else:
+        patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
+
+    if not patched:
+        log.warn('%s already patched.', pkg_info)
+        return False
+    # now let's move the files out of the way
+    for element in ('setuptools', 'pkg_resources.py', 'site.py'):
+        element = os.path.join(placeholder, element)
+        if os.path.exists(element):
+            _rename_path(element)
+        else:
+            log.warn('Could not find the %s element of the '
+                     'Setuptools distribution', element)
+    return True
+
+_remove_flat_installation = _no_sandbox(_remove_flat_installation)
+
+def _after_install(dist):
+    log.warn('After install bootstrap.')
+    placeholder = dist.get_command_obj('install').install_purelib
+    _create_fake_setuptools_pkg_info(placeholder)
+
+def _create_fake_setuptools_pkg_info(placeholder):
+    if not placeholder or not os.path.exists(placeholder):
+        log.warn('Could not find the install location')
+        return
+    pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
+    setuptools_file = 'setuptools-%s-py%s.egg-info' % \
+            (SETUPTOOLS_FAKED_VERSION, pyver)
+    pkg_info = os.path.join(placeholder, setuptools_file)
+    if os.path.exists(pkg_info):
+        log.warn('%s already exists', pkg_info)
+        return
+
+    log.warn('Creating %s', pkg_info)
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+
+    pth_file = os.path.join(placeholder, 'setuptools.pth')
+    log.warn('Creating %s', pth_file)
+    f = open(pth_file, 'w')
+    try:
+        f.write(os.path.join(os.curdir, setuptools_file))
+    finally:
+        f.close()
+
+_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
+
+def _patch_egg_dir(path):
+    # let's check if it's already patched
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    if os.path.exists(pkg_info):
+        if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
+            log.warn('%s already patched.', pkg_info)
+            return False
+    _rename_path(path)
+    os.mkdir(path)
+    os.mkdir(os.path.join(path, 'EGG-INFO'))
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+    return True
+
+_patch_egg_dir = _no_sandbox(_patch_egg_dir)
+
+def _before_install():
+    log.warn('Before install bootstrap.')
+    _fake_setuptools()
+
+
+def _under_prefix(location):
+    if 'install' not in sys.argv:
+        return True
+    args = sys.argv[sys.argv.index('install')+1:]
+    for index, arg in enumerate(args):
+        for option in ('--root', '--prefix'):
+            if arg.startswith('%s=' % option):
+                top_dir = arg.split('root=')[-1]
+                return location.startswith(top_dir)
+            elif arg == option:
+                if len(args) > index:
+                    top_dir = args[index+1]
+                    return location.startswith(top_dir)
+        if arg == '--user' and USER_SITE is not None:
+            return location.startswith(USER_SITE)
+    return True
+
+
+def _fake_setuptools():
+    log.warn('Scanning installed packages')
+    try:
+        import pkg_resources
+    except ImportError:
+        # we're cool
+        log.warn('Setuptools or Distribute does not seem to be installed.')
+        return
+    ws = pkg_resources.working_set
+    try:
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
+                                  replacement=False))
+    except TypeError:
+        # old distribute API
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
+
+    if setuptools_dist is None:
+        log.warn('No setuptools distribution found')
+        return
+    # detecting if it was already faked
+    setuptools_location = setuptools_dist.location
+    log.warn('Setuptools installation detected at %s', setuptools_location)
+
+    # if --root or --preix was provided, and if
+    # setuptools is not located in them, we don't patch it
+    if not _under_prefix(setuptools_location):
+        log.warn('Not patching, --root or --prefix is installing Distribute'
+                 ' in another location')
+        return
+
+    # let's see if its an egg
+    if not setuptools_location.endswith('.egg'):
+        log.warn('Non-egg installation')
+        res = _remove_flat_installation(setuptools_location)
+        if not res:
+            return
+    else:
+        log.warn('Egg installation')
+        pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
+        if (os.path.exists(pkg_info) and
+            _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
+            log.warn('Already patched.')
+            return
+        log.warn('Patching...')
+        # let's create a fake egg replacing setuptools one
+        res = _patch_egg_dir(setuptools_location)
+        if not res:
+            return
+    log.warn('Patched done.')
+    _relaunch()
+
+
+def _relaunch():
+    log.warn('Relaunching...')
+    # we have to relaunch the process
+    # pip marker to avoid a relaunch bug
+    if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
+        sys.argv[0] = 'setup.py'
+    args = [sys.executable] + sys.argv
+    sys.exit(subprocess.call(args))
+
+
+def _extractall(self, path=".", members=None):
+    """Extract all members from the archive to the current working
+       directory and set owner, modification time and permissions on
+       directories afterwards. `path' specifies a different directory
+       to extract to. `members' is optional and must be a subset of the
+       list returned by getmembers().
+    """
+    import copy
+    import operator
+    from tarfile import ExtractError
+    directories = []
+
+    if members is None:
+        members = self
+
+    for tarinfo in members:
+        if tarinfo.isdir():
+            # Extract directories with a safe mode.
+            directories.append(tarinfo)
+            tarinfo = copy.copy(tarinfo)
+            tarinfo.mode = 448 # decimal for oct 0700
+        self.extract(tarinfo, path)
+
+    # Reverse sort directories.
+    if sys.version_info < (2, 4):
+        def sorter(dir1, dir2):
+            return cmp(dir1.name, dir2.name)
+        directories.sort(sorter)
+        directories.reverse()
+    else:
+        directories.sort(key=operator.attrgetter('name'), reverse=True)
+
+    # Set correct owner, mtime and filemode on directories.
+    for tarinfo in directories:
+        dirpath = os.path.join(path, tarinfo.name)
+        try:
+            self.chown(tarinfo, dirpath)
+            self.utime(tarinfo, dirpath)
+            self.chmod(tarinfo, dirpath)
+        except ExtractError:
+            e = sys.exc_info()[1]
+            if self.errorlevel > 1:
+                raise
+            else:
+                self._dbg(1, "tarfile: %s" % e)
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    tarball = download_setuptools()
+    _install(tarball)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
diff --git a/snf-stats/examples/stats.gunicorn b/snf-stats/examples/stats.gunicorn
new file mode 100644
index 0000000..f8379c8
--- /dev/null
+++ b/snf-stats/examples/stats.gunicorn
@@ -0,0 +1,17 @@
+CONFIG = {
+ 'mode': 'django',
+ 'environment': {
+   'DJANGO_SETTINGS_MODULE': 'synnefo.settings',
+ },
+ 'working_dir': '/etc/synnefo',
+ 'user': 'www-data',
+ 'group': 'www-data',
+ 'args': (
+   '--bind=127.0.0.1:8080',
+   '--workers=12',
+   '--workers=1',
+   '--worker-class=gevent',
+   '--log-level=DEBUG',
+   '--timeout=90'
+   ),
+}
diff --git a/snf-stats/setup.py b/snf-stats/setup.py
new file mode 100644
index 0000000..c6d289b
--- /dev/null
+++ b/snf-stats/setup.py
@@ -0,0 +1,105 @@
+# Copyright 2011 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.
+#
+
+import distribute_setup
+distribute_setup.use_setuptools()
+
+import os
+
+from distutils.util import convert_path
+from fnmatch import fnmatchcase
+from setuptools import setup, find_packages
+
+HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
+
+try:
+    # try to update the version file
+    from synnefo.util.version import update_version
+    update_version('synnefo_stats', 'version', HERE)
+except ImportError:
+    pass
+
+from synnefo_stats.version import __version__
+
+# Package info
+VERSION = __version__
+README = open(os.path.join(HERE, 'README')).read()
+CHANGES = open(os.path.join(HERE, 'Changelog')).read()
+SHORT_DESCRIPTION = 'synnefo stats grapher'
+
+PACKAGES_ROOT = '.'
+PACKAGES = find_packages(PACKAGES_ROOT)
+
+# Package meta
+CLASSIFIERS = []
+
+# Package requirements
+INSTALL_REQUIRES = [
+    'gdmodule', 
+    'py-rrdtool',
+    'Django>=1.2, <1.3',
+    'snf-common>=0.9.0',
+]
+
+setup(
+    name = 'snf-stats',
+    version = VERSION,
+    license = 'BSD',
+    url = 'http://code.grnet.gr/',
+    description = SHORT_DESCRIPTION,
+    long_description=README + '\n\n' +  CHANGES,
+    classifiers = CLASSIFIERS,
+
+    author = 'Stratos Psomadakis',
+    author_email = 'psomas@grnet.gr',
+    maintainer = 'Stratos Psomadakis',
+    maintainer_email = 'psomas@grnet.gr',
+
+    packages = PACKAGES,
+    package_dir= {'': PACKAGES_ROOT},
+    include_package_data = True,
+    zip_safe = False,
+
+    install_requires = INSTALL_REQUIRES,
+
+    dependency_links = ['http://docs.dev.grnet.gr/pypi'],
+    entry_points={
+        'synnefo': [
+             'default_settings = synnefo_stats.synnefo_settings',
+             'web_apps = synnefo_stats.synnefo_settings:installed_apps',
+             'urls = synnefo_stats.urls:urlpatterns',
+             'loggers = synnefo_stats.synnefo_settings:loggers'
+        ]
+    }
+)
diff --git a/snf-stats/synnefo_stats/__init__.py b/snf-stats/synnefo_stats/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/snf-stats/synnefo_stats/grapher.py b/snf-stats/synnefo_stats/grapher.py
new file mode 100644
index 0000000..ee6ffb2
--- /dev/null
+++ b/snf-stats/synnefo_stats/grapher.py
@@ -0,0 +1,271 @@
+# Copyright 2011-2012 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 django.http import HttpResponse
+from django.views.decorators.http import require_http_methods
+
+import gd
+import os
+import sys
+import subprocess
+
+from cgi import escape
+from cStringIO import StringIO
+#from flup.server.fcgi import WSGIServer
+
+import rrdtool
+
+from synnefo_stats import settings
+
+class InternalError(Exception):
+    pass
+
+class NotFound(Exception):
+    pass
+
+def read_file(filepath):
+    try:
+        f = open(filepath, "r")
+    except EnvironmentError, e:
+        raise InternalError(str(e))
+
+    try:
+        data = f.read()
+    except EnvironmentError, e:
+        raise InternalError(str(e))
+    finally:
+        f.close()
+
+    return data
+
+
+def draw_cpu_bar(hostname):
+    fname = os.path.join(settings.RRD_PREFIX, os.path.basename(hostname), "cpu", "virt_cpu_total.rrd")
+
+    try:
+        values = rrdtool.fetch(fname, "AVERAGE")[2][-20:]
+    except rrdtool.error, e:
+        #raise InternalError(str(e))
+        values = [(0.0, )]
+
+    v = [x[0] for x in values if x[0] is not None]
+    if not v:
+        # Fallback in case we only get NaNs
+        v = [0.0]
+    # Pick the last value
+    value = v[-1]
+
+    image = gd.image((settings.IMAGE_WIDTH, settings.HEIGHT))
+
+    border_color = image.colorAllocate(settings.BAR_BORDER_COLOR)
+    white = image.colorAllocate((0xff, 0xff, 0xff))
+    background_color = image.colorAllocate(settings.BAR_BG_COLOR)
+
+    if value >= 90.0:
+        line_color = image.colorAllocate((0xff, 0x00, 0x00))
+    elif value >= 75.0:
+        line_color = image.colorAllocate((0xda, 0xaa, 0x00))
+    else:
+        line_color = image.colorAllocate((0x00, 0xa1, 0x00))
+
+    image.rectangle((0,0), (settings.WIDTH-1, settings.HEIGHT-1), border_color, background_color)
+    image.rectangle((1,1), (int(value/100.0 * (settings.WIDTH - 2)), settings.HEIGHT - 2), line_color, line_color)
+    image.string_ttf(settings.FONT, 8.0, 0.0, (settings.WIDTH + 1, settings.HEIGHT - 1), "CPU: %.1f%%" % value, white)
+
+    io = StringIO()
+    image.writePng(io)
+    io.seek(0)
+    data = io.getvalue()
+    io.close()
+    return data
+
+
+def draw_net_bar(hostname):
+    fname = os.path.join(settings.RRD_PREFIX, os.path.basename(hostname),
+                         "interface", "if_octets-eth0.rrd")
+
+    try:
+        values = rrdtool.fetch(fname, "AVERAGE")[2][-20:]
+    except rrdtool.error, e:
+        #raise InternalError(str(e))
+        values = [(0.0, 0.0)]
+
+    v = [x for x in values if x[0] is not None and x[1] is not None]
+    if not v:
+        # Fallback in case we only get NaNs
+        v = [(0.0, 0.0)]
+
+    rx_value, tx_value = v[-1]
+
+    # Convert to bits
+    rx_value = rx_value * 8 / 10**6
+    tx_value = tx_value * 8 / 10**6
+
+    max_value = (int(max(rx_value, tx_value)/50) + 1) * 50.0
+
+    image = gd.image((settings.IMAGE_WIDTH, settings.HEIGHT))
+
+    border_color = image.colorAllocate(settings.BAR_BORDER_COLOR)
+    white = image.colorAllocate((0xff, 0xff, 0xff))
+    background_color = image.colorAllocate(settings.BAR_BG_COLOR)
+
+    tx_line_color = image.colorAllocate((0x00, 0xa1, 0x00))
+    rx_line_color = image.colorAllocate((0x00, 0x00, 0xa1))
+
+    image.rectangle((0,0), (settings.WIDTH-1, settings.HEIGHT-1), border_color, background_color)
+    image.rectangle((1,1), (int(tx_value/max_value * (settings.WIDTH-2)), settings.HEIGHT/2 - 1), tx_line_color, tx_line_color)
+    image.rectangle((1,settings.HEIGHT/2), (int(rx_value/max_value * (settings.WIDTH-2)), settings.HEIGHT - 2), rx_line_color, rx_line_color)
+    image.string_ttf(settings.FONT, 8.0, 0.0, (settings.WIDTH + 1, settings.HEIGHT - 1), "TX/RX: %.2f/%.2f Mbps" % (tx_value, rx_value), white)
+
+    io = StringIO()
+    image.writePng(io)
+    io.seek(0)
+    data = io.getvalue()
+    io.close()
+    return data
+
+
+def draw_cpu_ts(hostname):
+    fname = os.path.join(settings.RRD_PREFIX, os.path.basename(hostname), "cpu", "virt_cpu_total.rrd")
+    outfname = os.path.join(settings.GRAPH_PREFIX, os.path.basename(hostname) + "-cpu.png")
+
+    try:
+        rrdtool.graph(outfname, "-s", "-1d", "-e", "-20s",
+                      #"-t", "CPU usage",
+                      "-v", "%",
+                      #"--lazy",
+                      "DEF:cpu=%s:ns:AVERAGE" % fname,
+                      "LINE1:cpu#00ff00:")
+    except rrdtool.error, e:
+        raise InternalError(str(e))
+
+    return read_file(outfname)
+
+
+def draw_cpu_ts_w(hostname):
+    fname = os.path.join(settings.RRD_PREFIX, os.path.basename(hostname), "cpu", "virt_cpu_total.rrd")
+    outfname = os.path.join(settings.GRAPH_PREFIX, os.path.basename(hostname) + "-cpu-weekly.png")
+
+    try:
+        rrdtool.graph(outfname, "-s", "-1w", "-e", "-20s",
+                      #"-t", "CPU usage",
+                      "-v", "%",
+                      #"--lazy",
+                      "DEF:cpu=%s:ns:AVERAGE" % fname,
+                      "LINE1:cpu#00ff00:")
+    except rrdtool.error, e:
+        raise InternalError(str(e))
+
+    return read_file(outfname)
+
+
+def draw_net_ts(hostname):
+    fname = os.path.join(settings.RRD_PREFIX, os.path.basename(hostname), "interface", "if_octets-eth0.rrd")
+    outfname = os.path.join(settings.GRAPH_PREFIX, os.path.basename(hostname) + "-net.png")
+    try:
+        rrdtool.graph(outfname, "-s", "-1d", "-e", "-20s",
+                  #"-t", "Network traffic",
+                  "--units", "si",
+                  "-v", "Bits/s",
+                  #"--lazy",
+                  "COMMENT:\t\t\tAverage network traffic\\n",
+                  "DEF:rx=%s:rx:AVERAGE" % fname,
+                  "DEF:tx=%s:tx:AVERAGE" % fname,
+                  "CDEF:rxbits=rx,8,*",
+                  "CDEF:txbits=tx,8,*",
+                  "LINE1:rxbits#00ff00:Incoming",
+                  "GPRINT:rxbits:AVERAGE:\t%4.0lf%sbps\t\g",
+                  "LINE1:txbits#0000ff:Outgoing",
+                  "GPRINT:txbits:AVERAGE:\t%4.0lf%sbps\\n")
+    except rrdtool.error, e:
+        raise InternalError(str(e))
+
+    return read_file(outfname)
+
+
+def draw_net_ts_w(hostname):
+    fname = os.path.join(settings.RRD_PREFIX, os.path.basename(hostname), "interface", "if_octets-eth0.rrd")
+    outfname = os.path.join(settings.GRAPH_PREFIX, os.path.basename(hostname) + "-net-weekly.png")
+    try:
+        rrdtool.graph(outfname, "-s", "-1w", "-e", "-20s",
+                  #"-t", "Network traffic",
+                  "--units", "si",
+                  "-v", "Bits/s",
+                  #"--lazy",
+                  "COMMENT:\t\t\tAverage network traffic\\n",
+                  "DEF:rx=%s:rx:AVERAGE" % fname,
+                  "DEF:tx=%s:tx:AVERAGE" % fname,
+                  "CDEF:rxbits=rx,8,*",
+                  "CDEF:txbits=tx,8,*",
+                  "LINE1:rxbits#00ff00:Incoming",
+                  "GPRINT:rxbits:AVERAGE:\t%4.0lf%sbps\t\g",
+                  "LINE1:txbits#0000ff:Outgoing",
+                  "GPRINT:txbits:AVERAGE:\t%4.0lf%sbps\\n")
+    except rrdtool.error, e:
+        raise InternalError(str(e))
+
+    return read_file(outfname)
+
+
+@require_http_methods(["GET"])
+def grapher(request, hostname, graph_type):
+    hostname = str(hostname)
+    graph = ""
+    ctype = "text/plain"
+    code = 200
+
+    try:
+        if graph_type == "cpu-bar":
+            graph = draw_cpu_bar(hostname)
+        elif graph_type == "cpu-ts":
+            graph = draw_cpu_ts(hostname)
+        elif graph_type == "net-bar":
+            graph = draw_net_bar(hostname)
+        elif graph_type == "net-ts":
+            graph = draw_net_ts(hostname)
+        elif graph_type == "cpu-ts-w":
+            graph = draw_cpu_ts_w(hostname)
+        elif graph_type == "net-ts-w":
+            graph = draw_net_ts_w(hostname)
+        else:
+            raise NotFound
+        ctype = "image/png"
+    except InternalError:
+        code = 500
+    except NotFound:
+        code = 404
+
+    response = HttpResponse(graph, content_type=ctype)
+    response.status_code = code
+
+    return response
diff --git a/snf-stats/synnefo_stats/settings.py b/snf-stats/synnefo_stats/settings.py
new file mode 100644
index 0000000..1f7d6ab
--- /dev/null
+++ b/snf-stats/synnefo_stats/settings.py
@@ -0,0 +1,19 @@
+## -*- coding: utf-8 -*-
+from django.conf import settings
+
+# Image properties
+IMAGE_WIDTH = getattr(settings, 'IMAGE_WIDTH', 210)
+WIDTH = getattr(settings, 'WIDTH', 68)
+HEIGHT = getattr(settings, 'HEIGHT', 10)
+
+# Path settings
+RRD_PREFIX = getattr(settings, 'RRD_PREFIX', "/var/lib/collectd/rrd/")
+GRAPH_PREFIX = getattr(settings, 'GRAPH_PREFIX', "/var/cache/snf-stats/")
+
+# Font settings
+FONT = getattr(settings, 'FONT', "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono.ttf")
+FONT = getattr(settings, 'FONT', "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf")
+
+# Bar settings
+BAR_BORDER_COLOR = getattr(settings, 'BAR_BORDER_COLOR', (0x5c, 0xa1, 0xc0))
+BAR_BG_COLOR = getattr(settings, 'BAR_BG_COLOR', (0xea, 0xea, 0xea))
diff --git a/snf-stats/synnefo_stats/synnefo_settings.py b/snf-stats/synnefo_stats/synnefo_settings.py
new file mode 100644
index 0000000..3302bad
--- /dev/null
+++ b/snf-stats/synnefo_stats/synnefo_settings.py
@@ -0,0 +1,46 @@
+# Copyright 2011-2012 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.
+#
+
+"""
+Django settings metadata. To be used in setup.py snf-webproject entry points.
+"""
+
+installed_apps = ['synnefo_stats']
+
+loggers = {
+    'synnefo_stats': {
+        'handlers': ['console'],
+        'level': 'INFO',
+    },
+}
diff --git a/snf-stats/synnefo_stats/urls.py b/snf-stats/synnefo_stats/urls.py
new file mode 100644
index 0000000..1c64d59
--- /dev/null
+++ b/snf-stats/synnefo_stats/urls.py
@@ -0,0 +1,39 @@
+# Copyright 2011-2012 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 django.conf.urls.defaults import patterns
+from synnefo_stats.grapher import grapher
+
+urlpatterns = patterns('',
+    (r'^(?P<hostname>\S+)/(?P<graph_type>\S+)/$', grapher),
+)
