mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-05 20:32:15 +00:00
Fix local HTTPS readiness for native OCI stacks
This commit is contained in:
@@ -14,11 +14,13 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
@@ -662,24 +664,35 @@ def attach_mounts(service, temporary):
|
|||||||
oci_nested_mounts.follow(vmid)
|
oci_nested_mounts.follow(vmid)
|
||||||
|
|
||||||
|
|
||||||
def web_answers(vmid, url, opener):
|
def web_answers(vmid, url, opener, verify_tls=False):
|
||||||
from oci_installation_state import container_curl
|
from oci_installation_state import container_curl
|
||||||
curl = container_curl(vmid, lambda *args: run(*args).encode())
|
curl = container_curl(vmid, lambda *args: run(*args).encode())
|
||||||
if curl != ['curl']:
|
if curl != ['curl']:
|
||||||
run(*curl, '-fsS', '--noproxy', '*', '-o', '/dev/null', '--max-time', '4', url)
|
tls_args = [] if verify_tls else ['--insecure']
|
||||||
|
run(*curl, '-fsS', '--noproxy', '*', '-o', '/dev/null', '--max-time', '4', *tls_args, url)
|
||||||
return True
|
return True
|
||||||
with opener.open(url,timeout=4) as response:
|
try:
|
||||||
return response.status<400
|
with opener.open(url,timeout=4) as response:
|
||||||
|
return response.status<400
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
error.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def wait_web(primary, url):
|
def wait_web(primary, url):
|
||||||
deadline = time.monotonic()+primary['healthcheck']['timeout_seconds']
|
deadline = time.monotonic()+primary['healthcheck']['timeout_seconds']
|
||||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
# Match the single-container installer and recovery probes: image-generated
|
||||||
|
# certificates are valid for local readiness, with strict TLS available.
|
||||||
|
verify_tls = primary['healthcheck'].get('verify_tls', False)
|
||||||
|
handlers = [urllib.request.ProxyHandler({})]
|
||||||
|
if not verify_tls:
|
||||||
|
handlers.append(urllib.request.HTTPSHandler(context=ssl._create_unverified_context()))
|
||||||
|
opener = urllib.request.build_opener(*handlers)
|
||||||
while True:
|
while True:
|
||||||
if run('pct','status',primary['vmid']).strip()!='status: running':
|
if run('pct','status',primary['vmid']).strip()!='status: running':
|
||||||
raise RuntimeError(f"{translate('The container stopped:')} {primary['name']} (CT {primary['vmid']})")
|
raise RuntimeError(f"{translate('The container stopped:')} {primary['name']} (CT {primary['vmid']})")
|
||||||
try:
|
try:
|
||||||
if web_answers(primary['vmid'], url, opener):
|
if web_answers(primary['vmid'], url, opener, verify_tls=verify_tls):
|
||||||
check = primary['healthcheck']
|
check = primary['healthcheck']
|
||||||
if check.get('type') == 'exec':
|
if check.get('type') == 'exec':
|
||||||
remaining = max(1, deadline-time.monotonic())
|
remaining = max(1, deadline-time.monotonic())
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Real self-signed image HTTPS readiness, without weakening strict probes."""
|
||||||
|
import copy
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'remote'))
|
||||||
|
import install_generic_stack as installer
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil.which('openssl'), 'openssl needed for local TLS fixture')
|
||||||
|
class HTTPSReadiness(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.directory = tempfile.TemporaryDirectory()
|
||||||
|
cert, key = (Path(cls.directory.name) / name for name in ('cert.pem', 'key.pem'))
|
||||||
|
subprocess.run(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
|
||||||
|
'-keyout', str(key), '-out', str(cert), '-days', '1',
|
||||||
|
'-subj', '/CN=localhost'], check=True, capture_output=True)
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_response(503 if self.path == '/unhealthy' else 200)
|
||||||
|
self.end_headers()
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
cls.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
||||||
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
context.load_cert_chain(cert, key)
|
||||||
|
cls.server.socket = context.wrap_socket(cls.server.socket, server_side=True)
|
||||||
|
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
|
||||||
|
cls.thread.start()
|
||||||
|
cls.url = f'https://127.0.0.1:{cls.server.server_port}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.server.shutdown()
|
||||||
|
cls.server.server_close()
|
||||||
|
cls.thread.join()
|
||||||
|
cls.directory.cleanup()
|
||||||
|
|
||||||
|
def probe(self, *, verify_tls=False, path='/'):
|
||||||
|
service = {'vmid': 9070, 'name': 'test', 'healthcheck': {
|
||||||
|
'type': 'http', 'timeout_seconds': 1, 'verify_tls': verify_tls}}
|
||||||
|
# Only Proxmox command discovery is mocked; TLS and HTTP are real.
|
||||||
|
with patch.object(installer, 'run', return_value='status: running'), \
|
||||||
|
patch('oci_installation_state.container_curl', return_value=['curl']):
|
||||||
|
installer.wait_web(service, self.url + path)
|
||||||
|
|
||||||
|
def test_image_generated_certificate_passes_readiness(self):
|
||||||
|
self.probe()
|
||||||
|
|
||||||
|
def test_explicit_strict_tls_still_rejects_self_signed_certificate(self):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, 'HTTP check'):
|
||||||
|
self.probe(verify_tls=True)
|
||||||
|
|
||||||
|
def test_tls_tolerance_does_not_accept_http_failure(self):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, 'HTTP check'):
|
||||||
|
self.probe(path='/unhealthy')
|
||||||
|
|
||||||
|
|
||||||
|
class NamespaceTLSReadiness(unittest.TestCase):
|
||||||
|
def test_vlan_curl_obeys_the_same_verification_policy(self):
|
||||||
|
for strict in (False, True):
|
||||||
|
with self.subTest(strict=strict), \
|
||||||
|
patch('oci_installation_state.container_curl', return_value=['nsenter', '--net=/proc/123/ns/net', 'curl']), \
|
||||||
|
patch.object(installer, 'run') as run:
|
||||||
|
installer.web_answers(9070, 'https://192.0.2.7/', None, verify_tls=strict)
|
||||||
|
args = run.call_args.args
|
||||||
|
self.assertEqual('--insecure' in args, not strict)
|
||||||
|
self.assertIn('-fsS', args)
|
||||||
|
self.assertIn('--noproxy', args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user