239 lines
8.8 KiB
Python
Executable file
239 lines
8.8 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
# wanlin.wang
|
||
# 2024/12/14, version 1.0
|
||
# 2024/12/21, version 1.1, add support for Python 3.6.8 and other improvements
|
||
|
||
|
||
import os
|
||
import subprocess
|
||
import shlex
|
||
|
||
# --- POSIX => NFSv4 基本权限映射表 ---
|
||
# 针对 'r', 'w', 'x' 做粗粒度映射到 NFSv4 权限子集。
|
||
# 这里示例,借助alias来简化设置。规则是
|
||
# - file/dir: r -> R , w -> W , x -> X
|
||
# - file: r -> rntcy, w -> watTNcCy, x -> xtcy
|
||
# - dir: r -> rntcy, w -> waDtTNcCy, x -> xtcy
|
||
PERM_MAP = {
|
||
'r': 'R',
|
||
'w': 'W',
|
||
'x': 'X',
|
||
}
|
||
|
||
def convert_posix_perm_to_nfs4(posix_perm):
|
||
"""
|
||
将类似 'r-x' 的 POSIX 权限字符串映射为简单的 NFSv4 权限字符串。
|
||
"""
|
||
perms = []
|
||
if 'r' in posix_perm:
|
||
perms.append(PERM_MAP['r'])
|
||
if 'w' in posix_perm:
|
||
perms.append(PERM_MAP['w'])
|
||
if 'x' in posix_perm:
|
||
perms.append(PERM_MAP['x'])
|
||
merged = "".join(perms) # 例如 ['rtncy', 'x'] -> 'rtncyx'
|
||
# 去重保持顺序
|
||
result = "".join(sorted(set(merged), key=merged.index))
|
||
return result
|
||
|
||
def apply_mask(posix_perm, mask_perm):
|
||
"""
|
||
把某个条目的 nominal 权限与 mask 做交集,得到实际生效权限 (effective perms)。
|
||
mask_perm & posix_perm 都是三字符形式,如 'rwx', 'r-x', '---'。
|
||
逻辑:如果 mask 里无 'w',则最终也无 'w'。
|
||
"""
|
||
effective = ''
|
||
for i, c in enumerate(['r','w','x']):
|
||
if c in posix_perm and c in mask_perm:
|
||
effective += c
|
||
else:
|
||
effective += '-'
|
||
return effective
|
||
|
||
def parse_getfacl_output(acl_output):
|
||
result = {
|
||
'owner': None,
|
||
'group': None,
|
||
'mask': None, # 普通mask
|
||
'default_mask': None, # 新增: default:mask
|
||
'acl_entries': [],
|
||
'default_acl_entries': [],
|
||
}
|
||
|
||
lines = acl_output.strip().split('\n')
|
||
for raw_line in lines:
|
||
line = raw_line.strip()
|
||
# 去掉行内注释
|
||
if '#' in line:
|
||
line = line.split('#', 1)[0].strip()
|
||
if not line:
|
||
continue
|
||
|
||
parts = line.split(':')
|
||
if line.startswith('default:'):
|
||
entry_type = parts[1] # user / group / mask / other
|
||
if entry_type == 'mask':
|
||
# default:mask::rwx
|
||
result['default_mask'] = parts[-1] # 'rwx'
|
||
else:
|
||
name = ''
|
||
perm = parts[-1]
|
||
if len(parts) == 4:
|
||
name = parts[2]
|
||
result['default_acl_entries'].append({
|
||
'type': entry_type,
|
||
'name': name,
|
||
'perm': perm
|
||
})
|
||
else:
|
||
entry_type = parts[0]
|
||
if entry_type == 'mask':
|
||
result['mask'] = parts[-1]
|
||
else:
|
||
name = ''
|
||
perm = parts[-1]
|
||
if entry_type in ('user','group') and len(parts) == 3:
|
||
name = parts[1]
|
||
result['acl_entries'].append({
|
||
'type': entry_type,
|
||
'name': name,
|
||
'perm': perm
|
||
})
|
||
|
||
return result
|
||
|
||
# def build_nfs4_acl_cmd(acl_info, path="/dummy", domain='localdomain'):
|
||
def build_nfs4_ace(acl_info, path="/dummy", domain='localdomain'):
|
||
ace_list = []
|
||
# 普通 mask
|
||
normal_mask = acl_info['mask'] if acl_info['mask'] else 'rwx'
|
||
# default mask
|
||
default_mask = acl_info.get('default_mask', None)
|
||
if not default_mask:
|
||
default_mask = 'rwx'
|
||
|
||
# 1) OWNER@
|
||
owner_acl = next((e for e in acl_info['acl_entries']
|
||
if e['type'] == 'user' and e['name'] == ''), None)
|
||
if owner_acl:
|
||
# owner 不受普通mask约束
|
||
eff = apply_mask(owner_acl['perm'], 'rwx')
|
||
else:
|
||
eff = 'rwx'
|
||
ace_list.append(f"A::OWNER@:{convert_posix_perm_to_nfs4(eff)}")
|
||
|
||
# 2) user:xxx 或 group:xxx (普通条目)
|
||
for entry in acl_info['acl_entries']:
|
||
if entry['type'] == 'user' and entry['name']:
|
||
eff = apply_mask(entry['perm'], normal_mask)
|
||
nfs4_perm = convert_posix_perm_to_nfs4(eff)
|
||
if nfs4_perm:
|
||
ace_list.append(f"A::{entry['name']}@{domain}:{nfs4_perm}")
|
||
if entry['type'] == 'group' and entry['name']:
|
||
eff = apply_mask(entry['perm'], normal_mask)
|
||
nfs4_perm = convert_posix_perm_to_nfs4(eff)
|
||
if nfs4_perm:
|
||
ace_list.append(f"A:g:{entry['name']}@{domain}:{nfs4_perm}")
|
||
else:
|
||
ace_list.append(f"D:g:{entry['name']}@{domain}:RWXdo")
|
||
|
||
# 3) group:: (空组)
|
||
group_acl = next((e for e in acl_info['acl_entries']
|
||
if e['type'] == 'group' and e['name'] == ''), None)
|
||
if group_acl:
|
||
eff = apply_mask(group_acl['perm'], normal_mask)
|
||
g_perm = convert_posix_perm_to_nfs4(eff)
|
||
if g_perm:
|
||
ace_list.append(f"A:g:GROUP@:{g_perm}")
|
||
else:
|
||
ace_list.append(f"D:g:GROUP@:RWXdo")
|
||
|
||
# 4) other::
|
||
other_acl = next((e for e in acl_info['acl_entries']
|
||
if e['type'] == 'other'), None)
|
||
if other_acl:
|
||
eff = apply_mask(other_acl['perm'], normal_mask)
|
||
o_perm = convert_posix_perm_to_nfs4(eff)
|
||
if o_perm:
|
||
ace_list.append(f"A::EVERYONE@:{o_perm}")
|
||
else:
|
||
ace_list.append(f"D::EVERYONE@:RWXdo")
|
||
|
||
# 5) default: ACL (带继承)
|
||
for def_entry in acl_info['default_acl_entries']:
|
||
entry_type = def_entry['type']
|
||
nominal = def_entry['perm']
|
||
# 应用 default_mask
|
||
eff = apply_mask(nominal, default_mask)
|
||
nfs4_perm = convert_posix_perm_to_nfs4(eff)
|
||
if not nfs4_perm:
|
||
continue
|
||
if entry_type == 'user':
|
||
if def_entry['name'] == '':
|
||
ace_list.append(f"A:fd:OWNER@:{nfs4_perm}")
|
||
else:
|
||
ace_list.append(f"A:fd:{def_entry['name']}@{domain}:{nfs4_perm}")
|
||
elif entry_type == 'group':
|
||
if def_entry['name'] == '':
|
||
ace_list.append(f"A:fdg:GROUP@:{nfs4_perm}")
|
||
else:
|
||
ace_list.append(f"A:fdg:{def_entry['name']}@{domain}:{nfs4_perm}")
|
||
elif entry_type == 'other':
|
||
pass
|
||
return ace_list
|
||
|
||
def convert_acl_for_directory(root_dir, new_root_dir, domain='localdomain', max_depth=3, nfs4_cmd_file='nfs4_acl_cmds.txt'):
|
||
"""
|
||
遍历 root_dir 下所有文件和目录,
|
||
读取POSIX ACL并转换为NFSv4 ACL (包含 default: 继承处理, mask限制)
|
||
"""
|
||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||
# 计算当前目录的深度
|
||
depth = dirpath[len(root_dir):].count(os.sep)
|
||
if depth >= max_depth:
|
||
# 如果当前目录的深度超过了最大深度,则不再递归遍历子目录。适用于大型目录结构,只在最上的几层控制。
|
||
dirnames[:] = []
|
||
|
||
all_paths = [dirpath] + [os.path.join(dirpath, f) for f in filenames]
|
||
for p in all_paths:
|
||
try:
|
||
# 获取 POSIX ACL。这里调用subprocess时,用了universal_newlines=True,以便输出是str而不是bytes。text=True也可以用于高于3.7的Python版本。
|
||
completed_proc = subprocess.run(["getfacl", p],
|
||
check=True,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
universal_newlines=True)
|
||
acl_output = completed_proc.stdout
|
||
except subprocess.CalledProcessError as e:
|
||
print(f"Warning: getfacl failed on {p}, error: {e}")
|
||
continue
|
||
|
||
acl_info = parse_getfacl_output(acl_output)
|
||
ace_list = build_nfs4_ace(acl_info, p, domain=domain)
|
||
if ace_list:
|
||
new_dir = p.replace(root_dir, new_root_dir)
|
||
nfs4_cmd = f"nfs4_setfacl -s \"{' '.join(shlex.quote(x) for x in ace_list)}\" {new_dir}"
|
||
print(f"\n# Got POSIX ACL from {p}, setting NFSv4 ACL for {new_dir} with command:")
|
||
print(nfs4_cmd)
|
||
with open(nfs4_cmd_file, 'a') as f:
|
||
f.write(f"\n# Directory: {dirpath}\n")
|
||
f.write(nfs4_cmd)
|
||
f.write("\n")
|
||
|
||
def main():
|
||
import sys
|
||
if len(sys.argv) < 3:
|
||
print(f"Usage: {sys.argv[0]} <directory> <new_directory> [domain]")
|
||
sys.exit(1)
|
||
|
||
root_dir = sys.argv[1]
|
||
new_root_dir = sys.argv[2]
|
||
domain = sys.argv[3] if len(sys.argv) > 3 else 'localdomain'
|
||
if not os.path.isdir(root_dir):
|
||
print(f"Error: {root_dir} is not a valid directory.")
|
||
sys.exit(1)
|
||
|
||
convert_acl_for_directory(root_dir, new_root_dir, domain)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|