Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

125 Zeilen
6.5 KiB

  1. """Promote the three validated v2 bundles without replacing platform identities."""
  2. import importlib.util
  3. import json
  4. import shlex
  5. import shutil
  6. import subprocess
  7. from pathlib import Path
  8. import paramiko
  9. from retrain_instructors_v2 import ROOT, REPORT, BUNDLES, IDS, FILES, api_session, API, HTTP, SERVICE, save
  10. RELEASE = '/home/digital-human-service/releases/instructors-v2-20260914'
  11. HOST_SOURCE = '/home/ai_person/ai_person_service/builtin_avatars'
  12. CONTAINER = 'digital-human-service'
  13. def connect():
  14. spec = importlib.util.spec_from_file_location('connection', ROOT / 'ai_person/ai_person_api/.run/test_file_service_tunnel.py')
  15. module = importlib.util.module_from_spec(spec)
  16. spec.loader.exec_module(module)
  17. host, port, user, password = module._read_connection()
  18. client = paramiko.SSHClient()
  19. client.load_system_host_keys()
  20. client.set_missing_host_key_policy(paramiko.RejectPolicy())
  21. client.connect(host, port=port, username=user, password=password, look_for_keys=False, allow_agent=False)
  22. return client
  23. def run(client, command):
  24. _, stdout, stderr = client.exec_command(command, timeout=300)
  25. output, error = stdout.read().decode('utf-8', 'replace'), stderr.read().decode('utf-8', 'replace')
  26. if stdout.channel.recv_exit_status() != 0:
  27. raise RuntimeError(output + error)
  28. return output.strip()
  29. def remote_promote(client, jobs, checks):
  30. # Use the service's own metadata operation. No SQL is issued against the platform database.
  31. script = r'''
  32. import hashlib,json,shutil
  33. from pathlib import Path
  34. from app.config import settings
  35. from app import database
  36. rows=PAYLOAD
  37. backup=settings.data_root/'backups'/'instructors-v2-20260914'
  38. if backup.exists():raise RuntimeError('Backup exists; refuse a second promotion')
  39. source=settings.project_root/'builtin_avatars'
  40. catalog={x['id']:x for x in json.loads((source/'catalog.json').read_text())}
  41. for row in rows:
  42. assert row['stable_id'] in catalog
  43. assets=settings.avatars_root/row['avatar_id']/'assets'
  44. assert database.get_avatar(row['avatar_id'])['status']=='ready'
  45. for name,digest in row['sha256'].items():assert hashlib.sha256((assets/name).read_bytes()).hexdigest()==digest
  46. backup.mkdir(parents=True)
  47. before=[]
  48. for row in rows:
  49. ident=row['stable_id']
  50. before.append(database.get_avatar(ident))
  51. shutil.copytree(source/ident,backup/'builtin'/ident)
  52. shutil.copytree(settings.avatars_root/ident,backup/'runtime'/ident)
  53. (backup/'before-metadata.json').write_text(json.dumps(before,ensure_ascii=False,indent=2))
  54. for row in rows:
  55. ident=row['stable_id'];assets=settings.avatars_root/row['avatar_id']/'assets'
  56. for root in (source,settings.avatars_root):
  57. stage=root/(ident+'.v2-stage')
  58. if stage.exists():raise RuntimeError('Unexpected stale staging directory')
  59. shutil.copytree(root/ident,stage)
  60. for name in row['sha256']:shutil.copy2(assets/name,stage/'assets'/name)
  61. previous=root/(ident+'.v1-previous')
  62. if previous.exists():raise RuntimeError('Unexpected previous directory')
  63. (root/ident).rename(previous)
  64. try:stage.rename(root/ident)
  65. except BaseException:previous.rename(root/ident);raise
  66. # Keep the renamed directories until all replacement checks finish.
  67. manifest=json.loads((assets/'manifest.json').read_text())
  68. spec=catalog[ident]
  69. database.upsert_builtin_avatar(avatar_id=ident,name=spec['name'],source_filename='built_in:'+spec['code'],
  70. frame_count=manifest['frame_count'],width=manifest['width'],height=manifest['height'],duration=manifest['duration'])
  71. print(json.dumps({'backup':str(backup),'updated':[row['stable_id'] for row in rows]}))
  72. '''.replace('PAYLOAD', repr([{**job, 'sha256': check['sha256']} for job, check in zip(jobs, checks)]))
  73. return run(client, 'docker exec -w /app ' + CONTAINER + ' python -c ' + shlex.quote(script))
  74. def main():
  75. if (REPORT / 'promotion.json').exists():
  76. raise RuntimeError('Already promoted; do not run twice')
  77. jobs = json.loads((REPORT / 'jobs.json').read_text('utf-8'))
  78. checks = json.loads((REPORT / 'generated-validation.json').read_text('utf-8'))
  79. playback = json.loads((REPORT / 'generated-viewer-validation.json').read_text('utf-8'))
  80. assert len(jobs) == len(checks) == len(playback) == 3
  81. assert all(not row['errors'] and row['driver']['active'] > 30 for row in playback)
  82. client = connect()
  83. try:
  84. base_image = run(client, "docker inspect digital-human-service --format '{{.Image}}'")
  85. run(client, f'mkdir -p {RELEASE}/backup-host {RELEASE}/builtin_avatars')
  86. for ident in IDS:
  87. run(client, f'test -d {HOST_SOURCE}/{ident} && cp -a {HOST_SOURCE}/{ident} {RELEASE}/backup-host/{ident}')
  88. save('promotion-start.json', {'baseImage': base_image, 'remoteRelease': RELEASE})
  89. result = json.loads(remote_promote(client, jobs, checks))
  90. save('promotion-runtime.json', result)
  91. for ident in IDS:
  92. run(client, f'docker cp {CONTAINER}:/app/builtin_avatars/{ident} {RELEASE}/builtin_avatars/{ident}')
  93. for filename in FILES:
  94. run(client, f'cp {RELEASE}/builtin_avatars/{ident}/assets/{filename} {HOST_SOURCE}/{ident}/assets/{filename}')
  95. shutil.copy2(REPORT / 'generated' / ident / 'assets' / filename, BUNDLES / ident / 'assets' / filename)
  96. # Persist bundles in the deployment image as well as the running container.
  97. # The base is the exact current image; no application source is upgraded.
  98. dockerfile = f'FROM {base_image}\nCOPY builtin_avatars/ /app/builtin_avatars/\nLABEL ai-person.avatar-material-version="instructors-v2-20260914"\n'
  99. with client.open_sftp() as sftp:
  100. with sftp.open(RELEASE + '/Dockerfile', 'w') as stream:
  101. stream.write(dockerfile)
  102. run(client, f'docker tag {base_image} digital-human-service:before-instructors-v2-20260914')
  103. output = run(client, f'docker build --network=none -t digital-human-service:instructors-v2-20260914 {RELEASE}')
  104. (REPORT / 'image-build.log').write_text(output, encoding='utf-8')
  105. run(client, 'docker tag digital-human-service:instructors-v2-20260914 digital-human-service:cpu')
  106. image_id = run(client, "docker image inspect digital-human-service:cpu --format '{{.Id}}'")
  107. save('promotion.json', {**result, 'oldImage': base_image, 'newImage': image_id, 'release': RELEASE})
  108. print(json.dumps({'updated': IDS, 'newImage': image_id, 'backup': result['backup']}), flush=True)
  109. finally:
  110. client.close()
  111. if __name__ == '__main__':
  112. main()