diff --git a/brand.js b/brand.js
index ab2b994..d62df77 100644
--- a/brand.js
+++ b/brand.js
@@ -6,17 +6,25 @@ const version=document.getElementById('manifest-version');
const brandCount=document.getElementById('brand-count');
const progress=document.getElementById('progress');
const rawPath=document.getElementById('raw-path');
+const MANIFEST_TIMEOUT_MS=15000;
let manifest={brands:[],canonicalRawBase:'https://raw.githubusercontent.com/puchadave/assets/main/'};
+for(const [id,el] of Object.entries({'brand-grid':brandGrid,'asset-list':assetList,'asset-filter':filter,'manifest-version':version,'brand-count':brandCount,'progress':progress,'raw-path':rawPath})){
+ if(!el)console.error(`brand.js: missing element #${id}; related portal features are disabled.`);
+}
+
function esc(v=''){return String(v).replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
function initials(name=''){return name.split(/[\s._-]+/).filter(Boolean).slice(0,2).map(x=>x[0]).join('').toUpperCase()||'•';}
function repoPath(path=''){return `https://github.com/puchadave/assets/tree/main/${path}`;}
function rawAsset(path=''){return `${manifest.canonicalRawBase||'https://raw.githubusercontent.com/puchadave/assets/main/'}${path}`;}
function fallbackMark(b){return `${esc(initials(b.name))}${esc(b.name)}`;}
+function brands(){return Array.isArray(manifest.brands)?manifest.brands:[];}
+
function renderBrands(){
- brandCount.textContent=manifest.brands.length;
- brandGrid.innerHTML=manifest.brands.map(b=>{
+ if(!brandGrid)return;
+ if(brandCount)brandCount.textContent=brands().length;
+ brandGrid.innerHTML=brands().map(b=>{
const primary=(b.assets||[]).find(a=>a.role==='primary'&&a.path);
const mark=primary
? `
`
@@ -31,22 +39,25 @@ function renderBrands(){
document.querySelectorAll('.brand-logo').forEach(img=>{
img.addEventListener('error',()=>{
- const b=manifest.brands.find(x=>x.id===img.dataset.brandId);
- if(b)img.closest('.mark').innerHTML=fallbackMark(b);
+ const b=brands().find(x=>x.id===img.dataset.brandId);
+ const mark=img.closest('.mark');
+ console.warn(`brand.js: logo for "${img.dataset.brandId}" could not be loaded (${img.src}).`);
+ if(b&&mark)mark.innerHTML=fallbackMark(b);
},{once:true});
});
document.querySelectorAll('[data-brand]').forEach(a=>a.addEventListener('click',()=>{
- filter.value=a.dataset.brand;
+ if(filter)filter.value=a.dataset.brand;
renderAssets(a.dataset.brand);
}));
}
function flattenAssets(){
- return manifest.brands.flatMap(b=>(b.assets||[]).map(a=>({...a,brand:b.name,brandId:b.id,base:b.path})));
+ return brands().flatMap(b=>(b.assets||[]).map(a=>({...a,brand:b.name,brandId:b.id,base:b.path})));
}
function renderAssets(q=''){
+ if(!assetList)return;
const needle=q.trim().toLowerCase();
const rows=flattenAssets().filter(a=>!needle||[a.brand,a.brandId,a.role,a.format,a.path,a.label].join(' ').toLowerCase().includes(needle));
if(!rows.length){
@@ -60,33 +71,64 @@ function renderAssets(q=''){
}).join('');
}
+async function fetchManifest(){
+ const r=await fetch(`${MANIFEST_URL}?v=${Date.now()}`,{cache:'no-store',signal:AbortSignal.timeout?.(MANIFEST_TIMEOUT_MS)});
+ if(!r.ok)throw new Error(`HTTP ${r.status} ${r.statusText} für ${MANIFEST_URL}`);
+ let data;
+ try{
+ data=await r.json();
+ }catch(e){
+ throw new Error(`Manifest ist kein gültiges JSON: ${e.message}`,{cause:e});
+ }
+ if(!data||!Array.isArray(data.brands))throw new Error('Manifest enthält kein "brands"-Array.');
+ return data;
+}
+
+function showManifestError(e){
+ const reason=e?.name==='TimeoutError'?`Zeitüberschreitung nach ${MANIFEST_TIMEOUT_MS/1000}s`:e?.message||String(e);
+ console.error('brand.js: Brand-Manifest konnte nicht geladen werden.',e);
+ if(version)version.textContent='manifest nicht verfügbar';
+ if(brandGrid)brandGrid.innerHTML=`!Manifest
Brand-Manifest nicht verfügbar
Die Portalstruktur ist online, aber das kanonische Brand-Manifest konnte nicht geladen werden.
${esc(reason)}
`;
+ if(assetList)assetList.innerHTML=`
Manifest konnte nicht geladen werden.${esc(reason)}
`;
+}
+
async function loadManifest(){
try{
- const r=await fetch(`${MANIFEST_URL}?v=${Date.now()}`,{cache:'no-store'});
- if(!r.ok)throw new Error(`HTTP ${r.status}`);
- manifest=await r.json();
- version.textContent=`manifest ${manifest.version||'–'}`;
+ manifest=await fetchManifest();
+ }catch(e){
+ showManifestError(e);
+ return;
+ }
+ try{
+ if(version)version.textContent=`manifest ${manifest.version||'–'}`;
renderBrands();
renderAssets();
}catch(e){
- brandGrid.innerHTML='!Manifest
Brand-Manifest nicht verfügbar
Die Portalstruktur ist online, aber das kanonische Brand-Manifest konnte nicht geladen werden.
';
- assetList.innerHTML='Manifest konnte nicht geladen werden.
';
- console.error(e);
+ showManifestError(new Error(`Manifest konnte nicht gerendert werden: ${e.message}`,{cause:e}));
}
}
filter?.addEventListener('input',e=>renderAssets(e.target.value));
document.getElementById('copy-path')?.addEventListener('click',async e=>{
+ const button=e.currentTarget;
+ const reset=()=>setTimeout(()=>{button.textContent='Kopieren';},1600);
try{
+ if(!rawPath)throw new Error('Element #raw-path fehlt.');
+ if(!navigator.clipboard)throw new Error('Clipboard-API nicht verfügbar (benötigt HTTPS).');
await navigator.clipboard.writeText(rawPath.textContent);
- e.currentTarget.textContent='Kopiert';
- setTimeout(()=>e.currentTarget.textContent='Kopieren',1200);
- }catch{}
+ button.textContent='Kopiert';
+ }catch(err){
+ console.error('brand.js: Pfad konnte nicht kopiert werden.',err);
+ button.textContent='Kopieren fehlgeschlagen';
+ }
+ reset();
});
-window.addEventListener('scroll',()=>{
- const h=document.documentElement.scrollHeight-innerHeight;
- progress.style.width=`${h>0?(scrollY/h)*100:0}%`;
-},{passive:true});
+if(progress){
+ window.addEventListener('scroll',()=>{
+ const h=document.documentElement.scrollHeight-innerHeight;
+ progress.style.width=`${h>0?(scrollY/h)*100:0}%`;
+ },{passive:true});
+}
window.addEventListener('pointermove',e=>{
document.documentElement.style.setProperty('--mx',`${e.clientX}px`);
document.documentElement.style.setProperty('--my',`${e.clientY}px`);
diff --git a/proxmox/install-webowie-proxmox-branding.sh b/proxmox/install-webowie-proxmox-branding.sh
index 2031762..e8026ab 100755
--- a/proxmox/install-webowie-proxmox-branding.sh
+++ b/proxmox/install-webowie-proxmox-branding.sh
@@ -7,46 +7,90 @@ PVE_IMAGES="/usr/share/pve-manager/images"
WTK_IMAGES="/usr/share/javascript/proxmox-widget-toolkit/images"
PVE_INDEX="/usr/share/pve-manager/index.html.tpl"
INSTALL="/usr/local/share/webowie-branding"
+LOG_FILE="/var/log/webowie-pve-branding.log"
+ASSETS=(proxmox_logo.png logo-128.png dd_logo.png favicon.ico proxmox_logo.svg pve-background-1920x1080.png)
log(){ printf '\033[38;2;93;217;232m[webOwie]\033[0m %s\n' "$*"; }
+warn(){ echo "WARN: $*" >&2; }
die(){ echo "ERROR: $*" >&2; exit 1; }
+trap 'echo "ERROR: ${BASH_SOURCE[0]}: aborted with status $? at line $LINENO: ${BASH_COMMAND}" >&2' ERR
[[ $EUID -eq 0 ]] || die "Run as root."
command -v pveversion >/dev/null || die "Proxmox VE not detected."
command -v curl >/dev/null || die "curl is required."
+
+fetch_assets(){
+ local stage="$1" f
+ for f in "${ASSETS[@]}"; do
+ curl -fsSL --retry 3 --connect-timeout 20 "$RAW/$f" -o "$stage/$f" \
+ || die "Could not download $RAW/$f (branch '$BRANCH'). Existing branding was left untouched."
+ [[ -s "$stage/$f" ]] || die "Downloaded asset is empty: $RAW/$f"
+ done
+}
+
+apply_assets(){
+ local f
+ for f in "${ASSETS[@]}"; do
+ [[ -s "$INSTALL/$f" ]] || die "Brand asset missing or empty: $INSTALL/$f. Run '$0 --install' again."
+ done
+ install -m0644 "$INSTALL/proxmox_logo.png" "$PVE_IMAGES/proxmox_logo.png"
+ install -m0644 "$INSTALL/logo-128.png" "$PVE_IMAGES/logo-128.png"
+ install -m0644 "$INSTALL/favicon.ico" "$PVE_IMAGES/favicon.ico"
+ if [[ -e "$PVE_IMAGES/dd_logo.png" ]]; then install -m0644 "$INSTALL/dd_logo.png" "$PVE_IMAGES/dd_logo.png"; fi
+ if [[ -e "$WTK_IMAGES/proxmox_logo.svg" ]]; then
+ install -m0644 "$INSTALL/proxmox_logo.svg" "$WTK_IMAGES/proxmox_logo.svg"
+ else
+ warn "Widget-toolkit logo not found at $WTK_IMAGES/proxmox_logo.svg; skipping that override."
+ fi
+}
+
+brand_index(){
+ [[ -f "$PVE_INDEX" ]] || { warn "$PVE_INDEX not found; skipping HTML branding."; return 0; }
+ grep -q '' "$PVE_INDEX" || die "No found in $PVE_INDEX; HTML branding cannot be injected."
+ sed -i '//,//d' "$PVE_INDEX"
+ sed -i -E 's#[^<]*#webOwie Infrastructure Control Plane · puchalla.it.com#' "$PVE_INDEX"
+ sed -i '/<\/head>/i\
+\
+\
+\
+' "$PVE_INDEX"
+ grep -q 'WEBOWIE-BRANDING-BEGIN' "$PVE_INDEX" || die "HTML branding block was not written to $PVE_INDEX."
+}
+
case "${1:---install}" in
--install)
+ [[ -f "$0" && -r "$0" ]] || die "Cannot self-install: run the script from a file, not from a pipe (curl -fsSL ... -o install.sh && bash install.sh)."
stamp="$(date +%Y%m%d-%H%M%S)"; backup="$BACKUP_ROOT/$stamp"; mkdir -p "$backup" "$INSTALL"
for f in "$PVE_IMAGES/proxmox_logo.png" "$PVE_IMAGES/logo-128.png" "$PVE_IMAGES/favicon.ico" "$PVE_IMAGES/dd_logo.png" "$WTK_IMAGES/proxmox_logo.svg" "$PVE_INDEX"; do [[ -f "$f" ]] || continue; mkdir -p "$backup$(dirname "$f")"; cp -a "$f" "$backup$f"; done
printf '%s\n' "$backup" >"$BACKUP_ROOT/.last"
- for f in proxmox_logo.png logo-128.png dd_logo.png favicon.ico proxmox_logo.svg pve-background-1920x1080.png; do curl -fsSL "$RAW/$f" -o "$INSTALL/$f"; done
- install -m0644 "$INSTALL/proxmox_logo.png" "$PVE_IMAGES/proxmox_logo.png"
- install -m0644 "$INSTALL/logo-128.png" "$PVE_IMAGES/logo-128.png"
- install -m0644 "$INSTALL/favicon.ico" "$PVE_IMAGES/favicon.ico"
- [[ -e "$PVE_IMAGES/dd_logo.png" ]] && install -m0644 "$INSTALL/dd_logo.png" "$PVE_IMAGES/dd_logo.png"
- [[ -e "$WTK_IMAGES/proxmox_logo.svg" ]] && install -m0644 "$INSTALL/proxmox_logo.svg" "$WTK_IMAGES/proxmox_logo.svg"
+ stage="$(mktemp -d)"; trap 'rm -rf "$stage"' EXIT
+ fetch_assets "$stage"
+ for f in "${ASSETS[@]}"; do install -m0644 "$stage/$f" "$INSTALL/$f"; done
+ apply_assets
install -m0644 "$INSTALL/pve-background-1920x1080.png" "$PVE_IMAGES/webowie-background.png"
- if [[ -f "$PVE_INDEX" ]]; then
- sed -i '//,//d' "$PVE_INDEX"
- sed -i -E 's#[^<]*#webOwie Infrastructure Control Plane · puchalla.it.com#' "$PVE_INDEX"
- sed -i '/<\/head>/i\
-\
-\
-\
-' "$PVE_INDEX"
- fi
+ brand_index
install -m0755 "$0" /usr/local/sbin/webowie-pve-branding
- cat >/etc/apt/apt.conf.d/99-webowie-branding <<'EOF'
-DPkg::Post-Invoke { "if [ -x /usr/local/sbin/webowie-pve-branding ]; then /usr/local/sbin/webowie-pve-branding --reapply >/dev/null 2>&1 || true; fi"; };
+ cat >/etc/apt/apt.conf.d/99-webowie-branding <>${LOG_FILE} 2>&1 || echo \"\$(date -Is) webowie-pve-branding --reapply failed\" >>${LOG_FILE}; fi"; };
EOF
- systemctl restart pveproxy.service; log "Installed. Backup: $backup" ;;
+ systemctl restart pveproxy.service || die "Branding installed, but restarting pveproxy.service failed. Check 'systemctl status pveproxy.service'."
+ log "Installed. Backup: $backup" ;;
--reapply)
[[ -d "$INSTALL" ]] || die "Brand assets are not installed."
- install -m0644 "$INSTALL/proxmox_logo.png" "$PVE_IMAGES/proxmox_logo.png"; install -m0644 "$INSTALL/logo-128.png" "$PVE_IMAGES/logo-128.png"; install -m0644 "$INSTALL/favicon.ico" "$PVE_IMAGES/favicon.ico"
- [[ -e "$PVE_IMAGES/dd_logo.png" ]] && install -m0644 "$INSTALL/dd_logo.png" "$PVE_IMAGES/dd_logo.png"
- [[ -e "$WTK_IMAGES/proxmox_logo.svg" ]] && install -m0644 "$INSTALL/proxmox_logo.svg" "$WTK_IMAGES/proxmox_logo.svg" ;;
+ apply_assets ;;
--restore)
- backup="$(cat "$BACKUP_ROOT/.last" 2>/dev/null || true)"; [[ -d "$backup" ]] || die "No backup found."
- while IFS= read -r -d '' src; do dst="${src#$backup}"; mkdir -p "$(dirname "$dst")"; cp -a "$src" "$dst"; done < <(find "$backup" -type f -print0)
- rm -f /etc/apt/apt.conf.d/99-webowie-branding; systemctl restart pveproxy.service; log "Restored $backup" ;;
- --status) echo "webOwie PVE Branding"; echo "Asset branch: $BRANCH"; echo "Source: $RAW"; pveversion | head -1 ;;
+ backup="$(cat "$BACKUP_ROOT/.last" 2>/dev/null || true)"
+ [[ -n "$backup" ]] || die "No backup marker at $BACKUP_ROOT/.last."
+ [[ -d "$backup" ]] || die "Recorded backup directory is missing: $backup"
+ restored=0
+ while IFS= read -r -d '' src; do dst="${src#"$backup"}"; mkdir -p "$(dirname "$dst")"; cp -a "$src" "$dst"; restored=$((restored+1)); done < <(find "$backup" -type f -print0)
+ [[ $restored -gt 0 ]] || die "Backup $backup contains no files; nothing was restored."
+ rm -f /etc/apt/apt.conf.d/99-webowie-branding
+ systemctl restart pveproxy.service || die "Files restored from $backup, but restarting pveproxy.service failed."
+ log "Restored $backup ($restored files)" ;;
+ --status)
+ echo "webOwie PVE Branding"; echo "Asset branch: $BRANCH"; echo "Source: $RAW"
+ if [[ -d "$INSTALL" ]]; then echo "Local assets: $INSTALL"; else echo "Local assets: not installed"; fi
+ version="$(pveversion || true)"
+ [[ -n "$version" ]] || die "pveversion returned no output."
+ printf '%s\n' "$version" | head -n1 ;;
*) die "Usage: $0 [--install|--reapply|--restore|--status]" ;;
esac
diff --git a/scripts/build-assets.py b/scripts/build-assets.py
index 39ba433..344f84c 100755
--- a/scripts/build-assets.py
+++ b/scripts/build-assets.py
@@ -16,19 +16,49 @@
"""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
-import cairosvg, io, json, shutil
+import cairosvg, io, json, shutil, sys
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "assets" / "img"
FONT_REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
+REQUIRED_CFG_KEYS = ("brand","tagline")
+
+class BuildError(RuntimeError):
+ """Raised for actionable build failures (missing sources, invalid config)."""
def fnt(size,bold=False):
- return ImageFont.truetype(FONT_BOLD if bold else FONT_REG,size)
+ path=FONT_BOLD if bold else FONT_REG
+ try:
+ return ImageFont.truetype(path,size)
+ except OSError as e:
+ raise BuildError(f"Font not available: {path} (install fonts-dejavu-core)") from e
def render_svg(path,width=1600):
- data=cairosvg.svg2png(url=str(path),output_width=width)
- return Image.open(io.BytesIO(data)).convert("RGBA")
+ if not Path(path).is_file():
+ raise BuildError(f"Canonical SVG source missing: {path}")
+ try:
+ data=cairosvg.svg2png(url=str(path),output_width=width)
+ return Image.open(io.BytesIO(data)).convert("RGBA")
+ except BuildError:
+ raise
+ except Exception as e:
+ raise BuildError(f"Could not render SVG {path} at width {width}: {e}") from e
+
+def load_cfg(path,accent):
+ try:
+ cfg=json.loads(path.read_text())
+ except FileNotFoundError as e:
+ raise BuildError(f"Brand config missing: {path}") from e
+ except json.JSONDecodeError as e:
+ raise BuildError(f"Brand config is not valid JSON: {path} ({e})") from e
+ if not isinstance(cfg,dict):
+ raise BuildError(f"Brand config must be a JSON object: {path}")
+ missing=[k for k in REQUIRED_CFG_KEYS if not cfg.get(k)]
+ if missing:
+ raise BuildError(f"Brand config {path} is missing required key(s): {', '.join(missing)}")
+ cfg["accent"]=accent
+ return cfg
def contain(img,size,padding=0,bg=(0,0,0,0)):
c=Image.new("RGBA",size,bg)
@@ -48,7 +78,10 @@ def grid(size,accent):
d=ImageDraw.Draw(im)
for x in range(0,w,80): d.line((x,0,x,h),fill=(35,38,42,90))
for y in range(0,h,80): d.line((0,y,w,y),fill=(35,38,42,90))
- rgb=tuple(int(accent[i:i+2],16) for i in (1,3,5))
+ try:
+ rgb=tuple(int(accent[i:i+2],16) for i in (1,3,5))
+ except (ValueError,IndexError) as e:
+ raise BuildError(f"Accent color must be a #RRGGBB hex value, got {accent!r}") from e
d.rectangle((0,0,10,h),fill=rgb+(255,))
d.rectangle((10,0,18,h),fill=rgb+(70,))
return im
@@ -78,10 +111,11 @@ def banner(logo,cfg,size,label):
("bnd.zone/subbrands/cybersicherheit","bnd.zone_cybersicherheit_horizontal.svg","bnd.zone_cybersicherheit_icon.svg","#001A44"),
]
-for rel,logo_name,icon_name,accent in BRANDS:
+def build_brand(rel,logo_name,icon_name,accent):
base=ASSETS/rel
- cfg=json.loads((base/"brand.json").read_text())
- cfg["accent"]=accent
+ if not base.is_dir():
+ raise BuildError(f"Brand directory missing: {base}")
+ cfg=load_cfg(base/"brand.json",accent)
svgdir=base/"logos/svg"
out_logo=base/"logos/png"; out_logo.mkdir(parents=True,exist_ok=True)
fav=base/"favicons"; fav.mkdir(parents=True,exist_ok=True)
@@ -104,23 +138,44 @@ def banner(logo,cfg,size,label):
for name,size in {"og-image-1200x630.png":(1200,630),"github-social-preview-1280x640.png":(1280,640),"x-cover-1500x500.png":(1500,500),"linkedin-cover-1584x396.png":(1584,396),"facebook-cover-1640x624.png":(1640,624),"youtube-banner-2560x1440.png":(2560,1440)}.items():
banner(logo,cfg,size,"social").save(social/name)
-web=ASSETS/"webOwie/corporate"
-svgdir=web/"logos/svg"
-pve=web/"proxmox"
-pve.mkdir(parents=True,exist_ok=True)
-web_logo=render_svg(svgdir/"webOwie_horizontal_transparent.svg",1800)
-web_icon=render_svg(svgdir/"webOwie_icon.svg",1024)
-contain(web_logo,(209,30),1,(0,0,0,0)).save(pve/"proxmox_logo.png")
-contain(web_icon,(128,128),18,"#000000").save(pve/"logo-128.png")
-contain(web_icon,(128,128),18,"#000000").save(pve/"dd_logo.png")
-favicon(web_icon,pve/"favicon.ico")
-shutil.copy2(svgdir/"webOwie_horizontal_transparent.svg",pve/"proxmox_logo.svg")
-bg=grid((1920,1080),"#5DD9E8")
-wm=contain(web_icon,(620,620),80,(0,0,0,0))
-wm.putalpha(wm.getchannel("A").point(lambda a:int(a*.11)))
-bg.alpha_composite(wm,(1200,310))
-d=ImageDraw.Draw(bg)
-d.text((90,900),"webOwie // INFRASTRUCTURE CONTROL PLANE",font=fnt(34,True),fill="#F5F5F5")
-d.text((90,955),"puchalla.it.com · Powered by Proxmox VE",font=fnt(22),fill="#BFC3C7")
-bg.convert("RGB").save(pve/"pve-background-1920x1080.png")
-print("Generated complete brand asset sets and Proxmox exports.")
+def build_proxmox_exports():
+ web=ASSETS/"webOwie/corporate"
+ svgdir=web/"logos/svg"
+ pve=web/"proxmox"
+ pve.mkdir(parents=True,exist_ok=True)
+ web_logo=render_svg(svgdir/"webOwie_horizontal_transparent.svg",1800)
+ web_icon=render_svg(svgdir/"webOwie_icon.svg",1024)
+ contain(web_logo,(209,30),1,(0,0,0,0)).save(pve/"proxmox_logo.png")
+ contain(web_icon,(128,128),18,"#000000").save(pve/"logo-128.png")
+ contain(web_icon,(128,128),18,"#000000").save(pve/"dd_logo.png")
+ favicon(web_icon,pve/"favicon.ico")
+ shutil.copy2(svgdir/"webOwie_horizontal_transparent.svg",pve/"proxmox_logo.svg")
+ bg=grid((1920,1080),"#5DD9E8")
+ wm=contain(web_icon,(620,620),80,(0,0,0,0))
+ wm.putalpha(wm.getchannel("A").point(lambda a:int(a*.11)))
+ bg.alpha_composite(wm,(1200,310))
+ d=ImageDraw.Draw(bg)
+ d.text((90,900),"webOwie // INFRASTRUCTURE CONTROL PLANE",font=fnt(34,True),fill="#F5F5F5")
+ d.text((90,955),"puchalla.it.com · Powered by Proxmox VE",font=fnt(22),fill="#BFC3C7")
+ bg.convert("RGB").save(pve/"pve-background-1920x1080.png")
+
+def main():
+ for rel,logo_name,icon_name,accent in BRANDS:
+ try:
+ build_brand(rel,logo_name,icon_name,accent)
+ except BuildError as e:
+ raise BuildError(f"[{rel}] {e}") from e
+ except Exception as e:
+ raise BuildError(f"[{rel}] unexpected failure: {type(e).__name__}: {e}") from e
+ try:
+ build_proxmox_exports()
+ except BuildError as e:
+ raise BuildError(f"[webOwie/corporate/proxmox] {e}") from e
+ print("Generated complete brand asset sets and Proxmox exports.")
+
+if __name__=="__main__":
+ try:
+ main()
+ except BuildError as e:
+ print(f"ERROR: asset build failed: {e}",file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/create-netbootxyz-alpine-lxc.sh b/scripts/create-netbootxyz-alpine-lxc.sh
index f1a5efa..be7bfbf 100644
--- a/scripts/create-netbootxyz-alpine-lxc.sh
+++ b/scripts/create-netbootxyz-alpine-lxc.sh
@@ -8,7 +8,6 @@ set -euo pipefail
# ============================================================
APP="netboot.xyz"
-CTID="${CTID:-$(pvesh get /cluster/nextid 2>/dev/null || echo 180)}"
HOSTNAME="${HOSTNAME:-netboot-xyz}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
ROOTFS_STORAGE="${ROOTFS_STORAGE:-local-lvm}"
@@ -31,16 +30,28 @@ die() {
exit 1
}
+warn() {
+ echo "WARN: $*" >&2
+}
+
need() {
command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"
}
+trap 'echo "ERROR: ${BASH_SOURCE[0]}: aborted with status $? at line $LINENO: ${BASH_COMMAND}" >&2' ERR
+
need pct
need pveam
need pvesh
[[ "$(id -u)" -eq 0 ]] || die "Run as root on the Proxmox host."
+if [[ -z "${CTID:-}" ]]; then
+ CTID="$(pvesh get /cluster/nextid)" \
+ || die "Could not determine a free CTID via 'pvesh get /cluster/nextid'. Pass one explicitly: CTID=180 bash $0"
+ [[ "${CTID}" =~ ^[0-9]+$ ]] || die "Unexpected CTID from pvesh: '${CTID}'. Pass one explicitly: CTID=180 bash $0"
+fi
+
echo "[1/8] Updating Proxmox template index..."
pveam update >/dev/null
@@ -103,8 +114,21 @@ if [[ "${GPU}" == "yes" ]]; then
fi
echo "[5/8] Starting container..."
-pct start "${CTID}"
-sleep 5
+pct start "${CTID}" || die "Could not start container ${CTID}. Check 'pct start ${CTID}' output and /var/log/pve."
+
+for _ in $(seq 1 30); do
+ [[ "$(pct status "${CTID}" 2>/dev/null)" == "status: running" ]] && break
+ sleep 1
+done
+[[ "$(pct status "${CTID}" 2>/dev/null)" == "status: running" ]] \
+ || die "Container ${CTID} did not reach state 'running'."
+
+for _ in $(seq 1 30); do
+ pct exec "${CTID}" -- /bin/sh -c 'exit 0' >/dev/null 2>&1 && break
+ sleep 1
+done
+pct exec "${CTID}" -- /bin/sh -c 'exit 0' >/dev/null 2>&1 \
+ || die "Container ${CTID} is running but does not accept 'pct exec' commands."
cat >/tmp/netbootxyz-install-alpine.sh <<'INSTALL'
#!/bin/sh
@@ -113,6 +137,14 @@ set -eu
WEBROOT="/var/www/html"
BASE_URL="https://github.com/netbootxyz/netboot.xyz/releases/latest/download"
+# Without these files the PXE server cannot serve UEFI or legacy BIOS clients.
+ESSENTIAL_FILES="
+netboot.xyz.efi
+netboot.xyz-snp.efi
+netboot.xyz.kpxe
+netboot.xyz-undionly.kpxe
+"
+
BOOT_FILES="
netboot.xyz.efi
netboot.xyz.efi.dsk
@@ -158,28 +190,43 @@ apk add --no-cache \
tftp-hpa \
tftp-hpa-openrc
-update-ca-certificates || true
+update-ca-certificates || echo "WARN: update-ca-certificates failed; TLS downloads may fail." >&2
echo "[container] Preparing webroot..."
mkdir -p "${WEBROOT}" /run/nginx
cd "${WEBROOT}"
echo "[container] Fetching netboot.xyz menus..."
-curl -fL --retry 5 --connect-timeout 20 \
- -o menus.tar.gz \
- "${BASE_URL}/menus.tar.gz"
+if ! curl -fL --retry 5 --connect-timeout 20 -o menus.tar.gz "${BASE_URL}/menus.tar.gz"; then
+ echo "ERROR: could not download ${BASE_URL}/menus.tar.gz" >&2
+ exit 1
+fi
tar -xzf menus.tar.gz
rm -f menus.tar.gz
echo "[container] Fetching netboot.xyz boot files..."
+FAILED_FILES=""
for f in ${BOOT_FILES}; do
echo " -> ${f}"
- curl -fL --retry 5 --connect-timeout 20 \
- -o "${f}" \
- "${BASE_URL}/${f}" || echo "WARN: could not fetch ${f}"
+ if ! curl -fL --retry 5 --connect-timeout 20 -o "${f}" "${BASE_URL}/${f}"; then
+ rm -f "${f}"
+ FAILED_FILES="${FAILED_FILES}${f} "
+ echo "WARN: could not fetch ${f}" >&2
+ fi
done
+MISSING_ESSENTIAL=""
+for f in ${ESSENTIAL_FILES}; do
+ [ -s "${f}" ] || MISSING_ESSENTIAL="${MISSING_ESSENTIAL}${f} "
+done
+if [ -n "${MISSING_ESSENTIAL}" ]; then
+ echo "ERROR: essential boot files could not be downloaded: ${MISSING_ESSENTIAL}" >&2
+ echo "ERROR: the PXE server would be unusable, aborting." >&2
+ exit 1
+fi
+[ -z "${FAILED_FILES}" ] || echo "WARN: optional boot files missing: ${FAILED_FILES}" >&2
+
echo "[container] Configuring nginx..."
cat >/etc/nginx/http.d/default.conf <<'NGINX'
server {
@@ -222,6 +269,10 @@ rc-service in.tftpd restart
touch /root/.netboot-xyz
IP="$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1 || true)"
+if [ -z "${IP}" ]; then
+ IP=""
+ echo "WARN: no IPv4 address on eth0; check DHCP or set IPCONFIG=ip=/,gw=." >&2
+fi
cat >/etc/motd <