import json, os, pathlib, re, stat, tempfile, tomllib
os.umask(0o077)
root=pathlib.Path(os.environ.get('AEGIS_MIGRATION_TEST_ROOT','/'))
assert root != pathlib.Path('/') or os.geteuid()==0
receipt=root/'var/lib/aegis-operator-migrations/user-ids-040'
receipt.mkdir(parents=True,exist_ok=True)
def atomic(path,data,mode,uid,gid):
fd,name=tempfile.mkstemp(prefix='.aegis-transition-',dir=path.parent)
try:
with os.fdopen(fd,'wb') as f:
f.write(data);f.flush();os.fsync(f.fileno());os.fchmod(f.fileno(),mode)
if os.geteuid()==0:os.fchown(f.fileno(),uid,gid)
os.replace(name,path)
finally:
pathlib.Path(name).unlink(missing_ok=True)
def update(path,old,new):
assert path.is_file() and not path.is_symlink()
assert path.read_bytes()==old,'File changed during migration: '+str(path)
st=path.stat()
backup=receipt/(str(path.relative_to(root)).replace('/','_')+'.before')
if not backup.exists():atomic(backup,old,0o600,os.getuid(),os.getgid())
atomic(path,new,stat.S_IMODE(st.st_mode),st.st_uid,st.st_gid)
print('Committed: '+str(path),flush=True)
p=root/'var/lib/aegis/principal-grants.toml'
if p.exists():
raw=p.read_bytes();value=tomllib.loads(raw.decode())
assert set(value)<= {'grants'}
for grant in value.get('grants',[]):
assert set(grant) in ({'login_principal','oauth_principal'},{'login_principal','user_id'})
new=re.sub(rb'(?m)^(\s*)oauth_principal(\s*=)',rb'\1user_id\2',raw)
checked=tomllib.loads(new.decode())
assert all(set(g)=={'login_principal','user_id'} for g in checked.get('grants',[]))
if new!=raw:update(p,raw,new)
print('Verified: local principal grants use user_id',flush=True)
def rename(value):
count=0
if isinstance(value,dict):
for old,new in [('oauth_principal','user_id'),('initial_oauth_principal','initial_user_id')]:
if old in value:
assert new not in value,'Conflicting cache identity fields'
value[new]=value.pop(old);count+=1
for child in value.values():count+=rename(child)
elif isinstance(value,list):
for child in value:count+=rename(child)
return count
cache=root/'var/lib/aegis/cache.json'
if cache.exists():
raw=cache.read_bytes();value=json.loads(raw)
if rename(value):update(cache,raw,(json.dumps(value,indent=2)+'\n').encode())
print('Verified: cached inventory uses user_id',flush=True)
for home in [root/'root',*(root/'home').glob('*')]:
for invitation in (home/'.aegis/enrollments').glob('*.json'):
raw=invitation.read_bytes();value=json.loads(raw)
assert set(value)=={'api_base','enrollment','refresh_token'},'Unexpected invitation format'
if rename(value):update(invitation,raw,(json.dumps(value,indent=2)+'\n').encode())
old='/var/lib/aegis/cargo-ca/hoek-deus-ca.pem';new='/etc/cargo/certificates/hoek-deus-ca.pem'
ca=root/old.lstrip('/');dest=root/new.lstrip('/')
if ca.exists():
assert ca.is_file() and not ca.is_symlink()
homes=[root/'root',*(root/'home').glob('*')]
configs=[home/'.cargo'/name for home in homes for name in ['config','config.toml']]
edits=[]
for config in configs:
if not config.exists():continue
assert config.is_file() and not config.is_symlink()
data=config.read_bytes()
if old.encode() in data:
updated=data.replace(old.encode(),new.encode());tomllib.loads(updated.decode())
edits.append((config,data,updated))
dest.parent.mkdir(parents=True,exist_ok=True,mode=0o755)
for directory in [root/'etc/cargo',dest.parent]:directory.chmod(0o755)
if dest.exists():assert dest.read_bytes()==ca.read_bytes(),'Shared Cargo CA destination differs'
else:
atomic(dest,ca.read_bytes(),0o644,0,0)
print('Committed: shared Cargo certificate at '+new,flush=True)
for config,data,updated in edits:update(config,data,updated)
assert all(old.encode() not in p.read_bytes() for p in configs if p.exists())
ca.unlink();print('Removed: '+old,flush=True)
if not any(ca.parent.iterdir()):ca.parent.rmdir()
if dest.exists():
for directory in [root/'etc/cargo',dest.parent]:directory.chmod(0o755)
print('Preparation complete. Backups retained at '+str(receipt),flush=True)