Cron未能运行Django命令
我有一个 Django 脚本,应该每天在指定的时间运行。我想用 crontab
来实现这个功能。这个脚本的作用是备份数据库,使用 gzip
压缩,然后上传到 bitbucket
。
以下是我 crontab
文件中相关的部分:
00 4 * * * root python /my_django_project_path/manage.py update_locations
47 16 * * * root python /my_django_project_path/manage.py database_bu
当我手动执行 python /my_django_project_path/manage.py database_bu
时,它运行得很好。但是通过 crontab 执行时,要么不执行,要么在执行过程中出现问题。更奇怪的是,第一个 crontab 命令(update_locations
)可以正常执行。
我阅读了 这个问题,尝试了以下方法,但都没有成功:
将命令改为:
47 16 * * * root (cd /my_django_project_path/ && python manage.py database_bu)
将命令改为:
47 16 * * * root /usr/bin/python /my_django_project_path/manage.py database_bu
在我的脚本中添加以下内容(尽管其他命令没有这个也能正常工作):
#!/usr/bin/python
from django.core.management import setup_environ
import settings
setup_environ(settings)
通过一个导出 Django 项目设置的脚本来运行所有内容:
/my_django_project_path/cron_command_executor.sh:
export DJANGO_SETTINGS_MODULE=my_django_project.settings
python manage.py ${*}
在 crontab 中添加以下内容:
47 16 * * * root ./my_django_project_path/cron_command_executor.sh database_bu
将用户更改为我的用户和 Apache 用户(www-data
)。
我的 crontab 文件末尾有一个换行符。
更新:
当我使用 sudo su
时,手动运行命令不再有效。它卡住了,什么也不做。
执行 tail -f /var/log/syslog
的输出是:
Mar 3 18:26:01 my-ip-address cron[726]: (system) RELOAD (/etc/crontab)
Mar 3 18:26:01 my-ip-address CRON[1184]: (root) CMD (python /my_django_project_path/manage.py database_bu)
更新:
我使用以下 .netrc
文件来防止 git 询问凭证:
machine bitbucket.org
login myusername
password mypassword
备份脚本的实际代码是:
import subprocess
import sh
import datetime
import gzip
from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args, **options):
execute_backup()
FILE_NAME = 'some_file_name.sql'
ARCHIVE_NAME = 'some_archive_name.gz'
REPO_NAME = 'some_repo_name'
GIT_USER = 'some_git_username' # You'll need to change this in .netrc as well.
MYSQL_USER = 'some_mysql_user'
MYSQL_PASS = 'some_mysql_pass'
DATABASE_TO_DUMP = 'SomeDatabase' # You can use --all-databases but be careful with it! It will dump everything!.
def dump_dbs_to_gzip():
# Dump arguments.
args = [
'mysqldump', '-u', MYSQL_USER, '-p%s' % (MYSQL_PASS),
'--add-drop-database',
DATABASE_TO_DUMP,
]
# Dump to file.
dump_file = open(FILE_NAME, 'w')
mysqldump_process = subprocess.Popen(args, stdout=dump_file)
retcode = mysqldump_process.wait()
dump_file.close()
if retcode > 0:
print 'Back-up error'
# Compress.
sql_file = open(FILE_NAME, 'r')
gz_file = gzip.open(ARCHIVE_NAME, 'wb')
gz_file.writelines(sql_file)
gz_file.close()
sql_file.close()
# Delete the original file.
sh.rm('-f', FILE_NAME)
def clone_repo():
# Set the repository location.
repo_origin = 'https://%s@bitbucket.org/%s/%s.git' % (GIT_USER, GIT_USER, REPO_NAME)
# Clone the repository in the /tmp folder.
sh.cd('/tmp')
sh.rm('-rf', REPO_NAME)
sh.git.clone(repo_origin)
sh.cd(REPO_NAME)
def commit_and_push():
# Commit and push.
sh.git.add('.')
sh.git.commit(m=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
sh.git.push('origin', 'master')
sh.cd('..')
sh.rm('-rf', REPO_NAME)
def execute_backup():
clone_repo()
dump_dbs_to_gzip()
commit_and_push()
if __name__ == "__main__":
execute_backup()
更新:
我通过 Chris Clark 的建议,直接调用脚本而不是通过 manage.py 解决了这个问题。不过,我仍然想知道是什么导致了这个问题,所以悬赏仍然有效。
更新 [已解决]:
在 /etc/environment
中添加以下行,并以我的用户账户而不是 root 运行,问题就解决了:
PWD=/my_django_project_path/helpers/management/commands
我仍然想知道为什么只有我的用户可以运行这个命令,如果有人知道解决办法,请分享一下。
4 个回答
我对阅读 strace
的输出不是很在行,但我觉得你发的那个链接显示你的程序调用了 git
,并在等待它结束。你提到要上传到 BitBucket,所以我猜测一下:这只是个大胆的猜测:git
试图推送到一个 ssh 远程服务器;当你以自己的身份运行时,ssh-agent
会自动帮你认证;但是当你以 root 身份运行时,就没有 ssh-agent
,所以 git
就会要求你输入 ssh 密码并等待你的输入。
你可以试着在 sudo su
下手动运行 git
看看。
如果这样还不行,你需要查看 git
的输出(或者你实际上调用的其他命令)。可以查看 sh 包的文档,了解如何重定向标准输出和标准错误。
这也是个不太确定的尝试——我们团队在用定时任务(cron)运行管理命令时遇到了一些问题。我们从来没有认真去查明为什么这些命令不稳定,但经过一番折腾后,我们决定直接调用Python函数,而不是通过manage.py来运行。从那以后,一切都顺利多了。
这让我想起一个让人很头疼的问题。你的 crontab 文件最后有没有换行符呢?根据 crontab 的手册:
...cron 要求 crontab 文件中的每一条记录最后都要有一个换行符。如果 crontab 的最后一条记录没有换行符,cron 就会认为这个 crontab 文件(至少部分)是坏的,并拒绝安装它。
既然你在某个版本的 python /my_django_project_path/manage.py database_bu
能正常工作,这就说明问题出在你的 cron 环境
上,或者是你设置 cron 的方式有问题,而不是脚本本身(比如上传文件的大小或网络连接并不是问题所在)。
首先,你是这样运行脚本的:
47 16 * * * root python /my_django_project_path/manage.py database_bu
你给它提供了一个用户名 root
,而这个用户和你当前的用户并不一样,而你当前用户下的命令是可以正常工作的。也就是说,如果同样的命令在 root
用户下用 sudo su
不能运行,这说明你的 root 用户账户配置得不太对。顺便说一句,作为 root 用户去调度任务几乎总是要避免的,因为这可能会导致奇怪的文件权限问题。
所以,试着用你当前用户来调度 cron 任务,像这样:
47 16 * * * cd /my_django_project_path/ && python manage.py database_bu
这可能仍然无法完全运行 cron 任务。在这种情况下,问题可能出在两个地方——你的 shell 环境中有一些变量在 cron 环境中缺失,或者你的 .netrc
文件没有被正确读取以获取凭证,或者两者都有。
根据我的经验,PATH
变量通常会引发最多的问题,所以在你的 shell 中运行 echo $PATH
,如果你得到的路径值是 /some/path:/some/other/path:/more/path/values
,那么就像这样运行你的 cron 任务:
47 16 * * * export PATH="/some/path:/some/other/path:/more/path/values" && cd /my_django_project_path/ && python manage.py database_bu
如果这样还是不行,接下来检查所有的环境变量。
在正常的 shell 中使用 printenv > ~/environment.txt
来获取所有在 shell 中设置的环境变量。然后使用以下的 cron 记录 * * * * * printenv > ~/cron_environment.txt
来识别 cron 环境中缺失的变量。或者,你也可以在脚本中使用一个小片段来获取环境变量的值。
import os
os.system("printenv")
对比这两个文件,找出任何其他不同的相关变量(比如 HOME
),然后尝试在脚本或 cron 记录中使用相同的变量,看看是否能正常工作。
如果事情还是不行,那么我认为剩下的问题可能是你的 .netrc
文件中的 bitbucket 凭证,里面保存了用户名和密码。.netrc
的内容可能在 cron 环境中不可用。
相反,创建并设置一个 ssh 密钥对,让备份通过 ssh
而不是 https
进行(最好在这一步生成一个没有密码的 ssh 密钥,以避免 ssh 密钥的一些问题)。
一旦你设置好了 ssh 密钥,你就需要相应地编辑项目根目录下的 .git/config
文件中的现有 origin URL(或者使用 git remote add origin_ssh url
为 ssh 协议添加一个新的远程 origin_ssh
)。
注意,https
的仓库 URL 是 https://user@bitbucket.org/user/repo.git
,而 ssh 的 URL 是 git@bitbucket.org:user/repo.git
。
PS:bitbucket
,或者说 git
并不是备份的理想解决方案,网上有很多关于更好备份策略的讨论。而且在调试时,建议每分钟运行一次 cron(* * * * *
),或者以类似的低频率运行,以便更快调试。
编辑
提问者在评论中说设置 PWD
变量对他有效。
PWD=/my_django_project_path/helpers/management/commands 添加到 /etc/environment
这正是我之前建议的,shell 中可用的一个环境变量在 cron 环境中缺失。
一般来说,cron 总是以减少的环境变量和权限运行,设置正确的变量会让 cron 正常工作。
而且,由于你使用 .netrc
文件来管理权限,这个文件是特定于你的账户的,因此在其他账户(包括 sudo
的 root
账户)下是无法使用的,除非你在其他账户中也配置相同的设置。