fix: 完善低权限备份恢复与只读部署密钥支持
这个提交包含在:
@@ -14,7 +14,7 @@
|
|||||||
- 程序:`/opt/grain-auto-deploy/auto_deploy.py`
|
- 程序:`/opt/grain-auto-deploy/auto_deploy.py`
|
||||||
- 配置:`/etc/grain-auto-deploy.json`
|
- 配置:`/etc/grain-auto-deploy.json`
|
||||||
- 版本及文件清单:`/var/lib/grain-auto-deploy/backend.json`、`frontend.json`
|
- 版本及文件清单:`/var/lib/grain-auto-deploy/backend.json`、`frontend.json`
|
||||||
- 备份:`/www/backup/grain-auto-deploy/`
|
- 备份:`/var/lib/grain-auto-deploy/backups/`
|
||||||
- 日志:`/var/log/grain-auto-deploy.log`,以及 `journalctl -u grain-auto-deploy.service`
|
- 日志:`/var/log/grain-auto-deploy.log`,以及 `journalctl -u grain-auto-deploy.service`
|
||||||
|
|
||||||
运维命令:
|
运维命令:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
import shutil
|
import shutil
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -64,6 +65,17 @@ def healthcheck(url, host):
|
|||||||
raise RuntimeError('Granary API health check failed')
|
raise RuntimeError('Granary API health check failed')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_file(source, live):
|
||||||
|
live.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temp = live.with_name(live.name + '.deploy-tmp')
|
||||||
|
shutil.copyfile(str(source), str(temp))
|
||||||
|
mode = live.stat().st_mode & 0o777 if live.exists() else 0o644
|
||||||
|
os.chmod(str(temp), mode)
|
||||||
|
# The service runs as www; replacing an older root-uploaded file must not
|
||||||
|
# attempt to chown it back to root or require elevated permissions.
|
||||||
|
os.replace(str(temp), str(live))
|
||||||
|
|
||||||
|
|
||||||
def publish(config, repo):
|
def publish(config, repo):
|
||||||
state_dir = Path(config['state'])
|
state_dir = Path(config['state'])
|
||||||
cache = state_dir / (repo['name'] + '.git')
|
cache = state_dir / (repo['name'] + '.git')
|
||||||
@@ -71,6 +83,10 @@ def publish(config, repo):
|
|||||||
if not cache.exists():
|
if not cache.exists():
|
||||||
run(['git', 'init', '--bare', str(cache)])
|
run(['git', 'init', '--bare', str(cache)])
|
||||||
git = ['git', '--git-dir=' + str(cache)]
|
git = ['git', '--git-dir=' + str(cache)]
|
||||||
|
if repo.get('key'):
|
||||||
|
ssh = 'ssh -i {} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile={}'.format(
|
||||||
|
shlex.quote(repo['key']), shlex.quote(repo['known_hosts']))
|
||||||
|
git += ['-c', 'core.sshCommand=' + ssh]
|
||||||
run(git + ['fetch', '--depth=1', repo['url'], '+refs/heads/master:refs/heads/deploy'])
|
run(git + ['fetch', '--depth=1', repo['url'], '+refs/heads/master:refs/heads/deploy'])
|
||||||
revision = run(git + ['rev-parse', 'refs/heads/deploy']).decode().strip()
|
revision = run(git + ['rev-parse', 'refs/heads/deploy']).decode().strip()
|
||||||
state_path = state_dir / (repo['name'] + '.json')
|
state_path = state_dir / (repo['name'] + '.json')
|
||||||
@@ -136,20 +152,7 @@ def publish(config, repo):
|
|||||||
try:
|
try:
|
||||||
for name in changes:
|
for name in changes:
|
||||||
live = destination(root, name)
|
live = destination(root, name)
|
||||||
live.parent.mkdir(parents=True, exist_ok=True)
|
replace_file(stage / name, live)
|
||||||
temp = live.with_name(live.name + '.deploy-tmp')
|
|
||||||
shutil.copyfile(str(stage / name), str(temp))
|
|
||||||
if live.exists():
|
|
||||||
stat = live.stat()
|
|
||||||
os.chmod(str(temp), stat.st_mode & 0o777)
|
|
||||||
if hasattr(os, 'chown'):
|
|
||||||
os.chown(str(temp), stat.st_uid, stat.st_gid)
|
|
||||||
else:
|
|
||||||
os.chmod(str(temp), 0o644)
|
|
||||||
if hasattr(os, 'chown'):
|
|
||||||
parent = live.parent.stat()
|
|
||||||
os.chown(str(temp), parent.st_uid, parent.st_gid)
|
|
||||||
os.replace(str(temp), str(live))
|
|
||||||
for name in deleted:
|
for name in deleted:
|
||||||
live = destination(root, name)
|
live = destination(root, name)
|
||||||
if live.is_file():
|
if live.is_file():
|
||||||
@@ -160,7 +163,7 @@ def publish(config, repo):
|
|||||||
for record in reversed(records):
|
for record in reversed(records):
|
||||||
live = destination(root, record['path'])
|
live = destination(root, record['path'])
|
||||||
if record['existed']:
|
if record['existed']:
|
||||||
shutil.copy2(str(backup / 'files' / record['path']), str(live))
|
replace_file(backup / 'files' / record['path'], live)
|
||||||
elif live.is_file():
|
elif live.is_file():
|
||||||
live.unlink()
|
live.unlink()
|
||||||
LOG.exception('%s rolled back; backup=%s', repo['name'], backup)
|
LOG.exception('%s rolled back; backup=%s', repo['name'], backup)
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ NoNewPrivileges=true
|
|||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ReadWritePaths=/var/lib/grain-auto-deploy /www/backup/grain-auto-deploy /www/wwwroot/grain.gynhc.com /var/log/grain-auto-deploy.log
|
ReadWritePaths=/var/lib/grain-auto-deploy /www/wwwroot/grain.gynhc.com /var/log/grain-auto-deploy.log
|
||||||
ReadOnlyPaths=/www/wwwroot/grain.gynhc.com/.git /www/wwwroot/grain.gynhc.com/application/database.php /www/wwwroot/grain.gynhc.com/application/config.php /www/wwwroot/grain.gynhc.com/application/extra/site.php
|
ReadOnlyPaths=/www/wwwroot/grain.gynhc.com/.git /www/wwwroot/grain.gynhc.com/application/database.php /www/wwwroot/grain.gynhc.com/application/config.php /www/wwwroot/grain.gynhc.com/application/extra/site.php
|
||||||
RestrictSUIDSGID=true
|
RestrictSUIDSGID=true
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户