com.aoindustries.aoserv.master.AccountCleaner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aoserv-master Show documentation
Show all versions of aoserv-master Show documentation
Master server for the AOServ Platform.
/*
* aoserv-master - Master server for the AOServ Platform.
* Copyright (C) 2003-2013, 2015, 2017, 2018, 2019, 2020 AO Industries, Inc.
* [email protected]
* 7262 Bull Pen Cir
* Mobile, AL 36695
*
* This file is part of aoserv-master.
*
* aoserv-master is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* aoserv-master is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with aoserv-master. If not, see .
*/
package com.aoindustries.aoserv.master;
import com.aoindustries.aoserv.client.account.Account;
import com.aoindustries.aoserv.client.backup.BackupReport;
import com.aoindustries.aoserv.client.linux.Group;
import com.aoindustries.aoserv.client.schema.Table;
import com.aoindustries.aoserv.master.dns.DnsService;
import com.aoindustries.collections.IntList;
import com.aoindustries.cron.CronDaemon;
import com.aoindustries.cron.CronJob;
import com.aoindustries.cron.Schedule;
import com.aoindustries.dbc.DatabaseConnection;
import com.aoindustries.util.ErrorPrinter;
import com.aoindustries.util.i18n.Money;
import com.aoindustries.util.logging.ProcessTimer;
import com.aoindustries.validation.ValidationException;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Automatically cleans out old account resources. The resources are left in tact for one month before cleaning.
*
* @author AO Industries, Inc.
*/
final public class AccountCleaner implements CronJob {
private static final Logger logger = Logger.getLogger(AccountCleaner.class.getName());
/**
* The maximum time for a cleaning.
*/
private static final long TIMER_MAX_TIME=20L*60*1000;
/**
* The interval in which the administrators will be reminded.
*/
private static final long TIMER_REMINDER_INTERVAL=6L*60*60*1000;
/**
* The number of days to keep canceled account resources.
*/
private static final int CANCELED_KEEP_DAYS = 30;
private static boolean started=false;
public static void start() {
synchronized(System.out) {
if(!started) {
System.out.print("Starting " + AccountCleaner.class.getSimpleName() + ": ");
CronDaemon.addCronJob(new AccountCleaner(), logger);
started=true;
System.out.println("Done");
}
}
}
private AccountCleaner() {
}
/**
* Runs at 5:25 am daily.
*/
private static final Schedule schedule = (minute, hour, dayOfMonth, month, dayOfWeek, year) -> minute==25 && hour==5;
@Override
public Schedule getSchedule() {
return schedule;
}
@Override
public int getThreadPriority() {
return Thread.NORM_PRIORITY-1;
}
@Override
public void run(int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {
try {
try (
ProcessTimer timer = new ProcessTimer(
logger,
AccountCleaner.class.getName(),
"runCronJob",
"Account Cleaner",
"Cleaning old account resources",
TIMER_MAX_TIME,
TIMER_REMINDER_INTERVAL
)
) {
MasterServer.executorService.submit(timer);
// Start the transaction
final InvalidateList invalidateList=new InvalidateList();
cleanNow(invalidateList);
MasterServer.invalidateTables(invalidateList, null);
}
} catch(ThreadDeath TD) {
throw TD;
} catch(Throwable T) {
logger.log(Level.SEVERE, null, T);
}
}
private static void cleanNow(InvalidateList invalidateList) throws IOException, SQLException, ValidationException {
final DatabaseConnection conn=MasterDatabase.getDatabase().createDatabaseConnection();
try {
boolean connRolledBack=false;
try {
StringBuilder message=new StringBuilder();
// backup.BackupReport
{
// Those that are part of canceled accounts
if(
conn.executeBooleanQuery(
"select\n"
+ " (\n"
+ " select\n"
+ " br.id\n"
+ " from\n"
+ " backup.\"BackupReport\" br,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ " where\n"
+ " br.package=pk.id\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS+"\n"
+ " limit 1\n"
+ " ) is not null"
)
) {
conn.executeUpdate(
"delete from\n"
+ " backup.\"BackupReport\"\n"
+ "where\n"
+ " id in (\n"
+ " select\n"
+ " br.id\n"
+ " from\n"
+ " backup.\"BackupReport\" br,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ " where\n"
+ " br.package=pk.id\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS+"\n"
+ " )"
);
invalidateList.addTable(conn, Table.TableID.BACKUP_REPORTS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
}
// Those that are older than BackupReport.SendmailSmtpStat.MAX_REPORT_AGE
if(
conn.executeBooleanQuery(
"select\n"
+ " (\n"
+ " select\n"
+ " id\n"
+ " from\n"
+ " backup.\"BackupReport\"\n"
+ " where\n"
+ " (CURRENT_DATE - date)>"+BackupReport.MAX_REPORT_AGE+"\n" // Convert to interval?
+ " limit 1\n"
+ " ) is not null"
)
) {
conn.executeUpdate(
"delete from\n"
+ " backup.\"BackupReport\"\n"
+ "where\n"
+ " (CURRENT_DATE - date)>"+BackupReport.MAX_REPORT_AGE // Convert to interval?
);
invalidateList.addTable(conn, Table.TableID.BACKUP_REPORTS, InvalidateList.allAccounts, InvalidateList.allHosts, false);
}
}
// account.Account
{
{
// look for any accounts that have been canceled but not disabled
List bus = conn.executeObjectListQuery(ObjectFactories.accountNameFactory,
"select accounting from account.\"Account\" where parent=? and canceled is not null and disable_log is null",
AccountHandler.getRootAccount()
);
if(!bus.isEmpty()) {
message
.append("The following account.Account ")
.append(bus.size()==1?"has":"have")
.append(" been canceled but not disabled:\n");
for (Account.Name bu : bus) {
message.append(bu).append('\n');
}
message.append('\n');
}
}
{
// look for any accounts that have been disabled for over two months but not canceled
List bus = conn.executeObjectListQuery(ObjectFactories.accountNameFactory,
"select\n"
+ " bu.accounting\n"
+ "from\n"
+ " account.\"Account\" bu,\n"
+ " account.\"DisableLog\" dl\n"
+ "where\n"
+ " bu.canceled is null\n"
+ " and bu.disable_log=dl.id\n"
+ " and (CURRENT_DATE - dl.time::date)>60"
);
if(bus.size()>0) {
message
.append("The following account.Account ")
.append(bus.size()==1?"has":"have")
.append(" been disabled for over 60 days but not canceled:\n");
for (Account.Name bu : bus) {
message.append(bu).append('\n');
}
message.append('\n');
}
}
}
// payment.CreditCard
{
IntList ccs=conn.executeIntListQuery(
"select\n"
+ " cc.id\n"
+ "from\n"
+ " payment.\"CreditCard\" cc,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " cc.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for(int c=0;c administrators=conn.executeObjectListQuery(
ObjectFactories.userNameFactory,
"select\n"
+ " ba.username\n"
+ "from\n"
+ " account.\"Administrator\" ba\n"
+ " inner join account.\"User\" un on ba.username = un.username\n"
+ " inner join billing.\"Package\" pk on un.package = pk.name\n"
+ " inner join account.\"Account\" bu on pk.accounting = bu.accounting\n"
+ "where\n"
+ " bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS + "\n"
// payment.Payment
// PostgresSQL 8.3 doing sequential scan on "or":
// + " and (select cct.id from payment.\"Payment\" cct where cct.credit_card_created_by=ba.username or cct.authorization_username=ba.username or cct.capture_username=ba.username or cct.void_username=ba.username limit 1) is null\n"
+ " and (select cct1.id from payment.\"Payment\" cct1 where cct1.credit_card_created_by = ba.username limit 1) is null\n"
+ " and (select cct2.id from payment.\"Payment\" cct2 where cct2.authorization_username = ba.username limit 1) is null\n"
+ " and (select cct3.id from payment.\"Payment\" cct3 where cct3.capture_username = ba.username limit 1) is null\n"
+ " and (select cct4.id from payment.\"Payment\" cct4 where cct4.void_username = ba.username limit 1) is null\n"
// payment.CreditCard
+ " and (select cc.id from payment.\"CreditCard\" cc where cc.created_by=ba.username limit 1) is null\n"
// account.DisableLog
+ " and (select dl.id from account.\"DisableLog\" dl where dl.disabled_by=ba.username limit 1) is null\n"
// billing.MonthlyCharge
+ " and (select mc.id from billing.\"MonthlyCharge\" mc where mc.created_by=ba.username limit 1) is null\n"
// billing.Package
+ " and (select pk2.id from billing.\"Package\" pk2 where pk2.created_by=ba.username limit 1) is null\n"
// signup.Request
+ " and (select sr.id from signup.\"Request\" sr where sr.completed_by=ba.username limit 1) is null\n"
// ticket.Action
// PostgresSQL 8.3 doing sequential scan on "or":
// + " and (select ta.id from ticket.Action ta where ta.administrator=ba.username or ta.old_assigned_to=ba.username or ta.new_assigned_to=ba.username limit 1) is null\n"
+ " and (select ta1.id from ticket.\"Action\" ta1 where ta1.administrator = ba.username limit 1) is null\n"
+ " and (select ta2.id from ticket.\"Action\" ta2 where ta2.old_assigned_to = ba.username limit 1) is null\n"
+ " and (select ta3.id from ticket.\"Action\" ta3 where ta3.new_assigned_to = ba.username limit 1) is null\n"
// ticket.Assignment
+ " and (select ta4.id from ticket.\"Assignment\" ta4 where ta4.administrator=ba.username limit 1) is null\n"
// ticket.Ticket
+ " and (select ti.id from ticket.\"Ticket\" ti where ti.created_by=ba.username limit 1) is null\n"
// billing.Transaction
+ " and (select tr.transid from billing.\"Transaction\" tr where tr.username=ba.username limit 1) is null"
);
for (com.aoindustries.aoserv.client.account.User.Name administrator : administrators) {
Account.Name account = AccountUserHandler.getAccountForUser(conn, administrator);
boolean hasBalance = false;
for(Money balance : BillingTransactionHandler.getConfirmedAccountBalance(conn, account)) {
if(balance.getUnscaledValue() > 0) {
hasBalance = true;
}
}
if(!hasBalance) AccountHandler.removeAdministrator(conn, invalidateList, administrator);
}
}
// scm.CvsRepository
{
IntList crs=conn.executeIntListQuery(
"select\n"
+ " cr.id\n"
+ "from\n"
+ " scm.\"CvsRepository\" cr,\n"
+ " linux.\"UserServer\" lsa,\n"
+ " account.\"User\" un,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " cr.linux_server_account=lsa.id\n"
+ " and lsa.username=un.username\n"
+ " and un.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for(int c=0;c dzs=conn.executeStringListQuery(
"select\n"
+ " dz.zone\n"
+ "from\n"
+ " dns.\"Zone\" dz,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " dz.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for (String dz : dzs) {
dnsService.removeDNSZone(conn, invalidateList, dz);
}
}
// email.List
{
IntList els=conn.executeIntListQuery(
"select\n"
+ " el.id\n"
+ "from\n"
+ " email.\"List\" el,\n"
+ " linux.\"GroupServer\" lsg,\n"
+ " linux.\"Group\" lg,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " el.linux_server_group=lsg.id\n"
+ " and lsg.name=lg.name\n"
+ " and lg.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS+"\n"
+ " and bu.accounting=pk.accounting\n"
+ " and pk.id=fbs.package"
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c las=conn.executeObjectListQuery(
ObjectFactories.linuxUserNameFactory,
"select\n"
+ " la.username\n"
+ "from\n"
+ " linux.\"User\" la,\n"
+ " account.\"User\" un,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " la.username=un.username\n"
+ " and un.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for (com.aoindustries.aoserv.client.linux.User.Name la : las) {
try {
LinuxAccountHandler.removeUser(conn, invalidateList, la);
} catch (SQLException err) {
System.err.println("SQLException trying to remove User: " + la);
throw err;
}
}
}
// linux.Group
{
List lgs=conn.executeObjectListQuery(ObjectFactories.groupNameFactory,
"select\n"
+ " lg.name\n"
+ "from\n"
+ " linux.\"Group\" lg,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " lg.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for (Group.Name lg : lgs) {
try {
LinuxAccountHandler.removeGroup(conn, invalidateList, lg);
} catch (SQLException err) {
System.err.println("SQLException trying to remove Group: " + lg);
throw err;
}
}
}
// mysql.Database
{
IntList mds=conn.executeIntListQuery(
"select\n"
+ " md.id\n"
+ "from\n"
+ " mysql.\"Database\" md,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " md.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for(int c=0;c mus=conn.executeObjectListQuery(ObjectFactories.mysqlUserNameFactory,
"select\n"
+ " mu.username\n"
+ "from\n"
+ " mysql.\"User\" mu,\n"
+ " account.\"User\" un,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " mu.username=un.username\n"
+ " and un.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for (com.aoindustries.aoserv.client.mysql.User.Name mu : mus) {
MysqlHandler.removeUser(conn, invalidateList, mu);
}
}
// postgresql.Database
{
IntList pds=conn.executeIntListQuery(
"select\n"
+ " pd.id\n"
+ "from\n"
+ " postgresql.\"Database\" pd,\n"
+ " postgresql.\"UserServer\" psu,\n"
+ " account.\"User\" un,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " pd.datdba=psu.id\n"
+ " and psu.username=un.username\n"
+ " and un.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for(int c=0;c pus=conn.executeObjectListQuery(ObjectFactories.postgresqlUserNameFactory,
"select\n"
+ " pu.username\n"
+ "from\n"
+ " postgresql.\"User\" pu,\n"
+ " account.\"User\" un,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " pu.username=un.username\n"
+ " and un.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for (com.aoindustries.aoserv.client.postgresql.User.Name pu : pus) {
PostgresqlHandler.removeUser(conn, invalidateList, pu);
}
}
// account.User
// delete all closed account.User, unless used by a business_administrator that was left behind
{
List uns=conn.executeObjectListQuery(
ObjectFactories.userNameFactory,
"select\n"
+ " un.username\n"
+ "from\n"
+ " account.\"User\" un,\n"
+ " billing.\"Package\" pk,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " un.package=pk.name\n"
+ " and pk.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS+"\n"
+ " and (select ba.username from account.\"Administrator\" ba where ba.username=un.username) is null"
);
for (com.aoindustries.aoserv.client.account.User.Name un : uns) {
AccountUserHandler.removeUser(conn, invalidateList, un);
}
}
// account.DisableLog
{
IntList dls=conn.executeIntListQuery(
"select\n"
+ " dl.id\n"
+ "from\n"
+ " account.\"DisableLog\" dl,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " dl.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS+"\n"
+ " and (select ba.username from account.\"Administrator\" ba where ba.disable_log=dl.id limit 1) is null\n"
+ " and (select bu2.accounting from account.\"Account\" bu2 where bu2.disable_log=dl.id limit 1) is null\n"
+ " and (select cr.id from scm.\"CvsRepository\" cr where cr.disable_log=dl.id limit 1) is null\n"
+ " and (select el.id from email.\"List\" el where el.disable_log=dl.id limit 1) is null\n"
+ " and (select ep.id from email.\"Pipe\" ep where ep.disable_log=dl.id limit 1) is null\n"
+ " and (select esr.id from email.\"SmtpRelay\" esr where esr.disable_log=dl.id limit 1) is null\n"
+ " and (select hst.id from \"web.tomcat\".\"SharedTomcat\" hst where hst.disable_log=dl.id limit 1) is null\n"
+ " and (select hsb.id from web.\"VirtualHost\" hsb where hsb.disable_log=dl.id limit 1) is null\n"
+ " and (select hs.id from web.\"Site\" hs where hs.disable_log=dl.id limit 1) is null\n"
+ " and (select la.username from linux.\"User\" la where la.disable_log=dl.id limit 1) is null\n"
+ " and (select lsa.id from linux.\"UserServer\" lsa where lsa.disable_log=dl.id limit 1) is null\n"
+ " and (select msu.id from mysql.\"UserServer\" msu where msu.disable_log=dl.id limit 1) is null\n"
+ " and (select mu.username from mysql.\"User\" mu where mu.disable_log=dl.id limit 1) is null\n"
+ " and (select pk.name from billing.\"Package\" pk where pk.disable_log=dl.id limit 1) is null\n"
+ " and (select psu.id from postgresql.\"UserServer\" psu where psu.disable_log=dl.id limit 1) is null\n"
+ " and (select pu.username from postgresql.\"User\" pu where pu.disable_log=dl.id limit 1) is null\n"
+ " and (select un.username from account.\"User\" un where un.disable_log=dl.id limit 1) is null"
);
for(int c=0;c=1; depth--) {
// non-default
IntList bss=conn.executeIntListQuery(
"select\n"
+ " bs.id\n"
+ "from\n"
+ " account.\"AccountHost\" bs,\n"
+ " account.\"Account\" bu\n"
+ "where\n"
+ " not bs.is_default\n"
+ " and bs.accounting=bu.accounting\n"
+ " and bu.canceled is not null\n"
+ " and (CURRENT_DATE - bu.canceled::date)>"+CANCELED_KEEP_DAYS
);
for(int c=0;c"+CANCELED_KEEP_DAYS
);
for(int c=0;c0) {
logger.log(Level.WARNING, message.toString());
}
} catch(RuntimeException | IOException err) {
if(conn.rollback()) {
connRolledBack=true;
invalidateList=null;
}
throw err;
} catch(SQLException err) {
if(conn.rollbackAndClose()) {
connRolledBack=true;
invalidateList=null;
}
throw err;
} finally {
if(!connRolledBack && !conn.isClosed()) conn.commit();
}
} finally {
conn.releaseConnection();
}
}
public static void main(String[] args) {
try {
InvalidateList invalidateList = new InvalidateList();
cleanNow(invalidateList);
for(Table.TableID tableId : Table.TableID.values()) {
List affectedHosts = invalidateList.getAffectedHosts(tableId);
if(affectedHosts != null) {
if(affectedHosts == InvalidateList.allHosts) {
System.out.println("invalidate " + tableId.name().toLowerCase(Locale.ENGLISH));
} else {
for(int affectedHost : affectedHosts) {
System.out.println("invalidate " + tableId.name().toLowerCase(Locale.ENGLISH) + " " + affectedHost);
}
}
}
}
} catch(RuntimeException | ValidationException | IOException | SQLException err) {
ErrorPrinter.printStackTraces(err);
System.exit(1);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy