86 lines
7.8 KiB
Python
86 lines
7.8 KiB
Python
"""Restore a verified destination snapshot into a new, guarded QA target only."""
|
|
from pathlib import Path
|
|
import json, datetime, subprocess, os, re, shutil, hashlib
|
|
R=Path(__file__).resolve().parent.parent; D=R/'storage/app/private/investmoz-v2/portal-audit'
|
|
read=lambda p: json.loads(Path(p).read_text(encoding='utf-8-sig'))
|
|
sha=lambda p: hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
|
old=read(D/'workflow-upgrade-target-backup-20261011.json'); base=read(D/'portal-stability-latest.json')
|
|
assert old['restore_verified'] and old['files_verified'] and len(old['tables'])==288
|
|
assert sha(old['dump_path'])==old['dump_sha256'] and sha(old['files_path'])==old['files_sha256']
|
|
stamp=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%d_%H%M%S')
|
|
db='workflow_upgrade_'+stamp+'_test'; source=D/('workflow-upgrade-final-source-'+stamp)
|
|
assert re.fullmatch(r'workflow_upgrade_\d{8}_\d{6}_test',db) and not source.exists()
|
|
pg=Path('C:/Program Files/PostgreSQL/17/bin'); env=dict(os.environ,PGPASSWORD='')
|
|
def run(args):
|
|
r=subprocess.run([str(a) for a in args],env=env,capture_output=True,text=True,timeout=300)
|
|
assert r.returncode==0, 'Isolated restore failed; no active operation'
|
|
return r.stdout.strip()
|
|
assert run([pg/'psql.exe','-h','127.0.0.1','-p','55441','-U','postgres','-d','postgres','-Atc','SELECT inet_server_port()'])=='55441'
|
|
run([pg/'createdb.exe','-h','127.0.0.1','-p','55441','-U','postgres',db])
|
|
run([pg/'pg_restore.exe','-h','127.0.0.1','-p','55441','-U','postgres','-d',db,'--exit-on-error','--no-owner','--no-privileges',old['dump_path']])
|
|
source.mkdir(); original=Path(base['source'])
|
|
for folder in ['app','config','routes','database','resources','public']:
|
|
shutil.copytree(original/folder,source/folder,ignore=shutil.ignore_patterns('build-qa-*','*.bin'))
|
|
(source/'bootstrap/cache').mkdir(parents=True)
|
|
for file in ['bootstrap/app.php','bootstrap/providers.php','qa-autoload.php','composer.json','composer.lock','package.json','package-lock.json','vite.config.ts']:
|
|
shutil.copyfile(original/file,source/file)
|
|
for folder in ['storage/app/private','storage/framework/cache/data','storage/framework/sessions','storage/framework/views','storage/logs','tools']:
|
|
(source/folder).mkdir(parents=True,exist_ok=True)
|
|
archive=Path(old['dump_path']).parent
|
|
shutil.copytree(archive/'files-restored/private',source/'storage/app/private',dirs_exist_ok=True)
|
|
runtime=read(archive/'effective-runtime.json')
|
|
text=(archive/'files-restored/runtime/.env').read_text(encoding='utf-8-sig')
|
|
values={'APP_ENV':'testing','APP_DEBUG':'false','APP_URL':'https://portal-investidor.test:18447',
|
|
'APP_KEY':runtime['app_key'],'APP_PREVIOUS_KEYS':','.join(runtime['previous_keys']),'APP_CIPHER':runtime['cipher'],
|
|
'DB_CONNECTION':'pgsql','DB_HOST':'127.0.0.1','DB_PORT':'55441','DB_DATABASE':db,'DB_USERNAME':'postgres','DB_PASSWORD':'','DB_URL':'',
|
|
'SESSION_COOKIE':'investmoz_workflow_upgrade_'+stamp+'_qa','SESSION_DOMAIN':'null','SESSION_PATH':'/','SESSION_SECURE_COOKIE':'true','SESSION_SAME_SITE':'lax',
|
|
'CACHE_STORE':'database','QUEUE_CONNECTION':'database','MAIL_MAILER':'log',
|
|
'PORTAL_MANAGER_EMAILS':'workflow-installer-qa@example.invalid,workflow-first-qa@example.invalid,workflow-second-qa@example.invalid',
|
|
'PORTAL_NOTIFICATIONS_ENABLED':'false','PORTAL_ORGANIZATION_COUNTRY_ENABLED':'true','PORTAL_PROFILE_ACCESS_ENABLED':'false',
|
|
'PORTAL_OPPORTUNITY_DASHBOARD_ACCESS_ENABLED':'false','PORTAL_OPPORTUNITY_SPECIALIZED_DRAFTS_ENABLED':'false',
|
|
'PORTAL_OPPORTUNITY_MODULAR_PROFILES_QA_ENABLED':'false','PORTAL_LEGACY_CONTENT_QA_ENABLED':'false','INVESTOR_PATH_ENABLED':'false',
|
|
'CLAMD_PORT':'13310','INERTIA_SSR_ENABLED':'false'}
|
|
manager=re.search(r'^PORTAL_MANAGER_EMAILS=(.*)$',text,re.M)
|
|
if manager: values['PORTAL_MANAGER_EMAILS']=manager.group(1).strip("\"'")+','+values['PORTAL_MANAGER_EMAILS']
|
|
for k,v in values.items():
|
|
assert len(re.findall('^'+k+'=',text,re.M))<=1
|
|
text=re.sub('^'+k+'=.*$',lambda _:k+'='+v,text,flags=re.M) if re.search('^'+k+'=',text,re.M) else text+'\n'+k+'='+v+'\n'
|
|
(source/'.env').write_text(text,encoding='utf-8')
|
|
subprocess.run(['powershell','-NoProfile','-Command',f"New-Item -ItemType Junction -Path '{source}/vendor' -Target '{R}/vendor' | Out-Null"],check=True,capture_output=True)
|
|
runtimephp='''<?php
|
|
ini_set('zend.exception_ignore_args','1');
|
|
$root=dirname(__DIR__);$settings=json_decode(file_get_contents($root.'/upgrade-qa.json'),true,512,JSON_THROW_ON_ERROR);
|
|
if(PHP_SAPI!=='cli'&&($_SERVER['HTTP_HOST']??'')!=='portal-investidor.test:18447'){http_response_code(421);exit;}
|
|
require $root.'/qa-autoload.php';
|
|
foreach(Dotenv\\Dotenv::parse(file_get_contents($root.'/.env'))as$n=>$v){if($v!==null){putenv($n.'='.$v);$_ENV[$n]=$_SERVER[$n]=$v;}}
|
|
$app=require $root.'/bootstrap/app.php';
|
|
(new Illuminate\\Foundation\\Bootstrap\\LoadEnvironmentVariables)->bootstrap($app);
|
|
(new Illuminate\\Foundation\\Bootstrap\\LoadConfiguration)->bootstrap($app);
|
|
$c=config('database.connections.'.config('database.default'));
|
|
throw_unless($c['driver']==='pgsql'&&empty($c['url'])&&$c['host']==='127.0.0.1'&&(int)$c['port']===55441&&$c['database']===$settings['target']['database']&&preg_match('/^workflow_upgrade_\\d{8}_\\d{6}_test$/D',$c['database'])&&!$app->configurationIsCached(),RuntimeException::class,'Exact isolated upgrade target required before providers');
|
|
$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$c['database'],$c['username'],$c['password']);$pdo->exec('BEGIN READ ONLY');
|
|
throw_unless($pdo->query('SELECT current_database()')->fetchColumn()===$c['database']&&(int)$pdo->query('SELECT inet_server_port()')->fetchColumn()===55441,RuntimeException::class,'Actual isolated target required');$pdo->exec('ROLLBACK');
|
|
$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
|
|
config(['inertia.ssr.enabled'=>false,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false]);return $app;
|
|
'''
|
|
(source/'tools/upgrade-runtime.php').write_text(runtimephp,encoding='utf-8')
|
|
index=(original/'public/index.php').read_text(encoding='utf-8').replace('../tools/portal-stability-runtime.php','../tools/upgrade-runtime.php')
|
|
(source/'public/index.php').write_text(index,encoding='utf-8')
|
|
conf=Path(base['compatible_apache_conf']).read_text(encoding='utf-8')
|
|
# Keep modules, PHP INI, production TLS and binary exactly as the compatible QA; give this server its own listeners/files.
|
|
pre=conf[:conf.index('<VirtualHost')]; pre=re.sub(r'^Listen .*\n','',pre,flags=re.M)
|
|
qa=source/'apache-qa';qa.mkdir()
|
|
pre=re.sub(r'^PidFile .*$',f'PidFile "{(qa/"httpd.pid").as_posix()}"',pre,flags=re.M)
|
|
pre=re.sub(r'^ErrorLog .*$',f'ErrorLog "{(qa/"error.log").as_posix()}"',pre,flags=re.M)
|
|
pre+='Listen 127.0.0.1:18447\nListen 127.0.0.1:18109\n'
|
|
vhost=conf[conf.index('<VirtualHost'):]; vhost=vhost[:vhost.index('</VirtualHost>')+len('</VirtualHost>')] # HTTP block
|
|
https=re.search(r'<VirtualHost 127\.0\.0\.1:18445>[\s\S]*?</VirtualHost>',conf).group()
|
|
vhost=vhost.replace(':18106',':18109').replace(':18444',':18447');https=https.replace(':18445',':18447').replace(original.as_posix(),source.as_posix())
|
|
(qa/'httpd.conf').write_text(pre+vhost+'\n'+https+'\n',encoding='utf-8')
|
|
plan={'source':str(source),'target':{'database':db,'port':55441},'base':'https://portal-investidor.test:18447','http_base':'http://portal-investidor.test:18109',
|
|
'backup_proof':str(D/'workflow-upgrade-target-backup-20261011.json'),'snapshot_directory':str(archive),
|
|
'apache_executable':base['compatible_apache_executable'],'apache_conf':str(qa/'httpd.conf'),'created_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
'active_writes':0,'legacy_source':base['source']}
|
|
(source/'upgrade-qa.json').write_text(json.dumps(plan,indent=2),encoding='utf-8');(D/'workflow-upgrade-final-latest.json').write_text(json.dumps(plan,indent=2),encoding='utf-8')
|
|
print(json.dumps({'result':'isolated_clone_prepared','target':plan['target'],'base':plan['base'],'tables_expected':288,'COUNTRY02_preserved':True,'active_writes':0}))
|