remove multitenant, now in hobo (#6491)

This commit is contained in:
Thomas NOËL 2015-03-12 18:55:15 +01:00
parent 1db4b2429e
commit 43d6b99c0f
23 changed files with 0 additions and 1283 deletions

View File

@ -1,99 +0,0 @@
Multitenant
-----------
An application for making a Django application multitenant for Entr'ouvert
customers.
Based on https://django-tenant-schemas.readthedocs.org/
It is developed, tested and supported on Django 1.7, but it should work with
Django 1.6 + south.
Install
-------
See also : https://django-tenant-schemas.readthedocs.org/
Set the tenant model:
TENANT_MODEL = 'multitenant.Tenant'
Where are tenants:
TENANT_BASE = '/var/lib/<project>/tenants'
Add the middlewares for multitenant, they must be first:
MIDDLEWARE_CLASSES = (
'entrouvert.djommon.multitenant.middleware.TenantMiddleware',
'entrouvert.djommon.multitenant.middleware.JSONSettingsMiddleware',
'entrouvert.djommon.multitenant.middleware.PythonSettingsMiddleware',
) + MIDDLEWARE_CLASSES
Define the shared applications:
SHARED_APPS = (
'tenant_schemas',
'entrouvert.djommon.multitenant',
# those are needed for the public apps to work
# add also any application needed by the public app
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
)
TENANT_APPS = INSTALLED_APPS
INSTALLED_APPS = ('entrouvert.djommon.multitenant',
'tenant_schemas') + INSTALLED_APPS
# or, with Django 1.6 or older:
# INSTALLED_APPS += ('tenant_schemas', 'entrouvert.djommon.multitenant')
Use multitenant database engine:
DATABASES = {
'default': {
'ENGINE': 'tenant_schemas.postgresql_backend',
'NAME': '<db_name>',
},
}
DATABASE_ROUTERS = (
'tenant_schemas.routers.TenantSyncRouter',
)
# With Django 1.6 or older, use multitenant south adapter:
# SOUTH_DATABASE_ADAPTERS = {'default': 'south.db.postgresql_psycopg2'}
Add the multitenant filesystem template loader and configure where the
multitenant templates are located:
TEMPLATE_LOADERS = (
'entrouvert.djommon.multitenant.template_loader.FilesystemLoader',
) + TEMPLATE_LOADERS
TENANT_TEMPLATE_DIRS = (TENANT_BASE,)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
) + TEMPLATE_CONTEXT_PROCESSORS
Usage
-----
Create a tenant:
manage.py create_tenant www.example.net
Migration of all tenants:
manage.py migrate_schemas
Add a super user to tenant:
manage.py tenant_command createsuperuser --domain www.example.net
Tenants are created in TENANT_BASE directory, for example :
/var/lib/project/tenants/www.example.net/
templates/ <-- override project templates
static/ <-- to be handled by HTTP server
media/
Each tenant is a PostgreSQL schema, named www_example_net

View File

@ -1,171 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from optparse import make_option
from django.conf import settings
from django.core.management import call_command, get_commands, load_command_class
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
try:
from django.utils.six.moves import input
except ImportError:
input = raw_input
from tenant_schemas.utils import get_public_schema_name
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
class BaseTenantCommand(BaseCommand):
"""
Generic command class useful for iterating any existing command
over all schemata. The actual command name is expected in the
class variable COMMAND_NAME of the subclass.
"""
def __new__(cls, *args, **kwargs):
"""
Sets option_list and help dynamically.
"""
obj = super(BaseTenantCommand, cls).__new__(cls, *args, **kwargs)
app_name = get_commands()[obj.COMMAND_NAME]
if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly.
cmdclass = app_name
else:
cmdclass = load_command_class(app_name, obj.COMMAND_NAME)
# inherit the options from the original command
obj.option_list = cmdclass.option_list
obj.option_list += (
make_option("-d", "--domain", dest="domain"),
)
obj.option_list += (
make_option("-p", "--skip-public", dest="skip_public", action="store_true", default=False),
)
# prepend the command's original help with the info about schemata iteration
obj.help = "Calls %s for all registered schemata. You can use regular %s options. "\
"Original help for %s: %s" % (obj.COMMAND_NAME, obj.COMMAND_NAME, obj.COMMAND_NAME,
getattr(cmdclass, 'help', 'none'))
return obj
def execute_command(self, tenant, command_name, *args, **options):
verbosity = int(options.get('verbosity'))
if verbosity >= 1:
print()
print(self.style.NOTICE("=== Switching to schema '") \
+ self.style.SQL_TABLE(tenant.schema_name)\
+ self.style.NOTICE("' then calling %s:" % command_name))
connection.set_tenant(tenant)
# call the original command with the args it knows
call_command(command_name, *args, **options)
def handle(self, *args, **options):
"""
Iterates a command over all registered schemata.
"""
if options['domain']:
# only run on a particular schema
connection.set_schema_to_public()
self.execute_command(TenantMiddleware.get_tenant_by_hostname(options['domain']), self.COMMAND_NAME, *args, **options)
else:
for tenant in TenantMiddleware.get_tenants():
if not(options['skip_public'] and tenant.schema_name == get_public_schema_name()):
self.execute_command(tenant, self.COMMAND_NAME, *args, **options)
class InteractiveTenantOption(object):
def __init__(self, *args, **kwargs):
super(InteractiveTenantOption, self).__init__(*args, **kwargs)
self.option_list += (
make_option("-d", "--domain", dest="domain", help="specify tenant domain"),
)
def get_tenant_from_options_or_interactive(self, **options):
all_tenants = list(TenantMiddleware.get_tenants())
if not all_tenants:
raise CommandError("""There are no tenants in the system.
To learn how create a tenant, see:
https://django-tenant-schemas.readthedocs.org/en/latest/use.html#creating-a-tenant""")
if options.get('domain'):
domain = options['domain']
else:
while True:
domain = input("Enter Tenant Domain ('?' to list): ")
if domain == '?':
print('\n'.join(["%s (schema %s)" % (t.domain_url, t.schema_name) for t in all_tenants]))
else:
break
if domain not in [t.domain_url for t in all_tenants]:
raise CommandError("Invalid tenant, '%s'" % (domain,))
return TenantMiddleware.get_tenant_by_hostname(domain)
class TenantWrappedCommand(InteractiveTenantOption, BaseCommand):
"""
Generic command class useful for running any existing command
on a particular tenant. The actual command name is expected in the
class variable COMMAND_NAME of the subclass.
"""
def __new__(cls, *args, **kwargs):
obj = super(TenantWrappedCommand, cls).__new__(cls, *args, **kwargs)
obj.command_instance = obj.COMMAND()
obj.option_list = obj.command_instance.option_list
return obj
def handle(self, *args, **options):
tenant = self.get_tenant_from_options_or_interactive(**options)
connection.set_tenant(tenant)
self.command_instance.execute(*args, **options)
class SyncCommon(BaseCommand):
option_list = (
make_option('--tenant', action='store_true', dest='tenant', default=False,
help='Tells Django to populate only tenant applications.'),
make_option('--shared', action='store_true', dest='shared', default=False,
help='Tells Django to populate only shared applications.'),
make_option("-d", "--domain", dest="domain"),
)
def handle(self, *args, **options):
self.sync_tenant = options.get('tenant')
self.sync_public = options.get('shared')
self.domain = options.get('domain')
self.installed_apps = settings.INSTALLED_APPS
self.args = args
self.options = options
if self.domain:
self.schema_name = TenantMiddleware.hostname2schema(domain)
else:
self.schema_name = options.get('schema_name')
if self.schema_name:
if self.sync_public:
raise CommandError("domain should only be used with the --tenant switch.")
elif self.schema_name == get_public_schema_name():
self.sync_public = True
else:
self.sync_tenant = True
elif not self.sync_public and not self.sync_tenant:
# no options set, sync both
self.sync_tenant = True
self.sync_public = True
if hasattr(settings, 'TENANT_APPS'):
self.tenant_apps = settings.TENANT_APPS
if hasattr(settings, 'SHARED_APPS'):
self.shared_apps = settings.SHARED_APPS
def _notice(self, output):
self.stdout.write(self.style.NOTICE(output))

View File

@ -1,19 +0,0 @@
from django.core.management.base import BaseCommand
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
from django.db import connection
class Command(BaseCommand):
help = "Create schemas for all declared tenants"
def handle(self, *args, **options):
verbosity = int(options.get('verbosity'))
connection.set_schema_to_public()
all_tenants = TenantMiddleware.get_tenants()
for tenant in all_tenants:
if verbosity >= 1:
print
print self.style.NOTICE("=== Creating schema ") \
+ self.style.SQL_TABLE(tenant.schema_name)
tenant.create_schema(check_if_exists=True)

View File

@ -1,36 +0,0 @@
import os
from django.db import connection
from django.core.management.base import CommandError, BaseCommand
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
class Command(BaseCommand):
help = "Create tenant(s) by hostname(s)"
def handle(self, *args, **options):
verbosity = int(options.get('verbosity'))
if not args:
raise CommandError("you must give at least one tenant hostname")
for hostname in args:
try:
tenant_base = TenantMiddleware.base()
except AttributeError:
raise CommandError("you must configure TENANT_BASE in your settings")
if not tenant_base:
raise CommandError("you must set a value to TENANT_BASE in your settings")
tenant_dir = os.path.join(tenant_base, hostname)
if not os.path.exists(tenant_dir):
os.mkdir(tenant_dir, 0755)
for folder in ('media', 'static', 'templates'):
path = os.path.join(tenant_dir, folder)
if not os.path.exists(path):
os.mkdir(path, 0755)
connection.set_schema_to_public()
tenant = TenantMiddleware.get_tenant_by_hostname(hostname)
if verbosity >= 1:
print
print self.style.NOTICE("=== Creating schema ") \
+ self.style.SQL_TABLE(tenant.schema_name)
tenant.create_schema(check_if_exists=True)

View File

@ -1,11 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from entrouvert.djommon.multitenant.management.commands import TenantWrappedCommand
from django.contrib.auth.management.commands import createsuperuser
class Command(TenantWrappedCommand):
COMMAND = createsuperuser.Command

View File

@ -1,30 +0,0 @@
import urllib2
import json
import sys
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from tenant_schemas.utils import tenant_context
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
class Command(BaseCommand):
help = 'Deploy a tenant from hobo'
def handle(self, base_url, **options):
environment = json.load(sys.stdin)
for service in environment['services']:
if service['base_url'] == base_url:
break
else:
raise CommandError('Service %s not found' % base_url)
hostname = urllib2.urlparse.urlsplit(base_url).netloc
call_command('create_tenant', hostname)
tenant = TenantMiddleware.get_tenant_by_hostname(hostname)
with tenant_context(tenant):
self.deploy_tenant(environment, service, options)
def deploy_tenant(self, environment, service, options):
pass

View File

@ -1,91 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from django.conf import settings
from django.db import connection
from south import migration
from south.migration.base import Migrations
from south.management.commands.migrate import Command as MigrateCommand
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
from entrouvert.djommon.multitenant.management.commands import SyncCommon
class Command(SyncCommon):
help = "Migrate schemas with South"
option_list = MigrateCommand.option_list + SyncCommon.option_list
def handle(self, *args, **options):
super(Command, self).handle(*args, **options)
if self.sync_public:
self.migrate_public_apps()
if self.sync_tenant:
self.migrate_tenant_apps(self.domain)
def _set_managed_apps(self, included_apps, excluded_apps):
""" while sync_schemas works by setting which apps are managed, on south we set which apps should be ignored """
ignored_apps = []
if excluded_apps:
for item in excluded_apps:
if item not in included_apps:
ignored_apps.append(item)
for app in ignored_apps:
app_label = app.split('.')[-1]
settings.SOUTH_MIGRATION_MODULES[app_label] = 'ignore'
def _save_south_settings(self):
self._old_south_modules = None
if hasattr(settings, "SOUTH_MIGRATION_MODULES") and settings.SOUTH_MIGRATION_MODULES is not None:
self._old_south_modules = settings.SOUTH_MIGRATION_MODULES.copy()
else:
settings.SOUTH_MIGRATION_MODULES = dict()
def _restore_south_settings(self):
settings.SOUTH_MIGRATION_MODULES = self._old_south_modules
def _clear_south_cache(self):
for mig in list(migration.all_migrations()):
delattr(mig._application, "migrations")
Migrations._clear_cache()
def _migrate_schema(self, tenant):
connection.set_tenant(tenant, include_public=False)
MigrateCommand().execute(*self.args, **self.options)
def migrate_tenant_apps(self, schema_name=None):
self._save_south_settings()
apps = self.tenant_apps or self.installed_apps
self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps)
if schema_name:
self._notice("=== Running migrate for schema: %s" % schema_name)
connection.set_schema_to_public()
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
self._migrate_schema(tenant)
else:
all_tenants = TenantMiddleware.get_tenants()
if not all_tenants:
self._notice("No tenants found")
for tenant in all_tenants:
Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache
self._notice("=== Running migrate for schema %s" % tenant.schema_name)
self._migrate_schema(tenant)
self._restore_south_settings()
def migrate_public_apps(self):
self._save_south_settings()
apps = self.shared_apps or self.installed_apps
self._set_managed_apps(included_apps=apps, excluded_apps=self.tenant_apps)
self._notice("=== Running migrate for schema public")
MigrateCommand().execute(*self.args, **self.options)
self._clear_south_cache()
self._restore_south_settings()

View File

@ -1,14 +0,0 @@
from django.core.management.base import BaseCommand
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
class Command(BaseCommand):
requires_model_validation = True
can_import_settings = True
option_list = BaseCommand.option_list
def handle(self, **options):
all_tenants = TenantMiddleware.get_tenants()
for tenant in all_tenants:
print("{0} {1}".format(tenant.schema_name, tenant.domain_url))

View File

@ -1,27 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
import django
from django.conf import settings
from django.core.management.base import CommandError, BaseCommand
if django.VERSION < (1, 7, 0):
try:
from south.management.commands.migrate import Command as MigrateCommand
except ImportError:
MigrateCommand = BaseCommand
else:
MigrateCommand = BaseCommand
class Command(MigrateCommand):
def handle(self, *args, **options):
database = options.get('database', 'default')
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' or
MigrateCommand is BaseCommand):
raise CommandError("migrate has been disabled, for database '{}'. Use migrate_schemas "
"instead. Please read the documentation if you don't know why you "
"shouldn't call migrate directly!".format(database))
super(Command, self).handle(*args, **options)

View File

@ -1,68 +0,0 @@
import django
from optparse import NO_DEFAULT
if django.VERSION >= (1, 7, 0):
from django.core.management.commands.migrate import Command as MigrateCommand
from django.db.migrations.recorder import MigrationRecorder
from django.db import connection
from django.conf import settings
from tenant_schemas.utils import get_public_schema_name
from entrouvert.djommon.multitenant.middleware import TenantMiddleware, TenantNotFound
from entrouvert.djommon.multitenant.management.commands import SyncCommon
class MigrateSchemasCommand(SyncCommon):
help = "Updates database schema. Manages both apps with migrations and those without."
def run_from_argv(self, argv):
"""
Changes the option_list to use the options from the wrapped command.
Adds schema parameter to specify which schema will be used when
executing the wrapped command.
"""
self.option_list += MigrateCommand.option_list
super(MigrateSchemasCommand, self).run_from_argv(argv)
def handle(self, *args, **options):
super(MigrateSchemasCommand, self).handle(*args, **options)
self.PUBLIC_SCHEMA_NAME = get_public_schema_name()
if self.sync_public and not self.schema_name:
self.schema_name = self.PUBLIC_SCHEMA_NAME
if self.sync_public:
self.run_migrations(self.schema_name, settings.SHARED_APPS)
if self.sync_tenant:
if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME:
self.run_migrations(self.schema_name, settings.TENANT_APPS)
else:
all_tenants = TenantMiddleware.get_tenants()
for tenant in all_tenants:
self.run_migrations(tenant.schema_name, settings.TENANT_APPS)
def run_migrations(self, schema_name, included_apps):
self._notice("=== Running migrate for schema %s" % schema_name)
connection.set_schema(schema_name)
command = MigrateCommand()
defaults = {}
for opt in MigrateCommand.option_list:
if opt.dest in self.options:
defaults[opt.dest] = self.options[opt.dest]
elif opt.default is NO_DEFAULT:
defaults[opt.dest] = None
else:
defaults[opt.dest] = opt.default
command.execute(*self.args, **defaults)
connection.set_schema_to_public()
def _notice(self, output):
self.stdout.write(self.style.NOTICE(output))
if django.VERSION >= (1, 7, 0):
Command = MigrateSchemasCommand
else:
from .legacy.migrate_schemas import Command

View File

@ -1,105 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
import django
if django.VERSION < (1, 7, 0):
from django.conf import settings
from django.db import connection
from south import migration
from south.migration.base import Migrations
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
from entrouvert.djommon.management.commands.safemigrate import Command as SafeMigrateCommand
from entrouvert.djommon.multitenant.management.commands.sync_schemas import Command as MTSyncCommand
from entrouvert.djommon.multitenant.management.commands.migrate_schemas import Command as MTMigrateCommand
from entrouvert.djommon.multitenant.management.commands import SyncCommon
class SafeMigrateCommand(SyncCommon):
help = "Safely migrate schemas with South"
option_list = MTMigrateCommand.option_list
def handle(self, *args, **options):
super(Command, self).handle(*args, **options)
MTSyncCommand().execute(*args, **options)
connection.set_schema_to_public()
if self.sync_public:
self.fake_public_apps()
if self.sync_tenant:
self.fake_tenant_apps(self.domain)
connection.set_schema_to_public()
MTMigrateCommand().execute(*args, **options)
def _set_managed_apps(self, included_apps, excluded_apps):
""" while sync_schemas works by setting which apps are managed, on south we set which apps should be ignored """
ignored_apps = []
if excluded_apps:
for item in excluded_apps:
if item not in included_apps:
ignored_apps.append(item)
for app in ignored_apps:
app_label = app.split('.')[-1]
settings.SOUTH_MIGRATION_MODULES[app_label] = 'ignore'
def _save_south_settings(self):
self._old_south_modules = None
if hasattr(settings, "SOUTH_MIGRATION_MODULES") and settings.SOUTH_MIGRATION_MODULES is not None:
self._old_south_modules = settings.SOUTH_MIGRATION_MODULES.copy()
else:
settings.SOUTH_MIGRATION_MODULES = dict()
def _restore_south_settings(self):
settings.SOUTH_MIGRATION_MODULES = self._old_south_modules
def _clear_south_cache(self):
for mig in list(migration.all_migrations()):
delattr(mig._application, "migrations")
Migrations._clear_cache()
def _fake_schema(self, tenant):
connection.set_tenant(tenant, include_public=False)
SafeMigrateCommand().fake_if_needed()
def fake_tenant_apps(self, schema_name=None):
self._save_south_settings()
apps = self.tenant_apps or self.installed_apps
self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps)
if schema_name:
self._notice("=== Running fake_if_needed for schema: %s" % schema_name)
connection.set_schema_to_public()
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
self._fake_schema(tenant)
else:
all_tenants = TenantMiddleware.get_tenants()
if not all_tenants:
self._notice("No tenants found")
for tenant in all_tenants:
Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache
self._notice("=== Running fake_if_needed for schema %s" % tenant.schema_name)
self._fake_schema(tenant)
self._restore_south_settings()
def fake_public_apps(self):
self._save_south_settings()
apps = self.shared_apps or self.installed_apps
self._set_managed_apps(included_apps=apps, excluded_apps=self.tenant_apps)
self._notice("=== Running fake_if_needed for schema public")
SafeMigrateCommand().fake_if_needed()
self._clear_south_cache()
self._restore_south_settings()
if django.VERSION < (1, 7, 0):
Command = SafeMigrateCommand
else:
raise RuntimeError('Django 1.7: please use migrate_schemas')

View File

@ -1,87 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
import django
from django.core.management.base import CommandError
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import get_apps, get_models
if 'south' in settings.INSTALLED_APPS:
from south.management.commands.syncdb import Command as SyncdbCommand
else:
from django.core.management.commands.syncdb import Command as SyncdbCommand
from django.db import connection
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
from entrouvert.djommon.multitenant.management.commands import SyncCommon
class Command(SyncCommon):
help = "Sync schemas based on TENANT_APPS and SHARED_APPS settings"
option_list = SyncdbCommand.option_list + SyncCommon.option_list
def handle(self, *args, **options):
if django.VERSION >= (1, 7, 0):
raise CommandError('This command is only meant to be used for 1.6'
' and older version of django. For 1.7, use'
' `migrate_schemas` instead.')
super(Command, self).handle(*args, **options)
if "south" in settings.INSTALLED_APPS:
self.options["migrate"] = False
# save original settings
for model in get_models(include_auto_created=True):
setattr(model._meta, 'was_managed', model._meta.managed)
ContentType.objects.clear_cache()
if self.sync_public:
self.sync_public_apps()
if self.sync_tenant:
self.sync_tenant_apps(self.domain)
# restore settings
for model in get_models(include_auto_created=True):
model._meta.managed = model._meta.was_managed
def _set_managed_apps(self, included_apps):
""" sets which apps are managed by syncdb """
for model in get_models(include_auto_created=True):
model._meta.managed = False
verbosity = int(self.options.get('verbosity'))
for app_model in get_apps():
app_name = app_model.__name__.replace('.models', '')
if app_name in included_apps:
for model in get_models(app_model, include_auto_created=True):
model._meta.managed = model._meta.was_managed
if model._meta.managed and verbosity >= 3:
self._notice("=== Include Model: %s: %s" % (app_name, model.__name__))
def _sync_tenant(self, tenant):
self._notice("=== Running syncdb for schema: %s" % tenant.schema_name)
connection.set_tenant(tenant, include_public=False)
SyncdbCommand().execute(**self.options)
def sync_tenant_apps(self, schema_name=None):
apps = self.tenant_apps or self.installed_apps
self._set_managed_apps(apps)
if schema_name:
tenant = TenantMiddleware.get_tenant_by_hostname(schema_name)
self._sync_tenant(tenant)
else:
all_tenants = TenantMiddleware.get_tenants()
if not all_tenants:
self._notice("No tenants found!")
for tenant in all_tenants:
self._sync_tenant(tenant)
def sync_public_apps(self):
self._notice("=== Running syncdb for schema public")
apps = self.shared_apps or self.installed_apps
self._set_managed_apps(apps)
SyncdbCommand().execute(**self.options)

View File

@ -1,26 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from django.core.management.base import CommandError
from django.conf import settings
from tenant_schemas.utils import django_is_in_test_mode
if 'south' in settings.INSTALLED_APPS:
from south.management.commands import syncdb
else:
from django.core.management.commands import syncdb
class Command(syncdb.Command):
def handle(self, *args, **options):
database = options.get('database', 'default')
if (settings.DATABASES[database]['ENGINE'] == 'tenant_schemas.postgresql_backend' and not
django_is_in_test_mode()):
raise CommandError("syncdb has been disabled, for database '{0}'. "
"Use sync_schemas instead. Please read the "
"documentation if you don't know why "
"you shouldn't call syncdb directly!".format(database))
super(Command, self).handle(*args, **options)

View File

@ -1,40 +0,0 @@
# this file derive from django-tenant-schemas
# Author: Bernardo Pires Carneiro
# Email: carneiro.be@gmail.com
# License: MIT license
# Home-page: http://github.com/bcarneiro/django-tenant-schemas
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command, get_commands, load_command_class
from django.db import connection
from entrouvert.djommon.multitenant.management.commands import InteractiveTenantOption
class Command(InteractiveTenantOption, BaseCommand):
help = "Wrapper around django commands for use with an individual tenant"
def run_from_argv(self, argv):
"""
Changes the option_list to use the options from the wrapped command.
Adds schema parameter to specifiy which schema will be used when
executing the wrapped command.
"""
# load the command object.
try:
app_name = get_commands()[argv[2]]
except KeyError:
raise CommandError("Unknown command: %r" % argv[2])
if isinstance(app_name, BaseCommand):
# if the command is already loaded, use it directly.
klass = app_name
else:
klass = load_command_class(app_name, argv[2])
super(Command, self).run_from_argv(argv)
def handle(self, *args, **options):
tenant = self.get_tenant_from_options_or_interactive(**options)
connection.set_tenant(tenant)
call_command(*args, **options)

View File

@ -1,180 +0,0 @@
import os
import json
import glob
from django.conf import settings, UserSettingsHolder
from django.db import connection
from django.http import Http404
from django.contrib.contenttypes.models import ContentType
from tenant_schemas.utils import get_tenant_model, remove_www_and_dev, get_public_schema_name
SENTINEL = object()
class TenantNotFound(RuntimeError):
pass
class TenantMiddleware(object):
"""
This middleware should be placed at the very top of the middleware stack.
Selects the proper database schema using the request host. Can fail in
various ways which is better than corrupting or revealing data...
"""
@classmethod
def base(cls):
return settings.TENANT_BASE
@classmethod
def hostname2schema(cls, hostname):
'''Convert hostname to PostgreSQL schema name'''
if hostname in getattr(settings, 'TENANT_MAPPING', {}):
return settings.TENANT_MAPPING[hostname]
return hostname.replace('.', '_').replace('-', '_')
@classmethod
def get_tenant_by_hostname(cls, hostname):
'''Retrieve a tenant object for this hostname'''
if not os.path.exists(os.path.join(cls.base(), hostname)):
raise TenantNotFound
schema = cls.hostname2schema(hostname)
return get_tenant_model()(schema_name=schema, domain_url=hostname)
@classmethod
def get_tenants(cls):
self = cls()
for path in glob.glob(os.path.join(cls.base(), '*')):
hostname = os.path.basename(path)
yield get_tenant_model()(
schema_name=self.hostname2schema(hostname),
domain_url=hostname)
def process_request(self, request):
# connection needs first to be at the public schema, as this is where the
# tenant informations are saved
connection.set_schema_to_public()
hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0])
try:
request.tenant = self.get_tenant_by_hostname(hostname_without_port)
except TenantNotFound:
raise Http404
connection.set_tenant(request.tenant)
# content type can no longer be cached as public and tenant schemas have different
# models. if someone wants to change this, the cache needs to be separated between
# public and shared schemas. if this cache isn't cleared, this can cause permission
# problems. for example, on public, a particular model has id 14, but on the tenants
# it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
# model will be fetched.
ContentType.objects.clear_cache()
# do we have a public-specific token?
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
class TenantSettingBaseMiddleware(object):
'''Base middleware classe for loading settings based on tenants
Child classes MUST override the load_tenant_settings() method.
'''
def __init__(self, *args, **kwargs):
self.tenants_settings = {}
def get_tenant_settings(self, wrapped, tenant):
'''Get last loaded settings for tenant, try to update it by loading
settings again is last loading time is less recent thant settings data
store. Compare with last modification time is done in the
load_tenant_settings() method.
'''
tenant_settings, last_time = self.tenants_settings.get(tenant.schema_name, (None,None))
if tenant_settings is None:
tenant_settings = UserSettingsHolder(wrapped)
tenant_settings, last_time = self.load_tenant_settings(wrapped, tenant, tenant_settings, last_time)
self.tenants_settings[tenant.schema_name] = tenant_settings, last_time
return tenant_settings
def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
'''Load tenant settings into tenant_settings object, eventually skip if
last_time is more recent than last update time for settings and return
the new value for tenant_settings and last_time'''
raise NotImplemented
def process_request(self, request):
if not hasattr(request, '_old_settings_wrapped'):
request._old_settings_wrapped = []
request._old_settings_wrapped.append(settings._wrapped)
settings._wrapped = self.get_tenant_settings(settings._wrapped, request.tenant)
def process_response(self, request, response):
if hasattr(request, '_old_settings_wrapped') and request._old_settings_wrapped:
settings._wrapped = request._old_settings_wrapped.pop()
return response
class FileBasedTenantSettingBaseMiddleware(TenantSettingBaseMiddleware):
FILENAME = None
def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
path = os.path.join(settings.TENANT_BASE, tenant.domain_url, self.FILENAME)
try:
new_time = os.stat(path).st_mtime
except OSError:
# file was removed
if not last_time is None:
return UserSettingsHolder(wrapped), None
else:
if last_time is None or new_time >= last_time:
# file is new
tenant_settings = UserSettingsHolder(wrapped)
self.load_file(tenant_settings, path)
return tenant_settings, new_time
# nothing has changed
return tenant_settings, last_time
class JSONSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
'''Load settings from a JSON file whose path is given by:
os.path.join(settings.TENANT_BASE % schema_name, 'settings.json')
The JSON file must be a dictionnary whose key/value will override
current settings.
'''
FILENAME = 'settings.json'
def load_file(sef, tenant_settings, path):
with file(path) as f:
json_settings = json.load(f)
for key in json_settings:
setattr(tenant_settings, key, json_settings[key])
class DictAdapter(dict):
'''Give dict interface to plain objects'''
def __init__(self, wrapped):
self.wrapped = wrapped
def __setitem__(self, key, value):
setattr(self.wrapped, key, value)
def __getitem__(self, key):
try:
return getattr(self.wrapped, key)
except AttributeError:
raise KeyError
class PythonSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
'''Load settings from a file whose path is given by:
os.path.join(settings.TENANT_BASE % schema_name, 'settings.py')
The file is executed in the same context as the classic settings file
using execfile.
'''
FILENAME = 'settings.py'
def load_file(self, tenant_settings, path):
execfile(path, DictAdapter(tenant_settings))

View File

@ -1,11 +0,0 @@
from tenant_schemas.models import TenantMixin
class Tenant(TenantMixin):
# default true, schema will be automatically created and synced when it is saved
auto_create_schema = False
def save(self):
pass
def __unicode__(self):
return u'%s' % self.schema_name

View File

@ -1,24 +0,0 @@
import os
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils._os import safe_join
from django.db import connection
from django.core.files.storage import FileSystemStorage
__all__ = ('TenantFileSystemStorage', )
class TenantFileSystemStorage(FileSystemStorage):
'''Lookup files first in $TENANT_BASE/<tenant.schema>/media/ then in default location'''
def path(self, name):
if connection.tenant:
location = safe_join(settings.TENANT_BASE, connection.tenant.domain_url, 'media')
else:
location = self.location
try:
path = safe_join(location, name)
except ValueError:
raise SuspiciousOperation("Attempted access to '%s' denied." % name)
return os.path.normpath(path)

View File

@ -1,115 +0,0 @@
"""
Wrapper class that takes a list of template loaders as an argument and attempts
to load templates from them in order, caching the result.
"""
import hashlib
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.template.base import TemplateDoesNotExist
from django.template.loader import BaseLoader, get_template_from_string, find_template_loader, make_origin
from django.utils.encoding import force_bytes
from django.utils._os import safe_join
from django.db import connection
class CachedLoader(BaseLoader):
is_usable = True
def __init__(self, loaders):
self.template_cache = {}
self._loaders = loaders
self._cached_loaders = []
@property
def loaders(self):
# Resolve loaders on demand to avoid circular imports
if not self._cached_loaders:
# Set self._cached_loaders atomically. Otherwise, another thread
# could see an incomplete list. See #17303.
cached_loaders = []
for loader in self._loaders:
cached_loaders.append(find_template_loader(loader))
self._cached_loaders = cached_loaders
return self._cached_loaders
def find_template(self, name, dirs=None):
for loader in self.loaders:
try:
template, display_name = loader(name, dirs)
return (template, make_origin(display_name, loader, name, dirs))
except TemplateDoesNotExist:
pass
raise TemplateDoesNotExist(name)
def load_template(self, template_name, template_dirs=None):
if connection.tenant:
key = '-'.join([str(connection.tenant.pk), template_name])
else:
key = template_name
if template_dirs:
# If template directories were specified, use a hash to differentiate
if connection.tenant:
key = '-'.join([str(connection.tenant.schema_name), template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
else:
key = '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
if key not in self.template_cache:
template, origin = self.find_template(template_name, template_dirs)
if not hasattr(template, 'render'):
try:
template = get_template_from_string(template, origin, template_name)
except TemplateDoesNotExist:
# If compiling the template we found raises TemplateDoesNotExist,
# back off to returning the source and display name for the template
# we were asked to load. This allows for correct identification (later)
# of the actual template that does not exist.
return template, origin
self.template_cache[key] = template
return self.template_cache[key], None
def reset(self):
"Empty the template cache."
self.template_cache.clear()
class FilesystemLoader(BaseLoader):
is_usable = True
def get_template_sources(self, template_name, template_dirs=None):
"""
Returns the absolute paths to "template_name", when appended to each
directory in "template_dirs". Any paths that don't lie inside one of the
template dirs are excluded from the result set, for security reasons.
"""
if not connection.tenant:
return
if not template_dirs:
try:
template_dirs = settings.TENANT_TEMPLATE_DIRS
except AttributeError:
raise ImproperlyConfigured('To use %s.%s you must define the TENANT_TEMPLATE_DIRS' % (__name__, FilesystemLoader.__name__))
for template_dir in template_dirs:
try:
yield safe_join(template_dir, connection.tenant.domain_url, 'templates', template_name)
except UnicodeDecodeError:
# The template dir name was a bytestring that wasn't valid UTF-8.
raise
except ValueError:
# The joined path was located outside of this particular
# template_dir (it might be inside another one, so this isn't
# fatal).
pass
def load_template_source(self, template_name, template_dirs=None):
tried = []
for filepath in self.get_template_sources(template_name, template_dirs):
try:
with open(filepath, 'rb') as fp:
return (fp.read().decode(settings.FILE_CHARSET), filepath)
except IOError:
tried.append(filepath)
if tried:
error_msg = "Tried %s" % tried
else:
error_msg = "Your TENANT_TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
raise TemplateDoesNotExist(error_msg)
load_template_source.is_usable = True

View File

@ -1,128 +0,0 @@
"""
Test multitenant framework
"""
import tempfile
import shutil
import os
import json
import StringIO
from django.conf.urls import patterns
from django.test import TestCase, Client
from django.http import HttpResponse
from django.template.response import TemplateResponse
try:
from django.test import override_settings
except ImportError: # django < 1.7
from django.test.utils import override_settings
def json_key(request, *args, **kwargs):
from django.conf import settings
return HttpResponse(settings.JSON_KEY + ' json')
def python_key(request, *args, **kwargs):
from django.conf import settings
return HttpResponse(settings.PYTHON_KEY + ' python')
def template(request, *args, **kwargs):
return TemplateResponse(request, 'tenant.html')
def upload(request):
from django.core.files.storage import default_storage
default_storage.save('upload', request.FILES['upload'])
return HttpResponse('')
def download(request):
from django.core.files.storage import default_storage
return HttpResponse(default_storage.open('upload').read())
urlpatterns = patterns('',
('^json_key/$', json_key),
('^python_key/$', python_key),
('^template/$', template),
('^upload/$', upload),
('^download/$', download),
)
@override_settings(
ROOT_URLCONF=__name__,
MIDDLEWARE_CLASSES=(
'entrouvert.djommon.multitenant.middleware.TenantMiddleware',
'entrouvert.djommon.multitenant.middleware.JSONSettingsMiddleware',
'entrouvert.djommon.multitenant.middleware.PythonSettingsMiddleware',
),
TEMPLATE_LOADERS = (
'entrouvert.djommon.multitenant.template_loader.FilesystemLoader',
),
DEFAULT_FILE_STORAGE = 'entrouvert.djommon.multitenant.storage.TenantFileSystemStorage',
)
class SimpleTest(TestCase):
TENANTS = ['tenant1', 'tenant2']
def setUp(self):
self.tenant_base = tempfile.mkdtemp()
for tenant in self.TENANTS:
tenant_dir = os.path.join(self.tenant_base, tenant)
os.mkdir(tenant_dir)
settings_py = os.path.join(tenant_dir, 'settings.json')
with file(settings_py, 'w') as f:
json.dump({'JSON_KEY': tenant}, f)
settings_json = os.path.join(tenant_dir, 'settings.py')
with file(settings_json, 'w') as f:
print >>f, 'PYTHON_KEY = %r' % tenant
templates_dir = os.path.join(tenant_dir, 'templates')
os.mkdir(templates_dir)
tenant_html = os.path.join(templates_dir, 'tenant.html')
with file(tenant_html, 'w') as f:
print >>f, tenant + ' template',
media_dir = os.path.join(tenant_dir, 'media')
os.mkdir(media_dir)
def tearDown(self):
shutil.rmtree(self.tenant_base, ignore_errors=True)
def tenant_settings(self):
return self.settings(
TENANT_BASE=self.tenant_base,
TENANT_TEMPLATE_DIRS=(self.tenant_base,)
)
def test_tenants(self):
with self.tenant_settings():
for tenant in self.TENANTS:
c = Client(HTTP_HOST=tenant)
response = c.get('/json_key/')
self.assertEqual(response.content, tenant + ' json')
response = c.get('/python_key/')
self.assertEqual(response.content, tenant + ' python')
response = c.get('/template/')
self.assertEqual(response.content, tenant + ' template')
def test_list_tenants(self):
from entrouvert.djommon.multitenant.middleware import TenantMiddleware
from tenant_schemas.utils import get_tenant_model
with self.tenant_settings():
l1 = set(map(str, TenantMiddleware.get_tenants()))
l2 = set(str(get_tenant_model()(schema_name=tenant,
domain_url=tenant)) for tenant in self.TENANTS)
self.assertEquals(l1, l2)
def test_storage(self):
from django.core.files.base import ContentFile
with self.tenant_settings():
for tenant in self.TENANTS:
c = Client(HTTP_HOST=tenant)
uploaded_file_path = os.path.join(self.tenant_base, tenant, 'media', 'upload')
self.assertFalse(os.path.exists(uploaded_file_path), uploaded_file_path)
response = c.post('/upload/', {'upload': ContentFile(tenant + ' upload', name='upload.txt')})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, '')
self.assertTrue(os.path.exists(uploaded_file_path))
self.assertEqual(file(uploaded_file_path).read(), tenant + ' upload')
response = c.get('/download/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, tenant + ' upload')

View File

@ -1 +0,0 @@
# Create your views here.