#!/usr/bin/env python3 """Smoke actual self-contained Linux Host/CLI artifacts with task-only PKI and LocalDb.""" import argparse, base64, datetime, hashlib, json, os, re, secrets, shutil, subprocess, time, uuid from pathlib import Path p = argparse.ArgumentParser(description=__doc__) p.add_argument('--artifacts', required=True, type=Path) p.add_argument('--image', default='mcr.microsoft.com/dotnet/sdk:10.0.103-noble') a = p.parse_args(); out = a.artifacts.resolve() run = out / ('runtime-' + uuid.uuid4().hex); run.mkdir(exist_ok=False) state = run / 'state'; state.mkdir(); os.chmod(state, 0o700) identifier = uuid.uuid4().hex[:12] web, ua, akka = [20000 + secrets.randbelow(10000) for _ in range(3)] while len({web, ua, akka}) != 3: web, ua, akka = [20000 + secrets.randbelow(10000) for _ in range(3)] key = 'otua_' + secrets.token_hex(8) + '_' + secrets.token_urlsafe(32) pepper, fetch = secrets.token_urlsafe(32), secrets.token_urlsafe(32) master = base64.b64encode(secrets.token_bytes(32)).decode() env = { 'ASPNETCORE_URLS': f'http://127.0.0.1:{web}', 'ASPNETCORE_ENVIRONMENT': 'Production', 'Cluster__Roles__0': 'driver', 'Cluster__SystemName': 'otopcua', 'Cluster__Hostname': '127.0.0.1', 'Cluster__PublicHostname': '127.0.0.1', 'Cluster__Port': str(akka), 'Cluster__SeedNodes__0': f'akka.tcp://otopcua@127.0.0.1:{akka}', 'ConfigSource__Mode': 'FetchAndCache', 'ConfigSource__CentralFetchEndpoints__0': 'http://127.0.0.1:1', 'ConfigSource__ApiKey': fetch, 'ConfigSource__FetchTimeoutSeconds': '1', 'LocalDb__Path': '/state/localdb.db', 'Secrets__SqlitePath': '/state/secrets.db', 'ZB_SECRETS_MASTER_KEY': master, 'Security__ApiKeys__Pepper': pepper, 'Security__Ldap__Enabled': 'false', 'Security__Acl__Enabled': 'false', 'OpcUa__ApplicationName': 'PublishSmoke-' + identifier, 'OpcUa__ApplicationUri': 'urn:localhost:PublishSmoke:' + identifier, 'OpcUa__PublicHostname': 'localhost', 'OpcUa__OpcUaPort': str(ua), 'OpcUa__PkiStoreRoot': '/state/server-pki-' + identifier, 'OpcUa__AllowAnonymous': 'false', 'OpcUa__AutoAcceptUntrustedClientCertificates': 'false', 'OpcUa__EnabledSecurityProfiles__0': 'Basic256Sha256SignAndEncrypt', 'SMOKE_API_KEY': key, 'XDG_DATA_HOME': '/state/cli-data', 'HOME': '/state/home', } envfile = run / 'private.env'; envfile.write_text(''.join(f'{k}={v}\n' for k,v in env.items())); os.chmod(envfile, 0o600) redactions = [key, key.split('_', 2)[2], pepper, fetch, master] def clean(text): for secret in redactions: text = text.replace(secret, '') return text records = [] raw_secret_findings = [] rolling_root = out / 'linux-arm64/host/logs' rolling_offsets = {p: p.stat().st_size for p in rolling_root.glob('*') if p.is_file()} def command(args, name, expected=0, timeout=90): start = datetime.datetime.now(datetime.timezone.utc).isoformat() result = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout) raw_secret_findings.append({'log': name, 'occurrences': sum(result.stdout.count(secret) for secret in redactions)}) (run / (name + '.log')).write_text(clean(result.stdout)) records.append({'name': name, 'utc': start, 'argv': [clean(str(x)) for x in args], 'exit': result.returncode}) (run / 'commands.json').write_text(json.dumps(records, indent=2)) if expected is not None and result.returncode != expected: raise RuntimeError(f'{name}: exit {result.returncode}; see {run / (name + ".log")}') return result mounts = ['--mount', f'type=bind,src={out},dst=/work', '--mount', f'type=bind,src={state},dst=/state'] base = ['docker','run','--rm','--network','none','--label','codex.task=opcua-publish-smoke','--env-file',str(envfile),*mounts,'-w','/work/linux-arm64/host',a.image] probe = ['dotnet','/work/probe/PublishProbe.dll'] cli = ['/work/linux-arm64/cli/ZB.MOM.WW.OtOpcUa.Client.CLI'] server_pki = state / ('server-pki-' + identifier) container = None; summary = {'run': str(run), 'image': a.image, 'ports': {'http':web,'ua':ua,'akka':akka}, 'boots':[], 'cases':[]} def copy_public(source, target): target.mkdir(parents=True, exist_ok=True) certs = list(source.glob('*.der')) if not certs: raise RuntimeError(f'No public certificates at {source}') for cert in certs: shutil.copy2(cert, target / cert.name) def inspect(): return json.loads(subprocess.check_output(['docker','inspect',container], text=True))[0] def save_logs(name): r = subprocess.run(['docker','logs',container],text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) raw_secret_findings.append({'log': name, 'occurrences': sum(r.stdout.count(secret) for secret in redactions)}) (run / (name + '.log')).write_text(clean(r.stdout)) def exec_probe(name, pki, expected, credential=None): args = ['docker','exec'] if credential: args += ['-e','SMOKE_API_KEY=' + credential] args += [container,*probe,'probe',pki,f'opc.tcp://localhost:{ua}/OtOpcUa',expected] r = command(args,name) payload = json.loads(r.stdout.strip().splitlines()[-1]); summary['cases'].append({'name':name,**payload}) try: command([*base,*probe,'seed','/state'],'seed') command([*base,*probe,'prepare','/state/client-pki'],'prepare-trusted-client') command([*base,*probe,'prepare','/state/untrusted-client-pki'],'prepare-untrusted-client') command([*base,*cli,'--help'],'cli-help') command([*base,*probe,'prepare-cli','/state/cli-data/OtOpcUaClient/pki'],'prepare-cli-certificate') failed = command([*base,*cli,'read','--url','opc.tcp://localhost:1/OtOpcUa','--security','encrypt','--username','apikey','--password',key,'--node','i=2259'],'cli-prepare-on-closed-port',expected=None) if failed.returncode == 0: raise RuntimeError('Closed-port CLI fixture preparation unexpectedly succeeded.') copy_public(state/'client-pki/own/certs',server_pki/'trusted/certs') copy_public(state/'cli-data/OtOpcUaClient/pki/own/certs',server_pki/'trusted/certs') name = 'otopcua-sdk2-publish-' + identifier result = command(['docker','create','--name',name,'--network','none','--label','codex.task=opcua-publish-smoke','--env-file',str(envfile),*mounts,'-w','/work/linux-arm64/host',a.image,'./OtOpcUa.Host'],'container-create') container = result.stdout.strip() summary['container'] = container for boot in [1,2]: command(['docker','start',container],f'boot{boot}-start') deadline = time.monotonic() + 90 health = None while time.monotonic() < deadline: if not inspect()['State']['Running']: save_logs(f'boot{boot}-host'); raise RuntimeError('Full Host exited before health readiness.') r = subprocess.run(['docker','exec',container,'curl','-fsS',f'http://127.0.0.1:{web}/health/ready'],text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) if r.returncode == 0: health = r.stdout; break time.sleep(1) if health is None: save_logs(f'boot{boot}-host'); raise RuntimeError('Full Host readiness did not become healthy.') if json.loads(health).get('status') != 'Healthy': raise RuntimeError('Readiness is degraded or unhealthy.') (run/f'boot{boot}-ready.json').write_text(health) command(['docker','exec',container,'curl','-fsS',f'http://127.0.0.1:{web}/healthz'],f'boot{boot}-live') command(['docker','exec',container,'curl','-fsS',f'http://127.0.0.1:{web}/health/active'],f'boot{boot}-active') certs = list((server_pki/'own/certs').glob('*.der')) if len(certs) != 1: raise RuntimeError('Expected exactly one application certificate.') cert_sha256 = hashlib.sha256(certs[0].read_bytes()).hexdigest() thumbprint = subprocess.check_output(['openssl','x509','-inform','DER','-in',str(certs[0]),'-noout','-fingerprint','-sha1'],text=True).strip() copy_public(server_pki/'own/certs',state/'client-pki/trusted/certs') copy_public(server_pki/'own/certs',state/'untrusted-client-pki/trusted/certs') exec_probe(f'boot{boot}-trusted-key','/state/client-pki','allow') exec_probe(f'boot{boot}-wrong-key','/state/client-pki','key-deny',key+'-wrong') if boot == 1: hold = state/'hold-server-trust'; shutil.move(str(state/'client-pki/trusted/certs'),hold) (state/'client-pki/trusted/certs').mkdir() try: exec_probe('untrusted-server','/state/client-pki','peer-deny') finally: shutil.rmtree(state/'client-pki/trusted/certs'); shutil.move(str(hold),state/'client-pki/trusted/certs') exec_probe('untrusted-client','/state/untrusted-client-pki','peer-deny') r = command(['docker','exec',container,*cli,'read','--url',f'opc.tcp://localhost:{ua}/OtOpcUa','--security','encrypt','--username','apikey','--password',key,'--node','i=2259'],f'boot{boot}-actual-cli-read') if not re.search(r'^Status:\s+Good(?:\s|$)', r.stdout, re.MULTILINE): raise RuntimeError('Actual CLI did not report a Good server status read.') command(['docker','stop','--time','30',container],f'boot{boot}-stop',timeout=45) stopped = inspect()['State']; save_logs(f'boot{boot}-host') if stopped['Running'] or stopped['ExitCode'] != 0 or stopped['OOMKilled']: raise RuntimeError('Full Host did not shut down cleanly.') summary['boots'].append({'boot':boot,'certificate_sha256':cert_sha256,'thumbprint':thumbprint,'exit':stopped['ExitCode'],'oomKilled':stopped['OOMKilled'],'health_ready':health}) if summary['boots'][0]['thumbprint'] != summary['boots'][1]['thumbprint']: raise RuntimeError('Application certificate changed between boots.') summary['certificate_preserved'] = True for path in rolling_root.glob('*'): if not path.is_file(): continue text = path.read_bytes()[rolling_offsets.get(path, 0):].decode(errors='replace') raw_secret_findings.append({'log': 'rolling-' + path.name, 'occurrences': sum(text.count(secret) for secret in redactions)}) (run / ('rolling-' + path.name)).write_text(clean(text)) if len(summary['boots']) != 2 or len(summary['cases']) != 6: raise RuntimeError('Required smoke cases did not all execute.') if any(x['occurrences'] for x in raw_secret_findings): raise RuntimeError('Task secret appeared in raw application output; see recorded occurrence counts.') summary['result'] = 'passed' finally: if container: save_logs('host-final') owned = inspect() if owned['Config']['Labels'].get('codex.task') != 'opcua-publish-smoke': raise RuntimeError('Task container ownership changed.') command(['docker','rm','-f',container],'container-cleanup') logs = '\n'.join(x.read_text(errors='replace') for x in run.glob('*.log')) summary['task_secret_occurrences_in_saved_logs'] = sum(logs.count(secret) for secret in redactions) summary['raw_log_secret_findings'] = raw_secret_findings summary['completed_utc'] = datetime.datetime.now(datetime.timezone.utc).isoformat() (run/'summary.json').write_text(json.dumps(summary,indent=2)) envfile.unlink(missing_ok=True) print(json.dumps(summary,indent=2))