Example #1
0
class Task:  #{{{
    name = 'task'
    immediately = False

    def __init__(self, ref: ScheduleGenerate) -> None:
        self.ref = ref
        self.db = DB()
        self.unit = Unit()

    def __del__(self) -> None:
        self.close()

    def __call__(self) -> bool:
        rc = False
        if self.db.isopen():
            self.ref.processtitle.push(self.name)
            try:
                self.execute()
                self.ref.pendings()
            except Exception as e:
                logger.exception('failed due to: %s' % e)
            else:
                rc = True
            finally:
                self.close()
                self.ref.processtitle.pop()
                self.ref.lockTitle()
        return rc

    def close(self) -> None:
        self.db.close()

    def configuration(self, key: str, default: Any = None) -> Any:
        return self.ref.configuration(key,
                                      name=self.name,
                                      default=self.default_for(key, default))

    def title(self, title: Optional[str] = None) -> None:
        self.ref.processtitle('%s%s' % (self.name,
                                        (' %s' % title) if title else ''))

    def default_for(self, key: str, default: Any) -> Any:
        return default

    #
    # to overwrite
    def execute(self) -> None:
        pass
Example #2
0
 def executor(self) -> bool:
     rc = True
     with DB() as self.db:
         self.timestamp = Timestamp(self.timestamp_name)
         self.config: Dict[int, Parameter] = {}
         self.companies: DefaultDict[int, Softbounce.Company] = defaultdict(
             Softbounce.Company)
         try:
             with log('parameter'):
                 self.setup_parameter()
             with log('remove-olds'):
                 self.remove_old_entries()
             with log('timestamp'):
                 self.setup_timestamp()
             with log('collect'):
                 self.collect_new_bounces()
             with log('timestamp'):
                 self.finalize_timestamp(True)
             with log('merge'):
                 self.merge_new_bounces()
             with log('convert'):
                 self.convert_to_hardbounce()
         except error as e:
             logger.exception('Failed due to %s' % e)
             self.finalize_timestamp(False)
             rc = False
         self.db.sync()
     return rc
Example #3
0
 def get_ready_to_run(self) -> List[METAFile]:
     (ready, stamps, finals) = self.scan_ready_to_run()
     if ready:
         for info in ready:
             info.stamp = stamps[info.basename]
         with DB() as db:
             invalids: Set[int] = set()
             for mailing in (Stream(ready).map(
                     lambda i: i.mailing).distinct().sorted()):
                 rq = db.querys(
                     'SELECT deleted '
                     'FROM mailing_tbl '
                     'WHERE mailing_id = :mailingID',
                     {'mailingID': mailing})
                 if rq is None:
                     logger.info('Mailing %d no more existing' % mailing)
                     invalids.add(mailing)
                 elif rq.deleted:
                     logger.info('Mailing %d is marked as deleted' %
                                 mailing)
                     invalids.add(mailing)
             if invalids:
                 for info in (Stream(ready).filter(
                         lambda i: i.mailing in invalids)):
                     self.move(info.path, self.deleted)
                     if info.stamp is not None:
                         self.move(info.stamp.path, self.deleted)
                 ready = (Stream(ready).filter(
                     lambda i: i.mailing not in invalids).list())
     if ready:
         logger.info(
             '{count:,d} files are ready to send'.format(count=len(ready)))
     return ready
Example #4
0
	def executor (self) -> bool:
		active = 0
		with DB () as db:
			companies: Dict[int, str] = {}
			now = datetime.now ()
			for row in db.queryc (
				'SELECT mdrop.status_id, mdrop.mailing_id, mdrop.genstatus, mdrop.genchange, mdrop.status_field, mt.shortname '
				'FROM maildrop_status_tbl mdrop INNER JOIN mailing_tbl mt ON mt.mailing_id = mdrop.mailing_id '
				'WHERE genchange > :limit AND (genstatus = 2 OR (genstatus = 1 AND status_field IN (\'A\', \'T\', \'W\')))',
				{
					'limit': datetime.fromordinal (now.toordinal () - 1)
				}
			):
				if self.is_active (now, row.genstatus, row.genchange, row.status_field):
					active += 1
					rqm = db.querys (
						'SELECT company_id, shortname '
						'FROM mailing_tbl '
						'WHERE mailing_id = :mailing_id',
						{'mailing_id': row.mailing_id}
					)
					if rqm is not None:
						try:
							company = companies[rqm.company_id]
						except KeyError:
							rqc = db.querys (
								'SELECT shortname '
								'FROM company_tbl '
								'WHERE company_id = :company_id',
								{'company_id': rqm.company_id}
							)
							company = companies[rqm.company_id] = rqc.shortname if rqc is not None else f'#{rqm.company_id}'
						status = 'is starting up to generate' if row.genstatus == 1 else 'is in generation'
						rqt = db.querys (
							'SELECT current_mails, total_mails '
							'FROM mailing_backend_log_tbl '
							'WHERE status_id = :status_id',
							{'status_id': row.status_id}
						)
						if rqt is None and row.status_field == 'W':
							rqt = db.querys (
								'SELECT current_mails, total_mails '
								'FROM world_mailing_backend_log_tbl '
								'WHERE mailing_id = :mailing_id',
								{'mailing_id': row.mailing_id}
							)
						if rqt is not None:
							status += ' (%d of %d are created)' % (rqt.current_mails, rqt.total_mails)
						else:
							status += ' (nothing created until now)'
						print ('Mailing %s (%d) for Company %s (%d) %s' % (row.shortname, row.mailing_id, company, rqm.company_id, status))
		if active > 0:
			print ('%d jobs still active' % active)
			return False
		return True
Example #5
0
    def log_release(self) -> None:
        application = 'BACKEND-{id}'.format(id=self.id.upper())
        now = datetime.now()
        try:
            with DB() as db:
                data: Dict[str, Any] = {
                    'host_name': fqdn,
                    'application_name': application
                }

                rq = db.querys(
                    db.qselect(
                        oracle=
                        ('SELECT version_number '
                         'FROM release_log_tbl '
                         'WHERE host_name = :host_name AND application_name = :application_name '
                         'ORDER BY startup_timestamp DESC'),
                        mysql=
                        ('SELECT version_number '
                         'FROM release_log_tbl '
                         'WHERE host_name = :host_name AND application_name = :application_name '
                         'ORDER BY startup_timestamp DESC '
                         'LIMIT 1')), data)
                if rq is None or rq.version_number != spec.version:
                    data.update({
                        'version_number': spec.version,
                        'startup_timestamp': now,
                        'build_time': spec.timestamp,
                        'build_host': spec.host,
                        'build_user': spec.user
                    })
                    count = db.update(
                        'INSERT INTO release_log_tbl '
                        '       (host_name, application_name, version_number, startup_timestamp, build_time, build_host, build_user) '
                        'VALUES '
                        '       (:host_name, :application_name, :version_number, :startup_timestamp, :build_time, :build_host, :build_user)',
                        data,
                        commit=True)
                    if count != 1:
                        raise error(
                            f'failed to create new record, expected 1 row, inserted {count} rows'
                        )
        except (IOError, error) as e:
            logger.debug(
                f'log_release: failed to write to database ({e}), try to spool information'
            )
            log_path = os.path.join(base, 'log')
            if os.path.isdir(log_path):
                with open(os.path.join(log_path, 'release.log'), 'a') as fd:
                    fd.write(
                        f'{licence};{fqdn};{application};{spec.version};{now:%Y-%m-%d %H:%M:%S};{spec.timestamp:%Y-%m-%d %H:%M:%S};{spec.host};{spec.user}\n'
                    )
            else:
                logger.debug(
                    f'no path {log_path} exists, no information is written')
Example #6
0
	def __db_sanity (self, r: Report) -> None:
		with DB () as db:
			key = 'mask-envelope-from'
			rq = db.querys (
				'SELECT count(*) AS cnt '
				'FROM company_info_tbl '
				'WHERE company_id = 0 AND cname = :cname',
				{'cname': key}
			)
			if rq is None or not rq.cnt:
				count = db.update (
					'INSERT INTO company_info_tbl ('
					'       company_id, cname, cvalue, description, creation_date, timestamp'
					') VALUES ('
					'       0, :cname, :cvalue, NULL, current_timestamp, current_timestamp'
					')', {
						'cname': key,
						'cvalue': 'false'
					}, commit = True
				)
				if count == 1:
					logger.info ('Added configuration for envelope address')
				else:
					logger.error ('Failed to set configuration for envelope address: %s' % db.last_error ())
			for (key, value) in [
				('LOGLEVEL', 'DEBUG'),
				('MAILDIR', '${home}/var/spool/ADMIN'),
				('BOUNDARY', 'OPENEMM'),
				('MAILER', 'OpenEMM ${ApplicationMajorVersion}.${ApplicationMinorVersion}')
			]:
				data: Dict[str, Optional[str]] = {
					'cls': 'mailout',
					'name': 'ini.{key}'.format (key = key.lower ())
				}
				rq = db.querys (
					'SELECT value '
					'FROM config_tbl '
					'WHERE class = :cls AND name = :name AND hostname IS NULL',
					data
				)
				if rq is not None:
					if rq.value != value:
						logger.info (f'{key}: keep DB value "{rq.value}" and not overwrite it with default value {value}')
				else:
					data['value'] = value
					db.update (
						'INSERT INTO config_tbl '
						'       (class, name, value, hostname, description, creation_date, change_date) '
						'VALUES '
						'       (:cls, :name, :value, NULL, NULL, current_timestamp, current_timestamp)',
						data,
						commit = True
					)
Example #7
0
 def setup(self) -> None:
     self.delay = BavUpdate.unit.parse('3m')
     self.fqdn = socket.getfqdn().lower()
     if not self.fqdn:
         self.fqdn = fqdn
     self.filter_domain = syscfg.get_str('filter-name',
                                         BavUpdate.default_filter_domain)
     if self.filter_domain == BavUpdate.default_filter_domain:
         with DB() as db:
             rq = db.querys(
                 'SELECT mailloop_domain FROM company_tbl WHERE company_id = 1'
             )
             if rq is not None and rq.mailloop_domain:
                 self.filter_domain = rq.mailloop_domain
     self.mta = MTA()
     self.domains: List[str] = []
     self.mtdom: Dict[str, int] = {}
     self.prefix = 'aml_'
     self.last = ''
     self.autoresponder: List[Autoresponder] = []
     self.read_mailertable()
     try:
         files = os.listdir(Autoresponder.directory)
         for fname in files:
             if len(fname
                    ) > 8 and fname[:3] == 'ar_' and fname[-5:] == '.mail':
                 with Ignore(ValueError, OSError):
                     rid = int(fname[3:-5])
                     st = os.stat(
                         os.path.join(Autoresponder.directory, fname))
                     self.autoresponder.append(
                         Autoresponder(rid,
                                       datetime.fromtimestamp(st.st_ctime),
                                       None, None))
     except OSError as e:
         logger.error(
             f'Unable to read directory {Autoresponder.directory}: {e}')
Example #8
0
 def __db_sanity(self, r: Report) -> None:
     with DB() as db:
         key = 'mask-envelope-from'
         rq = db.querys(
             'SELECT count(*) AS cnt '
             'FROM company_info_tbl '
             'WHERE company_id = 0 AND cname = :cname', {'cname': key})
         if rq is None or not rq.cnt:
             count = db.update(
                 'INSERT INTO company_info_tbl ('
                 '       company_id, cname, cvalue, description, creation_date, timestamp'
                 ') VALUES ('
                 '       0, :cname, :cvalue, NULL, current_timestamp, current_timestamp'
                 ')', {
                     'cname': key,
                     'cvalue': 'false'
                 },
                 commit=True)
             if count == 1:
                 logger.info('Added configuration for envelope address')
             else:
                 logger.error(
                     'Failed to set configuration for envelope address: %s'
                     % db.last_error())
Example #9
0
 def read_database(self, auto: List[Autoresponder]) -> List[str]:
     rc: List[str] = []
     with DB() as db:
         company_list: List[int] = []
         new_domains: Dict[str, BavUpdate.RID] = {}
         forwards: List[BavUpdate.Forward] = []
         seen_domains: Set[str] = set()
         accepted_forwards: Set[str] = set()
         ctab: Dict[int, str] = {}
         #
         rc.append('fbl@%s\taccept:rid=unsubscribe' % self.fixdomain)
         for domain in self.domains:
             if domain not in seen_domains:
                 rc.append('fbl@%s\talias:fbl@%s' %
                           (domain, self.fixdomain))
                 seen_domains.add(domain)
         if self.fixdomain not in seen_domains:
             new_domains[self.fixdomain] = BavUpdate.RID(
                 rid=0, domain=self.fixdomain)
         seen_domains.add(self.fixdomain)
         #
         missing = []
         for row in db.query(
                 'SELECT company_id, mailloop_domain FROM company_tbl WHERE status = :status',
             {'status': 'active'}):
             if row.mailloop_domain:
                 ctab[row.company_id] = row.mailloop_domain
                 if row.mailloop_domain not in seen_domains:
                     rc.append('fbl@%s\talias:fbl@%s' %
                               (row.mailloop_domain, self.fixdomain))
                     if row.mailloop_domain not in self.mtdom and row.mailloop_domain.lower(
                     ) != self.fqdn:
                         new_domains[row.mailloop_domain] = BavUpdate.RID(
                             rid=0, domain=row.mailloop_domain)
                     seen_domains.add(row.mailloop_domain)
             else:
                 missing.append(row.company_id)
             company_list.append(row.company_id)
         if missing:
             missing.sort()
             logger.debug('Missing mailloop_domain for %s' %
                          ', '.join([str(m) for m in missing]))
         #
         seen_rids: Set[int] = set()
         for row in db.query(
                 'SELECT rid, shortname, company_id, filter_address, '
                 '       forward_enable, forward, ar_enable, '
                 '       subscribe_enable, mailinglist_id, form_id, timestamp, '
                 '       spam_email, spam_required, spam_forward, '
                 '       autoresponder_mailing_id, security_token '
                 'FROM mailloop_tbl'):
             if row.company_id not in company_list or row.rid is None:
                 continue
             seen_rids.add(row.rid)
             domains: List[str] = [self.fixdomain]
             aliases: List[str] = []
             if row.filter_address is not None:
                 for alias in listsplit(row.filter_address):
                     if not alias.startswith(self.prefix):
                         with Ignore(ValueError):
                             domain_part = alias.split('@', 1)[-1]
                             if domain_part not in domains:
                                 domains.append(domain_part)
                                 if domain_part not in self.mtdom and domain_part not in new_domains:
                                     new_domains[
                                         domain_part] = BavUpdate.RID(
                                             rid=row.rid,
                                             domain=domain_part)
                             aliases.append(alias)
             #
             ar_enable = False
             if row.ar_enable and row.autoresponder_mailing_id:
                 if not row.security_token:
                     logger.error(
                         '%s: Autoresponder has mailing id, but no security token'
                         % row.rid)
                 else:
                     auto.append(
                         Autoresponder(
                             row.rid, row.timestamp if row.timestamp
                             is not None else datetime.now(),
                             row.autoresponder_mailing_id,
                             row.security_token))
                     ar_enable = True
             #
             try:
                 cdomain = ctab[row.company_id]
                 if cdomain not in domains:
                     if cdomain in self.domains:
                         domains.append(cdomain)
                     else:
                         logger.debug(
                             'Company\'s domain "%s" not found in mailertable'
                             % cdomain)
             except KeyError:
                 logger.debug(
                     'No domain for company found, further processing')
             extra = ['rid=%s' % row.rid]
             if row.company_id:
                 extra.append('cid=%d' % row.company_id)
             if row.forward_enable and row.forward:
                 extra.append('fwd=%s' % row.forward)
                 forwards.append(
                     BavUpdate.Forward(rid=row.rid, address=row.forward))
             if row.spam_email:
                 extra.append('spam_email=%s' % row.spam_email)
             if row.spam_forward:
                 extra.append('spam_fwd=%d' % row.spam_forward)
             if row.spam_required:
                 extra.append('spam_req=%d' % row.spam_required)
             if ar_enable:
                 extra.append('ar=%s' % row.rid)
                 if row.autoresponder_mailing_id:
                     extra.append('armid=%d' % row.autoresponder_mailing_id)
             if row.subscribe_enable and row.mailinglist_id and row.form_id:
                 extra.append('sub=%d:%d' %
                              (row.mailinglist_id, row.form_id))
             for domain in domains:
                 line = '%s%s@%s\taccept:%s' % (self.prefix, row.rid,
                                                domain, ','.join(extra))
                 logger.debug(f'Add line: {line}')
                 rc.append(line)
             if aliases and domains:
                 for alias in aliases:
                     rc.append('%s\talias:%s%s@%s' %
                               (alias, self.prefix, row.rid, domains[0]))
                     accepted_forwards.add(alias)
         #
         if seen_rids:
             rules: Dict[int, Dict[str, List[str]]] = {}
             for row in db.query(
                     'SELECT rid, section, pattern FROM mailloop_rule_tbl'):
                 if row.rid in seen_rids:
                     try:
                         rule = rules[row.rid]
                     except KeyError:
                         rule = rules[row.rid] = {}
                     try:
                         sect = rule[row.section]
                     except KeyError:
                         sect = rule[row.section] = []
                     sect.append(row.pattern)
             self.update_rules(rules)
         #
         for forward in forwards:
             with Ignore(ValueError):
                 fdomain = (forward.address.split('@', 1)[-1]).lower()
                 for domain in self.mtdom:
                     if domain == fdomain and forward.address not in accepted_forwards:
                         logger.warning(
                             '%s: using address "%s" with local handled domain "%s"'
                             % (forward.rid, forward.address, domain))
                 refuse = []
                 for (domain,
                      new_domain) in ((_d, _n)
                                      for (_d, _n) in new_domains.items()
                                      if _d == fdomain):
                     logger.warning(
                         '%s: try to add new domain for already existing forward address "%s" in %s, refused'
                         % (new_domain.rid, forward.address, forward.rid))
                     refuse.append(domain)
                 for domain in refuse:
                     del new_domains[domain]
         #
         if new_domains:
             if self.mta.mta == 'sendmail':
                 cmd = [self.control_sendmail, 'add']
                 for domain in new_domains:
                     cmd.append(domain)
                 logger.info(f'Found new domains, add them using {cmd}')
                 silent_call(*cmd)
                 logger.info('Restarting sendmail due to domain update')
                 silent_call(self.restart_sendmail)
             self.read_mailertable(new_domains)
     return rc
Example #10
0
    def read_mailertable(
            self,
            new_domains: Optional[Dict[str, BavUpdate.RID]] = None) -> None:
        self.domains.clear()
        self.mtdom.clear()
        if self.mta.mta == 'postfix':

            def find(key: str, default_value: str) -> BavUpdate.Filecontent:
                rc = BavUpdate.Filecontent(path=None,
                                           content=[],
                                           modified=False,
                                           hash=None)
                with Ignore(KeyError):
                    for element in self.mta.getlist(key):
                        hash: Optional[str]
                        path: str
                        try:
                            (hash, path) = element.split(':', 1)
                        except ValueError:
                            (hash, path) = (None, element)
                        if path.startswith(base):
                            if rc.path is None:
                                rc.path = path
                                rc.hash = hash
                            if not os.path.isfile(path):
                                create_path(os.path.dirname(path))
                                open(path, 'w').close()
                                if hash is not None:
                                    self.mta.postfix_make(path)
                    if rc.path is not None:
                        try:
                            with open(rc.path) as fd:
                                for line in (_l.strip() for _l in fd):
                                    try:
                                        (var, val) = [
                                            _v.strip()
                                            for _v in line.split(None, 1)
                                        ]
                                    except ValueError:
                                        var = line
                                        val = default_value
                                        rc.modified = True
                                    if var not in [
                                            _c.name for _c in rc.content
                                    ]:
                                        rc.content.append(
                                            BavUpdate.Domain(name=var,
                                                             value=val))
                                    else:
                                        rc.modified = True
                            logger.debug('Read %d lines from %s' %
                                         (len(rc.content), rc.path))
                        except OSError as e:
                            logger.error('Failed to read %s: %s' %
                                         (rc.path, e))
                    else:
                        logger.warning(
                            'No path for postfix parameter %s found' % key)
                return rc

            def save(ct: BavUpdate.Filecontent) -> None:
                if ct.path is not None and (ct.modified
                                            or not os.path.isfile(ct.path)):
                    try:
                        with open(ct.path, 'w') as fd:
                            if ct.content:
                                fd.write('\n'.join(
                                    ['%s\t%s' % _c
                                     for _c in ct.content]) + '\n')
                        logger.info('Written %d lines to %s' %
                                    (len(ct.content), ct.path))
                        if ct.hash is not None:
                            self.mta.postfix_make(ct.path)
                    except OSError as e:
                        logger.error('Failed to save %s: %s' % (ct.path, e))

            #
            relay_default_value = 'dummy'
            relays = find('relay_domains', relay_default_value)
            for d in relays.content:
                self.mtdom[d.name] = 0
            #
            def add_relay_domain(domain_to_add: str) -> None:
                if domain_to_add and domain_to_add not in self.mtdom:
                    relays.content.append(
                        BavUpdate.Domain(name=domain_to_add,
                                         value=relay_default_value))
                    relays.modified = True
                    self.mtdom[domain_to_add] = 0

            #
            if new_domains:
                for domain in new_domains:
                    add_relay_domain(domain)
            transport_default_value = 'mailloop:'
            transports = find('transport_maps', transport_default_value)
            with DB() as db:
                for row in db.query(
                        'SELECT mailloop_domain FROM company_tbl WHERE mailloop_domain IS NOT NULL AND status = :status',
                    {'status': 'active'}):
                    if row.mailloop_domain:
                        add_relay_domain(row.mailloop_domain.strip().lower())
            transport_domains = set([_c[0] for _c in transports.content])
            for d in relays.content[:]:
                if d.name not in transport_domains:
                    transports.content.append(
                        BavUpdate.Domain(name=d.name,
                                         value=transport_default_value))
                    transports.modified = True
                self.mtdom[d.name] += 1
            save(relays)
            save(transports)
            if relays.modified or transports.modified:
                cmd = which('smctrl')
                if cmd is not None:
                    n = silent_call(cmd, 'service', 'reload')
                    if n == 0:
                        logger.info('Reloaded')
                    else:
                        logger.error('Reloading failed: %d' % n)
            self.domains = [_c.name for _c in relays.content]
        else:
            try:
                for line in self.file_reader(
                        os.path.join(self.sendmail_base, 'mailertable')):
                    parts = line.split()
                    if len(parts) > 1 and not parts[0].startswith(
                            '.') and parts[1].startswith('procmail:'):
                        self.domains.append(parts[0])
                        self.mtdom[parts[0]] = 0
            except IOError as e:
                logger.error('Unable to read mailertable %s' % e)
            try:
                for line in self.file_reader(
                        os.path.join(self.sendmail_base, 'relay-domains')):
                    if line in self.mtdom:
                        self.mtdom[line] += 1
                    else:
                        logger.debug(
                            'We relay domain "%s" without catching it in mailertable'
                            % line)
                for key in self.mtdom.keys():
                    if self.mtdom[key] == 0:
                        logger.debug(
                            'We define domain "%s" in mailertable, but do not relay it'
                            % key)
            except IOError as e:
                logger.error('Unable to read relay-domains %s' % e)
Example #11
0
 def prepare(self) -> None:
     self.db = DB()
     self.mailings: List[Mailing] = []
     self.mailing_info: Dict[int, Recovery.MailingInfo] = {}
     self.report: List[str] = []
     self.db.check_open()
Example #12
0
class Recovery(CLI):  #{{{
    @dataclass
    class MailingInfo:
        company_id: int
        name: str
        exists: bool
        deleted: bool

    def add_arguments(self, parser: argparse.ArgumentParser) -> None:
        parser.add_argument('-n',
                            '--dryrun',
                            action='store_true',
                            help='just show')
        parser.add_argument(
            '-A',
            '--age',
            action="store",
            type=int,
            default=1,
            help='set maximum age for a mailing to be recovered in days')
        parser.add_argument(
            '-D',
            '--startup-delay',
            action="store",
            default="1m",
            help=
            'set delay to wait for startup of backend in seconds or an offset (e.g. "1m")',
            dest='startup_delay')
        parser.add_argument(
            'parameter',
            nargs='*',
            help='optional list of mailing_ids to restrict recovery to')

    def use_arguments(self, args: argparse.Namespace) -> None:
        unit = Unit()
        self.dryrun = args.dryrun
        self.max_age = args.age
        self.startup_delay = unit.parse(args.startup_delay)
        self.restrict_to_mailings = set(
            int(_p) for _p in args.parameter) if args.parameter else None

    def prepare(self) -> None:
        self.db = DB()
        self.mailings: List[Mailing] = []
        self.mailing_info: Dict[int, Recovery.MailingInfo] = {}
        self.report: List[str] = []
        self.db.check_open()

    def cleanup(self, success: bool) -> None:
        for m in self.mailings:
            m.done()
        self.db.close()

    def executor(self) -> bool:
        log.set_loglevel('debug')
        try:
            with Lock():
                with log('collect'):
                    self.collect_mailings()
                with log('recover'):
                    self.recover_mailings()
                with log('report'):
                    self.report_mailings()
            return True
        except error as e:
            logger.exception('Failed recovery: %s' % e)
            return False

    def __make_range(self, start: datetime, end: datetime) -> List[str]:  #{{{
        rc: List[str] = []
        current_day = start.toordinal()
        end_day = end.toordinal()
        while current_day <= end_day:
            day = datetime.fromordinal(current_day)
            rc.append(f'{day.year:04d}{day.month:02d}{day.day:02d}')
            current_day += 1
        return rc

    #}}}
    def __mail(self, mailing_id: int) -> Recovery.MailingInfo:  #{{{
        with Ignore(KeyError):
            return self.mailing_info[mailing_id]
        #
        rq = self.db.querys(
            'SELECT company_id, shortname, deleted FROM mailing_tbl WHERE mailing_id = :mid',
            {'mid': mailing_id})
        self.mailing_info[mailing_id] = rc = Recovery.MailingInfo(
            company_id=rq.company_id if rq is not None else 0,
            name=rq.shortname
            if rq is not None else f'#{mailing_id} not found',
            exists=rq is not None,
            deleted=bool(rq.deleted) if rq is not None else False)
        return rc

    #}}}
    def __mailing_name(self, mailing_id: int) -> str:  #{{{
        return self.__mail(mailing_id).name

    #}}}
    def __mailing_exists(self, mailing_id: int) -> bool:  #{{{
        return self.__mail(mailing_id).exists

    #}}}
    def __mailing_deleted(self, mailing_id: int) -> bool:  #{{{
        return self.__mail(mailing_id).deleted

    #}}}
    def __mailing_valid(self, mailing_id: int) -> bool:  #{{{
        return self.__mailing_exists(
            mailing_id) and not self.__mailing_deleted(mailing_id)

    #}}}
    def collect_mailings(self) -> None:  #{{{
        now = datetime.now()
        expire = now - timedelta(days=self.max_age)
        yesterday = now - timedelta(days=1)
        query = (
            'SELECT status_id, mailing_id '
            'FROM maildrop_status_tbl '
            'WHERE genstatus = 2 AND status_field = \'R\' AND genchange > :expire AND genchange < current_timestamp'
        )
        check_query = self.db.qselect(
            oracle=
            'SELECT count(*) FROM rulebased_sent_tbl WHERE mailing_id = :mid AND to_char (lastsent, \'YYYY-MM-DD\') = to_char (sysdate - 1, \'YYYY-MM-DD\')',
            mysql=
            'SELECT count(*) FROM rulebased_sent_tbl WHERE mailing_id = :mid AND date_format(lastsent, \'%%Y-%%m-%%d\') = \'%04d-%02d-%02d\''
            % (yesterday.year, yesterday.month, yesterday.day))
        update = ('UPDATE maildrop_status_tbl '
                  'SET genstatus = 1, genchange = current_timestamp '
                  'WHERE status_id = :sid')
        for (status_id, mailing_id) in self.db.queryc(query,
                                                      {'expire': expire}):
            if (self.restrict_to_mailings is None
                    or mailing_id in self.restrict_to_mailings
                ) and self.__mailing_valid(mailing_id):
                count = self.db.querys(check_query, {'mid': mailing_id})
                if count is not None and count[0] == 1:
                    logger.info('Reactivate rule based mailing %d: %s' %
                                (mailing_id, self.__mailing_name(mailing_id)))
                    if not self.dryrun:
                        self.db.update(update, {'sid': status_id})
                    self.report.append(
                        '%s [%d]: Reactivate rule based mailing' %
                        (self.__mailing_name(mailing_id), mailing_id))
                else:
                    logger.warning(
                        'Rule based mailing %d (%s) not reactivated as it had not been sent out yesterday'
                        % (mailing_id, self.__mailing_name(mailing_id)))
                    self.report.append(
                        '%s [%d]: Not reactivating rule based mailing as it had not been sent out yesterday'
                        % (self.__mailing_name(mailing_id), mailing_id))
        if not self.dryrun:
            self.db.sync()
        query = (
            'SELECT status_id, mailing_id, company_id, status_field, senddate '
            'FROM maildrop_status_tbl '
            'WHERE genstatus IN (1, 2) AND genchange > :expire AND genchange < current_timestamp AND status_field = \'W\''
        )
        for (status_id, mailing_id, company_id, status_field,
             senddate) in self.db.queryc(query, {'expire': expire}):
            if (self.restrict_to_mailings is None
                    or mailing_id in self.restrict_to_mailings
                ) and self.__mailing_valid(mailing_id):
                check = self.__make_range(senddate, now)
                self.mailings.append(
                    Mailing(status_id, status_field, mailing_id, company_id,
                            check))
                logger.info('Mark mailing %d (%s) for recovery' %
                            (mailing_id, self.__mailing_name(mailing_id)))
        self.mailings.sort(key=lambda m: m.status_id)
        logger.info('Found %d mailing(s) to recover' % len(self.mailings))

    #}}}
    def recover_mailings(self) -> None:  #{{{
        if not self.dryrun and self.mailings and self.startup_delay > 0:
            logger.info('Wait for backend to start up')
            n = self.startup_delay
            while n > 0:
                time.sleep(1)
                n -= 1
                if not self.running:
                    raise error('abort due to process termination')

        for m in self.mailings:
            m.collect_seen()
            if self.dryrun:
                print('%s: %d recipients already seen' %
                      (self.__mailing_name(m.mailing_id), len(m.seen)))
            else:
                m.create_filelist()
                count = 0
                for (total_mails, ) in self.db.query(
                        'SELECT total_mails FROM mailing_backend_log_tbl WHERE status_id = :sid',
                    {'sid': m.status_id}):
                    if not total_mails is None and total_mails > count:
                        count = total_mails
                m.set_generated_count(count)
                self.db.update(
                    'DELETE FROM mailing_backend_log_tbl WHERE status_id = :sid',
                    {'sid': m.status_id})
                self.db.update(
                    'DELETE FROM world_mailing_backend_log_tbl WHERE mailing_id = :mid',
                    {'mid': m.mailing_id})
                self.db.update(
                    'UPDATE maildrop_status_tbl SET genstatus = 1 WHERE status_id = :sid',
                    {'sid': m.status_id})
                self.db.sync()
                logger.info('Start backend using status_id %d for %s' %
                            (m.status_id, self.__mailing_name(m.mailing_id)))
                starter = agn3.emm.mailing.Mailing()
                if not starter.fire(status_id=m.status_id,
                                    cursor=self.db.cursor):
                    logger.error('Failed to trigger mailing %d')
                    self.report.append(
                        '%s [%d]: Failed to trigger mailing' %
                        (self.__mailing_name(m.mailing_id), m.mailing_id))
                    break
                self.db.sync()
            self.report.append(
                '%s [%d]: Start recovery using status_id %d' %
                (self.__mailing_name(m.mailing_id), m.mailing_id, m.status_id))
            if not self.dryrun:
                query = 'SELECT genstatus FROM maildrop_status_tbl WHERE status_id = :status_id'
                start = int(time.time())
                ok = True
                last_generation_status = 1
                while self.running and m.active and ok:
                    now = int(time.time())
                    self.db.sync(False)
                    row = self.db.querys(query, {'status_id': m.status_id})
                    if row is None or row[0] is None:
                        logger.info('Failed to query status for mailing %d' %
                                    m.mailing_id)
                        self.report.append(
                            '%s [%d]: Recovery failed due to missing status' %
                            (self.__mailing_name(m.mailing_id), m.mailing_id))
                        ok = False
                    else:
                        generation_status = row[0]
                        if generation_status != last_generation_status:
                            logger.info(
                                f'Mailings {m.mailing_id} generation status has changed from {last_generation_status} to {generation_status}'
                            )
                            last_generation_status = generation_status
                        if generation_status == 3:
                            logger.info('Mailing %d terminated as expected' %
                                        m.mailing_id)
                            self.report.append(
                                '%s [%d]: Recovery finished' %
                                (self.__mailing_name(
                                    m.mailing_id), m.mailing_id))
                            m.active = False
                        elif generation_status == 2:
                            if m.last:
                                current = 0
                                for (currentMails, ) in self.db.query(
                                        'SELECT current_mails FROM mailing_backend_log_tbl WHERE status_id = :sid',
                                    {'sid': m.status_id}):
                                    if not currentMails is None:
                                        current = currentMails
                                if current != m.current:
                                    logger.debug(
                                        f'Mailing {m.mailing_id} has created {current:,d} vs. {m.current:,d} when last checked'
                                    )
                                    m.current = current
                                    m.last = now
                                else:
                                    if (current > 0 and m.last + 1200 < now
                                        ) or (current == 0
                                              and m.last + 3600 < now):
                                        logger.info(
                                            'Mailing %d terminated due to inactivity after %d mails'
                                            % (m.mailing_id, current))
                                        self.report.append(
                                            '%s [%d]: Recovery timed out' %
                                            (self.__mailing_name(
                                                m.mailing_id), m.mailing_id))
                                        ok = False
                            else:
                                m.last = now
                        elif generation_status == 1:
                            if start + 1800 < now:
                                logger.info(
                                    'Mailing %d terminated while not starting up'
                                    % m.mailing_id)
                                self.report.append(
                                    '%s [%d]: Recovery not started' %
                                    (self.__mailing_name(
                                        m.mailing_id), m.mailing_id))
                                ok = False
                        elif generation_status > 3:
                            logger.info(
                                'Mailing %d terminated with status %d' %
                                (m.mailing_id, generation_status))
                            self.report.append(
                                '%s [%d]: Recovery ended with unexpected status %d'
                                % (self.__mailing_name(m.mailing_id),
                                   m.mailing_id, generation_status))
                            m.active = False
                    if m.active and ok:
                        if start + 30 * 60 < now:
                            logger.info(
                                'Failed due to global timeout to recover %d' %
                                m.mailing_id)
                            self.report.append(
                                '%s [%d]: Recovery ended due to global timeout'
                                % (self.__mailing_name(
                                    m.mailing_id), m.mailing_id))
                            ok = False
                        else:
                            time.sleep(1)
                if not m.active:
                    count = 0
                    for (total_mails, ) in self.db.query(
                            'SELECT total_mails FROM mailing_backend_log_tbl WHERE status_id = :sid',
                        {'sid': m.status_id}):
                        if not total_mails is None:
                            count = total_mails
                    count += len(m.seen)
                    self.db.update(
                        'UPDATE mailing_backend_log_tbl SET total_mails = :cnt, current_mails = :cnt WHERE status_id = :sid',
                        {
                            'sid': m.status_id,
                            'cnt': count
                        })
                    self.db.update(
                        'UPDATE world_mailing_backend_log_tbl SET total_mails = :cnt, current_mails = :cnt WHERE mailing_id = :mid',
                        {
                            'mid': m.mailing_id,
                            'cnt': count
                        })
                    self.db.sync()
                if not self.running or not ok:
                    break

    #}}}
    def report_mailings(self) -> None:  #{{{
        class MailInfo(NamedTuple):
            status_id: int
            status_field: str
            mailing_id: int
            mailing_name: str
            company_id: int
            deleted: bool
            genchange: datetime
            senddate: datetime

        mails = []
        query = 'SELECT status_id, mailing_id, genstatus, genchange, status_field, senddate FROM maildrop_status_tbl WHERE '
        query += 'genstatus IN (1, 2) AND status_field IN (\'W\', \'R\', \'D\')'
        for (status_id, mailing_id, genstatus, genchange, status_field,
             senddate) in self.db.queryc(query):
            if status_field in ('R', 'D') and genstatus == 1:
                continue
            info = self.__mail(mailing_id)
            mails.append(
                MailInfo(status_id=status_id,
                         status_field=status_field,
                         mailing_id=mailing_id,
                         mailing_name=info.name,
                         company_id=info.company_id,
                         deleted=info.deleted,
                         genchange=genchange,
                         senddate=senddate))
        if self.report or mails:
            template = os.path.join(base, 'scripts', 'recovery3.tmpl')
            try:
                with open(template, 'r') as fd:
                    content = fd.read()
                ns = {'host': fqdn, 'report': self.report, 'mails': mails}
                tmpl = Template(content)
                try:
                    body = tmpl.fill(ns)
                    charset = tmpl.property('charset', default='UTF-8')
                    subject = tmpl.property('subject')
                    if not subject:
                        subject = tmpl['subject']
                    if not subject:
                        subject = 'Recovery report for %s' % ns['host']
                    else:
                        subject = Template(subject).fill(ns)
                    sender = tmpl.property('sender', f'{user}@{fqdn}')
                    receiver = tmpl.property('receiver')
                    if receiver:
                        receiver = Template(receiver).fill(ns)
                        if self.dryrun:
                            print('From: %s' % sender)
                            print('To: %s' % receiver)
                            print('Subject: %s' % subject)
                            print('')
                            print(body)
                        else:
                            EMail.force_encoding(charset, 'qp')
                            mail = EMail()
                            if sender:
                                mail.set_sender(sender)
                            for recv in [
                                    _r.strip() for _r in receiver.split(',')
                            ]:
                                if recv:
                                    mail.add_to(recv)
                            if charset:
                                mail.set_charset(charset)
                            mail.set_subject(subject)
                            mail.set_text(body)
                            mail.send_mail()
                except error as e:
                    logger.error('Failed to fill template "%s": %s' %
                                 (template, e))
            except IOError as e:
                logger.error('Unable to find template "%s": %s' %
                             (template, e))
Example #13
0
 def read_database(self, auto: List[Autoresponder]) -> List[str]:
     rc: List[str] = []
     with DB() as db:
         company_list: List[int] = []
         new_domains: Dict[str, BavUpdate.RID] = {}
         forwards: List[BavUpdate.Forward] = []
         seen_domains: Set[str] = set()
         accepted_forwards: Set[str] = set()
         ctab: Dict[int, str] = {}
         #
         rc.append(f'fbl@{self.filter_domain}\taccept:rid=unsubscribe')
         for domain in self.domains:
             if domain not in seen_domains:
                 rc.append(f'fbl@{domain}\talias:fbl@{self.filter_domain}')
                 seen_domains.add(domain)
         if self.filter_domain not in seen_domains:
             new_domains[self.filter_domain] = BavUpdate.RID(
                 rid=0, domain=self.filter_domain)
         seen_domains.add(self.filter_domain)
         #
         missing = []
         for row in db.query(
                 'SELECT company_id, mailloop_domain FROM company_tbl WHERE status = :status',
             {'status': 'active'}):
             if row.mailloop_domain:
                 ctab[row.company_id] = row.mailloop_domain
                 if row.mailloop_domain not in seen_domains:
                     rc.append(
                         f'fbl@{row.mailloop_domain}\talias:fbl@{self.filter_domain}'
                     )
                     if row.mailloop_domain not in self.mtdom and row.mailloop_domain.lower(
                     ) != self.fqdn:
                         new_domains[row.mailloop_domain] = BavUpdate.RID(
                             rid=0, domain=row.mailloop_domain)
                     seen_domains.add(row.mailloop_domain)
             else:
                 missing.append(row.company_id)
             company_list.append(row.company_id)
         if missing:
             logger.debug(
                 'Missing mailloop_domain for companies {companies}'.format(
                     companies=Stream(missing).sorted().join(', ')))
         #
         seen_rids: Set[int] = set()
         seen_filter_addresses: Dict[str, str] = {}
         for row in db.query(
                 'SELECT rid, shortname, company_id, filter_address, '
                 '       forward_enable, forward, ar_enable, '
                 '       subscribe_enable, mailinglist_id, form_id, timestamp, '
                 '       spam_email, spam_required, spam_forward, '
                 '       autoresponder_mailing_id, security_token '
                 'FROM mailloop_tbl '
                 'ORDER BY rid'):
             if row.company_id not in company_list or row.rid is None:
                 if row.company_id not in company_list:
                     logger.debug('{row}: ignore due to inactive company')
                 elif row.rid is None:
                     logger.error(
                         '{row}: ignore due to empty rid, should never happen!'
                     )
                 continue
             #
             row_id = f'{row.rid} {row.shortname} [{row.company_id}]'
             seen_rids.add(row.rid)
             domains: List[str] = [self.filter_domain]
             aliases: List[str] = []
             if row.filter_address is not None:
                 for alias in listsplit(row.filter_address):
                     if not alias.startswith(self.prefix):
                         with Ignore(ValueError):
                             (local_part, domain_part) = alias.split('@', 1)
                             normalized_alias = '{local_part}@{domain_part}'.format(
                                 local_part=local_part,
                                 domain_part=domain_part.lower())
                             if normalized_alias in seen_filter_addresses:
                                 logger.warning(
                                     f'{row_id}: already seen "{alias}" as "{normalized_alias}" before ({seen_filter_addresses[normalized_alias]})'
                                 )
                             else:
                                 seen_filter_addresses[
                                     normalized_alias] = row_id
                                 if domain_part not in domains:
                                     domains.append(domain_part)
                                     if domain_part not in self.mtdom and domain_part not in new_domains:
                                         new_domains[
                                             domain_part] = BavUpdate.RID(
                                                 rid=row.rid,
                                                 domain=domain_part)
                                 aliases.append(alias)
             #
             ar_enable = False
             if row.ar_enable and row.autoresponder_mailing_id:
                 if not row.security_token:
                     logger.error(
                         f'{row_id}: Autoresponder has mailing id, but no security token, not used'
                     )
                 else:
                     auto.append(
                         Autoresponder(
                             row.rid, row.timestamp if row.timestamp
                             is not None else datetime.now(),
                             row.autoresponder_mailing_id,
                             row.security_token))
                     ar_enable = True
             #
             try:
                 cdomain = ctab[row.company_id]
                 if cdomain not in domains:
                     if cdomain in self.domains:
                         domains.append(cdomain)
                     else:
                         logger.debug(
                             f'{row_id}: company\'s domain "{cdomain}" not found in mailertable'
                         )
             except KeyError:
                 logger.debug(
                     f'{row_id}: no domain for company found, further processing'
                 )
             extra = [f'rid={row.rid}']
             if row.company_id:
                 extra.append(f'cid={row.company_id}')
             if row.forward_enable and row.forward:
                 forward = row.forward.strip()
                 if forward:
                     extra.append(f'fwd={forward}')
                     forwards.append(
                         BavUpdate.Forward(rid=row.rid, address=forward))
             if row.spam_email:
                 extra.append(f'spam_email={row.spam_email}')
             if row.spam_forward:
                 forward = row.spam_forward.strip()
                 if forward:
                     extra.append(f'spam_fwd={forward}')
             if row.spam_required:
                 extra.append(f'spam_req={row.spam_required}')
             if ar_enable:
                 extra.append(f'ar={row.rid}')
                 if row.autoresponder_mailing_id:
                     extra.append(f'armid={row.autoresponder_mailing_id}')
             if row.subscribe_enable and row.mailinglist_id and row.form_id:
                 extra.append(f'sub={row.mailinglist_id}:{row.form_id}')
             line = '{prefix}{rid}@{domain}\taccept:{extra}'.format(
                 prefix=self.prefix,
                 rid=row.rid,
                 domain=self.filter_domain,
                 extra=','.join([escape(_e) for _e in extra]))
             logger.debug(f'{row_id}: add line: {line}')
             rc.append(line)
             if aliases:
                 for alias in aliases:
                     rc.append(
                         f'{alias}\talias:{self.prefix}{row.rid}@{self.filter_domain}'
                     )
                     accepted_forwards.add(alias)
         #
         if seen_rids:
             rules: Dict[int, Dict[str, List[str]]] = {}
             for row in db.query(
                     'SELECT rid, section, pattern FROM mailloop_rule_tbl'):
                 if row.rid in seen_rids:
                     try:
                         rule = rules[row.rid]
                     except KeyError:
                         rule = rules[row.rid] = {}
                     try:
                         sect = rule[row.section]
                     except KeyError:
                         sect = rule[row.section] = []
                     sect.append(row.pattern)
             self.update_rules(rules)
         #
         for forward in forwards:
             with Ignore(ValueError):
                 fdomain = (forward.address.split('@', 1)[-1]).lower()
                 for domain in self.mtdom:
                     if domain == fdomain and forward.address not in accepted_forwards:
                         logger.warning(
                             f'{forward.ird}: using address "{forward.address}" with local handled domain "{domain}"'
                         )
                 refuse = []
                 for (domain,
                      new_domain) in ((_d, _n)
                                      for (_d, _n) in new_domains.items()
                                      if _d == fdomain):
                     logger.warning(
                         f'{new_domain.rid}: try to add new domain for already existing forward address "{forward.address}" in {forward.rid}, refused'
                     )
                     refuse.append(domain)
                 for domain in refuse:
                     del new_domains[domain]
         #
         if new_domains:
             if self.mta.mta == 'sendmail':
                 if os.access(BavUpdate.control_sendmail, os.X_OK):
                     cmd = [BavUpdate.control_sendmail, 'add']
                     for domain in new_domains:
                         cmd.append(domain)
                     logger.info(f'Found new domains, add them using {cmd}')
                     silent_call(*cmd)
                     if os.access(BavUpdate.restart_sendmail, os.X_OK):
                         logger.info(
                             'Restarting sendmail due to domain update')
                         silent_call(BavUpdate.restart_sendmail)
                     else:
                         logger.warning(
                             f'Missing {BavUpdate.restart_sendmail}, no restart of mta perfomed'
                         )
                 else:
                     logger.warning(
                         f'Missing {BavUpdate.control_sendmail}, no new domains are added'
                     )
             self.read_mailertable(new_domains)
     return rc
Example #14
0
 def executor(self) -> bool:
     rc = True
     (name, extension) = os.path.splitext(os.path.basename(self.filename))
     if not self.language:
         if extension.startswith('.'):
             extension = extension[1:]
         self.language = extension.lower()
     with DB() as db, db.request() as cursor:
         if not self.only_tags:
             with open(self.filename) as fd:
                 code = fd.read()
             if code.startswith('#!'):
                 code = code.split('\n', 1)[-1]
             code = re.split('\n[^\n]*%%\n', code)[0]
             rq = cursor.querys(
                 'SELECT tag_function_id, lang, description, code '
                 'FROM tag_function_tbl '
                 'WHERE name = :name AND company_id = :company_id', {
                     'name': name,
                     'company_id': self.company_id
                 })
             if rq is None:
                 data = {
                     'name': name,
                     'company_id': self.company_id,
                     'lang': self.language,
                     'code': code,
                     'tdesc': self.description
                 }
                 query = cursor.qselect(
                     oracle=
                     ('INSERT INTO tag_function_tbl '
                      '       (tag_function_id, company_id, creation_date, timestamp, name, lang, description, code) '
                      'VALUES '
                      '       (tag_function_tbl_seq.nextval, :company_id, current_timestamp, current_timestamp, :name, :lang, :tdesc, :code)'
                      ),
                     mysql=
                     ('INSERT INTO tag_function_tbl '
                      '       (company_id, creation_date, timestamp, name, lang, description, code) '
                      'VALUES '
                      '       (:company_id, current_timestamp, current_timestamp, :name, :lang, :tdesc, :code)'
                      ))
                 if self.dryrun:
                     print(f'{name}: Would execute {query} using {data}')
                 else:
                     if db.dbms == 'oracle' and db.db is not None:
                         cursor.set_input_sizes(code=db.db.driver.CLOB)
                     rows = cursor.update(query, data)
                     if rows == 1:
                         if not self.quiet:
                             print(
                                 f'{name}: code inserted setting language to "{self.language}" for company {self.company_id}'
                             )
                     else:
                         print(
                             '{name}: FAILED to insert code into database: {error}'
                             .format(name=name, error=db.last_error()))
                         rc = False
             elif rq.lang == self.language and str(rq.code) == code and (
                     not self.description
                     or self.description == rq.description):
                 if not self.quiet:
                     print(f'{name}: no change')
             else:
                 data = {
                     'fid': rq.tag_function_id,
                     'lang': self.language,
                     'code': code
                 }
                 if self.description:
                     data['tdesc'] = self.description
                     extra = ', description = :tdesc'
                 else:
                     extra = ''
                 query = (
                     'UPDATE tag_function_tbl '
                     f'SET code = :code, lang = :lang, timestamp = current_timestamp{extra} '
                     'WHERE tag_function_id = :fid')
                 if self.dryrun:
                     print(f'{name}: Would execute {query} using {data}')
                 else:
                     if db.dbms == 'oracle' and db.db is not None:
                         cursor.set_input_sizes(code=db.db.driver.CLOB)
                     rows = cursor.update(query, data)
                     if rows == 1:
                         if not self.quiet:
                             print(
                                 '{name}: code updated using language "{self.language}"'
                             )
                     else:
                         print('{name}: FAILED to update code: {error}'.
                               format(name=name, error=db.last_error()))
                         rc = False
         #
         for tag in self.tags:
             parts = tag.split(':', 2)
             cdesc = name
             tdesc: Optional[str] = None
             if len(parts) > 1:
                 tag = parts[0]
                 if parts[1]:
                     cdesc = '%s:%s' % (name, parts[1])
                 if len(parts) == 3 and parts[2]:
                     tdesc = parts[2]
             data = {'cdesc': cdesc}
             rq = cursor.querys(
                 'SELECT tag_id, type, selectvalue, description '
                 'FROM tag_tbl '
                 'WHERE tagname = :tname AND company_id = :company_id', {
                     'tname': tag,
                     'company_id': self.company_id
                 })
             if rq is None:
                 data['type'] = 'FUNCTION'
                 data['tname'] = tag
                 data['company_id'] = self.company_id
                 if tdesc is None:
                     tdesc = 'Created by script-tag'
                 data['tdesc'] = tdesc
                 query = cursor.qselect(
                     oracle=
                     ('INSERT INTO tag_tbl '
                      '       (tag_id, tagname, selectvalue, type, company_id, description, timestamp) '
                      'VALUES '
                      '       (tag_tbl_seq.nextval, :tname, :cdesc, :type, :company_id, :tdesc, current_timestamp)'
                      ),
                     mysql=
                     ('INSERT INTO tag_tbl '
                      '       (tagname, selectvalue, type, company_id, description, change_date) '
                      'VALUES '
                      '       (:tname, :cdesc, :type, :company_id, :tdesc, current_timestamp)'
                      ))
                 if self.dryrun:
                     print(f'Tag {tag}: Would execute {query} using {data}')
                 else:
                     rows = cursor.update(query, data)
                     if rows == 1:
                         if not self.quiet:
                             print(f'Tag {tag}: inserted into database')
                     else:
                         print(
                             'Tag {tag}: FAILED to insert into database: {error}'
                             .format(tag=tag, error=db.last_error()))
                         rc = False
             elif rq.selectvalue == cdesc and (not tdesc
                                               or rq.description == tdesc):
                 if not self.quiet:
                     print(f'Tag {tag}: no change')
             else:
                 data['tid'] = rq.tag_id
                 if tdesc:
                     data['tdesc'] = tdesc
                     extra = ', description = :tdesc'
                 else:
                     extra = ''
                 if rq.type != 'FUNCTION':
                     if not self.quiet:
                         print(f'Tag {tag}: modify type from {rq.type}')
                     data['type'] = 'FUNCTION'
                     extra += ', type = :type'
                 query = cursor.qselect(
                     oracle=
                     ('UPDATE tag_tbl '
                      f'SET selectvalue = :cdesc, timestamp = current_timestamp{extra} '
                      'WHERE tag_id = :tid'),
                     mysql=
                     ('UPDATE tag_tbl '
                      f'SET selectvalue = :cdesc, change_date = current_timestamp{extra} '
                      'WHERE tag_id = :tid'))
                 if self.dryrun:
                     print(f'Tag {tag}: Would execute (query) using {data}')
                 else:
                     rows = cursor.update(query, data)
                     if rows == 1:
                         if not self.quiet:
                             print(f'Tag {tag}: updated')
                     else:
                         print(
                             'Tag {tag}: FAILED to update: {error}'.format(
                                 tag=tag, error=db.last_error()))
                         rc = False
         cursor.sync(not self.dryrun and rc)
     return rc
Example #15
0
	def __init__ (self, ref: ScheduleGenerate) -> None:
		self.ref = ref
		self.db = DB ()
		self.unit = Unit ()