Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DB Backup Bug #2853

Merged
merged 46 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d8a6034
- pointing directly to the pg client directory
elipe17 Feb 16, 2024
6b0a1f0
- Add lots of logging
elipe17 Feb 16, 2024
9a94eb1
- fix lint
elipe17 Feb 16, 2024
9e670bd
- naming crontabs for easy understanding
elipe17 Feb 16, 2024
fe653b0
- Removing log entries
elipe17 Feb 16, 2024
d7b3040
- testing old method
elipe17 Feb 17, 2024
8cfdaed
Merge branch 'develop' into 2852-db-backup
elipe17 Feb 20, 2024
2097d8d
- installing postgres client 15
elipe17 Feb 20, 2024
fdfd31d
Merge branch '2852-db-backup' of https://github.com/raft-tech/TANF-ap…
elipe17 Feb 20, 2024
687c8d2
- print paths to see whats up
elipe17 Feb 20, 2024
27ff2db
- fix lint
elipe17 Feb 20, 2024
76c4b9d
- remove all traces of postgres before installing new postgres
elipe17 Feb 20, 2024
a2a073b
- disabling tests for speed
elipe17 Feb 20, 2024
e6a84a8
Revert "- remove all traces of postgres before installing new postgres"
elipe17 Feb 20, 2024
64c6907
Revert "- fix lint"
elipe17 Feb 20, 2024
5a2153a
Revert "- installing postgres client 15"
elipe17 Feb 20, 2024
1ce4d81
Revert "Revert "- fix lint""
elipe17 Feb 20, 2024
3b73f8d
- Add correct client to apt.yml
elipe17 Feb 20, 2024
30c3e51
- making tests even shorter
elipe17 Feb 20, 2024
72ad936
- trying clietn V14
elipe17 Feb 20, 2024
a2f94d3
- removing from apt and installing manually
elipe17 Feb 20, 2024
d65d6ec
Revert "- removing from apt and installing manually"
elipe17 Feb 20, 2024
2d41164
- revert
elipe17 Feb 20, 2024
68b9f8c
- Version 12 in apt.yml
elipe17 Feb 20, 2024
9faa526
- escaping quotes
elipe17 Feb 20, 2024
ce68563
Merge branch 'develop' into 2852-db-backup
elipe17 Feb 20, 2024
b606933
- forcing db name
elipe17 Feb 20, 2024
7257144
Merge branch '2852-db-backup' of https://github.com/raft-tech/TANF-ap…
elipe17 Feb 20, 2024
73a92a8
Revert "- forcing db name"
elipe17 Feb 20, 2024
447a774
- logging
elipe17 Feb 21, 2024
b3f0245
- more logging
elipe17 Feb 21, 2024
4600056
- Cleanup debug code
elipe17 Feb 21, 2024
2aeeac3
- Fix lint
elipe17 Feb 21, 2024
5a31397
Merge branch 'develop' into 2852-db-backup
elipe17 Feb 21, 2024
fcccf1a
- Adding back client search if hardcoded path doesn't exist
elipe17 Feb 22, 2024
70d2bdf
- fix syntax error
elipe17 Feb 22, 2024
9c69716
- fix lint
elipe17 Feb 22, 2024
d597f4f
- remove extra slash
elipe17 Feb 22, 2024
50a641d
Merge branch 'develop' into 2852-db-backup
ADPennington Feb 23, 2024
57fd384
- Adding log entries to backup task
elipe17 Feb 26, 2024
806569b
- Moving DB task to it's own file
elipe17 Feb 26, 2024
9521b3b
- fix lint
elipe17 Feb 26, 2024
83dcd4d
Merge branch 'develop' of https://github.com/raft-tech/TANF-app into …
elipe17 Feb 26, 2024
edd06b8
- Seperate out email tasks
elipe17 Feb 28, 2024
2f6d998
- update tests
elipe17 Feb 28, 2024
d1fd6e2
Merge branch 'develop' into 2852-db-backup
andrew-jameson Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tdrs-backend/tdpservice/email/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Shared celery email tasks for beat."""

from __future__ import absolute_import
from tdpservice.users.models import User, AccountApprovalStatusChoices
from django.contrib.auth.models import Group
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from celery import shared_task
from datetime import datetime, timedelta
import logging
from tdpservice.email.helpers.account_access_requests import send_num_access_requests_email
from tdpservice.email.helpers.account_deactivation_warning import send_deactivation_warning_email


logger = logging.getLogger(__name__)


@shared_task
def check_for_accounts_needing_deactivation_warning():
"""Check for accounts that need deactivation warning emails."""
deactivate_in_10_days = users_to_deactivate(10)
deactivate_in_3_days = users_to_deactivate(3)
deactivate_in_1_day = users_to_deactivate(1)

if deactivate_in_10_days:
send_deactivation_warning_email(deactivate_in_10_days, 10)
if deactivate_in_3_days:
send_deactivation_warning_email(deactivate_in_3_days, 3)
if deactivate_in_1_day:
send_deactivation_warning_email(deactivate_in_1_day, 1)

def users_to_deactivate(days):
"""Return a list of users that have not logged in in the last {180 - days} days."""
days = 180 - days
return User.objects.filter(
last_login__lte=datetime.now(tz=timezone.utc) - timedelta(days=days),
last_login__gte=datetime.now(tz=timezone.utc) - timedelta(days=days+1),
account_approval_status=AccountApprovalStatusChoices.APPROVED,
)

def get_ofa_admin_user_emails():
"""Return a list of OFA System Admin and OFA Admin users."""
return User.objects.filter(
groups__in=Group.objects.filter(name__in=('OFA Admin', 'OFA System Admin'))
).values_list('email', flat=True).distinct()

def get_num_access_requests():
"""Return the number of users requesting access."""
return User.objects.filter(
account_approval_status=AccountApprovalStatusChoices.ACCESS_REQUEST,
).count()

@shared_task
def email_admin_num_access_requests():
"""Send all OFA System Admins an email with how many users have requested access."""
recipient_email = get_ofa_admin_user_emails()
text_message = ''
subject = 'Number of Active Access Requests'
url = f'{settings.FRONTEND_BASE_URL}{reverse("admin:users_user_changelist")}?o=-2'
email_context = {
'date': datetime.today(),
'num_requests': get_num_access_requests(),
'admin_user_pg': url,
}

send_num_access_requests_email(recipient_email,
text_message,
subject,
email_context,
)
207 changes: 171 additions & 36 deletions tdrs-backend/tdpservice/scheduling/db_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
from django.conf import settings
import boto3
import logging
from tdpservice.users.models import User
from django.contrib.admin.models import ADDITION, ContentType, LogEntry


logger = logging.getLogger(__name__)


OS_ENV = os.environ
content_type = ContentType.objects.get_for_model(LogEntry)

def get_system_values():
"""Return dict of keys and settings to use whether local or deployed."""
Expand All @@ -26,18 +29,26 @@ def get_system_values():
sys_values['SPACE'] = json.loads(OS_ENV['VCAP_APPLICATION'])['space_name']

# Postgres client pg_dump directory
pgdump_search = subprocess.Popen(["find", "/", "-iname", "pg_dump"],
stderr=subprocess.DEVNULL, stdout=subprocess.PIPE)
pgdump_search.wait()
pg_dump_paths, pgdump_search_error = pgdump_search.communicate()
pg_dump_paths = pg_dump_paths.decode("utf-8").split('\n')
if pg_dump_paths[0] == '':
raise Exception("Postgres client is not found")

for _ in pg_dump_paths:
if 'pg_dump' in str(_) and 'postgresql' in str(_):
sys_values['POSTGRES_CLIENT'] = _[:_.find('pg_dump')]
print("Found PG client here: {}".format(_))
sys_values['POSTGRES_CLIENT_DIR'] = "/home/vcap/deps/0/apt/usr/lib/postgresql/12/bin/"
elipe17 marked this conversation as resolved.
Show resolved Hide resolved

# If the client directory and binaries don't exist, we need to find them.
if not (os.path.exists(sys_values['POSTGRES_CLIENT_DIR']) and
os.path.isfile(f"{sys_values['POSTGRES_CLIENT_DIR']}pg_dump")):
logger.warning("Couldn't find postgres client binaries at the hardcoded path: "
f"{sys_values['POSTGRES_CLIENT_DIR']}. Searching OS for client directory.")
pgdump_search = subprocess.Popen(["find", "/", "-iname", "pg_dump"],
stderr=subprocess.DEVNULL, stdout=subprocess.PIPE)
pgdump_search.wait()
pg_dump_paths, pgdump_search_error = pgdump_search.communicate()
pg_dump_paths = pg_dump_paths.decode("utf-8").split('\n')
if pg_dump_paths[0] == '':
raise Exception("Postgres client is not found")

for _ in pg_dump_paths:
if 'pg_dump' in str(_) and 'postgresql' in str(_):
sys_values['POSTGRES_CLIENT'] = _[:_.find('pg_dump')]

logger.info(f"Using postgres client at: {sys_values['POSTGRES_CLIENT_DIR']}")

sys_values['S3_ENV_VARS'] = json.loads(OS_ENV['VCAP_SERVICES'])['s3']
sys_values['S3_CREDENTIALS'] = sys_values['S3_ENV_VARS'][0]['credentials']
Expand Down Expand Up @@ -73,7 +84,8 @@ def get_system_values():

def backup_database(file_name,
postgres_client,
database_uri):
database_uri,
system_user):
"""Back up postgres database into file.

:param file_name: back up file name
Expand All @@ -82,15 +94,28 @@ def backup_database(file_name,
pg_dump -F c --no-acl --no-owner -f backup.pg postgresql://${USERNAME}:${PASSWORD}@${HOST}:${PORT}/${NAME}
"""
try:
os.system(postgres_client + "pg_dump -Fc --no-acl -f " + file_name + " -d " + database_uri)
print("Wrote pg dumpfile to {}".format(file_name))
cmd = postgres_client + "pg_dump -Fc --no-acl -f " + file_name + " -d " + database_uri
logger.info(f"Executing backup command: {cmd}")
os.system(cmd)
msg = "Successfully executed backup. Wrote pg dumpfile to {}".format(file_name)
logger.info(msg)
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Executed Database Backup",
action_flag=ADDITION,
change_message=msg
)
file_size = os.path.getsize(file_name)
logger.info(f"Pg dumpfile size in bytes: {file_size}.")
return True
except Exception as e:
print(e)
return False
logger.error(f"Caught Exception while backing up database. Exception: {e}")
raise e


def restore_database(file_name, postgres_client, database_uri):
def restore_database(file_name, postgres_client, database_uri, system_user):
"""Restore the database from filename.

:param file_name: database backup filename
Expand All @@ -100,10 +125,23 @@ def restore_database(file_name, postgres_client, database_uri):
DATABASE_DB_NAME] = get_database_credentials(database_uri)
os.environ['PGPASSWORD'] = DATABASE_PASSWORD
try:
os.system(postgres_client + "createdb " + "-U " + DATABASE_USERNAME + " -h " + DATABASE_HOST + " -T template0 "
+ DATABASE_DB_NAME)
logger.info("Begining database creation.")
cmd = (postgres_client + "createdb " + "-U " + DATABASE_USERNAME + " -h " + DATABASE_HOST + " -T template0 "
+ DATABASE_DB_NAME)
logger.info(f"Executing create command: {cmd}")
os.system(cmd)
msg = "Completed database creation."
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Executed Database create",
action_flag=ADDITION,
change_message=msg
)
logger.info(msg)
except Exception as e:
print(e)
logger.error(f"Caught exception while creating the database. Exception: {e}.")
return False

# write .pgpass
Expand All @@ -112,12 +150,25 @@ def restore_database(file_name, postgres_client, database_uri):
os.environ['PGPASSFILE'] = '/home/vcap/.pgpass'
os.system('chmod 0600 /home/vcap/.pgpass')

os.system(postgres_client + "pg_restore" + " -p " + DATABASE_PORT + " -h " +
DATABASE_HOST + " -U " + DATABASE_USERNAME + " -d " + DATABASE_DB_NAME + " " + file_name)
logger.info("Begining database restoration.")
cmd = (postgres_client + "pg_restore" + " -p " + DATABASE_PORT + " -h " +
DATABASE_HOST + " -U " + DATABASE_USERNAME + " -d " + DATABASE_DB_NAME + " " + file_name)
logger.info(f"Executing restore command: {cmd}")
os.system(cmd)
msg = "Completed database restoration."
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Executed Database restore",
action_flag=ADDITION,
change_message=msg
)
logger.info(msg)
return True


def upload_file(file_name, bucket, sys_values, object_name=None, region='us-gov-west-1'):
def upload_file(file_name, bucket, sys_values, system_user, object_name=None, region='us-gov-west-1'):
"""Upload a file to an S3 bucket.

:param file_name: file name being uploaded to s3 bucket
Expand All @@ -129,16 +180,27 @@ def upload_file(file_name, bucket, sys_values, object_name=None, region='us-gov-
if object_name is None:
object_name = os.path.basename(file_name)

logger.info(f"Uploading {file_name} to S3.")
s3_client = boto3.client('s3', region_name=sys_values['S3_REGION'])

s3_client.upload_file(file_name, bucket, object_name)
print("Uploaded {} to S3:{}{}".format(file_name, bucket, object_name))
msg = "Successfully uploaded {} to s3://{}/{}.".format(file_name, bucket, object_name)
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Executed database backup S3 upload",
action_flag=ADDITION,
change_message=msg
)
logger.info(msg)
return True


def download_file(bucket,
file_name,
region,
system_user,
object_name=None,
):
"""Download file from s3 bucket."""
Expand All @@ -150,9 +212,19 @@ def download_file(bucket,
"""
if object_name is None:
object_name = os.path.basename(file_name)
logger.info("Begining download for backup file.")
s3 = boto3.client('s3', region_name=region)
s3.download_file(bucket, object_name, file_name)
print("Downloaded s3 file {}{} to {}.".format(bucket, object_name, file_name))
msg = "Successfully downloaded s3 file {}/{} to {}.".format(bucket, object_name, file_name)
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Executed database backup S3 download",
action_flag=ADDITION,
change_message=msg
)
logger.info(msg)


def list_s3_files(sys_values):
Expand Down Expand Up @@ -187,7 +259,7 @@ def get_database_credentials(database_uri):
return [username, password, host, port, database_name]


def main(argv, sys_values):
def main(argv, sys_values, system_user):
"""Handle commandline args."""
arg_file = "/tmp/backup.pg"
arg_database = sys_values['DATABASE_URI']
Expand All @@ -210,31 +282,75 @@ def main(argv, sys_values):
raise e

if arg_to_backup:
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Begining Database Backup",
action_flag=ADDITION,
change_message="Begining database backup."
)
# back up database
backup_database(file_name=arg_file,
postgres_client=sys_values['POSTGRES_CLIENT'],
database_uri=arg_database)
postgres_client=sys_values['POSTGRES_CLIENT_DIR'],
database_uri=arg_database,
system_user=system_user)

# upload backup file
upload_file(file_name=arg_file,
bucket=sys_values['S3_BUCKET'],
sys_values=sys_values,
system_user=system_user,
region=sys_values['S3_REGION'],
object_name="backup"+arg_file)
object_name="backup"+arg_file,
)

LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Finished Database Backup",
action_flag=ADDITION,
change_message="Finished database backup."
)

logger.info(f"Deleting {arg_file} from local storage.")
os.system('rm ' + arg_file)

elif arg_to_restore:
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Begining Database Restore",
action_flag=ADDITION,
change_message="Begining database restore."
)

# download file from s3
download_file(bucket=sys_values['S3_BUCKET'],
file_name=arg_file,
region=sys_values['S3_REGION'],
object_name="backup"+arg_file)
system_user=system_user,
object_name="backup"+arg_file,
)

# restore database
restore_database(file_name=arg_file,
postgres_client=sys_values['POSTGRES_CLIENT'],
database_uri=arg_database)

postgres_client=sys_values['POSTGRES_CLIENT_DIR'],
database_uri=arg_database,
system_user=system_user)

LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Finished Database Restore",
action_flag=ADDITION,
change_message="Finished database restore."
)

logger.info(f"Deleting {arg_file} from local storage.")
os.system('rm ' + arg_file)


Expand All @@ -243,8 +359,27 @@ def run_backup(arg):
if settings.USE_LOCALSTACK is True:
logger.info("Won't backup locally")
else:
main([arg], sys_values=get_system_values())
try:
system_user, created = User.objects.get_or_create(username='system')
if created:
logger.debug('Created reserved system user.')
main([arg], sys_values=get_system_values(), system_user=system_user)
except Exception as e:
logger.error(f"Caught Exception in run_backup. Exception: {e}.")
LogEntry.objects.log_action(
user_id=system_user.pk,
content_type_id=content_type.pk,
object_id=None,
object_repr="Exception in run_backup",
action_flag=ADDITION,
change_message=str(e)
)
return False
return True


if __name__ == '__main__':
main(sys.argv[1:], get_system_values())
system_user, created = User.objects.get_or_create(username='system')
if created:
logger.debug('Created reserved system user.')
main(sys.argv[1:], get_system_values(), system_user)
Loading
Loading