import hashlib import re import sys import textwrap import requests from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration import ServiceController from orchestra.contrib.resources import ServiceMonitor from orchestra.utils.sys import sshrun from .. import settings class PhpListSaaSBackend(ServiceController): """ Creates a new phplist instance on a phpList multisite installation. The site is created by means of creating a new database per phpList site, but all sites share the same code. Different databases are used instead of prefixes because php-list reacts by launching the installation process. // config/config.php $site = getenv("SITE"); if ( $site == '' ) { $site = array_shift((explode(".",$_SERVER['HTTP_HOST']))); } $database_name = "phplist_mu_{$site}"; """ verbose_name = _("phpList SaaS") model = 'saas.SaaS' default_route_match = "saas.service == 'phplist'" serialize = True def error(self, msg): sys.stderr.write(msg + '\n') raise RuntimeError(msg) def _install_or_change_password(self, saas, server): """ configures the database for the new site through HTTP to /admin/ """ admin_link = 'https://%s/admin/' % saas.get_site_domain() sys.stdout.write('admin_link: %s\n' % admin_link) admin_content = requests.get(admin_link, verify=settings.SAAS_PHPLIST_VERIFY_SSL) admin_content = admin_content.content.decode('utf8') if admin_content.startswith('Cannot connect to Database'): self.error("Database is not yet configured.") install = re.search(r'([^"]+firstinstall[^"]+)', admin_content) if install: if not hasattr(saas, 'password'): self.error("Password is missing.") install_path = install.groups()[0] install_link = admin_link + install_path[1:] post = { 'adminname': saas.name, 'orgname': saas.account.username, 'adminemail': saas.account.username, 'adminpassword': saas.password, } response = requests.post(install_link, data=post, verify=settings.SAAS_PHPLIST_VERIFY_SSL) sys.stdout.write(response.content.decode('utf8')+'\n') if response.status_code != 200: self.error("Bad status code %i." % response.status_code) else: md5_password = hashlib.md5() md5_password.update(saas.password.encode('ascii')) context = self.get_context(saas) context['digest'] = md5_password.hexdigest() cmd = textwrap.dedent("""\ mysql \\ --host=%(db_host)s \\ --user=%(db_user)s \\ --password=%(db_pass)s \\ --execute='UPDATE phplist_admin SET password="%(digest)s" where ID=1; \\ UPDATE phplist_user_user SET password="%(digest)s" where ID=1;' \\ %(db_name)s""") % context sys.stdout.write('cmd: %s\n' % cmd) sshrun(server.get_address(), cmd) def save(self, saas): if hasattr(saas, 'password'): self.append(self._install_or_change_password, saas) context = self.get_context(saas) if context['crontab']: context['escaped_crontab'] = context['crontab'].replace('$', '\\$') self.append(textwrap.dedent("""\ # Configuring phpList crontabs if ! crontab -u %(user)s -l | grep 'phpList:"%(site_name)s"' > /dev/null; then cat << EOF | su %(user)s --shell /bin/bash -c 'crontab' $(crontab -u %(user)s -l) # %(banner)s - phpList:"%(site_name)s" %(escaped_crontab)s EOF fi""") % context ) def delete(self, saas): context = self.get_context(saas) if context['crontab']: context['crontab_regex'] = '\\|'.join(context['crontab'].splitlines()) context['crontab_regex'] = context['crontab_regex'].replace('*', '\\*') self.append(textwrap.dedent("""\ crontab -u %(user)s -l \\ | grep -v 'phpList:"%(site_name)s"\\|%(crontab_regex)s' \\ | su %(user)s --shell /bin/bash -c 'crontab' """) % context ) def get_context(self, saas): context = { 'banner': self.get_banner(), 'name': saas.name, 'site_name': saas.name, 'phplist_path': settings.SAAS_PHPLIST_PATH, 'user': settings.SAAS_PHPLIST_SYSTEMUSER, 'db_user': settings.SAAS_PHPLIST_DB_USER, 'db_pass': settings.SAAS_PHPLIST_DB_PASS, 'db_name': settings.SAAS_PHPLIST_DB_NAME, 'db_host': settings.SAAS_PHPLIST_DB_HOST, } context.update({ 'crontab': settings.SAAS_PHPLIST_CRONTAB % context, 'db_name': context['db_name'] % context, }) return context class PhpListTraffic(ServiceMonitor): verbose_name = _("phpList SaaS Traffic") model = 'saas.SaaS' default_route_match = "saas.service == 'phplist'" resource = ServiceMonitor.TRAFFIC script_executable = '/usr/bin/python' monthly_sum_old_values = True doc_settings = (settings, ('SAAS_PHPLIST_MAIL_LOG_PATH',) ) def prepare(self): mail_log = settings.SAAS_PHPLIST_MAIL_LOG_PATH context = { 'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"), 'mail_logs': str((mail_log, mail_log+'.1')), } self.append(textwrap.dedent("""\ import sys from datetime import datetime from dateutil import tz def prepare(object_id, list_domain, ini_date): global lists ini_date = to_local_timezone(ini_date) ini_date = int(ini_date.strftime('%Y%m%d%H%M%S')) lists[list_domain] = [ini_date, object_id, 0] def inside_period(month, day, time, ini_date): global months global end_datetime # Mar 9 17:13:22 month = months[month] year = end_datetime.year if month == '12' and end_datetime.month == 1: year = year+1 if len(day) == 1: day = '0' + day date = str(year) + month + day date += time.replace(':', '') return ini_date < int(date) < end_date def to_local_timezone(date, tzlocal=tz.tzlocal()): # Converts orchestra's UTC dates to local timezone date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S %Z') date = date.replace(tzinfo=tz.tzutc()) date = date.astimezone(tzlocal) return date maillogs = {mail_logs} end_datetime = to_local_timezone('{current_date}') end_date = int(end_datetime.strftime('%Y%m%d%H%M%S')) months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') months = dict((m, '%02d' % n) for n, m in enumerate(months, 1)) lists = {{}} id_to_domain = {{}} def monitor(lists, id_to_domain, maillogs): for maillog in maillogs: try: with open(maillog, 'r') as maillog: for line in maillog.readlines(): if ': message-id=<' in line: # Sep 15 09:36:51 web postfix/cleanup[8138]: C20FF244283: message-id= month, day, time, __, __, id, message_id = line.split()[:7] list_domain = message_id.split('@')[1][:-1] try: opts = lists[list_domain] except KeyError: pass else: ini_date = opts[0] if inside_period(month, day, time, ini_date): id = id[:-1] id_to_domain[id] = list_domain elif '>, size=' in line: # Sep 15 09:36:51 web postfix/qmgr[2296]: C20FF244283: from=, size=12252, nrcpt=1 (queue active) month, day, time, __, __, id, __, size = line.split()[:8] id = id[:-1] try: list_domain = id_to_domain[id] except KeyError: pass else: opts = lists[list_domain] size = int(size[5:-1]) opts[2] += size except IOError as e: sys.stderr.write(str(e)+'\\n') for opts in lists.values(): print opts[1], opts[2] """).format(**context) ) def commit(self): self.append('monitor(lists, id_to_domain, maillogs)') def monitor(self, saas): context = self.get_context(saas) self.append("prepare(%(object_id)s, '%(list_domain)s', '%(last_date)s')" % context) def get_context(self, saas): context = { 'list_domain': saas.get_site_domain(), 'object_id': saas.pk, 'last_date': self.get_last_date(saas.pk).strftime("%Y-%m-%d %H:%M:%S %Z"), } return context