""" This module defines `Celery`_ tasks to manage file system entities. .. _Celery: http://www.celeryproject.org/ """ from __future__ import absolute_import, unicode_literals from copy import deepcopy import os import subprocess from tempfile import mkstemp from gvafile import settings from celery import shared_task from celery.utils.log import get_task_logger _LOGGER = get_task_logger(__name__) SUDO_CMD = '/usr/bin/sudo' INSTALL_CMD = '/usr/bin/install' SETFACL_CMD = '/usr/bin/setfacl' RM_CMD = '/bin/rm' def log_and_raise(exception, message, *args): """ Log and raise a :py:class:`subprocess.CalledProcessError`. :param exception: exception :param str message: log message :param args: arguments to fill placeholders in message :raises Exception: raises an exception with the formatted message """ logargs = list(args) + [exception.returncode, exception.output] _LOGGER.error(message + "\nreturncode: %d\noutput:\n%s", *logargs) raise Exception(message % args) def _build_authorized_keys_path(username): """ Constructs the file path for the authorized_keys file for a given username. :param str username: the user name :return: the file name :rtype: str """ return os.path.join( settings.GVAFILE_SFTP_AUTHKEYS_DIRECTORY, username, 'keys') def _build_sftp_directory_name(username): """ Constructs the SFTP directory name for a given username. """ return os.path.join(settings.GVAFILE_SFTP_DIRECTORY, username) def _build_mail_directory_name(username): """ Constructs the mailbox directory name for a given username. """ return os.path.join(settings.GVAFILE_MAIL_DIRECTORY, username) @shared_task def setup_file_sftp_userdir(username, *args, **kwargs): """ This task creates the home directory for an SFTP user if it does not exist yet. :param str username: the username :raises Exception: if the SFTP directory of the user cannot be created :return: a dictionary with the key :py:const:`username` set to the username value and a new key :py:const:`sftp_directory` set to the path of the created SFTP directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.setup_file_sftp_userdir_chained` at other positions in the task chain. """ sftp_directory = _build_sftp_directory_name(username) try: subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', 'root', '-g', username, '-m', '0750', '-d', sftp_directory], stderr=subprocess.STDOUT) subprocess.check_output([ SUDO_CMD, SETFACL_CMD, '-m', 'www-data:--x', sftp_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not create SFTP directory %s for user %s', sftp_directory, username) _LOGGER.info( 'created sftp directory %s for user %s', sftp_directory, username) return {'username': username, 'sftp_directory': sftp_directory} @shared_task def setup_file_sftp_userdir_chained(previous_result, *args, **kwargs): """ This task creates the home directory for an SFTP user if it does not exist yet. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` key :raises Exception: if the SFTP directory of the user cannot be created :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`sftp_directory` key set to the path of the created SFTP directory :rtype: dict """ username = previous_result['username'] retval = deepcopy(previous_result) retval.update(setup_file_sftp_userdir(username)) return retval @shared_task def delete_file_sftp_userdir(username, *args, **kwargs): """ This task recursively deletes the home directory of an SFTP user if it exists. :param str username: the username :raises Exception: if the SFTP directory of the user cannot be removed :return: a dictionary with the key :py:const:`username` set to the username value and the new key :py:const:`sftp_directory` set to the path of the deleted SFTP directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.delete_file_sftp_userdir_chained` at other positions in the task chain. """ sftp_directory = _build_sftp_directory_name(username) try: subprocess.check_output([ SUDO_CMD, RM_CMD, '-r', '-f', sftp_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not remove SFTP directory for user %s', username) _LOGGER.info( "deleted sftp directory %s of user %s", sftp_directory, username) return {'username': username, 'sftp_directory': sftp_directory} @shared_task def delete_file_sftp_userdir_chained(previous_result, *args, **kwargs): """ This task recursively deletes the home directory of an SFTP user if it exists. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` key :raises Exception: if the SFTP directory of the user cannot be removed :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`sftp_directory` key set to the path of the removed SFTP directory :rtype: dict """ username = previous_result['username'] retval = deepcopy(previous_result) retval.update(delete_file_sftp_userdir(username)) return retval @shared_task def setup_file_mail_userdir(username, *args, **kwargs): """ This task creates the mail base directory for a user if it does not exist yet. :param str username: the username :raises Exception: if the mail base directory for the user cannot be created :return: a dictionary with the key :py:const:`username` set to the username value and a new key :py:const:`mail_directory` set to the path of the created mail directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.setup_file_mail_userdir_chained` at other positions in the task chain. """ mail_directory = _build_mail_directory_name(username) try: subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', username, '-g', username, '-m', '0500', '-d', mail_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not create mail base directory for user %s', username) _LOGGER.info( 'created mail directory %s for user %s', mail_directory, username) return {'username': username, 'mail_directory': mail_directory} @shared_task def setup_file_mail_userdir_chained(previous_result, *args, **kwargs): """ This task creates the mail base directory for a user if it does not exist yet. :param dict previous_result: a dictionary containing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` key :raises Exception: if the mail base directory for the user cannot be created :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`mail_directory` key set to the path of the created mail directory :rtype: dict """ username = previous_result['username'] retval = deepcopy(previous_result) retval.update(setup_file_mail_userdir(username)) return retval @shared_task def delete_file_mail_userdir(username, *args, **kwargs): """ This task recursively deletes the mail base directory for a user if it does not exist yet. :param str username: the username :raises Exception: if the mail base directory of the user cannot be deleted :return: a dictionary with the key :py:const:`username` set to the username value and a new key :py:const:`mail_directory` set to the path of the deleted mail directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.delete_file_mail_userdir_chained` at other positions in the task chain. """ mail_directory = _build_mail_directory_name(username) try: subprocess.check_output([ SUDO_CMD, RM_CMD, '-r', '-f', mail_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not remove mail base directory of user %s', username) _LOGGER.info( 'deleted mail directory %s of user %s', mail_directory, username) return {'username': username, 'mail_directory': mail_directory} @shared_task def delete_file_mail_userdir_chained(previous_result, *args, **kwargs): """ This task recursively deletes the mail base directory for a user if it does not exist yet. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` key :raises Exception: if the mail base directory of the user cannot be deleted :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`mail_directory` key set to the path of the deleted mail directory :rtype: str """ username = previous_result['username'] retval = deepcopy(previous_result) retval.update(delete_file_mail_userdir(username)) return retval @shared_task def create_file_mailbox(username, mailboxname, *args, **kwargs): """ This task creates a new mailbox directory for the given user and mailbox name. :param str username: the username :param str mailboxname: the mailbox name :raises Exception: if the mailbox directory cannot be created :return: a dictionary with the keys :py:const:`username` and :py:const:`mailboxname` set to the values of username and mailboxname and a new key :py:const:`mailbox_directory` set to the path of the created mailbox directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.create_file_mailbox_chained` at other positions in the task chain. """ mailbox_directory = os.path.join( _build_mail_directory_name(username), mailboxname) try: subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', username, '-g', username, '-m', '0700', '-d', mailbox_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not create mailbox %s for user %s', mailboxname, username) _LOGGER.info( 'created mailbox directory %s for user %s', mailbox_directory, username) return { 'username': username, 'mailboxname': mailboxname, 'mailbox_directory': mailbox_directory } @shared_task def create_file_mailbox_chained(previous_result, *args, **kwargs): """ This task creates a new mailbox directory for the given user and mailbox name. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` and a :py:const:`mailboxname` key :raises Exception: if the mailbox directory cannot be created :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`mailbox_directory` key set to the path of the created mailbox directory :rtype: dict """ username = previous_result['username'] mailboxname = previous_result['mailboxname'] retval = deepcopy(previous_result) retval.update(create_file_mailbox(username, mailboxname)) return retval @shared_task def delete_file_mailbox(username, mailboxname, *args, **kwargs): """ This task deletes the given mailbox of the given user. :param str username: the username :param str mailboxname: the mailbox name :raises Exception: if the mailbox directory cannot be deleted :return: a dictionary with the keys :py:const:`username` and :py:const:`mailboxname` set to the values of username and mailboxname and a new key :py:const:`mailbox_directory` set to the path of the deleted mailbox directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.delete_file_mailbox_chained` for other positions in the task chain. """ mailbox_directory = os.path.join( _build_mail_directory_name(username), mailboxname) try: subprocess.check_output([ SUDO_CMD, RM_CMD, '-r', '-f', mailbox_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not remove mailbox %s of user %s', mailboxname, username) _LOGGER.info( 'deleted mailbox directory %s of user %s', mailbox_directory, username) return { 'username': username, 'mailboxname': mailboxname, 'mailbox_directory': mailbox_directory } @shared_task def delete_file_mailbox_chained(previous_result, *args, **kwargs): """ This task deletes the given mailbox of the given user. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` and a :py:const:`mailboxname` key :raises Exception: if the mailbox directory cannot be deleted :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`mailbox_directory` key set to the path of the deleted mailbox directory :rtype: dict """ username = previous_result['username'] mailboxname = previous_result['mailboxname'] retval = deepcopy(previous_result) retval.update(delete_file_mailbox(username, mailboxname)) return retval @shared_task def create_file_website_hierarchy(username, sitename, *args, **kwargs): """ This task creates the directory hierarchy for a website. :param str username: the username :param str sitename: the sitename :raises Exception: if the website directory hierarchy directory cannot be created :return: a dictionary with the keys :py:const:`username` and :py:const:`sitename` set to the values of username and sitename and a new key :py:const:`website_directory` set to the path of the created website directory :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.create_file_website_hierarchy_chained` at other positions in the task chain """ website_directory = os.path.join( _build_sftp_directory_name(username), sitename) try: subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', 'root', '-g', username, '-m', '0750', '-d', website_directory], stderr=subprocess.STDOUT) subprocess.check_output([ SUDO_CMD, SETFACL_CMD, '-m', 'www-data:--x', website_directory], stderr=subprocess.STDOUT) htmldir = os.path.join(website_directory, 'html') subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', username, '-g', username, '-m', '0750', '-d', htmldir], stderr=subprocess.STDOUT) subprocess.check_output([ SUDO_CMD, SETFACL_CMD, '-m', 'www-data:r-x', htmldir], stderr=subprocess.STDOUT) subprocess.check_output([ SUDO_CMD, SETFACL_CMD, '-d', '-m', 'www-data:r-X', htmldir], stderr=subprocess.STDOUT) tmpdir = os.path.join(website_directory, 'tmp') subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', username, '-g', username, '-m', '0750', '-d', tmpdir], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, ("could not setup website file hierarchy for user %s's " "website %s"), username, sitename) _LOGGER.info( 'created website directory %s for user %s', website_directory, username) return { 'username': username, 'sitename': sitename, 'website_directory': website_directory, } @shared_task def create_file_website_hierarchy_chained(previous_result, *args, **kwargs): """ This task creates the directory hierarchy for a website. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` and a :py:const:`sitename` key :raises Exception: if the website directory hierarchy directory cannot be created :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`website_directory` key set to the path of the created website directory :rtype: dict """ username = previous_result['username'] sitename = previous_result['sitename'] retval = deepcopy(previous_result) retval.update(create_file_website_hierarchy(username, sitename)) return retval @shared_task def delete_file_website_hierarchy(username, sitename, *args, **kwargs): """ This task deletes a website hierarchy recursively. :param str username: a username :param str sitename: a site name :return: a dictionary with the keys :py:const:`username` and :py:const:`sitename` set to their original values and a new key :py:const:`website_directory` set to the path of the deleted website :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.delete_file_website_hierarchy_chained` at other positions in the task chain """ website_directory = os.path.join( _build_sftp_directory_name(username), sitename) try: subprocess.check_output([ SUDO_CMD, RM_CMD, '-r', '-f', website_directory], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, ("could not delete the website file hierarchy of user %s's " "website %s"), username, sitename) _LOGGER.info( 'deleted website directory %s of user %s', website_directory, username) return { 'username': username, 'sitename': sitename, 'website_directory': website_directory, } @shared_task def delete_file_website_hierarchy_chained(previous_result, *args, **kwargs): """ This task deletes the website hierarchy recursively. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` and a :py:const:`sitename` key :raises Exception: if the website directory hierarchy directory cannot be deleted :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`website_directory` set to the path of the deleted website directory :rtype: dict """ username = previous_result['username'] sitename = previous_result['sitename'] retval = deepcopy(previous_result) retval.update(delete_file_website_hierarchy(username, sitename)) return retval @shared_task def set_file_ssh_authorized_keys(username, ssh_keys, *args, **kwargs): """ This task set the authorized keys for ssh logins. :param str username: a username :param list ssh_keys: a list of ssh keys :raises Exception: if the update of the creation or update of ssh authorized_keys failed :return: a dictionary with the keys :py:const:`username` and :py:const:`ssh_keys` set to their original values and a new key :py:const:`ssh_authorized_keys` set to the path of the SSH authorized_keys file :rtype: dict .. note:: This variant can only be used at the beginning of a Celery task chain or as a standalone task. Use :py:func:`fileservertasks.tasks.set_file_ssh_authorized_keys_chained` at other positions in the task chain """ ssh_authorized_keys_file = _build_authorized_keys_path(username) if ssh_keys: try: authkeystemp, filename = mkstemp() conffile = os.fdopen(authkeystemp, 'w') conffile.write("\n".join(ssh_keys)) finally: if conffile: conffile.close() try: subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', username, '-g', username, '-m', '0500', '-d', os.path.dirname(ssh_authorized_keys_file)], stderr=subprocess.STDOUT) subprocess.check_output([ SUDO_CMD, INSTALL_CMD, '-o', username, '-g', username, '-m', '0400', filename, ssh_authorized_keys_file], stderr=subprocess.STDOUT) subprocess.check_output([ SUDO_CMD, RM_CMD, filename], stderr=subprocess.STDOUT) _LOGGER.info( 'set %d authorized_keys for user %s', len(ssh_keys), username) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not write authorized_keys file for user %s', username) else: try: subprocess.check_output([ SUDO_CMD, RM_CMD, '-rf', os.path.dirname(ssh_authorized_keys_file)], stderr=subprocess.STDOUT) _LOGGER.info( 'deleted authorized_keys of user %s', username) except subprocess.CalledProcessError as cpe: log_and_raise( cpe, 'could not remove the authorized_keys file of user %s', username) return { 'username': username, 'ssh_keys': ssh_keys, 'ssh_authorized_keys': ssh_authorized_keys_file, } @shared_task def set_file_ssh_authorized_keys_chained(previous_result, *args, **kwargs): """ This task sets the authorized keys for ssh logins. :param dict previous_result: a dictionary describing the result of the previous step in the Celery task chain. This dictionary must contain a :py:const:`username` and a :py:const:`ssh_keys` key :raises Exception: if the update of the creation or update of ssh authorized_keys failed :return: a copy of the :py:obj:`previous_result` dictionary with a new :py:const:`ssh_authorized_keys` set to the path of the SSH authorized_keys file :rtype: dict """ username = previous_result['username'] ssh_keys = previous_result['ssh_keys'] retval = deepcopy(previous_result) retval.update(set_file_ssh_authorized_keys(username, ssh_keys)) return retval