commit 7fb619a0eee6afe368723f1aa5286f927543bfb1
Author: Kostas Papadimitriou <kpap@grnet.gr>
Date:   Mon Jan 9 17:06:36 2012 +0200

    Added snf-common package
    
    split out of some commonly used code in a seperate package

diff --git a/runtests-venvs.sh b/runtests-venvs.sh
index e49ce53..f4852ae 100755
--- a/runtests-venvs.sh
+++ b/runtests-venvs.sh
@@ -42,30 +42,48 @@ virtualenv --no-site-packages -ppython2.6 env
 source env/bin/activate
 export PIP_DOWNLOAD_CACHE=/tmp/.pip_cache
 pip install -r requirements.pip
-rm -rf build dist
-python setup.py sdist
-pip install dist/synnefo-*.tar.gz
-cd env
-# avoid vncauthproxy errors
-rm bin/vncauthproxy.py
-echo "running django tests..." >&2
-snf-manage test aai admin api db helpdesk invitations logic userdata --settings=synnefo.settings.test
-cd ..
-deactivate
 
-rm -rf env
-virtualenv --no-site-packages -ppython2.7 env
-source env/bin/activate
-export PIP_DOWNLOAD_CACHE=/tmp/.pip_cache
-pip install -r requirements.pip
+cd snf-common
+rm -rf build dist
+python setup.py install
+cd ../snf-app
+rm -rf build dist
+python setup.py install
+cd ../snf-ganeti-tools
 rm -rf build dist
-python setup.py sdist
-pip install dist/synnefo-*.tar.gz
-cd env
+python setup.py install
+
+
+cd ../env
 # avoid vncauthproxy errors
 rm bin/vncauthproxy.py
 echo "running django tests..." >&2
+export SYNNEFO_SETTINGS_DIR=/etc/lala
 snf-manage test aai admin api db helpdesk invitations logic userdata --settings=synnefo.settings.test
 cd ..
 deactivate
-rm -rf env
+
+#rm -rf env
+#virtualenv --no-site-packages -ppython2.7 env
+#source env/bin/activate
+#export PIP_DOWNLOAD_CACHE=/tmp/.pip_cache
+#pip install -r requirements.pip
+
+#cd snf-common
+#rm -rf build dist
+#python setup.py install
+#cd ../snf-app
+#rm -rf build dist
+#python setup.py install
+#cd ../snf-ganeti-tools
+#rm -rf build dist
+#python setup.py install
+
+#cd env
+## avoid vncauthproxy errors
+#rm bin/vncauthproxy.py
+#echo "running django tests..." >&2
+#snf-manage test aai admin api db helpdesk invitations logic userdata --settings=synnefo.settings.test
+#cd ..
+#deactivate
+#rm -rf env
diff --git a/runtests.sh b/runtests.sh
index 343fae9..d5dc578 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -38,9 +38,8 @@
 set -e
 
 echo "Running snf-app tests..." >&2
-export PYTHONPATH=$PYTHONPATH:./snf-app
-python snf-app/synnefo/manage.py test aai admin api db helpdesk invitations logic userdata --settings=synnefo.settings.test
+python snf-manage test aai admin api db helpdesk invitations logic userdata --settings=synnefo.settings.test
 
 echo "Running snf-ganeti-tools tests..." >&2
-PYTHONPATH=snf-ganeti-tools:$PYTHONPATH ./snf-ganeti-tools/test/synnefo.ganeti_unittest.py
+./snf-ganeti-tools/test/synnefo.ganeti_unittest.py
 
diff --git a/snf-app/setup.py b/snf-app/setup.py
index 2d58824..222e98b 100644
--- a/snf-app/setup.py
+++ b/snf-app/setup.py
@@ -40,12 +40,12 @@ import os
 from distutils.util import convert_path
 from fnmatch import fnmatchcase
 from setuptools import setup, find_packages
-from synnefo.version import get_version
+from synnefo.version import vcs_version
 
 HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
 
 # Package info
-VERSION = get_version().replace(" ","")
+VERSION = vcs_version()
 README = open(os.path.join(HERE, 'README')).read()
 CHANGES = open(os.path.join(HERE, 'Changelog')).read()
 SHORT_DESCRIPTION = 'Package short description'
@@ -171,7 +171,7 @@ def find_package_data(
     return out
 
 setup(
-    name = 'synnefo',
+    name = 'snf-app',
     version = VERSION,
     license = 'BSD',
     url = 'http://code.grnet.gr/',
@@ -202,6 +202,10 @@ setup(
          'snf-admin = synnefo.tools.admin:main',
          'snf-cloud = synnefo.tools.cloud:main',
          ],
+     'synnefo': [
+         'settings = synnefo.app_settings',
+         'apps = synnefo.app_settings:getapps',
+         ]
       },
     )
 
diff --git a/snf-app/synnefo/app_settings/__init__.py b/snf-app/synnefo/app_settings/__init__.py
new file mode 100644
index 0000000..0073d0b
--- /dev/null
+++ b/snf-app/synnefo/app_settings/__init__.py
@@ -0,0 +1,4 @@
+from synnefo.app_settings.default import *
+
+def getapps():
+    return INSTALLED_APPS
diff --git a/snf-app/synnefo/app_settings/default/__init__.py b/snf-app/synnefo/app_settings/default/__init__.py
new file mode 100644
index 0000000..2325c00
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/__init__.py
@@ -0,0 +1,51 @@
+# 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.
+
+from synnefo.app_settings.default.admins import *
+from synnefo.app_settings.default.apps import *
+from synnefo.app_settings.default.deploy import *
+from synnefo.app_settings.default.logging import *
+from synnefo.app_settings.default.site import *
+from synnefo.app_settings.default.backend import *
+from synnefo.app_settings.default.database import *
+from synnefo.app_settings.default.queues import *
+from synnefo.app_settings.default.api import *
+from synnefo.app_settings.default.plankton import *
+from synnefo.app_settings.default.ui import *
+from synnefo.app_settings.default.userdata import *
+from synnefo.app_settings.default.aai import *
+from synnefo.app_settings.default.invitations import *
+from synnefo.app_settings.default.reconciliation import *
+from synnefo.app_settings.default.helpdesk import *
+from synnefo.app_settings.default.tests import *
+
diff --git a/snf-app/synnefo/app_settings/default/aai.py b/snf-app/synnefo/app_settings/default/aai.py
new file mode 100644
index 0000000..c840da3
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/aai.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# AAI configuration
+#####################
+
+# Unauthenticated HTTP requests to the UI get redirected to this URL
+LOGIN_URL = "/login"
+
+# Set the expiration time of newly created auth tokens
+# to be this many hours after their creation time.
+AUTH_TOKEN_DURATION = 30 * 24
+
+# Enable receiving a temporary auth token (using the ?test URL parameter) that
+# bypasses the authentication mechanism.
+#
+# Make sure there is an actual user in the db whose token matches
+# BYPASS_AUTHENTICATION_SECRET_TOKEN.
+#
+# WARNING, ACHTUNG, README, etc: DO NOT ENABLE THIS ON DEPLOYED VERSIONS!
+#
+BYPASS_AUTHENTICATION = False
+BYPASS_AUTHENTICATION_SECRET_TOKEN = '5e41595e9e884543fa048e07c1094d74'
+
+# Urls that bypass Shibboleth authentication
+AAI_SKIP_AUTH_URLS = ['/api', '/plankton', '/invitations/login']
diff --git a/snf-app/synnefo/app_settings/default/admins.py b/snf-app/synnefo/app_settings/default/admins.py
new file mode 100644
index 0000000..de94131
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/admins.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+#
+# Admin names and email addresses
+##################################
+
+ADMINS = (
+    # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+# Email configuration
+EMAIL_HOST = "127.0.0.1"
+EMAIL_HOST_USER = ""
+EMAIL_HOST_PASSWORD = ""
+EMAIL_SUBJECT_PREFIX = "[email-subject-prefix] "
+DEFAULT_CHARSET = 'utf-8'
+
+# Address to use for outgoing emails
+DEFAULT_FROM_EMAIL = "~okeanos <no-reply@grnet.gr>"
diff --git a/snf-app/synnefo/app_settings/default/api.py b/snf-app/synnefo/app_settings/default/api.py
new file mode 100644
index 0000000..206c7e6
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/api.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+#
+# API configuration
+#####################
+
+from deploy import *
+
+# The API implementation needs to accept and return absolute references
+# to its resources. Thus, it needs to know its public URL.
+API_ROOT_URL = APP_INSTALL_URL + '/api'
+
+# The API will return HTTP Bad Request if the ?changes-since
+# parameter refers to a point in time more than POLL_LIMIT seconds ago.
+POLL_LIMIT = 3600
+
+#
+# Network Configuration
+#
+
+# Synnefo assigns this link id to NICs connected on the public network.
+# An IP pool should be associated with this link by the Ganeti administrator.
+GANETI_PUBLIC_LINK = 'snf_public'
+# This link id is assigned to NICs that should be isolated from anything else
+# (e.g., right before the NIC gets deleted).
+# This value is also hardcoded in a fixture in db/fixtures/initial_data.json.
+GANETI_NULL_LINK = 'snf_null'
+
+# The pool of private network links to use is
+# $GANETI_LINK_PREFIX{1..$GANETI_MAX_LINK_NUMBER}.
+#
+# The prefix to use for private network links.
+GANETI_LINK_PREFIX = 'prv'
+# The number of private network links to use.
+GANETI_MAX_LINK_NUMBER = 100
+# Firewalling
+GANETI_FIREWALL_ENABLED_TAG = 'synnefo:network:0:protected'
+GANETI_FIREWALL_DISABLED_TAG = 'synnefo:network:0:unprotected'
+GANETI_FIREWALL_PROTECTED_TAG = 'synnefo:network:0:limited'
+
+# The default firewall profile that will be in effect if no tags are defined
+DEFAULT_FIREWALL_PROFILE = 'DISABLED'
+
+# our REST API would prefer to be explicit about trailing slashes
+APPEND_SLASH = False
+
+# Ignore disk size specified by flavor, always build the
+# machine with a 4GB (in the case of Windows: 14GB) disk.
+# This setting is helpful in development setups.
+#
+IGNORE_FLAVOR_DISK_SIZES = False
+
+# Quota
+#
+# Maximum number of VMs a user is allowed to have
+MAX_VMS_PER_USER = 3
+
+# URL templates for the stat graphs.
+# The API implementation replaces '%s' with the encrypted backend id.
+# FIXME: For now we do not encrypt the backend id.
+CPU_BAR_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/cpu-bar.png'
+CPU_TIMESERIES_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/cpu-ts.png'
+NET_BAR_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/net-bar.png'
+NET_TIMESERIES_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/net-ts.png'
+
+# Recommended refresh period for server stats
+STATS_REFRESH_PERIOD = 60
+
+# The maximum number of file path/content pairs that can be supplied on server
+# build
+MAX_PERSONALITY = 5
+
+# The maximum size, in bytes, for each personality file
+MAX_PERSONALITY_SIZE = 10240
+
+# Available storage types to be used as disk templates
+GANETI_DISK_TEMPLATES = ('blockdev', 'diskless', 'drbd', 'file', 'plain',
+                         'rbd',  'sharedfile')
+DEFAULT_GANETI_DISK_TEMPLATE = 'drbd'
diff --git a/snf-app/synnefo/app_settings/default/apps.py b/snf-app/synnefo/app_settings/default/apps.py
new file mode 100644
index 0000000..d9ded2d
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/apps.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+#
+# Core Django settings
+##################################
+
+INSTALLED_APPS = (
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.admin',
+    'synnefo.aai',
+    'synnefo.admin',
+    'synnefo.api',
+    'synnefo.ui',
+    'synnefo.db',
+    'synnefo.logic',
+    'synnefo.invitations',
+    'synnefo.helpdesk',
+    'synnefo.plankton',
+    'synnefo.ui.userdata',
+    'south'
+)
+
+SESSION_ENGINE = "django.contrib.sessions.backends.cache"
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
+#     'django.template.loaders.eggs.Loader',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+    'django.core.context_processors.request',
+    'django.core.context_processors.i18n',
+    'django.contrib.auth.context_processors.auth',
+    'django.core.context_processors.media'
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'synnefo.aai.middleware.SynnefoAuthMiddleware',
+    'synnefo.api.middleware.ApiAuthMiddleware',
+    'synnefo.helpdesk.middleware.HelpdeskMiddleware',
+    'django.middleware.locale.LocaleMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware'
+)
+
+ROOT_URLCONF = 'synnefo.urls'
+
+TEMPLATE_DIRS = (
+    # Put strings here, like "/home/html/django_templates"
+    # or "C:/www/django/templates".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+)
+
+LANGUAGES = (
+  #('el', u'Ελληνικά'),
+  ('en', 'English'),
+)
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'UTC'   # Warning: The API depends on the TIME_ZONE being UTC
+
diff --git a/snf-app/synnefo/app_settings/default/backend.py b/snf-app/synnefo/app_settings/default/backend.py
new file mode 100644
index 0000000..d225bb4
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/backend.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+#
+# Ganeti backend configuration
+###################################
+
+# The RAPI endpoint and associated credentials to use
+# for talking to the Ganeti backend.
+GANETI_MASTER_IP = "10.0.0.1"
+GANETI_CLUSTER_INFO = (GANETI_MASTER_IP, 5080, "username", "password")
+
+# This prefix gets used when determining the instance names
+# of Synnefo VMs at the Ganeti backend.
+# The dash must always appear in the name!
+BACKEND_PREFIX_ID = "snf-"
+
+# The following dictionary defines deployment-specific
+# arguments to the RAPI CreateInstance call.
+# At a minimum it should contain the
+# 'disk_template', 'os_provider', and 'hvparams' keys.
+#
+# More specifically:
+# a) disk_template:
+#    The disk template to use when creating the instance.
+#    Suggested values: 'plain', or 'drbd'.
+# b) os:
+#    The OS provider to use (customized Ganeti Instance Image)
+# c) hvparams:
+#    Hypervisor-specific parameters (serial_console = False, see #785)
+# d) If using the DRBD disk_template, you may want to include
+#    wait_for_sync = False (see #835).
+#
+GANETI_CREATEINSTANCE_KWARGS = {
+    'os': 'snf-image+default',
+    'hvparams': {'serial_console': False},
+    'wait_for_sync': False}
diff --git a/snf-app/synnefo/app_settings/default/database.py b/snf-app/synnefo/app_settings/default/database.py
new file mode 100644
index 0000000..4f43b86
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/database.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+#
+# Database settings
+####################
+
+import os
+
+DEFAULT_DB_PATH = '/usr/share/synnefo/'
+DATABASES = {
+    'default': {
+        # 'postgresql_psycopg2', 'postgresql','mysql', 'sqlite3' or 'oracle'
+        'ENGINE': 'sqlite3',
+         # ATTENTION: This *must* be the absolute path if using sqlite3.
+         # See: http://docs.djangoproject.com/en/dev/ref/settings/#name
+        'NAME': os.path.join(DEFAULT_DB_PATH, 'database.sqlite'),
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        # Set to empty string for localhost. Not used with sqlite3.
+        'HOST': '',
+        # Set to empty string for default. Not used with sqlite3.
+        'PORT': '',
+    }
+}
+
+if DATABASES['default']['ENGINE'].endswith('mysql'):
+    DATABASES['default']['OPTIONS'] = {
+            'init_command': 'SET storage_engine=INNODB; ' +
+                'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
+    }
diff --git a/snf-app/synnefo/app_settings/default/deploy.py b/snf-app/synnefo/app_settings/default/deploy.py
new file mode 100644
index 0000000..276cdf9
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/deploy.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+#
+# Deployment
+##################################
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+# Top-level URL for deployment. Numerous other URLs depend on this.
+APP_INSTALL_URL = "https://host:port"
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'ly6)mw6a7x%n)-e#zzk4jo6f2=uqu!1o%)2-(7lo+f9yd^k^bg'
diff --git a/snf-app/synnefo/app_settings/default/helpdesk.py b/snf-app/synnefo/app_settings/default/helpdesk.py
new file mode 100644
index 0000000..e4a5bec
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/helpdesk.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+#
+# Helpdesk application
+#
+
+# Duration for temporary auth tokens, created for impersonating a registered
+# user by helpdesk staff.
+HELPDESK_TOKEN_DURATION_MIN = 30
+
+# IP addresses of the machines allowed to connect as help desk
+HELPDESK_ALLOWED_IPS = ("127.0.0.1",)
+
diff --git a/snf-app/synnefo/app_settings/default/invitations.py b/snf-app/synnefo/app_settings/default/invitations.py
new file mode 100644
index 0000000..8585f75
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/invitations.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+#
+# Invitations settings
+##################################
+
+
+# Max number of invitations allowed per level
+INVITATIONS_PER_LEVEL = {
+   #Level  #Max Invitations
+    0   :   10000,
+    1   :   3,
+    2   :   2,
+    3   :   1,
+    4   :   0
+}
+
+# Key to encrypt X-Auth-Token with when sending invitations
+INVITATION_ENCR_KEY = '8d342f6e7a0366c632978a80257019af'
+
+# Days for which an invitation is active
+INVITATION_VALID_DAYS = 180
diff --git a/snf-app/synnefo/app_settings/default/logging.py b/snf-app/synnefo/app_settings/default/logging.py
new file mode 100644
index 0000000..9eb1780
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/logging.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+#
+# Logging configuration
+##################################
+
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': True,
+    
+    'formatters': {
+        'simple': {
+            'format': '%(message)s'
+        },
+        'verbose': {
+            'format': '%(asctime)s [%(levelname)s] %(message)s'
+        },
+        'django': {
+            'format': '[%(asctime)s] %(levelname)s %(message)s',
+            'datefmt': '%d/%b/%Y %H:%M:%S'
+        },
+    },
+    
+    'handlers': {
+        'null': {
+            'class': 'logging.NullHandler',
+        },
+        'console': {
+            'class': 'logging.StreamHandler',
+            'formatter': 'django'
+        },
+        'syslog': {
+            'class': 'logging.handlers.SysLogHandler',
+            'address': '/dev/log',
+#            'address': ('localhost', 514),
+            'facility': 'daemon',
+            'formatter': 'verbose',
+            'level': 'INFO',
+        },
+    },
+    
+    'loggers': {
+        'synnefo': {
+            'handlers': ['syslog'],
+            'level': 'INFO'
+        },
+        'synnefo.admin': {
+            'level': 'INFO',
+            'propagate': 1
+        },
+        'synnefo.api': {
+            'level': 'INFO',
+            'propagate': 1
+        },
+        'synnefo.db': {
+            'level': 'INFO',
+            'propagate': 1
+        },
+        'synnefo.logic': {
+            'level': 'INFO',
+            'propagate': 1
+        },
+    }    
+}
+
+
+DISPATCHER_LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': True,
+    
+    'formatters': {
+        'verbose': {
+            'format': '%(asctime)s [%(levelname)s] %(message)s'
+        },
+    },
+    
+    'handlers': {
+        'console': {
+            'class': 'logging.StreamHandler',
+            'formatter': 'verbose'
+        },
+        'file': {
+            'class': 'logging.handlers.WatchedFileHandler',
+            'filename': '/var/log/synnefo/dispatcher.log',
+            'formatter': 'verbose',
+            'level': 'DEBUG'
+        },
+    },
+    
+    'loggers': {
+        'synnefo': {'propagate': 1}
+    },
+    
+    'root': {
+        'handlers': ['console', 'file'],
+        'level': 'DEBUG',
+    }
+}
+
+
+SNFADMIN_LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': True,
+    
+    'formatters': {
+        'verbose': {
+            'format': '%(asctime)s [%(levelname)s] %(message)s'
+        },
+    },
+    
+    'handlers': {
+        'console': {
+            'class': 'logging.StreamHandler',
+            'formatter': 'verbose'
+        },
+    },
+    
+    'loggers': {
+        'synnefo': {'propagate': 1}
+    },
+    
+    'root': {
+        'handlers': ['console'],
+        'level': 'DEBUG',
+    }
+}
diff --git a/snf-app/synnefo/app_settings/default/plankton.py b/snf-app/synnefo/app_settings/default/plankton.py
new file mode 100644
index 0000000..73ce46f
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/plankton.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+#
+# Plankton configuration
+########################
+
+from os.path import join
+
+# Backend settings
+PITHOS_ROOT = '/usr/share/pithos'
+BACKEND_DB_MODULE = 'pithos.backends.lib.sqlalchemy'
+BACKEND_DB_CONNECTION = 'sqlite:///' + join(PITHOS_ROOT, 'backend.db')
+BACKEND_BLOCK_MODULE = 'pithos.backends.lib.hashfiler'
+BACKEND_BLOCK_PATH = join(PITHOS_ROOT, 'data/')
+
+# Default settings for new accounts.
+DEFAULT_QUOTA = 50 * 1024**3
+DEFAULT_VERSIONING = 'auto'
+
+# The Pithos container where all images are stored
+PITHOS_IMAGE_CONTAINER = 'images'
+
+ALLOWED_DISK_FORMATS = ('diskdump', 'dump', 'extdump', 'lvm', 'ntfsclone',
+        'ntfsdump')
+ALLOWED_CONTAINER_FORMATS = ('aki', 'ari', 'ami', 'bare', 'ovf')
+
+DEFAULT_DISK_FORMAT = 'dump'
+DEFAULT_CONTAINER_FORMAT = 'bare'
+
+# The owner of the images that will be marked as "system images" by the UI
+SYSTEM_IMAGES_OWNER = 'okeanos'
diff --git a/snf-app/synnefo/app_settings/default/queues.py b/snf-app/synnefo/app_settings/default/queues.py
new file mode 100644
index 0000000..d9b66b7
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/queues.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+#
+# Queues, exchanges and bindings for AMQP
+###########################################
+
+# Rabbit work queue endpoint
+RABBIT_HOST = "10.0.0.1:5672"
+RABBIT_USERNAME = "username"
+RABBIT_PASSWORD = "password"
+RABBIT_VHOST = "/"
+
+EXCHANGE_GANETI = "ganeti"  # Messages from Ganeti
+EXCHANGE_CRON = "cron"      # Messages from periodically triggered tasks
+EXCHANGE_API = "api"        # Messages from the REST API
+EXCHANGES = (EXCHANGE_GANETI, EXCHANGE_CRON, EXCHANGE_API)
diff --git a/snf-app/synnefo/app_settings/default/reconciliation.py b/snf-app/synnefo/app_settings/default/reconciliation.py
new file mode 100644
index 0000000..4dc01c4
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/reconciliation.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+#
+# Reconciliations
+###################
+
+# Minutes between reconciliations
+RECONCILIATION_MIN = 30
+
diff --git a/snf-app/synnefo/app_settings/default/site.py b/snf-app/synnefo/app_settings/default/site.py
new file mode 100644
index 0000000..a965b1d
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/site.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+#
+# Site-specific Django conf
+##################################
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+# FIXME?
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = '/static/'
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# app/directory static files map, used by link_static command to create
+# appropriate symlinks for static files
+STATIC_FILES = {
+    'synnefo.ui': '',
+    'synnefo.admin': 'admin',
+}
diff --git a/snf-app/synnefo/app_settings/default/tests.py b/snf-app/synnefo/app_settings/default/tests.py
new file mode 100644
index 0000000..fa0c2b8
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/tests.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+#
+# Special unit test mode
+#########################
+
+# A quick-n-dirty way which sets settings.TEST
+# if we're running unit tests.
+import sys, os
+TEST = False
+if len(sys.argv) >= 2:
+    if os.path.basename(sys.argv[0]) == 'manage.py' and \
+        (sys.argv[1] == 'test' or sys.argv[1] == 'hudson'):
+            TEST = True
diff --git a/snf-app/synnefo/app_settings/default/ui.py b/snf-app/synnefo/app_settings/default/ui.py
new file mode 100644
index 0000000..6f46e18
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/ui.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# UI settings
+###################
+
+from admins import *
+from site import *
+
+# base url for ui static files
+# if not set, defaults to MEDIA_URL + 'snf-<latest_ui_version>/'
+UI_MEDIA_URL = MEDIA_URL + 'snf/'
+
+# UI requests to the API layer time out after that many milliseconds
+TIMEOUT = 10 * 1000
+
+# A list of suggested server tags (server metadata keys)
+DEFAULT_KEYWORDS = ["OS", "Role", "Location", "Owner"]
+
+# A list of allowed icons for OS Images
+IMAGE_ICONS = ["redhat", "ubuntu", "debian", "windows", "gentoo", "archlinux",
+               "centos", "fedora", "freebsd", "netbsd", "openbsd", "slackware",
+               "suse", "kubuntu"]
+
+# How often should the UI request changes from the API
+UI_UPDATE_INTERVAL = 5000
+
+# Milieconds to increase the interval after UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT calls
+# of recurrent api requests
+UI_UPDATE_INTERVAL_INCREASE = UI_UPDATE_INTERVAL / 4
+UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT = 4
+
+# Maximum update interval
+UI_UPDATE_INTERVAL_MAX = UI_UPDATE_INTERVAL * 3
+
+# Fast update interval
+UI_UPDATE_INTERVAL_FAST = UI_UPDATE_INTERVAL / 2
+
+# List of emails used for sending the feedback messages to (following the ADMINS format)
+FEEDBACK_CONTACTS = (
+    # ('Contact Name', 'contact_email@domain.com'),
+)
+
+# Email from which the feedback emails will be sent from
+FEEDBACK_EMAIL_FROM = DEFAULT_FROM_EMAIL
+
+# URL to redirect user to when he logs out from the ui (if not set
+# settings.LOGIN_URL will be used)
+#LOGOUT_URL = ""
+
+# Flavor options that we provide to the user as predefined
+# cpu/ram/disk combinations on vm create wizard
+VM_CREATE_SUGGESTED_FLAVORS = {
+    'small': {
+        'cpu': 1,
+        'ram': 1024,
+        'disk': 20,
+        'disk_template': 'drbd'
+    },
+    'medium': {
+        'cpu': 2,
+        'ram': 2048,
+        'disk': 30,
+        'disk_template': 'drbd'
+
+    },
+    'large': {
+        'cpu': 4,
+        'ram': 4096,
+        'disk': 40,
+        'disk_template': 'drbd'
+
+    }
+}
+
+# A list of metadata keys to clone from image
+# to the virtual machine on its creation.
+VM_IMAGE_COMMON_METADATA = ["OS", "loginname", "logindomain"]
+
+# A list of suggested vm roles to display to user on create wizard
+VM_CREATE_SUGGESTED_ROLES = ["Database server", "File server", "Mail server", "Web server", "Proxy"]
+
+# Template to be used for suggesting the user a default name for newly created
+# vms. {0} gets replaced by the image OS value
+VM_CREATE_NAME_TPL = "My {0} server"
+
+# Name/description metadata for the available flavor disk templates
+# Dict key is the disk_template value as stored in database
+UI_FLAVORS_DISK_TEMPLATES_INFO = {
+    'drbd': {'name': 'DRBD',
+             'description': 'DRBD storage.'},
+}
+
+#######################
+# UI BEHAVIOUR SETTINGS
+#######################
+
+# Whether to increase the time of recurrent requests (networks/vms update) if
+# window loses its focus
+UI_DELAY_ON_BLUR = False
+
+# Whether not visible vm views will update their content if vm changes
+UI_UPDATE_HIDDEN_VIEWS = False
+
+# After how many timeouts of reccurent ajax requests to display the timeout
+# error overlay
+UI_SKIP_TIMEOUTS = 1
+
+# Whether UI should display error overlay for all Javascript exceptions
+UI_HANDLE_WINDOW_EXCEPTIONS = True
+
+# A list of os names that support ssh public key assignment
+UI_SUPPORT_SSH_OS_LIST = ['debian', 'fedora', 'okeanos', 'ubuntu', 'kubuntu', 'centos']
+
+# OS/username map to identify default user name for the specified os
+UI_OS_DEFAULT_USER_MAP = {
+    'debian':'root', 'fedora': 'root', 'okeanos': 'root',
+    'ubuntu': 'root', 'kubuntu': 'root', 'centos': 'root',
+    'windows': 'Administrator'
+}
diff --git a/snf-app/synnefo/app_settings/default/userdata.py b/snf-app/synnefo/app_settings/default/userdata.py
new file mode 100644
index 0000000..ac195f9
--- /dev/null
+++ b/snf-app/synnefo/app_settings/default/userdata.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+#
+# Userdata settings
+###################
+
+# Genetared SSH key bits length
+USERDATA_SSH_KEY_LENGTH = 2048
+
+# Generated SSH key exponent
+USERDATA_SSH_KEY_EXPONENT = 65537
+
+# Maximum number of ssh keys a user is allowed to have
+USERDATA_MAX_SSH_KEYS_PER_USER = 10
diff --git a/snf-app/synnefo/manage.py b/snf-app/synnefo/manage.py
index 5f7fbb5..59587d6 100755
--- a/snf-app/synnefo/manage.py
+++ b/snf-app/synnefo/manage.py
@@ -4,7 +4,7 @@ from django.core.management import ManagementUtility, setup_environ, \
 BaseCommand, LaxOptionParser, handle_default_options
 
 from optparse import Option, make_option
-from synnefo import get_version
+from synnefo.version import get_component_version
 
 import sys
 import os
@@ -36,7 +36,7 @@ class SynnefoManagementUtility(ManagementUtility):
         # These options could affect the commands that are available, so they
         # must be processed early.
         parser = LaxOptionParser(usage="%prog subcommand [options] [args]",
-                                 version=get_version(),
+                                 version=get_component_version('synnefo.manage'),
                                  option_list=option_list)
         self.autocomplete()
         try:
diff --git a/snf-app/synnefo/settings/__init__.py b/snf-app/synnefo/settings/__init__.py
deleted file mode 100644
index 7250171..0000000
--- a/snf-app/synnefo/settings/__init__.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# 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 os
-import sys
-import glob
-
-# import common settings
-from synnefo.settings.common import *
-
-SYNNEFO_SETTINGS_DIR = os.environ.get('SYNNEFO_SETTINGS_DIR', "/etc/synnefo/")
-
-if os.path.exists(SYNNEFO_SETTINGS_DIR):
-    # extend common settings with settings set in /etc/synnefo dir
-    conffiles = glob.glob(os.path.join(SYNNEFO_SETTINGS_DIR, '*.conf'))
-    conffiles.sort()
-    for f in conffiles:
-        try:
-            execfile(os.path.abspath(f))
-        except Exception as e:
-            print >>sys.stderr, "Failed to read settings file: %s" % \
-                                os.path.abspath(f)
diff --git a/snf-app/synnefo/settings/common/__init__.py b/snf-app/synnefo/settings/common/__init__.py
deleted file mode 100644
index 190fb19..0000000
--- a/snf-app/synnefo/settings/common/__init__.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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.
-
-from synnefo.settings.common.admins import *
-from synnefo.settings.common.apps import *
-from synnefo.settings.common.deploy import *
-from synnefo.settings.common.logging import *
-from synnefo.settings.common.site import *
-from synnefo.settings.common.backend import *
-from synnefo.settings.common.database import *
-from synnefo.settings.common.queues import *
-from synnefo.settings.common.api import *
-from synnefo.settings.common.plankton import *
-from synnefo.settings.common.ui import *
-from synnefo.settings.common.userdata import *
-from synnefo.settings.common.aai import *
-from synnefo.settings.common.invitations import *
-from synnefo.settings.common.reconciliation import *
-from synnefo.settings.common.helpdesk import *
-from synnefo.settings.common.tests import *
-
diff --git a/snf-app/synnefo/settings/common/aai.py b/snf-app/synnefo/settings/common/aai.py
deleted file mode 100644
index c840da3..0000000
--- a/snf-app/synnefo/settings/common/aai.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# AAI configuration
-#####################
-
-# Unauthenticated HTTP requests to the UI get redirected to this URL
-LOGIN_URL = "/login"
-
-# Set the expiration time of newly created auth tokens
-# to be this many hours after their creation time.
-AUTH_TOKEN_DURATION = 30 * 24
-
-# Enable receiving a temporary auth token (using the ?test URL parameter) that
-# bypasses the authentication mechanism.
-#
-# Make sure there is an actual user in the db whose token matches
-# BYPASS_AUTHENTICATION_SECRET_TOKEN.
-#
-# WARNING, ACHTUNG, README, etc: DO NOT ENABLE THIS ON DEPLOYED VERSIONS!
-#
-BYPASS_AUTHENTICATION = False
-BYPASS_AUTHENTICATION_SECRET_TOKEN = '5e41595e9e884543fa048e07c1094d74'
-
-# Urls that bypass Shibboleth authentication
-AAI_SKIP_AUTH_URLS = ['/api', '/plankton', '/invitations/login']
diff --git a/snf-app/synnefo/settings/common/admins.py b/snf-app/synnefo/settings/common/admins.py
deleted file mode 100644
index de94131..0000000
--- a/snf-app/synnefo/settings/common/admins.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Admin names and email addresses
-##################################
-
-ADMINS = (
-    # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-# Email configuration
-EMAIL_HOST = "127.0.0.1"
-EMAIL_HOST_USER = ""
-EMAIL_HOST_PASSWORD = ""
-EMAIL_SUBJECT_PREFIX = "[email-subject-prefix] "
-DEFAULT_CHARSET = 'utf-8'
-
-# Address to use for outgoing emails
-DEFAULT_FROM_EMAIL = "~okeanos <no-reply@grnet.gr>"
diff --git a/snf-app/synnefo/settings/common/api.py b/snf-app/synnefo/settings/common/api.py
deleted file mode 100644
index 206c7e6..0000000
--- a/snf-app/synnefo/settings/common/api.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# API configuration
-#####################
-
-from deploy import *
-
-# The API implementation needs to accept and return absolute references
-# to its resources. Thus, it needs to know its public URL.
-API_ROOT_URL = APP_INSTALL_URL + '/api'
-
-# The API will return HTTP Bad Request if the ?changes-since
-# parameter refers to a point in time more than POLL_LIMIT seconds ago.
-POLL_LIMIT = 3600
-
-#
-# Network Configuration
-#
-
-# Synnefo assigns this link id to NICs connected on the public network.
-# An IP pool should be associated with this link by the Ganeti administrator.
-GANETI_PUBLIC_LINK = 'snf_public'
-# This link id is assigned to NICs that should be isolated from anything else
-# (e.g., right before the NIC gets deleted).
-# This value is also hardcoded in a fixture in db/fixtures/initial_data.json.
-GANETI_NULL_LINK = 'snf_null'
-
-# The pool of private network links to use is
-# $GANETI_LINK_PREFIX{1..$GANETI_MAX_LINK_NUMBER}.
-#
-# The prefix to use for private network links.
-GANETI_LINK_PREFIX = 'prv'
-# The number of private network links to use.
-GANETI_MAX_LINK_NUMBER = 100
-# Firewalling
-GANETI_FIREWALL_ENABLED_TAG = 'synnefo:network:0:protected'
-GANETI_FIREWALL_DISABLED_TAG = 'synnefo:network:0:unprotected'
-GANETI_FIREWALL_PROTECTED_TAG = 'synnefo:network:0:limited'
-
-# The default firewall profile that will be in effect if no tags are defined
-DEFAULT_FIREWALL_PROFILE = 'DISABLED'
-
-# our REST API would prefer to be explicit about trailing slashes
-APPEND_SLASH = False
-
-# Ignore disk size specified by flavor, always build the
-# machine with a 4GB (in the case of Windows: 14GB) disk.
-# This setting is helpful in development setups.
-#
-IGNORE_FLAVOR_DISK_SIZES = False
-
-# Quota
-#
-# Maximum number of VMs a user is allowed to have
-MAX_VMS_PER_USER = 3
-
-# URL templates for the stat graphs.
-# The API implementation replaces '%s' with the encrypted backend id.
-# FIXME: For now we do not encrypt the backend id.
-CPU_BAR_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/cpu-bar.png'
-CPU_TIMESERIES_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/cpu-ts.png'
-NET_BAR_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/net-bar.png'
-NET_TIMESERIES_GRAPH_URL = 'http://stats.okeanos.grnet.gr/%s/net-ts.png'
-
-# Recommended refresh period for server stats
-STATS_REFRESH_PERIOD = 60
-
-# The maximum number of file path/content pairs that can be supplied on server
-# build
-MAX_PERSONALITY = 5
-
-# The maximum size, in bytes, for each personality file
-MAX_PERSONALITY_SIZE = 10240
-
-# Available storage types to be used as disk templates
-GANETI_DISK_TEMPLATES = ('blockdev', 'diskless', 'drbd', 'file', 'plain',
-                         'rbd',  'sharedfile')
-DEFAULT_GANETI_DISK_TEMPLATE = 'drbd'
diff --git a/snf-app/synnefo/settings/common/apps.py b/snf-app/synnefo/settings/common/apps.py
deleted file mode 100644
index d9ded2d..0000000
--- a/snf-app/synnefo/settings/common/apps.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Core Django settings
-##################################
-
-INSTALLED_APPS = (
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.sites',
-    'django.contrib.messages',
-    'django.contrib.admin',
-    'synnefo.aai',
-    'synnefo.admin',
-    'synnefo.api',
-    'synnefo.ui',
-    'synnefo.db',
-    'synnefo.logic',
-    'synnefo.invitations',
-    'synnefo.helpdesk',
-    'synnefo.plankton',
-    'synnefo.ui.userdata',
-    'south'
-)
-
-SESSION_ENGINE = "django.contrib.sessions.backends.cache"
-
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
-    'django.template.loaders.filesystem.Loader',
-    'django.template.loaders.app_directories.Loader',
-#     'django.template.loaders.eggs.Loader',
-)
-
-TEMPLATE_CONTEXT_PROCESSORS = (
-    'django.core.context_processors.request',
-    'django.core.context_processors.i18n',
-    'django.contrib.auth.context_processors.auth',
-    'django.core.context_processors.media'
-)
-
-MIDDLEWARE_CLASSES = (
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'synnefo.aai.middleware.SynnefoAuthMiddleware',
-    'synnefo.api.middleware.ApiAuthMiddleware',
-    'synnefo.helpdesk.middleware.HelpdeskMiddleware',
-    'django.middleware.locale.LocaleMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware'
-)
-
-ROOT_URLCONF = 'synnefo.urls'
-
-TEMPLATE_DIRS = (
-    # Put strings here, like "/home/html/django_templates"
-    # or "C:/www/django/templates".
-    # Always use forward slashes, even on Windows.
-    # Don't forget to use absolute paths, not relative paths.
-)
-
-LANGUAGES = (
-  #('el', u'Ελληνικά'),
-  ('en', 'English'),
-)
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'UTC'   # Warning: The API depends on the TIME_ZONE being UTC
-
diff --git a/snf-app/synnefo/settings/common/backend.py b/snf-app/synnefo/settings/common/backend.py
deleted file mode 100644
index d225bb4..0000000
--- a/snf-app/synnefo/settings/common/backend.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Ganeti backend configuration
-###################################
-
-# The RAPI endpoint and associated credentials to use
-# for talking to the Ganeti backend.
-GANETI_MASTER_IP = "10.0.0.1"
-GANETI_CLUSTER_INFO = (GANETI_MASTER_IP, 5080, "username", "password")
-
-# This prefix gets used when determining the instance names
-# of Synnefo VMs at the Ganeti backend.
-# The dash must always appear in the name!
-BACKEND_PREFIX_ID = "snf-"
-
-# The following dictionary defines deployment-specific
-# arguments to the RAPI CreateInstance call.
-# At a minimum it should contain the
-# 'disk_template', 'os_provider', and 'hvparams' keys.
-#
-# More specifically:
-# a) disk_template:
-#    The disk template to use when creating the instance.
-#    Suggested values: 'plain', or 'drbd'.
-# b) os:
-#    The OS provider to use (customized Ganeti Instance Image)
-# c) hvparams:
-#    Hypervisor-specific parameters (serial_console = False, see #785)
-# d) If using the DRBD disk_template, you may want to include
-#    wait_for_sync = False (see #835).
-#
-GANETI_CREATEINSTANCE_KWARGS = {
-    'os': 'snf-image+default',
-    'hvparams': {'serial_console': False},
-    'wait_for_sync': False}
diff --git a/snf-app/synnefo/settings/common/database.py b/snf-app/synnefo/settings/common/database.py
deleted file mode 100644
index 4f43b86..0000000
--- a/snf-app/synnefo/settings/common/database.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Database settings
-####################
-
-import os
-
-DEFAULT_DB_PATH = '/usr/share/synnefo/'
-DATABASES = {
-    'default': {
-        # 'postgresql_psycopg2', 'postgresql','mysql', 'sqlite3' or 'oracle'
-        'ENGINE': 'sqlite3',
-         # ATTENTION: This *must* be the absolute path if using sqlite3.
-         # See: http://docs.djangoproject.com/en/dev/ref/settings/#name
-        'NAME': os.path.join(DEFAULT_DB_PATH, 'database.sqlite'),
-        'USER': '',                      # Not used with sqlite3.
-        'PASSWORD': '',                  # Not used with sqlite3.
-        # Set to empty string for localhost. Not used with sqlite3.
-        'HOST': '',
-        # Set to empty string for default. Not used with sqlite3.
-        'PORT': '',
-    }
-}
-
-if DATABASES['default']['ENGINE'].endswith('mysql'):
-    DATABASES['default']['OPTIONS'] = {
-            'init_command': 'SET storage_engine=INNODB; ' +
-                'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
-    }
diff --git a/snf-app/synnefo/settings/common/deploy.py b/snf-app/synnefo/settings/common/deploy.py
deleted file mode 100644
index 276cdf9..0000000
--- a/snf-app/synnefo/settings/common/deploy.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Deployment
-##################################
-
-DEBUG = True
-TEMPLATE_DEBUG = DEBUG
-
-# Top-level URL for deployment. Numerous other URLs depend on this.
-APP_INSTALL_URL = "https://host:port"
-
-# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'ly6)mw6a7x%n)-e#zzk4jo6f2=uqu!1o%)2-(7lo+f9yd^k^bg'
diff --git a/snf-app/synnefo/settings/common/helpdesk.py b/snf-app/synnefo/settings/common/helpdesk.py
deleted file mode 100644
index e4a5bec..0000000
--- a/snf-app/synnefo/settings/common/helpdesk.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Helpdesk application
-#
-
-# Duration for temporary auth tokens, created for impersonating a registered
-# user by helpdesk staff.
-HELPDESK_TOKEN_DURATION_MIN = 30
-
-# IP addresses of the machines allowed to connect as help desk
-HELPDESK_ALLOWED_IPS = ("127.0.0.1",)
-
diff --git a/snf-app/synnefo/settings/common/invitations.py b/snf-app/synnefo/settings/common/invitations.py
deleted file mode 100644
index 8585f75..0000000
--- a/snf-app/synnefo/settings/common/invitations.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Invitations settings
-##################################
-
-
-# Max number of invitations allowed per level
-INVITATIONS_PER_LEVEL = {
-   #Level  #Max Invitations
-    0   :   10000,
-    1   :   3,
-    2   :   2,
-    3   :   1,
-    4   :   0
-}
-
-# Key to encrypt X-Auth-Token with when sending invitations
-INVITATION_ENCR_KEY = '8d342f6e7a0366c632978a80257019af'
-
-# Days for which an invitation is active
-INVITATION_VALID_DAYS = 180
diff --git a/snf-app/synnefo/settings/common/logging.py b/snf-app/synnefo/settings/common/logging.py
deleted file mode 100644
index 9eb1780..0000000
--- a/snf-app/synnefo/settings/common/logging.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Logging configuration
-##################################
-
-LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': True,
-    
-    'formatters': {
-        'simple': {
-            'format': '%(message)s'
-        },
-        'verbose': {
-            'format': '%(asctime)s [%(levelname)s] %(message)s'
-        },
-        'django': {
-            'format': '[%(asctime)s] %(levelname)s %(message)s',
-            'datefmt': '%d/%b/%Y %H:%M:%S'
-        },
-    },
-    
-    'handlers': {
-        'null': {
-            'class': 'logging.NullHandler',
-        },
-        'console': {
-            'class': 'logging.StreamHandler',
-            'formatter': 'django'
-        },
-        'syslog': {
-            'class': 'logging.handlers.SysLogHandler',
-            'address': '/dev/log',
-#            'address': ('localhost', 514),
-            'facility': 'daemon',
-            'formatter': 'verbose',
-            'level': 'INFO',
-        },
-    },
-    
-    'loggers': {
-        'synnefo': {
-            'handlers': ['syslog'],
-            'level': 'INFO'
-        },
-        'synnefo.admin': {
-            'level': 'INFO',
-            'propagate': 1
-        },
-        'synnefo.api': {
-            'level': 'INFO',
-            'propagate': 1
-        },
-        'synnefo.db': {
-            'level': 'INFO',
-            'propagate': 1
-        },
-        'synnefo.logic': {
-            'level': 'INFO',
-            'propagate': 1
-        },
-    }    
-}
-
-
-DISPATCHER_LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': True,
-    
-    'formatters': {
-        'verbose': {
-            'format': '%(asctime)s [%(levelname)s] %(message)s'
-        },
-    },
-    
-    'handlers': {
-        'console': {
-            'class': 'logging.StreamHandler',
-            'formatter': 'verbose'
-        },
-        'file': {
-            'class': 'logging.handlers.WatchedFileHandler',
-            'filename': '/var/log/synnefo/dispatcher.log',
-            'formatter': 'verbose',
-            'level': 'DEBUG'
-        },
-    },
-    
-    'loggers': {
-        'synnefo': {'propagate': 1}
-    },
-    
-    'root': {
-        'handlers': ['console', 'file'],
-        'level': 'DEBUG',
-    }
-}
-
-
-SNFADMIN_LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': True,
-    
-    'formatters': {
-        'verbose': {
-            'format': '%(asctime)s [%(levelname)s] %(message)s'
-        },
-    },
-    
-    'handlers': {
-        'console': {
-            'class': 'logging.StreamHandler',
-            'formatter': 'verbose'
-        },
-    },
-    
-    'loggers': {
-        'synnefo': {'propagate': 1}
-    },
-    
-    'root': {
-        'handlers': ['console'],
-        'level': 'DEBUG',
-    }
-}
diff --git a/snf-app/synnefo/settings/common/plankton.py b/snf-app/synnefo/settings/common/plankton.py
deleted file mode 100644
index 73ce46f..0000000
--- a/snf-app/synnefo/settings/common/plankton.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Plankton configuration
-########################
-
-from os.path import join
-
-# Backend settings
-PITHOS_ROOT = '/usr/share/pithos'
-BACKEND_DB_MODULE = 'pithos.backends.lib.sqlalchemy'
-BACKEND_DB_CONNECTION = 'sqlite:///' + join(PITHOS_ROOT, 'backend.db')
-BACKEND_BLOCK_MODULE = 'pithos.backends.lib.hashfiler'
-BACKEND_BLOCK_PATH = join(PITHOS_ROOT, 'data/')
-
-# Default settings for new accounts.
-DEFAULT_QUOTA = 50 * 1024**3
-DEFAULT_VERSIONING = 'auto'
-
-# The Pithos container where all images are stored
-PITHOS_IMAGE_CONTAINER = 'images'
-
-ALLOWED_DISK_FORMATS = ('diskdump', 'dump', 'extdump', 'lvm', 'ntfsclone',
-        'ntfsdump')
-ALLOWED_CONTAINER_FORMATS = ('aki', 'ari', 'ami', 'bare', 'ovf')
-
-DEFAULT_DISK_FORMAT = 'dump'
-DEFAULT_CONTAINER_FORMAT = 'bare'
-
-# The owner of the images that will be marked as "system images" by the UI
-SYSTEM_IMAGES_OWNER = 'okeanos'
diff --git a/snf-app/synnefo/settings/common/queues.py b/snf-app/synnefo/settings/common/queues.py
deleted file mode 100644
index d9b66b7..0000000
--- a/snf-app/synnefo/settings/common/queues.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Queues, exchanges and bindings for AMQP
-###########################################
-
-# Rabbit work queue endpoint
-RABBIT_HOST = "10.0.0.1:5672"
-RABBIT_USERNAME = "username"
-RABBIT_PASSWORD = "password"
-RABBIT_VHOST = "/"
-
-EXCHANGE_GANETI = "ganeti"  # Messages from Ganeti
-EXCHANGE_CRON = "cron"      # Messages from periodically triggered tasks
-EXCHANGE_API = "api"        # Messages from the REST API
-EXCHANGES = (EXCHANGE_GANETI, EXCHANGE_CRON, EXCHANGE_API)
diff --git a/snf-app/synnefo/settings/common/reconciliation.py b/snf-app/synnefo/settings/common/reconciliation.py
deleted file mode 100644
index 4dc01c4..0000000
--- a/snf-app/synnefo/settings/common/reconciliation.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Reconciliations
-###################
-
-# Minutes between reconciliations
-RECONCILIATION_MIN = 30
-
diff --git a/snf-app/synnefo/settings/common/site.py b/snf-app/synnefo/settings/common/site.py
deleted file mode 100644
index a965b1d..0000000
--- a/snf-app/synnefo/settings/common/site.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Site-specific Django conf
-##################################
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# FIXME?
-SITE_ID = 1
-
-# If you set this to False, Django will make some optimizations so as not
-# to load the internationalization machinery.
-USE_I18N = True
-
-# If you set this to False, Django will not format dates, numbers and
-# calendars according to the current locale
-USE_L10N = True
-
-# Absolute path to the directory that holds media.
-# Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
-
-# URL that handles the media served from MEDIA_ROOT. Make sure to use a
-# trailing slash if there is a path component (optional in other cases).
-# Examples: "http://media.lawrence.com", "http://example.com/media/"
-MEDIA_URL = '/static/'
-
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
-# trailing slash.
-# Examples: "http://foo.com/media/", "/media/".
-ADMIN_MEDIA_PREFIX = '/media/'
-
-# app/directory static files map, used by link_static command to create
-# appropriate symlinks for static files
-STATIC_FILES = {
-    'synnefo.ui': '',
-    'synnefo.admin': 'admin',
-}
diff --git a/snf-app/synnefo/settings/common/tests.py b/snf-app/synnefo/settings/common/tests.py
deleted file mode 100644
index fa0c2b8..0000000
--- a/snf-app/synnefo/settings/common/tests.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Special unit test mode
-#########################
-
-# A quick-n-dirty way which sets settings.TEST
-# if we're running unit tests.
-import sys, os
-TEST = False
-if len(sys.argv) >= 2:
-    if os.path.basename(sys.argv[0]) == 'manage.py' and \
-        (sys.argv[1] == 'test' or sys.argv[1] == 'hudson'):
-            TEST = True
diff --git a/snf-app/synnefo/settings/common/ui.py b/snf-app/synnefo/settings/common/ui.py
deleted file mode 100644
index 6f46e18..0000000
--- a/snf-app/synnefo/settings/common/ui.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# UI settings
-###################
-
-from admins import *
-from site import *
-
-# base url for ui static files
-# if not set, defaults to MEDIA_URL + 'snf-<latest_ui_version>/'
-UI_MEDIA_URL = MEDIA_URL + 'snf/'
-
-# UI requests to the API layer time out after that many milliseconds
-TIMEOUT = 10 * 1000
-
-# A list of suggested server tags (server metadata keys)
-DEFAULT_KEYWORDS = ["OS", "Role", "Location", "Owner"]
-
-# A list of allowed icons for OS Images
-IMAGE_ICONS = ["redhat", "ubuntu", "debian", "windows", "gentoo", "archlinux",
-               "centos", "fedora", "freebsd", "netbsd", "openbsd", "slackware",
-               "suse", "kubuntu"]
-
-# How often should the UI request changes from the API
-UI_UPDATE_INTERVAL = 5000
-
-# Milieconds to increase the interval after UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT calls
-# of recurrent api requests
-UI_UPDATE_INTERVAL_INCREASE = UI_UPDATE_INTERVAL / 4
-UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT = 4
-
-# Maximum update interval
-UI_UPDATE_INTERVAL_MAX = UI_UPDATE_INTERVAL * 3
-
-# Fast update interval
-UI_UPDATE_INTERVAL_FAST = UI_UPDATE_INTERVAL / 2
-
-# List of emails used for sending the feedback messages to (following the ADMINS format)
-FEEDBACK_CONTACTS = (
-    # ('Contact Name', 'contact_email@domain.com'),
-)
-
-# Email from which the feedback emails will be sent from
-FEEDBACK_EMAIL_FROM = DEFAULT_FROM_EMAIL
-
-# URL to redirect user to when he logs out from the ui (if not set
-# settings.LOGIN_URL will be used)
-#LOGOUT_URL = ""
-
-# Flavor options that we provide to the user as predefined
-# cpu/ram/disk combinations on vm create wizard
-VM_CREATE_SUGGESTED_FLAVORS = {
-    'small': {
-        'cpu': 1,
-        'ram': 1024,
-        'disk': 20,
-        'disk_template': 'drbd'
-    },
-    'medium': {
-        'cpu': 2,
-        'ram': 2048,
-        'disk': 30,
-        'disk_template': 'drbd'
-
-    },
-    'large': {
-        'cpu': 4,
-        'ram': 4096,
-        'disk': 40,
-        'disk_template': 'drbd'
-
-    }
-}
-
-# A list of metadata keys to clone from image
-# to the virtual machine on its creation.
-VM_IMAGE_COMMON_METADATA = ["OS", "loginname", "logindomain"]
-
-# A list of suggested vm roles to display to user on create wizard
-VM_CREATE_SUGGESTED_ROLES = ["Database server", "File server", "Mail server", "Web server", "Proxy"]
-
-# Template to be used for suggesting the user a default name for newly created
-# vms. {0} gets replaced by the image OS value
-VM_CREATE_NAME_TPL = "My {0} server"
-
-# Name/description metadata for the available flavor disk templates
-# Dict key is the disk_template value as stored in database
-UI_FLAVORS_DISK_TEMPLATES_INFO = {
-    'drbd': {'name': 'DRBD',
-             'description': 'DRBD storage.'},
-}
-
-#######################
-# UI BEHAVIOUR SETTINGS
-#######################
-
-# Whether to increase the time of recurrent requests (networks/vms update) if
-# window loses its focus
-UI_DELAY_ON_BLUR = False
-
-# Whether not visible vm views will update their content if vm changes
-UI_UPDATE_HIDDEN_VIEWS = False
-
-# After how many timeouts of reccurent ajax requests to display the timeout
-# error overlay
-UI_SKIP_TIMEOUTS = 1
-
-# Whether UI should display error overlay for all Javascript exceptions
-UI_HANDLE_WINDOW_EXCEPTIONS = True
-
-# A list of os names that support ssh public key assignment
-UI_SUPPORT_SSH_OS_LIST = ['debian', 'fedora', 'okeanos', 'ubuntu', 'kubuntu', 'centos']
-
-# OS/username map to identify default user name for the specified os
-UI_OS_DEFAULT_USER_MAP = {
-    'debian':'root', 'fedora': 'root', 'okeanos': 'root',
-    'ubuntu': 'root', 'kubuntu': 'root', 'centos': 'root',
-    'windows': 'Administrator'
-}
diff --git a/snf-app/synnefo/settings/common/userdata.py b/snf-app/synnefo/settings/common/userdata.py
deleted file mode 100644
index ac195f9..0000000
--- a/snf-app/synnefo/settings/common/userdata.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Userdata settings
-###################
-
-# Genetared SSH key bits length
-USERDATA_SSH_KEY_LENGTH = 2048
-
-# Generated SSH key exponent
-USERDATA_SSH_KEY_EXPONENT = 65537
-
-# Maximum number of ssh keys a user is allowed to have
-USERDATA_MAX_SSH_KEYS_PER_USER = 10
diff --git a/snf-app/synnefo/settings/test.py b/snf-app/synnefo/settings/test.py
deleted file mode 100644
index 25b969a..0000000
--- a/snf-app/synnefo/settings/test.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from synnefo.settings import *
-
-DEBUG = True
-TEST = True
-
-DATABASES = {
-    'default': {
-        'ENGINE': 'sqlite3',
-        'NAME': '/tmp/synnefo_test_db.sqlite',
-    }
-}
-
-LOGGING['handlers']['console']['level'] = 'WARNING'
diff --git a/snf-app/synnefo/ui/views.py b/snf-app/synnefo/ui/views.py
index 27714c2..2955434 100644
--- a/snf-app/synnefo/ui/views.py
+++ b/snf-app/synnefo/ui/views.py
@@ -46,7 +46,7 @@ from django.core.urlresolvers import reverse
 from django.core.mail import send_mail
 from django.http import Http404
 
-from synnefo import get_version
+from synnefo.version import get_version
 
 SYNNEFO_JS_LIB_VERSION = get_version()
 IMAGE_ICONS = settings.IMAGE_ICONS
diff --git a/snf-app/synnefo/util/__init__.py b/snf-app/synnefo/util/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/snf-app/synnefo/util/dictconfig.py b/snf-app/synnefo/util/dictconfig.py
deleted file mode 100644
index 167831b..0000000
--- a/snf-app/synnefo/util/dictconfig.py
+++ /dev/null
@@ -1,553 +0,0 @@
-# This is a copy of the Python logging.config.dictconfig module.
-# It is provided here for backwards compatibility for Python versions
-# prior to 2.7.
-#
-# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved.
-#
-# Permission to use, copy, modify, and distribute this software and its
-# documentation for any purpose and without fee is hereby granted,
-# provided that the above copyright notice appear in all copies and that
-# both that copyright notice and this permission notice appear in
-# supporting documentation, and that the name of Vinay Sajip
-# not be used in advertising or publicity pertaining to distribution
-# of the software without specific, written prior permission.
-# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
-# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
-# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
-# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
-# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-import logging.handlers
-import re
-import sys
-import types
-
-IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
-
-def valid_ident(s):
-    m = IDENTIFIER.match(s)
-    if not m:
-        raise ValueError('Not a valid Python identifier: %r' % s)
-    return True
-
-#
-# This function is defined in logging only in recent versions of Python
-#
-try:
-    from logging import _checkLevel
-except ImportError:
-    def _checkLevel(level):
-        if isinstance(level, int):
-            rv = level
-        elif str(level) == level:
-            if level not in logging._levelNames:
-                raise ValueError('Unknown level: %r' % level)
-            rv = logging._levelNames[level]
-        else:
-            raise TypeError('Level not an integer or a '
-                            'valid string: %r' % level)
-        return rv
-
-# The ConvertingXXX classes are wrappers around standard Python containers,
-# and they serve to convert any suitable values in the container. The
-# conversion converts base dicts, lists and tuples to their wrapped
-# equivalents, whereas strings which match a conversion format are converted
-# appropriately.
-#
-# Each wrapper should have a configurator attribute holding the actual
-# configurator to use for conversion.
-
-class ConvertingDict(dict):
-    """A converting dictionary wrapper."""
-
-    def __getitem__(self, key):
-        value = dict.__getitem__(self, key)
-        result = self.configurator.convert(value)
-        #If the converted value is different, save for next time
-        if value is not result:
-            self[key] = result
-            if type(result) in (ConvertingDict, ConvertingList,
-                                ConvertingTuple):
-                result.parent = self
-                result.key = key
-        return result
-
-    def get(self, key, default=None):
-        value = dict.get(self, key, default)
-        result = self.configurator.convert(value)
-        #If the converted value is different, save for next time
-        if value is not result:
-            self[key] = result
-            if type(result) in (ConvertingDict, ConvertingList,
-                                ConvertingTuple):
-                result.parent = self
-                result.key = key
-        return result
-
-    def pop(self, key, default=None):
-        value = dict.pop(self, key, default)
-        result = self.configurator.convert(value)
-        if value is not result:
-            if type(result) in (ConvertingDict, ConvertingList,
-                                ConvertingTuple):
-                result.parent = self
-                result.key = key
-        return result
-
-class ConvertingList(list):
-    """A converting list wrapper."""
-    def __getitem__(self, key):
-        value = list.__getitem__(self, key)
-        result = self.configurator.convert(value)
-        #If the converted value is different, save for next time
-        if value is not result:
-            self[key] = result
-            if type(result) in (ConvertingDict, ConvertingList,
-                                ConvertingTuple):
-                result.parent = self
-                result.key = key
-        return result
-
-    def pop(self, idx=-1):
-        value = list.pop(self, idx)
-        result = self.configurator.convert(value)
-        if value is not result:
-            if type(result) in (ConvertingDict, ConvertingList,
-                                ConvertingTuple):
-                result.parent = self
-        return result
-
-class ConvertingTuple(tuple):
-    """A converting tuple wrapper."""
-    def __getitem__(self, key):
-        value = tuple.__getitem__(self, key)
-        result = self.configurator.convert(value)
-        if value is not result:
-            if type(result) in (ConvertingDict, ConvertingList,
-                                ConvertingTuple):
-                result.parent = self
-                result.key = key
-        return result
-
-class BaseConfigurator(object):
-    """
-    The configurator base class which defines some useful defaults.
-    """
-
-    CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
-
-    WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
-    DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
-    INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
-    DIGIT_PATTERN = re.compile(r'^\d+$')
-
-    value_converters = {
-        'ext' : 'ext_convert',
-        'cfg' : 'cfg_convert',
-    }
-
-    # We might want to use a different one, e.g. importlib
-    importer = __import__
-
-    def __init__(self, config):
-        self.config = ConvertingDict(config)
-        self.config.configurator = self
-
-    def resolve(self, s):
-        """
-        Resolve strings to objects using standard import and attribute
-        syntax.
-        """
-        name = s.split('.')
-        used = name.pop(0)
-        try:
-            found = self.importer(used)
-            for frag in name:
-                used += '.' + frag
-                try:
-                    found = getattr(found, frag)
-                except AttributeError:
-                    self.importer(used)
-                    found = getattr(found, frag)
-            return found
-        except ImportError:
-            e, tb = sys.exc_info()[1:]
-            v = ValueError('Cannot resolve %r: %s' % (s, e))
-            v.__cause__, v.__traceback__ = e, tb
-            raise v
-
-    def ext_convert(self, value):
-        """Default converter for the ext:// protocol."""
-        return self.resolve(value)
-
-    def cfg_convert(self, value):
-        """Default converter for the cfg:// protocol."""
-        rest = value
-        m = self.WORD_PATTERN.match(rest)
-        if m is None:
-            raise ValueError("Unable to convert %r" % value)
-        else:
-            rest = rest[m.end():]
-            d = self.config[m.groups()[0]]
-            #print d, rest
-            while rest:
-                m = self.DOT_PATTERN.match(rest)
-                if m:
-                    d = d[m.groups()[0]]
-                else:
-                    m = self.INDEX_PATTERN.match(rest)
-                    if m:
-                        idx = m.groups()[0]
-                        if not self.DIGIT_PATTERN.match(idx):
-                            d = d[idx]
-                        else:
-                            try:
-                                n = int(idx) # try as number first (most likely)
-                                d = d[n]
-                            except TypeError:
-                                d = d[idx]
-                if m:
-                    rest = rest[m.end():]
-                else:
-                    raise ValueError('Unable to convert '
-                                     '%r at %r' % (value, rest))
-        #rest should be empty
-        return d
-
-    def convert(self, value):
-        """
-        Convert values to an appropriate type. dicts, lists and tuples are
-        replaced by their converting alternatives. Strings are checked to
-        see if they have a conversion format and are converted if they do.
-        """
-        if not isinstance(value, ConvertingDict) and isinstance(value, dict):
-            value = ConvertingDict(value)
-            value.configurator = self
-        elif not isinstance(value, ConvertingList) and isinstance(value, list):
-            value = ConvertingList(value)
-            value.configurator = self
-        elif not isinstance(value, ConvertingTuple) and\
-                 isinstance(value, tuple):
-            value = ConvertingTuple(value)
-            value.configurator = self
-        elif isinstance(value, basestring): # str for py3k
-            m = self.CONVERT_PATTERN.match(value)
-            if m:
-                d = m.groupdict()
-                prefix = d['prefix']
-                converter = self.value_converters.get(prefix, None)
-                if converter:
-                    suffix = d['suffix']
-                    converter = getattr(self, converter)
-                    value = converter(suffix)
-        return value
-
-    def configure_custom(self, config):
-        """Configure an object with a user-supplied factory."""
-        c = config.pop('()')
-        if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
-            c = self.resolve(c)
-        props = config.pop('.', None)
-        # Check for valid identifiers
-        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
-        result = c(**kwargs)
-        if props:
-            for name, value in props.items():
-                setattr(result, name, value)
-        return result
-
-    def as_tuple(self, value):
-        """Utility function which converts lists to tuples."""
-        if isinstance(value, list):
-            value = tuple(value)
-        return value
-
-class DictConfigurator(BaseConfigurator):
-    """
-    Configure logging using a dictionary-like object to describe the
-    configuration.
-    """
-
-    def configure(self):
-        """Do the configuration."""
-
-        config = self.config
-        if 'version' not in config:
-            raise ValueError("dictionary doesn't specify a version")
-        if config['version'] != 1:
-            raise ValueError("Unsupported version: %s" % config['version'])
-        incremental = config.pop('incremental', False)
-        EMPTY_DICT = {}
-        logging._acquireLock()
-        try:
-            if incremental:
-                handlers = config.get('handlers', EMPTY_DICT)
-                # incremental handler config only if handler name
-                # ties in to logging._handlers (Python 2.7)
-                if sys.version_info[:2] == (2, 7):
-                    for name in handlers:
-                        if name not in logging._handlers:
-                            raise ValueError('No handler found with '
-                                             'name %r'  % name)
-                        else:
-                            try:
-                                handler = logging._handlers[name]
-                                handler_config = handlers[name]
-                                level = handler_config.get('level', None)
-                                if level:
-                                    handler.setLevel(_checkLevel(level))
-                            except StandardError, e:
-                                raise ValueError('Unable to configure handler '
-                                                 '%r: %s' % (name, e))
-                loggers = config.get('loggers', EMPTY_DICT)
-                for name in loggers:
-                    try:
-                        self.configure_logger(name, loggers[name], True)
-                    except StandardError, e:
-                        raise ValueError('Unable to configure logger '
-                                         '%r: %s' % (name, e))
-                root = config.get('root', None)
-                if root:
-                    try:
-                        self.configure_root(root, True)
-                    except StandardError, e:
-                        raise ValueError('Unable to configure root '
-                                         'logger: %s' % e)
-            else:
-                disable_existing = config.pop('disable_existing_loggers', True)
-
-                logging._handlers.clear()
-                del logging._handlerList[:]
-
-                # Do formatters first - they don't refer to anything else
-                formatters = config.get('formatters', EMPTY_DICT)
-                for name in formatters:
-                    try:
-                        formatters[name] = self.configure_formatter(
-                                                            formatters[name])
-                    except StandardError, e:
-                        raise ValueError('Unable to configure '
-                                         'formatter %r: %s' % (name, e))
-                # Next, do filters - they don't refer to anything else, either
-                filters = config.get('filters', EMPTY_DICT)
-                for name in filters:
-                    try:
-                        filters[name] = self.configure_filter(filters[name])
-                    except StandardError, e:
-                        raise ValueError('Unable to configure '
-                                         'filter %r: %s' % (name, e))
-
-                # Next, do handlers - they refer to formatters and filters
-                # As handlers can refer to other handlers, sort the keys
-                # to allow a deterministic order of configuration
-                handlers = config.get('handlers', EMPTY_DICT)
-                for name in sorted(handlers):
-                    try:
-                        handler = self.configure_handler(handlers[name])
-                        handler.name = name
-                        handlers[name] = handler
-                    except StandardError, e:
-                        raise ValueError('Unable to configure handler '
-                                         '%r: %s' % (name, e))
-                # Next, do loggers - they refer to handlers and filters
-
-                #we don't want to lose the existing loggers,
-                #since other threads may have pointers to them.
-                #existing is set to contain all existing loggers,
-                #and as we go through the new configuration we
-                #remove any which are configured. At the end,
-                #what's left in existing is the set of loggers
-                #which were in the previous configuration but
-                #which are not in the new configuration.
-                root = logging.root
-                existing = root.manager.loggerDict.keys()
-                #The list needs to be sorted so that we can
-                #avoid disabling child loggers of explicitly
-                #named loggers. With a sorted list it is easier
-                #to find the child loggers.
-                existing.sort()
-                #We'll keep the list of existing loggers
-                #which are children of named loggers here...
-                child_loggers = []
-                #now set up the new ones...
-                loggers = config.get('loggers', EMPTY_DICT)
-                for name in loggers:
-                    if name in existing:
-                        i = existing.index(name)
-                        prefixed = name + "."
-                        pflen = len(prefixed)
-                        num_existing = len(existing)
-                        i = i + 1 # look at the entry after name
-                        while (i < num_existing) and\
-                              (existing[i][:pflen] == prefixed):
-                            child_loggers.append(existing[i])
-                            i = i + 1
-                        existing.remove(name)
-                    try:
-                        self.configure_logger(name, loggers[name])
-                    except StandardError, e:
-                        raise ValueError('Unable to configure logger '
-                                         '%r: %s' % (name, e))
-
-                #Disable any old loggers. There's no point deleting
-                #them as other threads may continue to hold references
-                #and by disabling them, you stop them doing any logging.
-                #However, don't disable children of named loggers, as that's
-                #probably not what was intended by the user.
-                for log in existing:
-                    logger = root.manager.loggerDict[log]
-                    if log in child_loggers:
-                        logger.level = logging.NOTSET
-                        logger.handlers = []
-                        logger.propagate = True
-                    elif disable_existing:
-                        logger.disabled = True
-
-                # And finally, do the root logger
-                root = config.get('root', None)
-                if root:
-                    try:
-                        self.configure_root(root)
-                    except StandardError, e:
-                        raise ValueError('Unable to configure root '
-                                         'logger: %s' % e)
-        finally:
-            logging._releaseLock()
-
-    def configure_formatter(self, config):
-        """Configure a formatter from a dictionary."""
-        if '()' in config:
-            factory = config['()'] # for use in exception handler
-            try:
-                result = self.configure_custom(config)
-            except TypeError, te:
-                if "'format'" not in str(te):
-                    raise
-                #Name of parameter changed from fmt to format.
-                #Retry with old name.
-                #This is so that code can be used with older Python versions
-                #(e.g. by Django)
-                config['fmt'] = config.pop('format')
-                config['()'] = factory
-                result = self.configure_custom(config)
-        else:
-            fmt = config.get('format', None)
-            dfmt = config.get('datefmt', None)
-            result = logging.Formatter(fmt, dfmt)
-        return result
-
-    def configure_filter(self, config):
-        """Configure a filter from a dictionary."""
-        if '()' in config:
-            result = self.configure_custom(config)
-        else:
-            name = config.get('name', '')
-            result = logging.Filter(name)
-        return result
-
-    def add_filters(self, filterer, filters):
-        """Add filters to a filterer from a list of names."""
-        for f in filters:
-            try:
-                filterer.addFilter(self.config['filters'][f])
-            except StandardError, e:
-                raise ValueError('Unable to add filter %r: %s' % (f, e))
-
-    def configure_handler(self, config):
-        """Configure a handler from a dictionary."""
-        formatter = config.pop('formatter', None)
-        if formatter:
-            try:
-                formatter = self.config['formatters'][formatter]
-            except StandardError, e:
-                raise ValueError('Unable to set formatter '
-                                 '%r: %s' % (formatter, e))
-        level = config.pop('level', None)
-        filters = config.pop('filters', None)
-        if '()' in config:
-            c = config.pop('()')
-            if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
-                c = self.resolve(c)
-            factory = c
-        else:
-            klass = self.resolve(config.pop('class'))
-            #Special case for handler which refers to another handler
-            if issubclass(klass, logging.handlers.MemoryHandler) and\
-                'target' in config:
-                try:
-                    config['target'] = self.config['handlers'][config['target']]
-                except StandardError, e:
-                    raise ValueError('Unable to set target handler '
-                                     '%r: %s' % (config['target'], e))
-            elif issubclass(klass, logging.handlers.SMTPHandler) and\
-                'mailhost' in config:
-                config['mailhost'] = self.as_tuple(config['mailhost'])
-            elif issubclass(klass, logging.handlers.SysLogHandler) and\
-                'address' in config:
-                config['address'] = self.as_tuple(config['address'])
-            factory = klass
-        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
-        try:
-            result = factory(**kwargs)
-        except TypeError, te:
-            if "'stream'" not in str(te):
-                raise
-            #The argument name changed from strm to stream
-            #Retry with old name.
-            #This is so that code can be used with older Python versions
-            #(e.g. by Django)
-            kwargs['strm'] = kwargs.pop('stream')
-            result = factory(**kwargs)
-        if formatter:
-            result.setFormatter(formatter)
-        if level is not None:
-            result.setLevel(_checkLevel(level))
-        if filters:
-            self.add_filters(result, filters)
-        return result
-
-    def add_handlers(self, logger, handlers):
-        """Add handlers to a logger from a list of names."""
-        for h in handlers:
-            try:
-                logger.addHandler(self.config['handlers'][h])
-            except StandardError, e:
-                raise ValueError('Unable to add handler %r: %s' % (h, e))
-
-    def common_logger_config(self, logger, config, incremental=False):
-        """
-        Perform configuration which is common to root and non-root loggers.
-        """
-        level = config.get('level', None)
-        if level is not None:
-            logger.setLevel(_checkLevel(level))
-        if not incremental:
-            #Remove any existing handlers
-            for h in logger.handlers[:]:
-                logger.removeHandler(h)
-            handlers = config.get('handlers', None)
-            if handlers:
-                self.add_handlers(logger, handlers)
-            filters = config.get('filters', None)
-            if filters:
-                self.add_filters(logger, filters)
-
-    def configure_logger(self, name, config, incremental=False):
-        """Configure a non-root logger from a dictionary."""
-        logger = logging.getLogger(name)
-        self.common_logger_config(logger, config, incremental)
-        propagate = config.get('propagate', None)
-        if propagate is not None:
-            logger.propagate = propagate
-
-    def configure_root(self, config, incremental=False):
-        """Configure a root logger from a dictionary."""
-        root = logging.getLogger()
-        self.common_logger_config(root, config, incremental)
-
-dictConfigClass = DictConfigurator
-
-def dictConfig(config):
-    """Configure logging using a dictionary."""
-    dictConfigClass(config).configure()
diff --git a/snf-app/synnefo/util/log.py b/snf-app/synnefo/util/log.py
deleted file mode 100644
index 69903dc..0000000
--- a/snf-app/synnefo/util/log.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# 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 logging
-
-from django.conf import settings
-
-from synnefo.util.dictconfig import dictConfig
-
-
-class NullHandler(logging.Handler):
-    def emit(self, record):
-        pass
-
-logging.NullHandler = NullHandler
-
-
-def disable_unused_loggers():
-    """Disable handlers that are not used by any logger"""
-    
-    logging = settings.LOGGING
-    
-    active_handlers = set()
-    loggers = logging.get('loggers', {})
-    for logger in loggers.values():
-        active_handlers.update(logger.get('handlers', []))
-    
-    handlers = logging.get('handlers', {})
-    for handler in handlers:
-        if handler not in active_handlers:
-            handlers[handler] = {'class': 'logging.NullHandler'}
-
-
-disable_unused_loggers()
-dictConfig(settings.LOGGING)
-
-
-def getLogger(name):
-    return logging.getLogger(name)
diff --git a/snf-app/synnefo/util/mac2eui64.py b/snf-app/synnefo/util/mac2eui64.py
deleted file mode 100755
index 7988575..0000000
--- a/snf-app/synnefo/util/mac2eui64.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# 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 sys
-from IPy import IP
-
-# Adapted from NFDHCPD's mac2eui64 utility, repository commit:feca7bb95
-
-def mac2eui64(mac, prefixstr):
-    try:
-        prefix = IP(prefixstr)
-    except ValueError:
-        raise Exception("Invalid IPv6 prefix '%s'" % prefixstr)
-
-    if prefix.version() != 6:
-        raise Exception("%s is not a valid IPv6 prefix" % prefixstr)
-
-    if prefix.prefixlen() != 64:
-        raise Exception("Cannot generate an EUI-64 address on a non-64 subnet")
-
-    mac_parts = mac.split(":")
-    pfx_parts = prefix.net().strFullsize().split(":")
-
-    if len(mac_parts) != 6:
-        raise Exception("%s is not a valid MAC-48 address" % mac)
-
-    eui64 = mac_parts[:3] + [ "ff", "fe" ] + mac_parts[3:]
-
-    eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
-
-    ip = ":".join(pfx_parts[:4])
-    for l in range(0, len(eui64), 2):
-        ip += ":%s" % "".join(eui64[l:l+2])
-
-    return IP(ip).strCompressed()
-
-# vim: set ts=4 sts=4 sw=4 et ai :
diff --git a/snf-app/synnefo/util/rapi.py b/snf-app/synnefo/util/rapi.py
deleted file mode 100644
index 5d168ec..0000000
--- a/snf-app/synnefo/util/rapi.py
+++ /dev/null
@@ -1,1618 +0,0 @@
-#
-#
-
-# Copyright (C) 2010 Google Inc.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA.
-
-
-"""Ganeti RAPI client.
-
-@attention: To use the RAPI client, the application B{must} call
-            C{pycurl.global_init} during initialization and
-            C{pycurl.global_cleanup} before exiting the process. This is very
-            important in multi-threaded programs. See curl_global_init(3) and
-            curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
-            can be used.
-
-"""
-
-# No Ganeti-specific modules should be imported. The RAPI client is supposed to
-# be standalone.
-
-import logging
-import socket
-import urllib
-import threading
-import pycurl
-
-try:
-  import simplejson as json
-except ImportError:
-  import json
-
-try:
-  from cStringIO import StringIO
-except ImportError:
-  from StringIO import StringIO
-
-
-GANETI_RAPI_PORT = 5080
-GANETI_RAPI_VERSION = 2
-
-HTTP_DELETE = "DELETE"
-HTTP_GET = "GET"
-HTTP_PUT = "PUT"
-HTTP_POST = "POST"
-HTTP_OK = 200
-HTTP_NOT_FOUND = 404
-HTTP_APP_JSON = "application/json"
-
-REPLACE_DISK_PRI = "replace_on_primary"
-REPLACE_DISK_SECONDARY = "replace_on_secondary"
-REPLACE_DISK_CHG = "replace_new_secondary"
-REPLACE_DISK_AUTO = "replace_auto"
-
-NODE_ROLE_DRAINED = "drained"
-NODE_ROLE_MASTER_CANDIATE = "master-candidate"
-NODE_ROLE_MASTER = "master"
-NODE_ROLE_OFFLINE = "offline"
-NODE_ROLE_REGULAR = "regular"
-
-# Internal constants
-_REQ_DATA_VERSION_FIELD = "__version__"
-_INST_CREATE_REQV1 = "instance-create-reqv1"
-_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
-_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
-_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
-_INST_CREATE_V0_PARAMS = frozenset([
-  "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
-  "hypervisor", "file_storage_dir", "file_driver", "dry_run",
-  ])
-_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
-
-# Older pycURL versions don't have all error constants
-try:
-  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
-  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
-except AttributeError:
-  _CURLE_SSL_CACERT = 60
-  _CURLE_SSL_CACERT_BADFILE = 77
-
-_CURL_SSL_CERT_ERRORS = frozenset([
-  _CURLE_SSL_CACERT,
-  _CURLE_SSL_CACERT_BADFILE,
-  ])
-
-
-class Error(Exception):
-  """Base error class for this module.
-
-  """
-  pass
-
-
-class CertificateError(Error):
-  """Raised when a problem is found with the SSL certificate.
-
-  """
-  pass
-
-
-class GanetiApiError(Error):
-  """Generic error raised from Ganeti API.
-
-  """
-  def __init__(self, msg, code=None):
-    Error.__init__(self, msg)
-    self.code = code
-
-
-def UsesRapiClient(fn):
-  """Decorator for code using RAPI client to initialize pycURL.
-
-  """
-  def wrapper(*args, **kwargs):
-    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
-    # one thread running. This check is just a safety measure -- it doesn't
-    # cover all cases.
-    assert threading.activeCount() == 1, \
-           "Found active threads when initializing pycURL"
-
-    pycurl.global_init(pycurl.GLOBAL_ALL)
-    try:
-      return fn(*args, **kwargs)
-    finally:
-      pycurl.global_cleanup()
-
-  return wrapper
-
-
-def GenericCurlConfig(verbose=False, use_signal=False,
-                      use_curl_cabundle=False, cafile=None, capath=None,
-                      proxy=None, verify_hostname=False,
-                      connect_timeout=None, timeout=None,
-                      _pycurl_version_fn=pycurl.version_info):
-  """Curl configuration function generator.
-
-  @type verbose: bool
-  @param verbose: Whether to set cURL to verbose mode
-  @type use_signal: bool
-  @param use_signal: Whether to allow cURL to use signals
-  @type use_curl_cabundle: bool
-  @param use_curl_cabundle: Whether to use cURL's default CA bundle
-  @type cafile: string
-  @param cafile: In which file we can find the certificates
-  @type capath: string
-  @param capath: In which directory we can find the certificates
-  @type proxy: string
-  @param proxy: Proxy to use, None for default behaviour and empty string for
-                disabling proxies (see curl_easy_setopt(3))
-  @type verify_hostname: bool
-  @param verify_hostname: Whether to verify the remote peer certificate's
-                          commonName
-  @type connect_timeout: number
-  @param connect_timeout: Timeout for establishing connection in seconds
-  @type timeout: number
-  @param timeout: Timeout for complete transfer in seconds (see
-                  curl_easy_setopt(3)).
-
-  """
-  if use_curl_cabundle and (cafile or capath):
-    raise Error("Can not use default CA bundle when CA file or path is set")
-
-  def _ConfigCurl(curl, logger):
-    """Configures a cURL object
-
-    @type curl: pycurl.Curl
-    @param curl: cURL object
-
-    """
-    logger.debug("Using cURL version %s", pycurl.version)
-
-    # pycurl.version_info returns a tuple with information about the used
-    # version of libcurl. Item 5 is the SSL library linked to it.
-    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
-    # 0, '1.2.3.3', ...)
-    sslver = _pycurl_version_fn()[5]
-    if not sslver:
-      raise Error("No SSL support in cURL")
-
-    lcsslver = sslver.lower()
-    if lcsslver.startswith("openssl/"):
-      pass
-    elif lcsslver.startswith("gnutls/"):
-      if capath:
-        raise Error("cURL linked against GnuTLS has no support for a"
-                    " CA path (%s)" % (pycurl.version, ))
-    else:
-      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
-                                sslver)
-
-    curl.setopt(pycurl.VERBOSE, verbose)
-    curl.setopt(pycurl.NOSIGNAL, not use_signal)
-
-    # Whether to verify remote peer's CN
-    if verify_hostname:
-      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
-      # certificate must indicate that the server is the server to which you
-      # meant to connect, or the connection fails. [...] When the value is 1,
-      # the certificate must contain a Common Name field, but it doesn't matter
-      # what name it says. [...]"
-      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
-    else:
-      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
-
-    if cafile or capath or use_curl_cabundle:
-      # Require certificates to be checked
-      curl.setopt(pycurl.SSL_VERIFYPEER, True)
-      if cafile:
-        curl.setopt(pycurl.CAINFO, str(cafile))
-      if capath:
-        curl.setopt(pycurl.CAPATH, str(capath))
-      # Not changing anything for using default CA bundle
-    else:
-      # Disable SSL certificate verification
-      curl.setopt(pycurl.SSL_VERIFYPEER, False)
-
-    if proxy is not None:
-      curl.setopt(pycurl.PROXY, str(proxy))
-
-    # Timeouts
-    if connect_timeout is not None:
-      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
-    if timeout is not None:
-      curl.setopt(pycurl.TIMEOUT, timeout)
-
-  return _ConfigCurl
-
-
-class GanetiRapiClient(object): # pylint: disable-msg=R0904
-  """Ganeti RAPI client.
-
-  """
-  USER_AGENT = "Ganeti RAPI Client"
-  _json_encoder = json.JSONEncoder(sort_keys=True)
-
-  def __init__(self, host, port=GANETI_RAPI_PORT,
-               username=None, password=None,
-               logger=logging.getLogger('synnefo.util'),
-               curl_config_fn=None, curl_factory=None):
-    """Initializes this class.
-
-    @type host: string
-    @param host: the ganeti cluster master to interact with
-    @type port: int
-    @param port: the port on which the RAPI is running (default is 5080)
-    @type username: string
-    @param username: the username to connect with
-    @type password: string
-    @param password: the password to connect with
-    @type curl_config_fn: callable
-    @param curl_config_fn: Function to configure C{pycurl.Curl} object
-    @param logger: Logging object
-
-    """
-    self._username = username
-    self._password = password
-    self._logger = logger
-    self._curl_config_fn = curl_config_fn
-    self._curl_factory = curl_factory
-
-    try:
-      socket.inet_pton(socket.AF_INET6, host)
-      address = "[%s]:%s" % (host, port)
-    except socket.error:
-      address = "%s:%s" % (host, port)
-
-    self._base_url = "https://%s" % address
-
-    if username is not None:
-      if password is None:
-        raise Error("Password not specified")
-    elif password:
-      raise Error("Specified password without username")
-
-  def _CreateCurl(self):
-    """Creates a cURL object.
-
-    """
-    # Create pycURL object if no factory is provided
-    if self._curl_factory:
-      curl = self._curl_factory()
-    else:
-      curl = pycurl.Curl()
-
-    # Default cURL settings
-    curl.setopt(pycurl.VERBOSE, False)
-    curl.setopt(pycurl.FOLLOWLOCATION, False)
-    curl.setopt(pycurl.MAXREDIRS, 5)
-    curl.setopt(pycurl.NOSIGNAL, True)
-    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
-    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
-    curl.setopt(pycurl.SSL_VERIFYPEER, False)
-    curl.setopt(pycurl.HTTPHEADER, [
-      "Accept: %s" % HTTP_APP_JSON,
-      "Content-type: %s" % HTTP_APP_JSON,
-      ])
-
-    assert ((self._username is None and self._password is None) ^
-            (self._username is not None and self._password is not None))
-
-    if self._username:
-      # Setup authentication
-      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
-      curl.setopt(pycurl.USERPWD,
-                  str("%s:%s" % (self._username, self._password)))
-
-    # Call external configuration function
-    if self._curl_config_fn:
-      self._curl_config_fn(curl, self._logger)
-
-    return curl
-
-  @staticmethod
-  def _EncodeQuery(query):
-    """Encode query values for RAPI URL.
-
-    @type query: list of two-tuples
-    @param query: Query arguments
-    @rtype: list
-    @return: Query list with encoded values
-
-    """
-    result = []
-
-    for name, value in query:
-      if value is None:
-        result.append((name, ""))
-
-      elif isinstance(value, bool):
-        # Boolean values must be encoded as 0 or 1
-        result.append((name, int(value)))
-
-      elif isinstance(value, (list, tuple, dict)):
-        raise ValueError("Invalid query data type %r" % type(value).__name__)
-
-      else:
-        result.append((name, value))
-
-    return result
-
-  def _SendRequest(self, method, path, query, content):
-    """Sends an HTTP request.
-
-    This constructs a full URL, encodes and decodes HTTP bodies, and
-    handles invalid responses in a pythonic way.
-
-    @type method: string
-    @param method: HTTP method to use
-    @type path: string
-    @param path: HTTP URL path
-    @type query: list of two-tuples
-    @param query: query arguments to pass to urllib.urlencode
-    @type content: str or None
-    @param content: HTTP body content
-
-    @rtype: str
-    @return: JSON-Decoded response
-
-    @raises CertificateError: If an invalid SSL certificate is found
-    @raises GanetiApiError: If an invalid response is returned
-
-    """
-    assert path.startswith("/")
-
-    curl = self._CreateCurl()
-
-    if content is not None:
-      encoded_content = self._json_encoder.encode(content)
-    else:
-      encoded_content = ""
-
-    # Build URL
-    urlparts = [self._base_url, path]
-    if query:
-      urlparts.append("?")
-      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
-
-    url = "".join(urlparts)
-
-    self._logger.debug("Sending request %s %s (content=%r)",
-                       method, url, encoded_content)
-
-    # Buffer for response
-    encoded_resp_body = StringIO()
-
-    # Configure cURL
-    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
-    curl.setopt(pycurl.URL, str(url))
-    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
-    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
-
-    try:
-      # Send request and wait for response
-      try:
-        curl.perform()
-      except pycurl.error, err:
-        if err.args[0] in _CURL_SSL_CERT_ERRORS:
-          raise CertificateError("SSL certificate error %s" % err)
-
-        raise GanetiApiError(str(err))
-    finally:
-      # Reset settings to not keep references to large objects in memory
-      # between requests
-      curl.setopt(pycurl.POSTFIELDS, "")
-      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
-
-    # Get HTTP response code
-    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
-
-    # Was anything written to the response buffer?
-    if encoded_resp_body.tell():
-      response_content = json.loads(encoded_resp_body.getvalue())
-    else:
-      response_content = None
-
-    if http_code != HTTP_OK:
-      if isinstance(response_content, dict):
-        msg = ("%s %s: %s" %
-               (response_content["code"],
-                response_content["message"],
-                response_content["explain"]))
-      else:
-        msg = str(response_content)
-
-      raise GanetiApiError(msg, code=http_code)
-
-    return response_content
-
-  def GetVersion(self):
-    """Gets the Remote API version running on the cluster.
-
-    @rtype: int
-    @return: Ganeti Remote API version
-
-    """
-    return self._SendRequest(HTTP_GET, "/version", None, None)
-
-  def GetFeatures(self):
-    """Gets the list of optional features supported by RAPI server.
-
-    @rtype: list
-    @return: List of optional features
-
-    """
-    try:
-      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
-                               None, None)
-    except GanetiApiError, err:
-      # Older RAPI servers don't support this resource
-      if err.code == HTTP_NOT_FOUND:
-        return []
-
-      raise
-
-  def GetOperatingSystems(self):
-    """Gets the Operating Systems running in the Ganeti cluster.
-
-    @rtype: list of str
-    @return: operating systems
-
-    """
-    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
-                             None, None)
-
-  def GetInfo(self):
-    """Gets info about the cluster.
-
-    @rtype: dict
-    @return: information about the cluster
-
-    """
-    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
-                             None, None)
-
-  def RedistributeConfig(self):
-    """Tells the cluster to redistribute its configuration files.
-
-    @return: job id
-
-    """
-    return self._SendRequest(HTTP_PUT,
-                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
-                             None, None)
-
-  def ModifyCluster(self, **kwargs):
-    """Modifies cluster parameters.
-
-    More details for parameters can be found in the RAPI documentation.
-
-    @rtype: int
-    @return: job id
-
-    """
-    body = kwargs
-
-    return self._SendRequest(HTTP_PUT,
-                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
-
-  def GetClusterTags(self):
-    """Gets the cluster tags.
-
-    @rtype: list of str
-    @return: cluster tags
-
-    """
-    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
-                             None, None)
-
-  def AddClusterTags(self, tags, dry_run=False):
-    """Adds tags to the cluster.
-
-    @type tags: list of str
-    @param tags: tags to add to the cluster
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
-                             query, None)
-
-  def DeleteClusterTags(self, tags, dry_run=False):
-    """Deletes tags from the cluster.
-
-    @type tags: list of str
-    @param tags: tags to delete
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    """
-    query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
-                             query, None)
-
-  def GetInstances(self, bulk=False):
-    """Gets information about instances on the cluster.
-
-    @type bulk: bool
-    @param bulk: whether to return all information about all instances
-
-    @rtype: list of dict or list of str
-    @return: if bulk is True, info about the instances, else a list of instances
-
-    """
-    query = []
-    if bulk:
-      query.append(("bulk", 1))
-
-    instances = self._SendRequest(HTTP_GET,
-                                  "/%s/instances" % GANETI_RAPI_VERSION,
-                                  query, None)
-    if bulk:
-      return instances
-    else:
-      return [i["id"] for i in instances]
-
-  def GetInstance(self, instance):
-    """Gets information about an instance.
-
-    @type instance: str
-    @param instance: instance whose info to return
-
-    @rtype: dict
-    @return: info about the instance
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/instances/%s" %
-                              (GANETI_RAPI_VERSION, instance)), None, None)
-
-  def GetInstanceInfo(self, instance, static=None):
-    """Gets information about an instance.
-
-    @type instance: string
-    @param instance: Instance name
-    @rtype: string
-    @return: Job ID
-
-    """
-    if static is not None:
-      query = [("static", static)]
-    else:
-      query = None
-
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/instances/%s/info" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def CreateInstance(self, mode, name, disk_template, disks, nics,
-                     **kwargs):
-    """Creates a new instance.
-
-    More details for parameters can be found in the RAPI documentation.
-
-    @type mode: string
-    @param mode: Instance creation mode
-    @type name: string
-    @param name: Hostname of the instance to create
-    @type disk_template: string
-    @param disk_template: Disk template for instance (e.g. plain, diskless,
-                          file, or drbd)
-    @type disks: list of dicts
-    @param disks: List of disk definitions
-    @type nics: list of dicts
-    @param nics: List of NIC definitions
-    @type dry_run: bool
-    @keyword dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = []
-
-    if kwargs.get("dry_run"):
-      query.append(("dry-run", 1))
-
-    if _INST_CREATE_REQV1 in self.GetFeatures():
-      # All required fields for request data version 1
-      body = {
-        _REQ_DATA_VERSION_FIELD: 1,
-        "mode": mode,
-        "name": name,
-        "disk_template": disk_template,
-        "disks": disks,
-        "nics": nics,
-        }
-
-      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
-      if conflicts:
-        raise GanetiApiError("Required fields can not be specified as"
-                             " keywords: %s" % ", ".join(conflicts))
-
-      body.update((key, value) for key, value in kwargs.iteritems()
-                  if key != "dry_run")
-    else:
-      # Old request format (version 0)
-
-      # The following code must make sure that an exception is raised when an
-      # unsupported setting is requested by the caller. Otherwise this can lead
-      # to bugs difficult to find. The interface of this function must stay
-      # exactly the same for version 0 and 1 (e.g. they aren't allowed to
-      # require different data types).
-
-      # Validate disks
-      for idx, disk in enumerate(disks):
-        unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
-        if unsupported:
-          raise GanetiApiError("Server supports request version 0 only, but"
-                               " disk %s specifies the unsupported parameters"
-                               " %s, allowed are %s" %
-                               (idx, unsupported,
-                                list(_INST_CREATE_V0_DISK_PARAMS)))
-
-      assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
-              "size" in _INST_CREATE_V0_DISK_PARAMS)
-      disk_sizes = [disk["size"] for disk in disks]
-
-      # Validate NICs
-      if not nics:
-        raise GanetiApiError("Server supports request version 0 only, but"
-                             " no NIC specified")
-      elif len(nics) > 1:
-        raise GanetiApiError("Server supports request version 0 only, but"
-                             " more than one NIC specified")
-
-      assert len(nics) == 1
-
-      unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
-      if unsupported:
-        raise GanetiApiError("Server supports request version 0 only, but"
-                             " NIC 0 specifies the unsupported parameters %s,"
-                             " allowed are %s" %
-                             (unsupported, list(_INST_NIC_PARAMS)))
-
-      # Validate other parameters
-      unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
-                     _INST_CREATE_V0_DPARAMS)
-      if unsupported:
-        allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
-        raise GanetiApiError("Server supports request version 0 only, but"
-                             " the following unsupported parameters are"
-                             " specified: %s, allowed are %s" %
-                             (unsupported, list(allowed)))
-
-      # All required fields for request data version 0
-      body = {
-        _REQ_DATA_VERSION_FIELD: 0,
-        "name": name,
-        "disk_template": disk_template,
-        "disks": disk_sizes,
-        }
-
-      # NIC fields
-      assert len(nics) == 1
-      assert not (set(body.keys()) & set(nics[0].keys()))
-      body.update(nics[0])
-
-      # Copy supported fields
-      assert not (set(body.keys()) & set(kwargs.keys()))
-      body.update(dict((key, value) for key, value in kwargs.items()
-                       if key in _INST_CREATE_V0_PARAMS))
-
-      # Merge dictionaries
-      for i in (value for key, value in kwargs.items()
-                if key in _INST_CREATE_V0_DPARAMS):
-        assert not (set(body.keys()) & set(i.keys()))
-        body.update(i)
-
-      assert not (set(kwargs.keys()) -
-                  (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
-      assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
-
-    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
-                             query, body)
-
-  def DeleteInstance(self, instance, dry_run=False):
-    """Deletes an instance.
-
-    @type instance: str
-    @param instance: the instance to delete
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_DELETE,
-                             ("/%s/instances/%s" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def ModifyInstance(self, instance, **kwargs):
-    """Modifies an instance.
-
-    More details for parameters can be found in the RAPI documentation.
-
-    @type instance: string
-    @param instance: Instance name
-    @rtype: int
-    @return: job id
-
-    """
-    body = kwargs
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/modify" %
-                              (GANETI_RAPI_VERSION, instance)), None, body)
-
-  def ActivateInstanceDisks(self, instance, ignore_size=None):
-    """Activates an instance's disks.
-
-    @type instance: string
-    @param instance: Instance name
-    @type ignore_size: bool
-    @param ignore_size: Whether to ignore recorded size
-    @return: job id
-
-    """
-    query = []
-    if ignore_size:
-      query.append(("ignore_size", 1))
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/activate-disks" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def DeactivateInstanceDisks(self, instance):
-    """Deactivates an instance's disks.
-
-    @type instance: string
-    @param instance: Instance name
-    @return: job id
-
-    """
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/deactivate-disks" %
-                              (GANETI_RAPI_VERSION, instance)), None, None)
-
-  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
-    """Grows a disk of an instance.
-
-    More details for parameters can be found in the RAPI documentation.
-
-    @type instance: string
-    @param instance: Instance name
-    @type disk: integer
-    @param disk: Disk index
-    @type amount: integer
-    @param amount: Grow disk by this amount (MiB)
-    @type wait_for_sync: bool
-    @param wait_for_sync: Wait for disk to synchronize
-    @rtype: int
-    @return: job id
-
-    """
-    body = {
-      "amount": amount,
-      }
-
-    if wait_for_sync is not None:
-      body["wait_for_sync"] = wait_for_sync
-
-    return self._SendRequest(HTTP_POST,
-                             ("/%s/instances/%s/disk/%s/grow" %
-                              (GANETI_RAPI_VERSION, instance, disk)),
-                             None, body)
-
-  def GetInstanceTags(self, instance):
-    """Gets tags for an instance.
-
-    @type instance: str
-    @param instance: instance whose tags to return
-
-    @rtype: list of str
-    @return: tags for the instance
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/instances/%s/tags" %
-                              (GANETI_RAPI_VERSION, instance)), None, None)
-
-  def AddInstanceTags(self, instance, tags, dry_run=False):
-    """Adds tags to an instance.
-
-    @type instance: str
-    @param instance: instance to add tags to
-    @type tags: list of str
-    @param tags: tags to add to the instance
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/tags" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def DeleteInstanceTags(self, instance, tags, dry_run=False):
-    """Deletes tags from an instance.
-
-    @type instance: str
-    @param instance: instance to delete tags from
-    @type tags: list of str
-    @param tags: tags to delete
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    """
-    query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_DELETE,
-                             ("/%s/instances/%s/tags" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
-                     dry_run=False):
-    """Reboots an instance.
-
-    @type instance: str
-    @param instance: instance to rebot
-    @type reboot_type: str
-    @param reboot_type: one of: hard, soft, full
-    @type ignore_secondaries: bool
-    @param ignore_secondaries: if True, ignores errors for the secondary node
-        while re-assembling disks (in hard-reboot mode only)
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    """
-    query = []
-    if reboot_type:
-      query.append(("type", reboot_type))
-    if ignore_secondaries is not None:
-      query.append(("ignore_secondaries", ignore_secondaries))
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_POST,
-                             ("/%s/instances/%s/reboot" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def ShutdownInstance(self, instance, dry_run=False):
-    """Shuts down an instance.
-
-    @type instance: str
-    @param instance: the instance to shut down
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    """
-    query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/shutdown" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def StartupInstance(self, instance, dry_run=False):
-    """Starts up an instance.
-
-    @type instance: str
-    @param instance: the instance to start up
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    """
-    query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/startup" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def ReinstallInstance(self, instance, os=None, no_startup=False,
-                        osparams=None):
-    """Reinstalls an instance.
-
-    @type instance: str
-    @param instance: The instance to reinstall
-    @type os: str or None
-    @param os: The operating system to reinstall. If None, the instance's
-        current operating system will be installed again
-    @type no_startup: bool
-    @param no_startup: Whether to start the instance automatically
-
-    """
-    if _INST_REINSTALL_REQV1 in self.GetFeatures():
-      body = {
-        "start": not no_startup,
-        }
-      if os is not None:
-        body["os"] = os
-      if osparams is not None:
-        body["osparams"] = osparams
-      return self._SendRequest(HTTP_POST,
-                               ("/%s/instances/%s/reinstall" %
-                                (GANETI_RAPI_VERSION, instance)), None, body)
-
-    # Use old request format
-    if osparams:
-      raise GanetiApiError("Server does not support specifying OS parameters"
-                           " for instance reinstallation")
-
-    query = []
-    if os:
-      query.append(("os", os))
-    if no_startup:
-      query.append(("nostartup", 1))
-    return self._SendRequest(HTTP_POST,
-                             ("/%s/instances/%s/reinstall" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
-                           remote_node=None, iallocator=None, dry_run=False):
-    """Replaces disks on an instance.
-
-    @type instance: str
-    @param instance: instance whose disks to replace
-    @type disks: list of ints
-    @param disks: Indexes of disks to replace
-    @type mode: str
-    @param mode: replacement mode to use (defaults to replace_auto)
-    @type remote_node: str or None
-    @param remote_node: new secondary node to use (for use with
-        replace_new_secondary mode)
-    @type iallocator: str or None
-    @param iallocator: instance allocator plugin to use (for use with
-                       replace_auto mode)
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [
-      ("mode", mode),
-      ]
-
-    if disks:
-      query.append(("disks", ",".join(str(idx) for idx in disks)))
-
-    if remote_node:
-      query.append(("remote_node", remote_node))
-
-    if iallocator:
-      query.append(("iallocator", iallocator))
-
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_POST,
-                             ("/%s/instances/%s/replace-disks" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def PrepareExport(self, instance, mode):
-    """Prepares an instance for an export.
-
-    @type instance: string
-    @param instance: Instance name
-    @type mode: string
-    @param mode: Export mode
-    @rtype: string
-    @return: Job ID
-
-    """
-    query = [("mode", mode)]
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/prepare-export" %
-                              (GANETI_RAPI_VERSION, instance)), query, None)
-
-  def ExportInstance(self, instance, mode, destination, shutdown=None,
-                     remove_instance=None,
-                     x509_key_name=None, destination_x509_ca=None):
-    """Exports an instance.
-
-    @type instance: string
-    @param instance: Instance name
-    @type mode: string
-    @param mode: Export mode
-    @rtype: string
-    @return: Job ID
-
-    """
-    body = {
-      "destination": destination,
-      "mode": mode,
-      }
-
-    if shutdown is not None:
-      body["shutdown"] = shutdown
-
-    if remove_instance is not None:
-      body["remove_instance"] = remove_instance
-
-    if x509_key_name is not None:
-      body["x509_key_name"] = x509_key_name
-
-    if destination_x509_ca is not None:
-      body["destination_x509_ca"] = destination_x509_ca
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/export" %
-                              (GANETI_RAPI_VERSION, instance)), None, body)
-
-  def MigrateInstance(self, instance, mode=None, cleanup=None):
-    """Migrates an instance.
-
-    @type instance: string
-    @param instance: Instance name
-    @type mode: string
-    @param mode: Migration mode
-    @type cleanup: bool
-    @param cleanup: Whether to clean up a previously failed migration
-
-    """
-    body = {}
-
-    if mode is not None:
-      body["mode"] = mode
-
-    if cleanup is not None:
-      body["cleanup"] = cleanup
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/migrate" %
-                              (GANETI_RAPI_VERSION, instance)), None, body)
-
-  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
-    """Changes the name of an instance.
-
-    @type instance: string
-    @param instance: Instance name
-    @type new_name: string
-    @param new_name: New instance name
-    @type ip_check: bool
-    @param ip_check: Whether to ensure instance's IP address is inactive
-    @type name_check: bool
-    @param name_check: Whether to ensure instance's name is resolvable
-
-    """
-    body = {
-      "new_name": new_name,
-      }
-
-    if ip_check is not None:
-      body["ip_check"] = ip_check
-
-    if name_check is not None:
-      body["name_check"] = name_check
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/instances/%s/rename" %
-                              (GANETI_RAPI_VERSION, instance)), None, body)
-
-  def GetInstanceConsole(self, instance):
-    """Request information for connecting to instance's console.
-
-    @type instance: string
-    @param instance: Instance name
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/instances/%s/console" %
-                              (GANETI_RAPI_VERSION, instance)), None, None)
-
-  def GetJobs(self):
-    """Gets all jobs for the cluster.
-
-    @rtype: list of int
-    @return: job ids for the cluster
-
-    """
-    return [int(j["id"])
-            for j in self._SendRequest(HTTP_GET,
-                                       "/%s/jobs" % GANETI_RAPI_VERSION,
-                                       None, None)]
-
-  def GetJobStatus(self, job_id):
-    """Gets the status of a job.
-
-    @type job_id: int
-    @param job_id: job id whose status to query
-
-    @rtype: dict
-    @return: job status
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
-                             None, None)
-
-  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
-    """Waits for job changes.
-
-    @type job_id: int
-    @param job_id: Job ID for which to wait
-
-    """
-    body = {
-      "fields": fields,
-      "previous_job_info": prev_job_info,
-      "previous_log_serial": prev_log_serial,
-      }
-
-    return self._SendRequest(HTTP_GET,
-                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
-                             None, body)
-
-  def CancelJob(self, job_id, dry_run=False):
-    """Cancels a job.
-
-    @type job_id: int
-    @param job_id: id of the job to delete
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    """
-    query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_DELETE,
-                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
-                             query, None)
-
-  def GetNodes(self, bulk=False):
-    """Gets all nodes in the cluster.
-
-    @type bulk: bool
-    @param bulk: whether to return all information about all instances
-
-    @rtype: list of dict or str
-    @return: if bulk is true, info about nodes in the cluster,
-        else list of nodes in the cluster
-
-    """
-    query = []
-    if bulk:
-      query.append(("bulk", 1))
-
-    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
-                              query, None)
-    if bulk:
-      return nodes
-    else:
-      return [n["id"] for n in nodes]
-
-  def GetNode(self, node):
-    """Gets information about a node.
-
-    @type node: str
-    @param node: node whose info to return
-
-    @rtype: dict
-    @return: info about the node
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
-                             None, None)
-
-  def EvacuateNode(self, node, iallocator=None, remote_node=None,
-                   dry_run=False, early_release=False):
-    """Evacuates instances from a Ganeti node.
-
-    @type node: str
-    @param node: node to evacuate
-    @type iallocator: str or None
-    @param iallocator: instance allocator to use
-    @type remote_node: str
-    @param remote_node: node to evaucate to
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-    @type early_release: bool
-    @param early_release: whether to enable parallelization
-
-    @rtype: list
-    @return: list of (job ID, instance name, new secondary node); if
-        dry_run was specified, then the actual move jobs were not
-        submitted and the job IDs will be C{None}
-
-    @raises GanetiApiError: if an iallocator and remote_node are both
-        specified
-
-    """
-    if iallocator and remote_node:
-      raise GanetiApiError("Only one of iallocator or remote_node can be used")
-
-    query = []
-    if iallocator:
-      query.append(("iallocator", iallocator))
-    if remote_node:
-      query.append(("remote_node", remote_node))
-    if dry_run:
-      query.append(("dry-run", 1))
-    if early_release:
-      query.append(("early_release", 1))
-
-    return self._SendRequest(HTTP_POST,
-                             ("/%s/nodes/%s/evacuate" %
-                              (GANETI_RAPI_VERSION, node)), query, None)
-
-  def MigrateNode(self, node, mode=None, dry_run=False):
-    """Migrates all primary instances from a node.
-
-    @type node: str
-    @param node: node to migrate
-    @type mode: string
-    @param mode: if passed, it will overwrite the live migration type,
-        otherwise the hypervisor default will be used
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = []
-    if mode is not None:
-      query.append(("mode", mode))
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_POST,
-                             ("/%s/nodes/%s/migrate" %
-                              (GANETI_RAPI_VERSION, node)), query, None)
-
-  def GetNodeRole(self, node):
-    """Gets the current role for a node.
-
-    @type node: str
-    @param node: node whose role to return
-
-    @rtype: str
-    @return: the current role for a node
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/nodes/%s/role" %
-                              (GANETI_RAPI_VERSION, node)), None, None)
-
-  def SetNodeRole(self, node, role, force=False):
-    """Sets the role for a node.
-
-    @type node: str
-    @param node: the node whose role to set
-    @type role: str
-    @param role: the role to set for the node
-    @type force: bool
-    @param force: whether to force the role change
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [
-      ("force", force),
-      ]
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/nodes/%s/role" %
-                              (GANETI_RAPI_VERSION, node)), query, role)
-
-  def GetNodeStorageUnits(self, node, storage_type, output_fields):
-    """Gets the storage units for a node.
-
-    @type node: str
-    @param node: the node whose storage units to return
-    @type storage_type: str
-    @param storage_type: storage type whose units to return
-    @type output_fields: str
-    @param output_fields: storage type fields to return
-
-    @rtype: int
-    @return: job id where results can be retrieved
-
-    """
-    query = [
-      ("storage_type", storage_type),
-      ("output_fields", output_fields),
-      ]
-
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/nodes/%s/storage" %
-                              (GANETI_RAPI_VERSION, node)), query, None)
-
-  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
-    """Modifies parameters of storage units on the node.
-
-    @type node: str
-    @param node: node whose storage units to modify
-    @type storage_type: str
-    @param storage_type: storage type whose units to modify
-    @type name: str
-    @param name: name of the storage unit
-    @type allocatable: bool or None
-    @param allocatable: Whether to set the "allocatable" flag on the storage
-                        unit (None=no modification, True=set, False=unset)
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [
-      ("storage_type", storage_type),
-      ("name", name),
-      ]
-
-    if allocatable is not None:
-      query.append(("allocatable", allocatable))
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/nodes/%s/storage/modify" %
-                              (GANETI_RAPI_VERSION, node)), query, None)
-
-  def RepairNodeStorageUnits(self, node, storage_type, name):
-    """Repairs a storage unit on the node.
-
-    @type node: str
-    @param node: node whose storage units to repair
-    @type storage_type: str
-    @param storage_type: storage type to repair
-    @type name: str
-    @param name: name of the storage unit to repair
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [
-      ("storage_type", storage_type),
-      ("name", name),
-      ]
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/nodes/%s/storage/repair" %
-                              (GANETI_RAPI_VERSION, node)), query, None)
-
-  def GetNodeTags(self, node):
-    """Gets the tags for a node.
-
-    @type node: str
-    @param node: node whose tags to return
-
-    @rtype: list of str
-    @return: tags for the node
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             ("/%s/nodes/%s/tags" %
-                              (GANETI_RAPI_VERSION, node)), None, None)
-
-  def AddNodeTags(self, node, tags, dry_run=False):
-    """Adds tags to a node.
-
-    @type node: str
-    @param node: node to add tags to
-    @type tags: list of str
-    @param tags: tags to add to the node
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/nodes/%s/tags" %
-                              (GANETI_RAPI_VERSION, node)), query, tags)
-
-  def DeleteNodeTags(self, node, tags, dry_run=False):
-    """Delete tags from a node.
-
-    @type node: str
-    @param node: node to remove tags from
-    @type tags: list of str
-    @param tags: tags to remove from the node
-    @type dry_run: bool
-    @param dry_run: whether to perform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_DELETE,
-                             ("/%s/nodes/%s/tags" %
-                              (GANETI_RAPI_VERSION, node)), query, None)
-
-  def GetGroups(self, bulk=False):
-    """Gets all node groups in the cluster.
-
-    @type bulk: bool
-    @param bulk: whether to return all information about the groups
-
-    @rtype: list of dict or str
-    @return: if bulk is true, a list of dictionaries with info about all node
-        groups in the cluster, else a list of names of those node groups
-
-    """
-    query = []
-    if bulk:
-      query.append(("bulk", 1))
-
-    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
-                               query, None)
-    if bulk:
-      return groups
-    else:
-      return [g["name"] for g in groups]
-
-  def GetGroup(self, group):
-    """Gets information about a node group.
-
-    @type group: str
-    @param group: name of the node group whose info to return
-
-    @rtype: dict
-    @return: info about the node group
-
-    """
-    return self._SendRequest(HTTP_GET,
-                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
-                             None, None)
-
-  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
-    """Creates a new node group.
-
-    @type name: str
-    @param name: the name of node group to create
-    @type alloc_policy: str
-    @param alloc_policy: the desired allocation policy for the group, if any
-    @type dry_run: bool
-    @param dry_run: whether to peform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    body = {
-      "name": name,
-      "alloc_policy": alloc_policy
-      }
-
-    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
-                             query, body)
-
-  def ModifyGroup(self, group, **kwargs):
-    """Modifies a node group.
-
-    More details for parameters can be found in the RAPI documentation.
-
-    @type group: string
-    @param group: Node group name
-    @rtype: int
-    @return: job id
-
-    """
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/groups/%s/modify" %
-                              (GANETI_RAPI_VERSION, group)), None, kwargs)
-
-  def DeleteGroup(self, group, dry_run=False):
-    """Deletes a node group.
-
-    @type group: str
-    @param group: the node group to delete
-    @type dry_run: bool
-    @param dry_run: whether to peform a dry run
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    return self._SendRequest(HTTP_DELETE,
-                             ("/%s/groups/%s" %
-                              (GANETI_RAPI_VERSION, group)), query, None)
-
-  def RenameGroup(self, group, new_name):
-    """Changes the name of a node group.
-
-    @type group: string
-    @param group: Node group name
-    @type new_name: string
-    @param new_name: New node group name
-
-    @rtype: int
-    @return: job id
-
-    """
-    body = {
-      "new_name": new_name,
-      }
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/groups/%s/rename" %
-                              (GANETI_RAPI_VERSION, group)), None, body)
-
-
-  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
-    """Assigns nodes to a group.
-
-    @type group: string
-    @param group: Node gropu name
-    @type nodes: list of strings
-    @param nodes: List of nodes to assign to the group
-
-    @rtype: int
-    @return: job id
-
-    """
-    query = []
-
-    if force:
-      query.append(("force", 1))
-
-    if dry_run:
-      query.append(("dry-run", 1))
-
-    body = {
-      "nodes": nodes,
-      }
-
-    return self._SendRequest(HTTP_PUT,
-                             ("/%s/groups/%s/assign-nodes" %
-                             (GANETI_RAPI_VERSION, group)), query, body)
diff --git a/snf-app/synnefo/version.py b/snf-app/synnefo/version.py
deleted file mode 100644
index 1137150..0000000
--- a/snf-app/synnefo/version.py
+++ /dev/null
@@ -1,32 +0,0 @@
-VERSION = (0, 8, 0, 'alpha', 0)
-__version__ = VERSION
-
-def get_version():
-    """
-    Utility to parse version tuple to string
-    """
-    version = '%s.%s' % (VERSION[0], VERSION[1])
-    if VERSION[2]:
-        version = '%s.%s' % (version, VERSION[2])
-    if VERSION[3:] == ('alpha', 0):
-        version = '%s pre-alpha' % version
-    else:
-        if VERSION[3] != 'final':
-            version = '%s %s %s' % (version, VERSION[3], VERSION[4])
-    return version
-
-def vcs_version():
-    """
-    Return current git HEAD commit information
-    """
-    import subprocess
-    callgit = lambda(cmd): subprocess.Popen(
-            ['/bin/sh', '-c', cmd],
-            stdout=subprocess.PIPE).communicate()[0].strip()
-
-    branch = callgit('git branch | grep -Ei "\* (.*)" | cut -f2 -d" "')
-    revid = callgit("git --no-pager log --max-count=1 | cut -f2 -d' ' | head -1")
-    revno = callgit('git --no-pager log --oneline | wc -l')
-
-    return branch, revid, revno
-
diff --git a/snf-common/Changelog b/snf-common/Changelog
new file mode 100644
index 0000000..a5693d9
--- /dev/null
+++ b/snf-common/Changelog
@@ -0,0 +1,2 @@
+Changelog
+=========
diff --git a/snf-common/README b/snf-common/README
new file mode 100644
index 0000000..9d457ac
--- /dev/null
+++ b/snf-common/README
@@ -0,0 +1,3 @@
+README
+=======
+Synnefo top level package
diff --git a/snf-common/distribute_setup.py b/snf-common/distribute_setup.py
new file mode 100644
index 0000000..10d6684
--- /dev/null
+++ b/snf-common/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-common/setup.py b/snf-common/setup.py
new file mode 100644
index 0000000..c166cbf
--- /dev/null
+++ b/snf-common/setup.py
@@ -0,0 +1,92 @@
+# 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
+from synnefo.version import vcs_version
+
+HERE = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
+
+# Package info
+VERSION = vcs_version()
+README = open(os.path.join(HERE, 'README')).read()
+CHANGES = open(os.path.join(HERE, 'Changelog')).read()
+SHORT_DESCRIPTION = 'Package short description'
+
+PACKAGES_ROOT = '.'
+PACKAGES = find_packages(PACKAGES_ROOT, exclude=['okeanos_site'])
+
+# Package meta
+CLASSIFIERS = []
+
+# Package requirements
+INSTALL_REQUIRES = [
+]
+
+EXTRAS_REQUIRES = {
+}
+
+TESTS_REQUIRES = [
+]
+
+setup(
+    name = 'snf-common',
+    version = VERSION,
+    license = 'BSD',
+    url = 'http://code.grnet.gr/',
+    description = SHORT_DESCRIPTION,
+    long_description=README + '\n\n' +  CHANGES,
+    classifiers = CLASSIFIERS,
+
+    author = 'Package author',
+    author_email = 'author@grnet.gr',
+    maintainer = 'Package maintainer',
+    maintainer_email = 'maintainer@grnet.gr',
+
+    packages = PACKAGES,
+    package_dir= {'': PACKAGES_ROOT},
+    include_package_data = True,
+    zip_safe = False,
+
+    install_requires = INSTALL_REQUIRES,
+    extras_require = EXTRAS_REQUIRES,
+    tests_require = TESTS_REQUIRES,
+)
+
diff --git a/snf-common/synnefo/__init__.py b/snf-common/synnefo/__init__.py
new file mode 100644
index 0000000..2e2033b
--- /dev/null
+++ b/snf-common/synnefo/__init__.py
@@ -0,0 +1,7 @@
+# this is a namespace package
+try:
+    import pkg_resources
+    pkg_resources.declare_namespace(__name__)
+except ImportError:
+    import pkgutil
+    __path__ = pkgutil.extend_path(__path__, __name__)
diff --git a/snf-common/synnefo/settings/__init__.py b/snf-common/synnefo/settings/__init__.py
new file mode 100644
index 0000000..9c3ba34
--- /dev/null
+++ b/snf-common/synnefo/settings/__init__.py
@@ -0,0 +1,61 @@
+# 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 os
+import sys
+import glob
+import pkg_resources
+
+from synnefo.util.entry_points import extend_settings
+
+# import default settings
+from synnefo.settings.default import *
+
+# autodetect default settings provided by synnefo applications
+extend_settings('synnefo', __name__)
+
+# extend default settings with settings provided within *.conf user files
+# located in directory specified in the SYNNEFO_SETTINGS_DIR
+# environmental variable
+SYNNEFO_SETTINGS_DIR = os.environ.get('SYNNEFO_SETTINGS_DIR', "/etc/synnefo/")
+if os.path.exists(SYNNEFO_SETTINGS_DIR):
+    conffiles = glob.glob(os.path.join(SYNNEFO_SETTINGS_DIR, '*.conf'))
+    conffiles.sort()
+    for f in conffiles:
+        try:
+            execfile(os.path.abspath(f))
+        except Exception as e:
+            print >>sys.stderr, "Failed to read settings file: %s" % \
+                                os.path.abspath(f)
+
+
diff --git a/snf-common/synnefo/settings/default/__init__.py b/snf-common/synnefo/settings/default/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/snf-common/synnefo/settings/test.py b/snf-common/synnefo/settings/test.py
new file mode 100644
index 0000000..785ef51
--- /dev/null
+++ b/snf-common/synnefo/settings/test.py
@@ -0,0 +1,14 @@
+from synnefo.settings import *
+
+DEBUG = True
+TEST = True
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'sqlite3',
+        'NAME': '/tmp/synnefo_test_db.sqlite',
+    }
+}
+
+LOGGING['handlers']['console']['level'] = 'WARNING'
+LOGIN_URL = 'http://host:port/'
diff --git a/snf-common/synnefo/util/__init__.py b/snf-common/synnefo/util/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/snf-common/synnefo/util/dictconfig.py b/snf-common/synnefo/util/dictconfig.py
new file mode 100644
index 0000000..167831b
--- /dev/null
+++ b/snf-common/synnefo/util/dictconfig.py
@@ -0,0 +1,553 @@
+# This is a copy of the Python logging.config.dictconfig module.
+# It is provided here for backwards compatibility for Python versions
+# prior to 2.7.
+#
+# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose and without fee is hereby granted,
+# provided that the above copyright notice appear in all copies and that
+# both that copyright notice and this permission notice appear in
+# supporting documentation, and that the name of Vinay Sajip
+# not be used in advertising or publicity pertaining to distribution
+# of the software without specific, written prior permission.
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import logging.handlers
+import re
+import sys
+import types
+
+IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
+
+def valid_ident(s):
+    m = IDENTIFIER.match(s)
+    if not m:
+        raise ValueError('Not a valid Python identifier: %r' % s)
+    return True
+
+#
+# This function is defined in logging only in recent versions of Python
+#
+try:
+    from logging import _checkLevel
+except ImportError:
+    def _checkLevel(level):
+        if isinstance(level, int):
+            rv = level
+        elif str(level) == level:
+            if level not in logging._levelNames:
+                raise ValueError('Unknown level: %r' % level)
+            rv = logging._levelNames[level]
+        else:
+            raise TypeError('Level not an integer or a '
+                            'valid string: %r' % level)
+        return rv
+
+# The ConvertingXXX classes are wrappers around standard Python containers,
+# and they serve to convert any suitable values in the container. The
+# conversion converts base dicts, lists and tuples to their wrapped
+# equivalents, whereas strings which match a conversion format are converted
+# appropriately.
+#
+# Each wrapper should have a configurator attribute holding the actual
+# configurator to use for conversion.
+
+class ConvertingDict(dict):
+    """A converting dictionary wrapper."""
+
+    def __getitem__(self, key):
+        value = dict.__getitem__(self, key)
+        result = self.configurator.convert(value)
+        #If the converted value is different, save for next time
+        if value is not result:
+            self[key] = result
+            if type(result) in (ConvertingDict, ConvertingList,
+                                ConvertingTuple):
+                result.parent = self
+                result.key = key
+        return result
+
+    def get(self, key, default=None):
+        value = dict.get(self, key, default)
+        result = self.configurator.convert(value)
+        #If the converted value is different, save for next time
+        if value is not result:
+            self[key] = result
+            if type(result) in (ConvertingDict, ConvertingList,
+                                ConvertingTuple):
+                result.parent = self
+                result.key = key
+        return result
+
+    def pop(self, key, default=None):
+        value = dict.pop(self, key, default)
+        result = self.configurator.convert(value)
+        if value is not result:
+            if type(result) in (ConvertingDict, ConvertingList,
+                                ConvertingTuple):
+                result.parent = self
+                result.key = key
+        return result
+
+class ConvertingList(list):
+    """A converting list wrapper."""
+    def __getitem__(self, key):
+        value = list.__getitem__(self, key)
+        result = self.configurator.convert(value)
+        #If the converted value is different, save for next time
+        if value is not result:
+            self[key] = result
+            if type(result) in (ConvertingDict, ConvertingList,
+                                ConvertingTuple):
+                result.parent = self
+                result.key = key
+        return result
+
+    def pop(self, idx=-1):
+        value = list.pop(self, idx)
+        result = self.configurator.convert(value)
+        if value is not result:
+            if type(result) in (ConvertingDict, ConvertingList,
+                                ConvertingTuple):
+                result.parent = self
+        return result
+
+class ConvertingTuple(tuple):
+    """A converting tuple wrapper."""
+    def __getitem__(self, key):
+        value = tuple.__getitem__(self, key)
+        result = self.configurator.convert(value)
+        if value is not result:
+            if type(result) in (ConvertingDict, ConvertingList,
+                                ConvertingTuple):
+                result.parent = self
+                result.key = key
+        return result
+
+class BaseConfigurator(object):
+    """
+    The configurator base class which defines some useful defaults.
+    """
+
+    CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
+
+    WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
+    DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
+    INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
+    DIGIT_PATTERN = re.compile(r'^\d+$')
+
+    value_converters = {
+        'ext' : 'ext_convert',
+        'cfg' : 'cfg_convert',
+    }
+
+    # We might want to use a different one, e.g. importlib
+    importer = __import__
+
+    def __init__(self, config):
+        self.config = ConvertingDict(config)
+        self.config.configurator = self
+
+    def resolve(self, s):
+        """
+        Resolve strings to objects using standard import and attribute
+        syntax.
+        """
+        name = s.split('.')
+        used = name.pop(0)
+        try:
+            found = self.importer(used)
+            for frag in name:
+                used += '.' + frag
+                try:
+                    found = getattr(found, frag)
+                except AttributeError:
+                    self.importer(used)
+                    found = getattr(found, frag)
+            return found
+        except ImportError:
+            e, tb = sys.exc_info()[1:]
+            v = ValueError('Cannot resolve %r: %s' % (s, e))
+            v.__cause__, v.__traceback__ = e, tb
+            raise v
+
+    def ext_convert(self, value):
+        """Default converter for the ext:// protocol."""
+        return self.resolve(value)
+
+    def cfg_convert(self, value):
+        """Default converter for the cfg:// protocol."""
+        rest = value
+        m = self.WORD_PATTERN.match(rest)
+        if m is None:
+            raise ValueError("Unable to convert %r" % value)
+        else:
+            rest = rest[m.end():]
+            d = self.config[m.groups()[0]]
+            #print d, rest
+            while rest:
+                m = self.DOT_PATTERN.match(rest)
+                if m:
+                    d = d[m.groups()[0]]
+                else:
+                    m = self.INDEX_PATTERN.match(rest)
+                    if m:
+                        idx = m.groups()[0]
+                        if not self.DIGIT_PATTERN.match(idx):
+                            d = d[idx]
+                        else:
+                            try:
+                                n = int(idx) # try as number first (most likely)
+                                d = d[n]
+                            except TypeError:
+                                d = d[idx]
+                if m:
+                    rest = rest[m.end():]
+                else:
+                    raise ValueError('Unable to convert '
+                                     '%r at %r' % (value, rest))
+        #rest should be empty
+        return d
+
+    def convert(self, value):
+        """
+        Convert values to an appropriate type. dicts, lists and tuples are
+        replaced by their converting alternatives. Strings are checked to
+        see if they have a conversion format and are converted if they do.
+        """
+        if not isinstance(value, ConvertingDict) and isinstance(value, dict):
+            value = ConvertingDict(value)
+            value.configurator = self
+        elif not isinstance(value, ConvertingList) and isinstance(value, list):
+            value = ConvertingList(value)
+            value.configurator = self
+        elif not isinstance(value, ConvertingTuple) and\
+                 isinstance(value, tuple):
+            value = ConvertingTuple(value)
+            value.configurator = self
+        elif isinstance(value, basestring): # str for py3k
+            m = self.CONVERT_PATTERN.match(value)
+            if m:
+                d = m.groupdict()
+                prefix = d['prefix']
+                converter = self.value_converters.get(prefix, None)
+                if converter:
+                    suffix = d['suffix']
+                    converter = getattr(self, converter)
+                    value = converter(suffix)
+        return value
+
+    def configure_custom(self, config):
+        """Configure an object with a user-supplied factory."""
+        c = config.pop('()')
+        if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
+            c = self.resolve(c)
+        props = config.pop('.', None)
+        # Check for valid identifiers
+        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
+        result = c(**kwargs)
+        if props:
+            for name, value in props.items():
+                setattr(result, name, value)
+        return result
+
+    def as_tuple(self, value):
+        """Utility function which converts lists to tuples."""
+        if isinstance(value, list):
+            value = tuple(value)
+        return value
+
+class DictConfigurator(BaseConfigurator):
+    """
+    Configure logging using a dictionary-like object to describe the
+    configuration.
+    """
+
+    def configure(self):
+        """Do the configuration."""
+
+        config = self.config
+        if 'version' not in config:
+            raise ValueError("dictionary doesn't specify a version")
+        if config['version'] != 1:
+            raise ValueError("Unsupported version: %s" % config['version'])
+        incremental = config.pop('incremental', False)
+        EMPTY_DICT = {}
+        logging._acquireLock()
+        try:
+            if incremental:
+                handlers = config.get('handlers', EMPTY_DICT)
+                # incremental handler config only if handler name
+                # ties in to logging._handlers (Python 2.7)
+                if sys.version_info[:2] == (2, 7):
+                    for name in handlers:
+                        if name not in logging._handlers:
+                            raise ValueError('No handler found with '
+                                             'name %r'  % name)
+                        else:
+                            try:
+                                handler = logging._handlers[name]
+                                handler_config = handlers[name]
+                                level = handler_config.get('level', None)
+                                if level:
+                                    handler.setLevel(_checkLevel(level))
+                            except StandardError, e:
+                                raise ValueError('Unable to configure handler '
+                                                 '%r: %s' % (name, e))
+                loggers = config.get('loggers', EMPTY_DICT)
+                for name in loggers:
+                    try:
+                        self.configure_logger(name, loggers[name], True)
+                    except StandardError, e:
+                        raise ValueError('Unable to configure logger '
+                                         '%r: %s' % (name, e))
+                root = config.get('root', None)
+                if root:
+                    try:
+                        self.configure_root(root, True)
+                    except StandardError, e:
+                        raise ValueError('Unable to configure root '
+                                         'logger: %s' % e)
+            else:
+                disable_existing = config.pop('disable_existing_loggers', True)
+
+                logging._handlers.clear()
+                del logging._handlerList[:]
+
+                # Do formatters first - they don't refer to anything else
+                formatters = config.get('formatters', EMPTY_DICT)
+                for name in formatters:
+                    try:
+                        formatters[name] = self.configure_formatter(
+                                                            formatters[name])
+                    except StandardError, e:
+                        raise ValueError('Unable to configure '
+                                         'formatter %r: %s' % (name, e))
+                # Next, do filters - they don't refer to anything else, either
+                filters = config.get('filters', EMPTY_DICT)
+                for name in filters:
+                    try:
+                        filters[name] = self.configure_filter(filters[name])
+                    except StandardError, e:
+                        raise ValueError('Unable to configure '
+                                         'filter %r: %s' % (name, e))
+
+                # Next, do handlers - they refer to formatters and filters
+                # As handlers can refer to other handlers, sort the keys
+                # to allow a deterministic order of configuration
+                handlers = config.get('handlers', EMPTY_DICT)
+                for name in sorted(handlers):
+                    try:
+                        handler = self.configure_handler(handlers[name])
+                        handler.name = name
+                        handlers[name] = handler
+                    except StandardError, e:
+                        raise ValueError('Unable to configure handler '
+                                         '%r: %s' % (name, e))
+                # Next, do loggers - they refer to handlers and filters
+
+                #we don't want to lose the existing loggers,
+                #since other threads may have pointers to them.
+                #existing is set to contain all existing loggers,
+                #and as we go through the new configuration we
+                #remove any which are configured. At the end,
+                #what's left in existing is the set of loggers
+                #which were in the previous configuration but
+                #which are not in the new configuration.
+                root = logging.root
+                existing = root.manager.loggerDict.keys()
+                #The list needs to be sorted so that we can
+                #avoid disabling child loggers of explicitly
+                #named loggers. With a sorted list it is easier
+                #to find the child loggers.
+                existing.sort()
+                #We'll keep the list of existing loggers
+                #which are children of named loggers here...
+                child_loggers = []
+                #now set up the new ones...
+                loggers = config.get('loggers', EMPTY_DICT)
+                for name in loggers:
+                    if name in existing:
+                        i = existing.index(name)
+                        prefixed = name + "."
+                        pflen = len(prefixed)
+                        num_existing = len(existing)
+                        i = i + 1 # look at the entry after name
+                        while (i < num_existing) and\
+                              (existing[i][:pflen] == prefixed):
+                            child_loggers.append(existing[i])
+                            i = i + 1
+                        existing.remove(name)
+                    try:
+                        self.configure_logger(name, loggers[name])
+                    except StandardError, e:
+                        raise ValueError('Unable to configure logger '
+                                         '%r: %s' % (name, e))
+
+                #Disable any old loggers. There's no point deleting
+                #them as other threads may continue to hold references
+                #and by disabling them, you stop them doing any logging.
+                #However, don't disable children of named loggers, as that's
+                #probably not what was intended by the user.
+                for log in existing:
+                    logger = root.manager.loggerDict[log]
+                    if log in child_loggers:
+                        logger.level = logging.NOTSET
+                        logger.handlers = []
+                        logger.propagate = True
+                    elif disable_existing:
+                        logger.disabled = True
+
+                # And finally, do the root logger
+                root = config.get('root', None)
+                if root:
+                    try:
+                        self.configure_root(root)
+                    except StandardError, e:
+                        raise ValueError('Unable to configure root '
+                                         'logger: %s' % e)
+        finally:
+            logging._releaseLock()
+
+    def configure_formatter(self, config):
+        """Configure a formatter from a dictionary."""
+        if '()' in config:
+            factory = config['()'] # for use in exception handler
+            try:
+                result = self.configure_custom(config)
+            except TypeError, te:
+                if "'format'" not in str(te):
+                    raise
+                #Name of parameter changed from fmt to format.
+                #Retry with old name.
+                #This is so that code can be used with older Python versions
+                #(e.g. by Django)
+                config['fmt'] = config.pop('format')
+                config['()'] = factory
+                result = self.configure_custom(config)
+        else:
+            fmt = config.get('format', None)
+            dfmt = config.get('datefmt', None)
+            result = logging.Formatter(fmt, dfmt)
+        return result
+
+    def configure_filter(self, config):
+        """Configure a filter from a dictionary."""
+        if '()' in config:
+            result = self.configure_custom(config)
+        else:
+            name = config.get('name', '')
+            result = logging.Filter(name)
+        return result
+
+    def add_filters(self, filterer, filters):
+        """Add filters to a filterer from a list of names."""
+        for f in filters:
+            try:
+                filterer.addFilter(self.config['filters'][f])
+            except StandardError, e:
+                raise ValueError('Unable to add filter %r: %s' % (f, e))
+
+    def configure_handler(self, config):
+        """Configure a handler from a dictionary."""
+        formatter = config.pop('formatter', None)
+        if formatter:
+            try:
+                formatter = self.config['formatters'][formatter]
+            except StandardError, e:
+                raise ValueError('Unable to set formatter '
+                                 '%r: %s' % (formatter, e))
+        level = config.pop('level', None)
+        filters = config.pop('filters', None)
+        if '()' in config:
+            c = config.pop('()')
+            if not hasattr(c, '__call__') and hasattr(types, 'ClassType') and type(c) != types.ClassType:
+                c = self.resolve(c)
+            factory = c
+        else:
+            klass = self.resolve(config.pop('class'))
+            #Special case for handler which refers to another handler
+            if issubclass(klass, logging.handlers.MemoryHandler) and\
+                'target' in config:
+                try:
+                    config['target'] = self.config['handlers'][config['target']]
+                except StandardError, e:
+                    raise ValueError('Unable to set target handler '
+                                     '%r: %s' % (config['target'], e))
+            elif issubclass(klass, logging.handlers.SMTPHandler) and\
+                'mailhost' in config:
+                config['mailhost'] = self.as_tuple(config['mailhost'])
+            elif issubclass(klass, logging.handlers.SysLogHandler) and\
+                'address' in config:
+                config['address'] = self.as_tuple(config['address'])
+            factory = klass
+        kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
+        try:
+            result = factory(**kwargs)
+        except TypeError, te:
+            if "'stream'" not in str(te):
+                raise
+            #The argument name changed from strm to stream
+            #Retry with old name.
+            #This is so that code can be used with older Python versions
+            #(e.g. by Django)
+            kwargs['strm'] = kwargs.pop('stream')
+            result = factory(**kwargs)
+        if formatter:
+            result.setFormatter(formatter)
+        if level is not None:
+            result.setLevel(_checkLevel(level))
+        if filters:
+            self.add_filters(result, filters)
+        return result
+
+    def add_handlers(self, logger, handlers):
+        """Add handlers to a logger from a list of names."""
+        for h in handlers:
+            try:
+                logger.addHandler(self.config['handlers'][h])
+            except StandardError, e:
+                raise ValueError('Unable to add handler %r: %s' % (h, e))
+
+    def common_logger_config(self, logger, config, incremental=False):
+        """
+        Perform configuration which is common to root and non-root loggers.
+        """
+        level = config.get('level', None)
+        if level is not None:
+            logger.setLevel(_checkLevel(level))
+        if not incremental:
+            #Remove any existing handlers
+            for h in logger.handlers[:]:
+                logger.removeHandler(h)
+            handlers = config.get('handlers', None)
+            if handlers:
+                self.add_handlers(logger, handlers)
+            filters = config.get('filters', None)
+            if filters:
+                self.add_filters(logger, filters)
+
+    def configure_logger(self, name, config, incremental=False):
+        """Configure a non-root logger from a dictionary."""
+        logger = logging.getLogger(name)
+        self.common_logger_config(logger, config, incremental)
+        propagate = config.get('propagate', None)
+        if propagate is not None:
+            logger.propagate = propagate
+
+    def configure_root(self, config, incremental=False):
+        """Configure a root logger from a dictionary."""
+        root = logging.getLogger()
+        self.common_logger_config(root, config, incremental)
+
+dictConfigClass = DictConfigurator
+
+def dictConfig(config):
+    """Configure logging using a dictionary."""
+    dictConfigClass(config).configure()
diff --git a/snf-common/synnefo/util/entry_points.py b/snf-common/synnefo/util/entry_points.py
new file mode 100644
index 0000000..b69348f
--- /dev/null
+++ b/snf-common/synnefo/util/entry_points.py
@@ -0,0 +1,44 @@
+import sys
+import pkg_resources
+import inspect
+
+
+def get_entry_ponts(ns, name):
+    for entry_point in pkg_resources.iter_entry_points(group=ns):
+        if entry_point.name == name:
+            yield entry_point
+
+
+def extend_settings(ns, module_name):
+    """
+    Extend module from entry_point hooks
+    """
+    settings = sys.modules[module_name]
+    # apps hook
+    app_entry_points = get_entry_ponts(ns, 'apps')
+    # settings hook
+    settings_entry_points = get_entry_ponts(ns, 'settings')
+
+    # extend INSTALLED_APPS setting
+    NEW_INSTALLED_APPS = list(getattr(settings, 'INSTALLED_APPS', []));
+
+    # if failed to execute app hook as function parse it as string
+    # synnefo.app:get_additional_apps or app1,app2,app3
+    for e in app_entry_points:
+        try:
+            NEW_INSTALLED_APPS = list(e.load()())
+        except Exception, e:
+            for e in app_entry_points:
+                NEW_INSTALLED_APPS = NEW_INSTALLED_APPS + \
+                        e.module_name.split(",")
+
+    # extend additional settings
+    # TODO: existing settings logic ??
+    for e in settings_entry_points:
+        module = e.load()
+        for k in dir(module):
+            if k == k.upper():
+                setattr(settings, k, getattr(module, k))
+
+    setattr(settings, 'INSTALLED_APPS', NEW_INSTALLED_APPS)
+
diff --git a/snf-common/synnefo/util/log.py b/snf-common/synnefo/util/log.py
new file mode 100644
index 0000000..fda844e
--- /dev/null
+++ b/snf-common/synnefo/util/log.py
@@ -0,0 +1,69 @@
+# 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 logging
+
+from synnefo import settings
+
+from synnefo.util.dictconfig import dictConfig
+
+
+class NullHandler(logging.Handler):
+    def emit(self, record):
+        pass
+
+logging.NullHandler = NullHandler
+
+
+def disable_unused_loggers():
+    """Disable handlers that are not used by any logger"""
+
+    logging = settings.LOGGING
+
+    active_handlers = set()
+    loggers = logging.get('loggers', {})
+    for logger in loggers.values():
+        active_handlers.update(logger.get('handlers', []))
+
+    handlers = logging.get('handlers', {})
+    for handler in handlers:
+        if handler not in active_handlers:
+            handlers[handler] = {'class': 'logging.NullHandler'}
+
+
+disable_unused_loggers()
+dictConfig(settings.LOGGING)
+
+
+def getLogger(name):
+    return logging.getLogger(name)
diff --git a/snf-common/synnefo/util/mac2eui64.py b/snf-common/synnefo/util/mac2eui64.py
new file mode 100755
index 0000000..7988575
--- /dev/null
+++ b/snf-common/synnefo/util/mac2eui64.py
@@ -0,0 +1,68 @@
+# 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 sys
+from IPy import IP
+
+# Adapted from NFDHCPD's mac2eui64 utility, repository commit:feca7bb95
+
+def mac2eui64(mac, prefixstr):
+    try:
+        prefix = IP(prefixstr)
+    except ValueError:
+        raise Exception("Invalid IPv6 prefix '%s'" % prefixstr)
+
+    if prefix.version() != 6:
+        raise Exception("%s is not a valid IPv6 prefix" % prefixstr)
+
+    if prefix.prefixlen() != 64:
+        raise Exception("Cannot generate an EUI-64 address on a non-64 subnet")
+
+    mac_parts = mac.split(":")
+    pfx_parts = prefix.net().strFullsize().split(":")
+
+    if len(mac_parts) != 6:
+        raise Exception("%s is not a valid MAC-48 address" % mac)
+
+    eui64 = mac_parts[:3] + [ "ff", "fe" ] + mac_parts[3:]
+
+    eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
+
+    ip = ":".join(pfx_parts[:4])
+    for l in range(0, len(eui64), 2):
+        ip += ":%s" % "".join(eui64[l:l+2])
+
+    return IP(ip).strCompressed()
+
+# vim: set ts=4 sts=4 sw=4 et ai :
diff --git a/snf-common/synnefo/util/rapi.py b/snf-common/synnefo/util/rapi.py
new file mode 100644
index 0000000..5d168ec
--- /dev/null
+++ b/snf-common/synnefo/util/rapi.py
@@ -0,0 +1,1618 @@
+#
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""Ganeti RAPI client.
+
+@attention: To use the RAPI client, the application B{must} call
+            C{pycurl.global_init} during initialization and
+            C{pycurl.global_cleanup} before exiting the process. This is very
+            important in multi-threaded programs. See curl_global_init(3) and
+            curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
+            can be used.
+
+"""
+
+# No Ganeti-specific modules should be imported. The RAPI client is supposed to
+# be standalone.
+
+import logging
+import socket
+import urllib
+import threading
+import pycurl
+
+try:
+  import simplejson as json
+except ImportError:
+  import json
+
+try:
+  from cStringIO import StringIO
+except ImportError:
+  from StringIO import StringIO
+
+
+GANETI_RAPI_PORT = 5080
+GANETI_RAPI_VERSION = 2
+
+HTTP_DELETE = "DELETE"
+HTTP_GET = "GET"
+HTTP_PUT = "PUT"
+HTTP_POST = "POST"
+HTTP_OK = 200
+HTTP_NOT_FOUND = 404
+HTTP_APP_JSON = "application/json"
+
+REPLACE_DISK_PRI = "replace_on_primary"
+REPLACE_DISK_SECONDARY = "replace_on_secondary"
+REPLACE_DISK_CHG = "replace_new_secondary"
+REPLACE_DISK_AUTO = "replace_auto"
+
+NODE_ROLE_DRAINED = "drained"
+NODE_ROLE_MASTER_CANDIATE = "master-candidate"
+NODE_ROLE_MASTER = "master"
+NODE_ROLE_OFFLINE = "offline"
+NODE_ROLE_REGULAR = "regular"
+
+# Internal constants
+_REQ_DATA_VERSION_FIELD = "__version__"
+_INST_CREATE_REQV1 = "instance-create-reqv1"
+_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
+_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"])
+_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
+_INST_CREATE_V0_PARAMS = frozenset([
+  "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
+  "hypervisor", "file_storage_dir", "file_driver", "dry_run",
+  ])
+_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
+
+# Older pycURL versions don't have all error constants
+try:
+  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
+  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
+except AttributeError:
+  _CURLE_SSL_CACERT = 60
+  _CURLE_SSL_CACERT_BADFILE = 77
+
+_CURL_SSL_CERT_ERRORS = frozenset([
+  _CURLE_SSL_CACERT,
+  _CURLE_SSL_CACERT_BADFILE,
+  ])
+
+
+class Error(Exception):
+  """Base error class for this module.
+
+  """
+  pass
+
+
+class CertificateError(Error):
+  """Raised when a problem is found with the SSL certificate.
+
+  """
+  pass
+
+
+class GanetiApiError(Error):
+  """Generic error raised from Ganeti API.
+
+  """
+  def __init__(self, msg, code=None):
+    Error.__init__(self, msg)
+    self.code = code
+
+
+def UsesRapiClient(fn):
+  """Decorator for code using RAPI client to initialize pycURL.
+
+  """
+  def wrapper(*args, **kwargs):
+    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
+    # one thread running. This check is just a safety measure -- it doesn't
+    # cover all cases.
+    assert threading.activeCount() == 1, \
+           "Found active threads when initializing pycURL"
+
+    pycurl.global_init(pycurl.GLOBAL_ALL)
+    try:
+      return fn(*args, **kwargs)
+    finally:
+      pycurl.global_cleanup()
+
+  return wrapper
+
+
+def GenericCurlConfig(verbose=False, use_signal=False,
+                      use_curl_cabundle=False, cafile=None, capath=None,
+                      proxy=None, verify_hostname=False,
+                      connect_timeout=None, timeout=None,
+                      _pycurl_version_fn=pycurl.version_info):
+  """Curl configuration function generator.
+
+  @type verbose: bool
+  @param verbose: Whether to set cURL to verbose mode
+  @type use_signal: bool
+  @param use_signal: Whether to allow cURL to use signals
+  @type use_curl_cabundle: bool
+  @param use_curl_cabundle: Whether to use cURL's default CA bundle
+  @type cafile: string
+  @param cafile: In which file we can find the certificates
+  @type capath: string
+  @param capath: In which directory we can find the certificates
+  @type proxy: string
+  @param proxy: Proxy to use, None for default behaviour and empty string for
+                disabling proxies (see curl_easy_setopt(3))
+  @type verify_hostname: bool
+  @param verify_hostname: Whether to verify the remote peer certificate's
+                          commonName
+  @type connect_timeout: number
+  @param connect_timeout: Timeout for establishing connection in seconds
+  @type timeout: number
+  @param timeout: Timeout for complete transfer in seconds (see
+                  curl_easy_setopt(3)).
+
+  """
+  if use_curl_cabundle and (cafile or capath):
+    raise Error("Can not use default CA bundle when CA file or path is set")
+
+  def _ConfigCurl(curl, logger):
+    """Configures a cURL object
+
+    @type curl: pycurl.Curl
+    @param curl: cURL object
+
+    """
+    logger.debug("Using cURL version %s", pycurl.version)
+
+    # pycurl.version_info returns a tuple with information about the used
+    # version of libcurl. Item 5 is the SSL library linked to it.
+    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
+    # 0, '1.2.3.3', ...)
+    sslver = _pycurl_version_fn()[5]
+    if not sslver:
+      raise Error("No SSL support in cURL")
+
+    lcsslver = sslver.lower()
+    if lcsslver.startswith("openssl/"):
+      pass
+    elif lcsslver.startswith("gnutls/"):
+      if capath:
+        raise Error("cURL linked against GnuTLS has no support for a"
+                    " CA path (%s)" % (pycurl.version, ))
+    else:
+      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
+                                sslver)
+
+    curl.setopt(pycurl.VERBOSE, verbose)
+    curl.setopt(pycurl.NOSIGNAL, not use_signal)
+
+    # Whether to verify remote peer's CN
+    if verify_hostname:
+      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
+      # certificate must indicate that the server is the server to which you
+      # meant to connect, or the connection fails. [...] When the value is 1,
+      # the certificate must contain a Common Name field, but it doesn't matter
+      # what name it says. [...]"
+      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
+    else:
+      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
+
+    if cafile or capath or use_curl_cabundle:
+      # Require certificates to be checked
+      curl.setopt(pycurl.SSL_VERIFYPEER, True)
+      if cafile:
+        curl.setopt(pycurl.CAINFO, str(cafile))
+      if capath:
+        curl.setopt(pycurl.CAPATH, str(capath))
+      # Not changing anything for using default CA bundle
+    else:
+      # Disable SSL certificate verification
+      curl.setopt(pycurl.SSL_VERIFYPEER, False)
+
+    if proxy is not None:
+      curl.setopt(pycurl.PROXY, str(proxy))
+
+    # Timeouts
+    if connect_timeout is not None:
+      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
+    if timeout is not None:
+      curl.setopt(pycurl.TIMEOUT, timeout)
+
+  return _ConfigCurl
+
+
+class GanetiRapiClient(object): # pylint: disable-msg=R0904
+  """Ganeti RAPI client.
+
+  """
+  USER_AGENT = "Ganeti RAPI Client"
+  _json_encoder = json.JSONEncoder(sort_keys=True)
+
+  def __init__(self, host, port=GANETI_RAPI_PORT,
+               username=None, password=None,
+               logger=logging.getLogger('synnefo.util'),
+               curl_config_fn=None, curl_factory=None):
+    """Initializes this class.
+
+    @type host: string
+    @param host: the ganeti cluster master to interact with
+    @type port: int
+    @param port: the port on which the RAPI is running (default is 5080)
+    @type username: string
+    @param username: the username to connect with
+    @type password: string
+    @param password: the password to connect with
+    @type curl_config_fn: callable
+    @param curl_config_fn: Function to configure C{pycurl.Curl} object
+    @param logger: Logging object
+
+    """
+    self._username = username
+    self._password = password
+    self._logger = logger
+    self._curl_config_fn = curl_config_fn
+    self._curl_factory = curl_factory
+
+    try:
+      socket.inet_pton(socket.AF_INET6, host)
+      address = "[%s]:%s" % (host, port)
+    except socket.error:
+      address = "%s:%s" % (host, port)
+
+    self._base_url = "https://%s" % address
+
+    if username is not None:
+      if password is None:
+        raise Error("Password not specified")
+    elif password:
+      raise Error("Specified password without username")
+
+  def _CreateCurl(self):
+    """Creates a cURL object.
+
+    """
+    # Create pycURL object if no factory is provided
+    if self._curl_factory:
+      curl = self._curl_factory()
+    else:
+      curl = pycurl.Curl()
+
+    # Default cURL settings
+    curl.setopt(pycurl.VERBOSE, False)
+    curl.setopt(pycurl.FOLLOWLOCATION, False)
+    curl.setopt(pycurl.MAXREDIRS, 5)
+    curl.setopt(pycurl.NOSIGNAL, True)
+    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
+    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
+    curl.setopt(pycurl.SSL_VERIFYPEER, False)
+    curl.setopt(pycurl.HTTPHEADER, [
+      "Accept: %s" % HTTP_APP_JSON,
+      "Content-type: %s" % HTTP_APP_JSON,
+      ])
+
+    assert ((self._username is None and self._password is None) ^
+            (self._username is not None and self._password is not None))
+
+    if self._username:
+      # Setup authentication
+      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
+      curl.setopt(pycurl.USERPWD,
+                  str("%s:%s" % (self._username, self._password)))
+
+    # Call external configuration function
+    if self._curl_config_fn:
+      self._curl_config_fn(curl, self._logger)
+
+    return curl
+
+  @staticmethod
+  def _EncodeQuery(query):
+    """Encode query values for RAPI URL.
+
+    @type query: list of two-tuples
+    @param query: Query arguments
+    @rtype: list
+    @return: Query list with encoded values
+
+    """
+    result = []
+
+    for name, value in query:
+      if value is None:
+        result.append((name, ""))
+
+      elif isinstance(value, bool):
+        # Boolean values must be encoded as 0 or 1
+        result.append((name, int(value)))
+
+      elif isinstance(value, (list, tuple, dict)):
+        raise ValueError("Invalid query data type %r" % type(value).__name__)
+
+      else:
+        result.append((name, value))
+
+    return result
+
+  def _SendRequest(self, method, path, query, content):
+    """Sends an HTTP request.
+
+    This constructs a full URL, encodes and decodes HTTP bodies, and
+    handles invalid responses in a pythonic way.
+
+    @type method: string
+    @param method: HTTP method to use
+    @type path: string
+    @param path: HTTP URL path
+    @type query: list of two-tuples
+    @param query: query arguments to pass to urllib.urlencode
+    @type content: str or None
+    @param content: HTTP body content
+
+    @rtype: str
+    @return: JSON-Decoded response
+
+    @raises CertificateError: If an invalid SSL certificate is found
+    @raises GanetiApiError: If an invalid response is returned
+
+    """
+    assert path.startswith("/")
+
+    curl = self._CreateCurl()
+
+    if content is not None:
+      encoded_content = self._json_encoder.encode(content)
+    else:
+      encoded_content = ""
+
+    # Build URL
+    urlparts = [self._base_url, path]
+    if query:
+      urlparts.append("?")
+      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
+
+    url = "".join(urlparts)
+
+    self._logger.debug("Sending request %s %s (content=%r)",
+                       method, url, encoded_content)
+
+    # Buffer for response
+    encoded_resp_body = StringIO()
+
+    # Configure cURL
+    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
+    curl.setopt(pycurl.URL, str(url))
+    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
+    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
+
+    try:
+      # Send request and wait for response
+      try:
+        curl.perform()
+      except pycurl.error, err:
+        if err.args[0] in _CURL_SSL_CERT_ERRORS:
+          raise CertificateError("SSL certificate error %s" % err)
+
+        raise GanetiApiError(str(err))
+    finally:
+      # Reset settings to not keep references to large objects in memory
+      # between requests
+      curl.setopt(pycurl.POSTFIELDS, "")
+      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
+
+    # Get HTTP response code
+    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
+
+    # Was anything written to the response buffer?
+    if encoded_resp_body.tell():
+      response_content = json.loads(encoded_resp_body.getvalue())
+    else:
+      response_content = None
+
+    if http_code != HTTP_OK:
+      if isinstance(response_content, dict):
+        msg = ("%s %s: %s" %
+               (response_content["code"],
+                response_content["message"],
+                response_content["explain"]))
+      else:
+        msg = str(response_content)
+
+      raise GanetiApiError(msg, code=http_code)
+
+    return response_content
+
+  def GetVersion(self):
+    """Gets the Remote API version running on the cluster.
+
+    @rtype: int
+    @return: Ganeti Remote API version
+
+    """
+    return self._SendRequest(HTTP_GET, "/version", None, None)
+
+  def GetFeatures(self):
+    """Gets the list of optional features supported by RAPI server.
+
+    @rtype: list
+    @return: List of optional features
+
+    """
+    try:
+      return self._SendRequest(HTTP_GET, "/%s/features" % GANETI_RAPI_VERSION,
+                               None, None)
+    except GanetiApiError, err:
+      # Older RAPI servers don't support this resource
+      if err.code == HTTP_NOT_FOUND:
+        return []
+
+      raise
+
+  def GetOperatingSystems(self):
+    """Gets the Operating Systems running in the Ganeti cluster.
+
+    @rtype: list of str
+    @return: operating systems
+
+    """
+    return self._SendRequest(HTTP_GET, "/%s/os" % GANETI_RAPI_VERSION,
+                             None, None)
+
+  def GetInfo(self):
+    """Gets info about the cluster.
+
+    @rtype: dict
+    @return: information about the cluster
+
+    """
+    return self._SendRequest(HTTP_GET, "/%s/info" % GANETI_RAPI_VERSION,
+                             None, None)
+
+  def RedistributeConfig(self):
+    """Tells the cluster to redistribute its configuration files.
+
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             "/%s/redistribute-config" % GANETI_RAPI_VERSION,
+                             None, None)
+
+  def ModifyCluster(self, **kwargs):
+    """Modifies cluster parameters.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @rtype: int
+    @return: job id
+
+    """
+    body = kwargs
+
+    return self._SendRequest(HTTP_PUT,
+                             "/%s/modify" % GANETI_RAPI_VERSION, None, body)
+
+  def GetClusterTags(self):
+    """Gets the cluster tags.
+
+    @rtype: list of str
+    @return: cluster tags
+
+    """
+    return self._SendRequest(HTTP_GET, "/%s/tags" % GANETI_RAPI_VERSION,
+                             None, None)
+
+  def AddClusterTags(self, tags, dry_run=False):
+    """Adds tags to the cluster.
+
+    @type tags: list of str
+    @param tags: tags to add to the cluster
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
+                             query, None)
+
+  def DeleteClusterTags(self, tags, dry_run=False):
+    """Deletes tags from the cluster.
+
+    @type tags: list of str
+    @param tags: tags to delete
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
+                             query, None)
+
+  def GetInstances(self, bulk=False):
+    """Gets information about instances on the cluster.
+
+    @type bulk: bool
+    @param bulk: whether to return all information about all instances
+
+    @rtype: list of dict or list of str
+    @return: if bulk is True, info about the instances, else a list of instances
+
+    """
+    query = []
+    if bulk:
+      query.append(("bulk", 1))
+
+    instances = self._SendRequest(HTTP_GET,
+                                  "/%s/instances" % GANETI_RAPI_VERSION,
+                                  query, None)
+    if bulk:
+      return instances
+    else:
+      return [i["id"] for i in instances]
+
+  def GetInstance(self, instance):
+    """Gets information about an instance.
+
+    @type instance: str
+    @param instance: instance whose info to return
+
+    @rtype: dict
+    @return: info about the instance
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/instances/%s" %
+                              (GANETI_RAPI_VERSION, instance)), None, None)
+
+  def GetInstanceInfo(self, instance, static=None):
+    """Gets information about an instance.
+
+    @type instance: string
+    @param instance: Instance name
+    @rtype: string
+    @return: Job ID
+
+    """
+    if static is not None:
+      query = [("static", static)]
+    else:
+      query = None
+
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/instances/%s/info" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def CreateInstance(self, mode, name, disk_template, disks, nics,
+                     **kwargs):
+    """Creates a new instance.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type mode: string
+    @param mode: Instance creation mode
+    @type name: string
+    @param name: Hostname of the instance to create
+    @type disk_template: string
+    @param disk_template: Disk template for instance (e.g. plain, diskless,
+                          file, or drbd)
+    @type disks: list of dicts
+    @param disks: List of disk definitions
+    @type nics: list of dicts
+    @param nics: List of NIC definitions
+    @type dry_run: bool
+    @keyword dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+
+    if kwargs.get("dry_run"):
+      query.append(("dry-run", 1))
+
+    if _INST_CREATE_REQV1 in self.GetFeatures():
+      # All required fields for request data version 1
+      body = {
+        _REQ_DATA_VERSION_FIELD: 1,
+        "mode": mode,
+        "name": name,
+        "disk_template": disk_template,
+        "disks": disks,
+        "nics": nics,
+        }
+
+      conflicts = set(kwargs.iterkeys()) & set(body.iterkeys())
+      if conflicts:
+        raise GanetiApiError("Required fields can not be specified as"
+                             " keywords: %s" % ", ".join(conflicts))
+
+      body.update((key, value) for key, value in kwargs.iteritems()
+                  if key != "dry_run")
+    else:
+      # Old request format (version 0)
+
+      # The following code must make sure that an exception is raised when an
+      # unsupported setting is requested by the caller. Otherwise this can lead
+      # to bugs difficult to find. The interface of this function must stay
+      # exactly the same for version 0 and 1 (e.g. they aren't allowed to
+      # require different data types).
+
+      # Validate disks
+      for idx, disk in enumerate(disks):
+        unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS
+        if unsupported:
+          raise GanetiApiError("Server supports request version 0 only, but"
+                               " disk %s specifies the unsupported parameters"
+                               " %s, allowed are %s" %
+                               (idx, unsupported,
+                                list(_INST_CREATE_V0_DISK_PARAMS)))
+
+      assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and
+              "size" in _INST_CREATE_V0_DISK_PARAMS)
+      disk_sizes = [disk["size"] for disk in disks]
+
+      # Validate NICs
+      if not nics:
+        raise GanetiApiError("Server supports request version 0 only, but"
+                             " no NIC specified")
+      elif len(nics) > 1:
+        raise GanetiApiError("Server supports request version 0 only, but"
+                             " more than one NIC specified")
+
+      assert len(nics) == 1
+
+      unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS
+      if unsupported:
+        raise GanetiApiError("Server supports request version 0 only, but"
+                             " NIC 0 specifies the unsupported parameters %s,"
+                             " allowed are %s" %
+                             (unsupported, list(_INST_NIC_PARAMS)))
+
+      # Validate other parameters
+      unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS -
+                     _INST_CREATE_V0_DPARAMS)
+      if unsupported:
+        allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS)
+        raise GanetiApiError("Server supports request version 0 only, but"
+                             " the following unsupported parameters are"
+                             " specified: %s, allowed are %s" %
+                             (unsupported, list(allowed)))
+
+      # All required fields for request data version 0
+      body = {
+        _REQ_DATA_VERSION_FIELD: 0,
+        "name": name,
+        "disk_template": disk_template,
+        "disks": disk_sizes,
+        }
+
+      # NIC fields
+      assert len(nics) == 1
+      assert not (set(body.keys()) & set(nics[0].keys()))
+      body.update(nics[0])
+
+      # Copy supported fields
+      assert not (set(body.keys()) & set(kwargs.keys()))
+      body.update(dict((key, value) for key, value in kwargs.items()
+                       if key in _INST_CREATE_V0_PARAMS))
+
+      # Merge dictionaries
+      for i in (value for key, value in kwargs.items()
+                if key in _INST_CREATE_V0_DPARAMS):
+        assert not (set(body.keys()) & set(i.keys()))
+        body.update(i)
+
+      assert not (set(kwargs.keys()) -
+                  (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS))
+      assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS)
+
+    return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION,
+                             query, body)
+
+  def DeleteInstance(self, instance, dry_run=False):
+    """Deletes an instance.
+
+    @type instance: str
+    @param instance: the instance to delete
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/instances/%s" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def ModifyInstance(self, instance, **kwargs):
+    """Modifies an instance.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type instance: string
+    @param instance: Instance name
+    @rtype: int
+    @return: job id
+
+    """
+    body = kwargs
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/modify" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
+  def ActivateInstanceDisks(self, instance, ignore_size=None):
+    """Activates an instance's disks.
+
+    @type instance: string
+    @param instance: Instance name
+    @type ignore_size: bool
+    @param ignore_size: Whether to ignore recorded size
+    @return: job id
+
+    """
+    query = []
+    if ignore_size:
+      query.append(("ignore_size", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/activate-disks" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def DeactivateInstanceDisks(self, instance):
+    """Deactivates an instance's disks.
+
+    @type instance: string
+    @param instance: Instance name
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/deactivate-disks" %
+                              (GANETI_RAPI_VERSION, instance)), None, None)
+
+  def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
+    """Grows a disk of an instance.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type instance: string
+    @param instance: Instance name
+    @type disk: integer
+    @param disk: Disk index
+    @type amount: integer
+    @param amount: Grow disk by this amount (MiB)
+    @type wait_for_sync: bool
+    @param wait_for_sync: Wait for disk to synchronize
+    @rtype: int
+    @return: job id
+
+    """
+    body = {
+      "amount": amount,
+      }
+
+    if wait_for_sync is not None:
+      body["wait_for_sync"] = wait_for_sync
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/instances/%s/disk/%s/grow" %
+                              (GANETI_RAPI_VERSION, instance, disk)),
+                             None, body)
+
+  def GetInstanceTags(self, instance):
+    """Gets tags for an instance.
+
+    @type instance: str
+    @param instance: instance whose tags to return
+
+    @rtype: list of str
+    @return: tags for the instance
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/instances/%s/tags" %
+                              (GANETI_RAPI_VERSION, instance)), None, None)
+
+  def AddInstanceTags(self, instance, tags, dry_run=False):
+    """Adds tags to an instance.
+
+    @type instance: str
+    @param instance: instance to add tags to
+    @type tags: list of str
+    @param tags: tags to add to the instance
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/tags" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def DeleteInstanceTags(self, instance, tags, dry_run=False):
+    """Deletes tags from an instance.
+
+    @type instance: str
+    @param instance: instance to delete tags from
+    @type tags: list of str
+    @param tags: tags to delete
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/instances/%s/tags" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
+                     dry_run=False):
+    """Reboots an instance.
+
+    @type instance: str
+    @param instance: instance to rebot
+    @type reboot_type: str
+    @param reboot_type: one of: hard, soft, full
+    @type ignore_secondaries: bool
+    @param ignore_secondaries: if True, ignores errors for the secondary node
+        while re-assembling disks (in hard-reboot mode only)
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    """
+    query = []
+    if reboot_type:
+      query.append(("type", reboot_type))
+    if ignore_secondaries is not None:
+      query.append(("ignore_secondaries", ignore_secondaries))
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/instances/%s/reboot" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def ShutdownInstance(self, instance, dry_run=False):
+    """Shuts down an instance.
+
+    @type instance: str
+    @param instance: the instance to shut down
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/shutdown" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def StartupInstance(self, instance, dry_run=False):
+    """Starts up an instance.
+
+    @type instance: str
+    @param instance: the instance to start up
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/startup" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def ReinstallInstance(self, instance, os=None, no_startup=False,
+                        osparams=None):
+    """Reinstalls an instance.
+
+    @type instance: str
+    @param instance: The instance to reinstall
+    @type os: str or None
+    @param os: The operating system to reinstall. If None, the instance's
+        current operating system will be installed again
+    @type no_startup: bool
+    @param no_startup: Whether to start the instance automatically
+
+    """
+    if _INST_REINSTALL_REQV1 in self.GetFeatures():
+      body = {
+        "start": not no_startup,
+        }
+      if os is not None:
+        body["os"] = os
+      if osparams is not None:
+        body["osparams"] = osparams
+      return self._SendRequest(HTTP_POST,
+                               ("/%s/instances/%s/reinstall" %
+                                (GANETI_RAPI_VERSION, instance)), None, body)
+
+    # Use old request format
+    if osparams:
+      raise GanetiApiError("Server does not support specifying OS parameters"
+                           " for instance reinstallation")
+
+    query = []
+    if os:
+      query.append(("os", os))
+    if no_startup:
+      query.append(("nostartup", 1))
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/instances/%s/reinstall" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
+                           remote_node=None, iallocator=None, dry_run=False):
+    """Replaces disks on an instance.
+
+    @type instance: str
+    @param instance: instance whose disks to replace
+    @type disks: list of ints
+    @param disks: Indexes of disks to replace
+    @type mode: str
+    @param mode: replacement mode to use (defaults to replace_auto)
+    @type remote_node: str or None
+    @param remote_node: new secondary node to use (for use with
+        replace_new_secondary mode)
+    @type iallocator: str or None
+    @param iallocator: instance allocator plugin to use (for use with
+                       replace_auto mode)
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [
+      ("mode", mode),
+      ]
+
+    if disks:
+      query.append(("disks", ",".join(str(idx) for idx in disks)))
+
+    if remote_node:
+      query.append(("remote_node", remote_node))
+
+    if iallocator:
+      query.append(("iallocator", iallocator))
+
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/instances/%s/replace-disks" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def PrepareExport(self, instance, mode):
+    """Prepares an instance for an export.
+
+    @type instance: string
+    @param instance: Instance name
+    @type mode: string
+    @param mode: Export mode
+    @rtype: string
+    @return: Job ID
+
+    """
+    query = [("mode", mode)]
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/prepare-export" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def ExportInstance(self, instance, mode, destination, shutdown=None,
+                     remove_instance=None,
+                     x509_key_name=None, destination_x509_ca=None):
+    """Exports an instance.
+
+    @type instance: string
+    @param instance: Instance name
+    @type mode: string
+    @param mode: Export mode
+    @rtype: string
+    @return: Job ID
+
+    """
+    body = {
+      "destination": destination,
+      "mode": mode,
+      }
+
+    if shutdown is not None:
+      body["shutdown"] = shutdown
+
+    if remove_instance is not None:
+      body["remove_instance"] = remove_instance
+
+    if x509_key_name is not None:
+      body["x509_key_name"] = x509_key_name
+
+    if destination_x509_ca is not None:
+      body["destination_x509_ca"] = destination_x509_ca
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/export" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
+  def MigrateInstance(self, instance, mode=None, cleanup=None):
+    """Migrates an instance.
+
+    @type instance: string
+    @param instance: Instance name
+    @type mode: string
+    @param mode: Migration mode
+    @type cleanup: bool
+    @param cleanup: Whether to clean up a previously failed migration
+
+    """
+    body = {}
+
+    if mode is not None:
+      body["mode"] = mode
+
+    if cleanup is not None:
+      body["cleanup"] = cleanup
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/migrate" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
+  def RenameInstance(self, instance, new_name, ip_check=None, name_check=None):
+    """Changes the name of an instance.
+
+    @type instance: string
+    @param instance: Instance name
+    @type new_name: string
+    @param new_name: New instance name
+    @type ip_check: bool
+    @param ip_check: Whether to ensure instance's IP address is inactive
+    @type name_check: bool
+    @param name_check: Whether to ensure instance's name is resolvable
+
+    """
+    body = {
+      "new_name": new_name,
+      }
+
+    if ip_check is not None:
+      body["ip_check"] = ip_check
+
+    if name_check is not None:
+      body["name_check"] = name_check
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/rename" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
+  def GetInstanceConsole(self, instance):
+    """Request information for connecting to instance's console.
+
+    @type instance: string
+    @param instance: Instance name
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/instances/%s/console" %
+                              (GANETI_RAPI_VERSION, instance)), None, None)
+
+  def GetJobs(self):
+    """Gets all jobs for the cluster.
+
+    @rtype: list of int
+    @return: job ids for the cluster
+
+    """
+    return [int(j["id"])
+            for j in self._SendRequest(HTTP_GET,
+                                       "/%s/jobs" % GANETI_RAPI_VERSION,
+                                       None, None)]
+
+  def GetJobStatus(self, job_id):
+    """Gets the status of a job.
+
+    @type job_id: int
+    @param job_id: job id whose status to query
+
+    @rtype: dict
+    @return: job status
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
+                             None, None)
+
+  def WaitForJobChange(self, job_id, fields, prev_job_info, prev_log_serial):
+    """Waits for job changes.
+
+    @type job_id: int
+    @param job_id: Job ID for which to wait
+
+    """
+    body = {
+      "fields": fields,
+      "previous_job_info": prev_job_info,
+      "previous_log_serial": prev_log_serial,
+      }
+
+    return self._SendRequest(HTTP_GET,
+                             "/%s/jobs/%s/wait" % (GANETI_RAPI_VERSION, job_id),
+                             None, body)
+
+  def CancelJob(self, job_id, dry_run=False):
+    """Cancels a job.
+
+    @type job_id: int
+    @param job_id: id of the job to delete
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
+                             query, None)
+
+  def GetNodes(self, bulk=False):
+    """Gets all nodes in the cluster.
+
+    @type bulk: bool
+    @param bulk: whether to return all information about all instances
+
+    @rtype: list of dict or str
+    @return: if bulk is true, info about nodes in the cluster,
+        else list of nodes in the cluster
+
+    """
+    query = []
+    if bulk:
+      query.append(("bulk", 1))
+
+    nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
+                              query, None)
+    if bulk:
+      return nodes
+    else:
+      return [n["id"] for n in nodes]
+
+  def GetNode(self, node):
+    """Gets information about a node.
+
+    @type node: str
+    @param node: node whose info to return
+
+    @rtype: dict
+    @return: info about the node
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             "/%s/nodes/%s" % (GANETI_RAPI_VERSION, node),
+                             None, None)
+
+  def EvacuateNode(self, node, iallocator=None, remote_node=None,
+                   dry_run=False, early_release=False):
+    """Evacuates instances from a Ganeti node.
+
+    @type node: str
+    @param node: node to evacuate
+    @type iallocator: str or None
+    @param iallocator: instance allocator to use
+    @type remote_node: str
+    @param remote_node: node to evaucate to
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+    @type early_release: bool
+    @param early_release: whether to enable parallelization
+
+    @rtype: list
+    @return: list of (job ID, instance name, new secondary node); if
+        dry_run was specified, then the actual move jobs were not
+        submitted and the job IDs will be C{None}
+
+    @raises GanetiApiError: if an iallocator and remote_node are both
+        specified
+
+    """
+    if iallocator and remote_node:
+      raise GanetiApiError("Only one of iallocator or remote_node can be used")
+
+    query = []
+    if iallocator:
+      query.append(("iallocator", iallocator))
+    if remote_node:
+      query.append(("remote_node", remote_node))
+    if dry_run:
+      query.append(("dry-run", 1))
+    if early_release:
+      query.append(("early_release", 1))
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/nodes/%s/evacuate" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def MigrateNode(self, node, mode=None, dry_run=False):
+    """Migrates all primary instances from a node.
+
+    @type node: str
+    @param node: node to migrate
+    @type mode: string
+    @param mode: if passed, it will overwrite the live migration type,
+        otherwise the hypervisor default will be used
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+    if mode is not None:
+      query.append(("mode", mode))
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/nodes/%s/migrate" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def GetNodeRole(self, node):
+    """Gets the current role for a node.
+
+    @type node: str
+    @param node: node whose role to return
+
+    @rtype: str
+    @return: the current role for a node
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/nodes/%s/role" %
+                              (GANETI_RAPI_VERSION, node)), None, None)
+
+  def SetNodeRole(self, node, role, force=False):
+    """Sets the role for a node.
+
+    @type node: str
+    @param node: the node whose role to set
+    @type role: str
+    @param role: the role to set for the node
+    @type force: bool
+    @param force: whether to force the role change
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [
+      ("force", force),
+      ]
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/nodes/%s/role" %
+                              (GANETI_RAPI_VERSION, node)), query, role)
+
+  def GetNodeStorageUnits(self, node, storage_type, output_fields):
+    """Gets the storage units for a node.
+
+    @type node: str
+    @param node: the node whose storage units to return
+    @type storage_type: str
+    @param storage_type: storage type whose units to return
+    @type output_fields: str
+    @param output_fields: storage type fields to return
+
+    @rtype: int
+    @return: job id where results can be retrieved
+
+    """
+    query = [
+      ("storage_type", storage_type),
+      ("output_fields", output_fields),
+      ]
+
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/nodes/%s/storage" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=None):
+    """Modifies parameters of storage units on the node.
+
+    @type node: str
+    @param node: node whose storage units to modify
+    @type storage_type: str
+    @param storage_type: storage type whose units to modify
+    @type name: str
+    @param name: name of the storage unit
+    @type allocatable: bool or None
+    @param allocatable: Whether to set the "allocatable" flag on the storage
+                        unit (None=no modification, True=set, False=unset)
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [
+      ("storage_type", storage_type),
+      ("name", name),
+      ]
+
+    if allocatable is not None:
+      query.append(("allocatable", allocatable))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/nodes/%s/storage/modify" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def RepairNodeStorageUnits(self, node, storage_type, name):
+    """Repairs a storage unit on the node.
+
+    @type node: str
+    @param node: node whose storage units to repair
+    @type storage_type: str
+    @param storage_type: storage type to repair
+    @type name: str
+    @param name: name of the storage unit to repair
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [
+      ("storage_type", storage_type),
+      ("name", name),
+      ]
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/nodes/%s/storage/repair" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def GetNodeTags(self, node):
+    """Gets the tags for a node.
+
+    @type node: str
+    @param node: node whose tags to return
+
+    @rtype: list of str
+    @return: tags for the node
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             ("/%s/nodes/%s/tags" %
+                              (GANETI_RAPI_VERSION, node)), None, None)
+
+  def AddNodeTags(self, node, tags, dry_run=False):
+    """Adds tags to a node.
+
+    @type node: str
+    @param node: node to add tags to
+    @type tags: list of str
+    @param tags: tags to add to the node
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/nodes/%s/tags" %
+                              (GANETI_RAPI_VERSION, node)), query, tags)
+
+  def DeleteNodeTags(self, node, tags, dry_run=False):
+    """Delete tags from a node.
+
+    @type node: str
+    @param node: node to remove tags from
+    @type tags: list of str
+    @param tags: tags to remove from the node
+    @type dry_run: bool
+    @param dry_run: whether to perform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = [("tag", t) for t in tags]
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/nodes/%s/tags" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def GetGroups(self, bulk=False):
+    """Gets all node groups in the cluster.
+
+    @type bulk: bool
+    @param bulk: whether to return all information about the groups
+
+    @rtype: list of dict or str
+    @return: if bulk is true, a list of dictionaries with info about all node
+        groups in the cluster, else a list of names of those node groups
+
+    """
+    query = []
+    if bulk:
+      query.append(("bulk", 1))
+
+    groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
+                               query, None)
+    if bulk:
+      return groups
+    else:
+      return [g["name"] for g in groups]
+
+  def GetGroup(self, group):
+    """Gets information about a node group.
+
+    @type group: str
+    @param group: name of the node group whose info to return
+
+    @rtype: dict
+    @return: info about the node group
+
+    """
+    return self._SendRequest(HTTP_GET,
+                             "/%s/groups/%s" % (GANETI_RAPI_VERSION, group),
+                             None, None)
+
+  def CreateGroup(self, name, alloc_policy=None, dry_run=False):
+    """Creates a new node group.
+
+    @type name: str
+    @param name: the name of node group to create
+    @type alloc_policy: str
+    @param alloc_policy: the desired allocation policy for the group, if any
+    @type dry_run: bool
+    @param dry_run: whether to peform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    body = {
+      "name": name,
+      "alloc_policy": alloc_policy
+      }
+
+    return self._SendRequest(HTTP_POST, "/%s/groups" % GANETI_RAPI_VERSION,
+                             query, body)
+
+  def ModifyGroup(self, group, **kwargs):
+    """Modifies a node group.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type group: string
+    @param group: Node group name
+    @rtype: int
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/modify" %
+                              (GANETI_RAPI_VERSION, group)), None, kwargs)
+
+  def DeleteGroup(self, group, dry_run=False):
+    """Deletes a node group.
+
+    @type group: str
+    @param group: the node group to delete
+    @type dry_run: bool
+    @param dry_run: whether to peform a dry run
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    return self._SendRequest(HTTP_DELETE,
+                             ("/%s/groups/%s" %
+                              (GANETI_RAPI_VERSION, group)), query, None)
+
+  def RenameGroup(self, group, new_name):
+    """Changes the name of a node group.
+
+    @type group: string
+    @param group: Node group name
+    @type new_name: string
+    @param new_name: New node group name
+
+    @rtype: int
+    @return: job id
+
+    """
+    body = {
+      "new_name": new_name,
+      }
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/rename" %
+                              (GANETI_RAPI_VERSION, group)), None, body)
+
+
+  def AssignGroupNodes(self, group, nodes, force=False, dry_run=False):
+    """Assigns nodes to a group.
+
+    @type group: string
+    @param group: Node gropu name
+    @type nodes: list of strings
+    @param nodes: List of nodes to assign to the group
+
+    @rtype: int
+    @return: job id
+
+    """
+    query = []
+
+    if force:
+      query.append(("force", 1))
+
+    if dry_run:
+      query.append(("dry-run", 1))
+
+    body = {
+      "nodes": nodes,
+      }
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/groups/%s/assign-nodes" %
+                             (GANETI_RAPI_VERSION, group)), query, body)
diff --git a/snf-common/synnefo/version.py b/snf-common/synnefo/version.py
new file mode 100644
index 0000000..89d07d1
--- /dev/null
+++ b/snf-common/synnefo/version.py
@@ -0,0 +1,57 @@
+import pkg_resources
+
+
+def get_dist_from_module(modname):
+    pkgroot = pkg_resources.get_provider(modname).egg_root
+    return list(pkg_resources.find_distributions(pkgroot))[0]
+
+
+def get_dist(dist_name):
+    return pkg_resouces.get_distribution(dist_name)
+
+
+def get_dist_version(dist_name):
+    """
+    Get the version for the specified distribution name
+    """
+    try:
+        return get_dist(dist_name).version
+    except Exception, e:
+        return 'unknown'
+
+
+def get_component_version(modname):
+    """
+    Return the version of a synnefo module/package based on its
+    corresponding distributed package version
+    """
+    try:
+        return get_dist_from_module(modname).version
+    except Exception, e:
+        return 'unknown'
+
+
+def vcs_info():
+    """
+    Return current git HEAD commit information
+    """
+    import subprocess
+    callgit = lambda(cmd): subprocess.Popen(
+            ['/bin/sh', '-c', cmd],
+            stdout=subprocess.PIPE).communicate()[0].strip()
+
+    branch = callgit('git branch | grep -Ei "\* (.*)" | cut -f2 -d" "')
+    revid = callgit("git --no-pager log --max-count=1 | cut -f2 -d' ' | head -1")
+    revno = callgit('git --no-pager log --oneline | wc -l')
+    desc = callgit('git describe')
+
+    return branch, revid, revno, desc
+
+
+def vcs_version():
+    """
+    Package version based on `git describe`, compatible with setuptools
+    version format
+    """
+    return vcs_info()[3].replace('v', '')
+
diff --git a/snf-ganeti-tools/setup.py b/snf-ganeti-tools/setup.py
index e3ae556..4c93209 100644
--- a/snf-ganeti-tools/setup.py
+++ b/snf-ganeti-tools/setup.py
@@ -1,10 +1,11 @@
 #!/usr/bin/env python
 
 from setuptools import setup
+from synnefo.version import vcs_version
 
 setup(
     name="snf-ganeti-tools",
-    version="0.7.1",
+    version=vcs_version(),
     description="Synnefo Ganeti supplementary tools",
     author="Synnefo Development Team",
     author_email="synnefo@lists.grnet.gr",
@@ -17,7 +18,6 @@ setup(
         'pyinotify',
         'amqplib',
         'prctl',
-        'ganeti',
     ],
     scripts=['snf-ganeti-eventd.py', 'snf-ganeti-hook.py',
              'snf-progress-monitor.py'],
