aegis-tool 0.3.11

Aegis SSH client and managed host agent.
Documentation
"""Operator-authorized 0.3.11 preparation, invoked by the managed systemd installation."""
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)
 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()
print('Preparation complete. Backups retained at '+str(receipt),flush=True)