Source code for cnxdb.init.main
# -*- coding: utf-8 -*-
import os
import logging
import sys
import warnings
import psycopg2
from . import exceptions
from .manifest import get_schema
here = os.path.abspath(os.path.dirname(__file__))
SCHEMA_DIR = os.path.join(here, '..', 'schema')
logger = logging.getLogger('cnxdb')
SCHEMA_INITIALIZED_CHECK = """\
DO LANGUAGE plpgsql
$$
BEGIN
IF EXISTS (SELECT 1 FROM pg_class WHERE relname='modules') THEN
RAISE EXCEPTION USING MESSAGE = 'Database schema is initialized.';
END IF;
END;
$$;
"""
def _has_schema(cursor):
"""Checks for the existence of the database schema.
Returns a boolean to indicate if the database schema exists or not.
"""
try:
cursor.execute(SCHEMA_INITIALIZED_CHECK)
except psycopg2.InternalError as exc:
if 'Database schema is initialized.' in exc.args[0]:
return True
else: # pragma: no cover
return False
else:
return False
[docs]def init_db(engine, as_venv_importable=False):
"""Initialize the database schema, including tables, functions
and triggers.
:param engine: The database connection engine
:type engine: sqlalchemy.engine.Engine
:param bool as_venv_importable: Flag to trigger
the use of :func:`init_venv` from this function
:return: None
"""
conn = engine.raw_connection()
with conn.cursor() as cursor:
if _has_schema(cursor):
raise exceptions.DBSchemaInitialized()
for schema_part in get_schema(SCHEMA_DIR):
cursor.execute(schema_part)
conn.commit()
conn.close()
if as_venv_importable:
init_venv(engine)
ACTIVATE_VENV_SQL_FUNCTION = """\
CREATE FUNCTION venv.activate_venv()
RETURNS void LANGUAGE plpythonu AS $_$
import sys
import os
import site
old_os_path = os.environ.get('PATH','')
os.environ['PATH'] = os.path.dirname(os.path.abspath('{activate_path}')) \
+ os.pathsep + old_os_path
base = os.path.dirname(os.path.dirname(os.path.abspath('{activate_path}')))
site_packages = os.path.join(base, 'lib', 'python{sys_version}', \
'site-packages')
prev_sys_path = list(sys.path)
site.addsitedir(site_packages)
sys.real_prefix = sys.prefix
sys.prefix = base
# Move the added items to the front of the path:
new_sys_path = []
for item in list(sys.path):
if item not in prev_sys_path:
new_sys_path.append(item)
sys.path.remove(item)
sys.path[:0] = new_sys_path
$_$"""
def _is_localhost_connection(db_connection):
"""Given a database connection, check this is a connection to localhost.
"""
# If you are connecting to a database that is not localhost,
# don't initalize with virtualenv
db_dict = dict(p.split('=') for p in db_connection.dsn.split())
return db_dict.get('host', 'localhost') != 'localhost'
[docs]def init_venv(engine):
"""Sets up the database's plpython environment
for use with a python virtual environment (aka venv).
This function can only be run on the same system as the database
or on a system with an identical path to a compatible python venv.
The results of this is that plpythonu interpreters will execute
with the contents of the venv's site-packages available
for use rather than use the default system python environment only.
:param engine: The database connection engine
:type engine: sqlalchemy.engine.Engine
:return: None
"""
# If virtualenv is active, use that for postgres
if hasattr(sys, 'real_prefix'): # attr is only present within a venv
activate_path = os.path.join(os.path.realpath(sys.prefix),
'bin/activate_this.py')
else: # pragma: no cover
return
conn = engine.raw_connection()
if _is_localhost_connection(conn): # pragma: no cover
warnings.warn("An attempt to use ``init_venv`` was made, "
"but not on the same host as the postgres service.")
with conn.cursor() as cursor:
cursor.execute("SELECT current_database();")
db_name = cursor.fetchone()[0]
cursor.execute("SELECT schema_name "
"FROM information_schema.schemata "
"WHERE schema_name = 'venv';")
try:
# Does the schema already exist?
cursor.fetchone()[0]
except TypeError:
cursor.execute("CREATE SCHEMA venv")
try:
cursor.execute("SAVEPOINT session_preload")
cursor.execute("LOAD 'session_exec.so'")
cursor.execute("ALTER DATABASE \"{}\" SET "
"session_preload_libraries ="
"'session_exec'".format(db_name))
except psycopg2.ProgrammingError as e: # pragma: no cover
if e.message.startswith(
'unrecognized configuration parameter'):
cursor.execute("ROLLBACK TO SAVEPOINT "
"session_preload")
logger.warning("Postgresql < 9.4: make sure "
"to set "
"'local_preload_libraries "
"= session_exec' in "
"postgresql.conf and restart")
else: # pragma: no cover
raise
except psycopg2.OperationalError as e: # pragma: no cover
if 'could not access file "session_exec' in e.message:
cursor.execute("ROLLBACK TO SAVEPOINT "
"session_preload")
logger.error("session_exec not found")
raise
cursor.execute("ALTER DATABASE \"{}\" "
"SET session_exec.login_name = "
"'venv.activate_venv'"
.format(db_name))
sql = ACTIVATE_VENV_SQL_FUNCTION.format(
activate_path=activate_path,
sys_version=sys.version[:3])
cursor.execute(sql)
conn.commit()
conn.close()
__all__ = (
'init_db',
'init_venv',
)