{% extends "base.html" %} {% import '_ui.html' as ui %} {% block title %}BPRT sync - PXE Manager{% endblock %} {% macro size(n) -%} {%- if n is none -%}-{%- elif n >= 1000000000 -%}{{ '%.2f' | format(n / 1000000000) }} GB{%- elif n >= 1000000 -%}{{ '%.0f' | format(n / 1000000) }} MB{%- elif n >= 1000 -%}{{ '%.0f' | format(n / 1000) }} KB{%- else -%}{{ n }} B{%- endif -%} {%- endmacro %} {# Long package names: allow a line break after each underscore, nowhere else. #} {% macro fname(n) -%} {%- for part in n.split('_') %}{{ part }}{% if not loop.last %}_{% endif %}{% endfor -%} {%- endmacro %} {% macro val_chip(v) -%} {% if not v %}{{ ui.chip('Not validated', 'off') }} {% elif v.stale %}{{ ui.chip('Changed since validation', 'warn') }} {% elif v.state == 'valid' %}{{ ui.chip('Valid', 'ok') }} {% else %}{{ ui.chip(v.errors | length ~ ' error' ~ ('s' if v.errors | length != 1), 'err') }}{% endif %} {%- endmacro %} {% macro job_panel(job) -%} {% if job.running %}
{{ {'manifest': 'Hashing the live BPRT', 'validate': 'Validating', 'activate': 'Activating', 'unzip': 'Unpacking the zip'}.get(job.kind, job.kind) }} {% if job.arg %}{{ job.arg }}{% endif %} {{ job.step or 'Starting' }} {{ job.current or '' }} {% if job.percent is not none %}{{ job.percent }}%{% endif %}
{% elif job.state in ('error', 'interrupted') %}
The last BPRT {{ job.kind }} job {{ 'failed' if job.state == 'error' else 'stopped before it finished' }} {% if job.arg %}({{ job.arg }}){% endif %}{% if job.finished %}, {{ job.finished | local_dt }}{% endif %}{% if job.error %}: {{ job.error }}{% endif %} {% if job.kind == 'activate' %}
The live BPRT is only replaced by the last step (two renames), so it is either the old folder or the new one, never a half-built one.
{% endif %}
{% elif job.state == 'done' and job.result %}
Last job: {{ job.kind }}{% if job.arg %} {{ job.arg }}{% endif %}, {{ job.result }}{% if job.finished %} ({{ job.finished | local_dt }}){% endif %}.
{% endif %} {%- endmacro %} {% macro imaging_alert(imaging) -%} {% if imaging %}
{{ imaging | length }} PC{{ 's are' if imaging | length != 1 else ' is' }} imaging now. PESetup copies BPRT to each PC while it images; a PC copying during the swap can end up with a mix of old and new files. Activate or roll back when these have finished: {% for s in imaging[:8] %}{{ s.hostname or s.id }}{% if s.stage %} ({{ s.stage }}){% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if imaging | length > 8 %} and {{ imaging | length - 8 }} more{% endif %}. Imaging progress
{% endif %} {%- endmacro %} {% block extra_head %} {% endblock %} {% block content %} {% if detail %} {# ======================= one incoming batch ============================== #} {% set b = detail %}

Incoming batch {{ b.name }}

{{ val_chip(res) }} {% if res %}Validated {{ res.validated | local_dt }}{% endif %}
Back to BPRT sync

Where this batch came from

{% if b.sync %}
From
{{ b.sync.computer or 'unknown PC' }}{% if b.sync.user %} ({{ b.sync.user }}){% endif %}
{% if b.sync.created %}
Made
{{ b.sync.created | local_dt }}
{% endif %}
Files
{{ b.sync.listed }} listed, {{ b.files }} uploaded ({{ size(b.bytes) }})
{% endif %}
Folder {{ incoming }}/{{ b.name }}{% if b.sync and b.sync.source %}. Source on the PC {{ b.sync.source }}{% endif %}.
{% if b.sync_error %}
{{ b.sync_error }}
{% endif %}
{{ job_panel(job) }} {% if not res %}

Validate

Validation hashes every uploaded file against sync.json, checks that every other listed file is unchanged in the live BPRT, applies the vendor's package rules and works out what activating the batch would change. Nothing on the share changes.

{% else %} {% if res.errors %}

{{ res.errors | length }} error{{ 's' if res.errors | length != 1 }}

This batch cannot be activated. Fix the cause on the work PC and run Sync-BPRT.ps1 again (it makes a new batch), then delete this one.

    {% for e in res.errors %}
  • {{ e }}
  • {% endfor %}
{% endif %} {% if res.stale %}
{{ res.stale }}: validate again before activating.
{% endif %} {% if res.warnings %}
{{ res.warnings | length }} warning{{ 's' if res.warnings | length != 1 }}
    {% for w in res.warnings %}
  • {{ w }}
  • {% endfor %}
{% endif %} {% if res.counts %}
New files{{ res.counts.new }}
Changed{{ res.counts.changed }}
Removed{{ res.counts.removed }}
Unchanged{{ res.counts.unchanged }}

MCL packages

Live now, then after activation.
{% for p in res.ppkg %}
{{ p.prefix }} {% for o in p.old %}
{{ ui.chip(o.version, 'neutral') }} {{ fname(o.name) }}
{% else %}none{% endfor %}
to {% for n in p.new %}
{{ ui.chip(n.version, 'w11') }} {{ fname(n.name) }}
{% else %}none (removed){% endfor %}
{% else %}
No package changes.
{% endfor %}
{% if res.zips or res.bom_fix %}

Large files and fixes

{% for z in res.zips %}
{{ z.path }}{{ ('live ' ~ size(z.old_size)) if z.old_size is not none else 'new file' }}now {{ size(z.new_size) }}
{% endfor %} {% if res.bom_fix %}
Activation adds a UTF-8 byte order mark to {{ res.bom_fix | length }} script{{ 's' if res.bom_fix | length != 1 }} that hold{{ 's' if res.bom_fix | length == 1 }} non-ASCII text without one (Windows PowerShell 5.1 would read them as ANSI):
    {% for f in res.bom_fix %}
  • {{ f }}
  • {% endfor %}
{% endif %}
{% endif %}

Files

{% for key, label in [('new', 'New'), ('changed', 'Changed'), ('removed', 'Removed (not in the vendor folder any more; kept in the archive)'), ('unchanged', 'Unchanged')] %} {% set rows = res.diff[key] %} {% if rows %}
{{ label }} {{ ui.chip(rows | length, 'neutral') }}
    {% for f in rows %}
  • {{ f }}{% if f in renamed %} (kept as {{ renamed[f] }}){% endif %}
  • {% endfor %}
{% endif %} {% endfor %}
Site files carried over from the live BPRT as they are: {% for o in overlay %}{{ o }}{% if not loop.last %}, {% endif %}{% endfor %}.
{% endif %}

Activate

{{ imaging_alert(imaging) }}
What activating does

Builds BPRT.new beside the live folder (hard links: unchanged files take no space), puts the site files back on top, checks it once more, then swaps it in with two renames: the live folder becomes BPRT-archive/full-<time> (roll back to it at any time) and BPRT.new becomes BPRT. Every image links to that path, so they all switch at once.

{% if res.state != 'valid' %}Activate needs a valid batch.{% elif res.stale %}Validate again first: something changed.{% endif %}
{% endif %}
{% else %} {# ======================= overview ======================================= #} {% set n_pk = packages | selectattr('prefix') | list | length %}
Updates

BPRT sync

{% if not live.exists %}{{ ui.chip('No live BPRT folder', 'fail') }} {% elif live.missing_required %}{{ ui.chip(live.missing_required | length ~ ' required file' ~ ('s' if live.missing_required | length != 1) ~ ' missing', 'fail') }} {% else %}{{ ui.chip(n_pk ~ ' MCL packages live', 'ok') }}{% endif %} {% set waiting = batches | rejectattr('uploading') | list | length %} {% if waiting %}{{ waiting }} incoming batch{{ 'es' if waiting != 1 }} waiting{% else %}No batch waiting{% endif %}
{% if script_available %}Download Sync-BPRT.ps1{% endif %}
{% if live.missing_required %}
Missing files the bulk-enroll flow calls by path: {% for r in live.missing_required %}{{ r }}{% if not loop.last %}, {% endif %}{% endfor %}. Enrollment breaks on every PC until they are back.
{% endif %} {{ imaging_alert(imaging) }} {{ job_panel(job) }}
What BPRT sync does

_shared/Applications/BPRT is the vendor's bulk-enroll folder (MCL packages, Office zips, orchestrator scripts) that every image links to and PESetup copies to every PC. The vendor keeps it current on the GE work PC with MediaCreator Lite; Sync-BPRT.ps1 on that PC compares it with this server's copy and uploads only what changed, over SSH, into BPRT-incoming. Here a batch is validated, its changes shown, and activated with the old folder archived. The site's own files ({% for o in overlay %}{{ o }}{% if not loop.last %}, {% endif %}{% endfor %}) are never touched by a sync.

{# ---- Live folder --------------------------------------------------------- #}

Live BPRT

{% if live.exists %}{% if live.missing_required %}{{ ui.chip('Required files missing', 'fail') }}{% else %}{{ ui.chip('Required files present', 'ok') }}{% endif %}{% endif %}
{% if not live.exists %}
{{ live.path }} does not exist.
{% else %}
Files
{{ live.count }}
Size
{{ size(live.bytes) }}
{% if live.newest %}
Newest file
{{ live.newest | timestamp_fmt }}
{% endif %}
Folder {{ live.path }}
{% if live.is_link %}
{{ live.path }} is a symlink; activation needs it to be a real folder and refuses to run.
{% endif %} {% for l in links if l.state in ('copy', 'elsewhere', 'none') %}
{{ l.image }} {% if l.state == 'copy' %}has its own copy of BPRT ({{ l.path }} is a real folder; an image import does this), so a sync here does not reach its PCs. {% elif l.state == 'elsewhere' %}links BPRT to {{ l.target or '?' }}, not to the shared folder, so a sync here does not reach its PCs. {% else %}has no Deploy/Applications/BPRT.{% endif %}
{% endfor %} {% if live.bom_needed %}
{{ live.bom_needed | length }} script{{ 's' if live.bom_needed | length != 1 }} hold{{ 's' if live.bom_needed | length == 1 }} non-ASCII text without a UTF-8 byte order mark, which Windows PowerShell 5.1 misreads: {% for r in live.bom_needed %}{{ r }}{% if not loop.last %}, {% endif %}{% endfor %}. The next activation fixes the copies it brings in.
{% endif %}
MCL packages
{% set clashes = packages | selectattr('conflict') | list %} {% if clashes %}
{% for p in clashes %}
{{ p.filename }} also matches {{ p.conflict_with | join(', ') }}: one prefix finding two packages breaks enrollment for that tenant and purpose.
{% endfor %}
{% endif %} {% for p in packages %}
{{ p.prefix or '-' }}{% if p.conflict %} {{ ui.chip('Duplicate', 'err') }}{% endif %} {{ p.filename }} {{ ui.chip(p.version or '?', 'w11') }} {{ size(p.size) }}
{% else %}
No packages.
{% endfor %}
A single package can also be replaced on Enrollment packages.
Site files (kept on every sync)
{% for o in live.overlay %}
{{ o.name }} {% if o.ok %}{{ ui.chip('Present', 'ok') }} {% elif o.present %}{{ ui.chip('Vendor copy, not our shim', 'err') }} {% else %}{{ ui.chip('Missing', 'fail' if o.name == 'Register-BulkEnrollTask-Standard.ps1' or o.name in renamed else 'off') }}{% endif %} {% if o.vendor_copy is defined %}{% if o.vendor_copy %}{{ ui.chip('vendor dialog kept', 'neutral') }}{% else %}{{ ui.chip('no vendor dialog: ' ~ renamed[o.name] ~ ' missing', 'warn') }}{% endif %}{% endif %}
{% endfor %}
Select-PCConfig.ps1 is our shim (repo playbook/shopfloor-setup/BPRT); the vendor's dialog is kept as Select-PCConfig-vendor.ps1. The standard and engineer unattends run Register-BulkEnrollTask-Standard.ps1 (specialize Order 39).
{% endif %}
{# ---- Incoming batches ---------------------------------------------------- #}

Incoming batches

Validate a batch, read its changes, then activate it from its page.
{{ batches | length }}
{% for b in batches %}
{{ b.name }} {% set frm = ((b.sync.computer or '-') ~ (', ' ~ (b.sync.created | local_dt) if b.sync.created else '')) if b.sync else '-' %} {{ frm }} {{ b.files }} file{{ 's' if b.files != 1 }}, {{ size(b.bytes) }} {% if b.uploading %}{{ ui.chip('Uploading', 'run') }} {% elif b.sync_error %}{{ ui.chip('Bad sync.json', 'err') }} {% elif not b.sync %}{{ ui.chip('No sync.json', 'err') }} {% else %}{{ val_chip(b.validation) }}{% endif %}
{% if b.uploading %}
Sync-BPRT.ps1 renames the folder when the upload is complete.
{% endif %} {% if b.sync_error %}
{{ b.sync_error }}
{% elif not b.uploading and not b.sync %}
sync.json is uploaded last; this batch is incomplete.
{% endif %} {% set v = b.validation %} {% if v %} {% if v.errors %}
    {% for e in v.errors %}
  • {{ e }}
  • {% endfor %}
{% endif %} {% if v.stale %}
{{ v.stale }}
{% endif %} {% if v.warnings %}
    {% for w in v.warnings %}
  • {{ w }}
  • {% endfor %}
{% endif %} {% if v.counts %}
{{ v.counts.new }} new, {{ v.counts.changed }} changed, {{ v.counts.removed }} removed, {{ v.counts.unchanged }} unchanged.
{% endif %} {% for p in v.ppkg or [] %}
{{ p.prefix }}: {{ p.old | map(attribute='version') | join(', ') or 'none' }} to {{ p.new | map(attribute='version') | join(', ') or 'none' }}
{% endfor %}
Validated {{ v.validated | local_dt }}.
{% elif b.sync %}
Not validated yet. Validate hashes the upload and works out what activating would change; nothing on the share changes.
{% endif %} {% if b.sync %}
From {{ b.sync.computer or 'unknown PC' }}{% if b.sync.created %}, {{ b.sync.created | local_dt }}{% endif %}. {{ b.sync.listed }} vendor files listed{% if b.sync.user %}, by {{ b.sync.user }}{% endif %}{% if b.sync.source %}, from {{ b.sync.source }}{% endif %}.
{% endif %}
{% if not b.uploading and b.sync %} {% if not b.validation %}
{% else %} Review {% endif %} {% endif %} {% call ui.more('batch ' ~ b.name) %} {% if not b.uploading and b.sync %}Open batch page
{% endif %}
{% endcall %}
{% else %}
Nothing waiting. Run Sync-BPRT.ps1 on the work PC (below), or upload a zip.
{% endfor %}
{# ---- How to sync --------------------------------------------------------- #}

Sync from the work PC

Run Sync-BPRT.ps1 on the GE work PC; it uploads only what changed.
  1. Let MediaCreator Lite finish updating its cache on the GE work PC.
  2. Get Sync-BPRT.ps1 {% if script_available %}(download it from here) {% else %}from the pxe repo, webapp/static/Sync-BPRT.ps1{% endif %}, and run it in Windows PowerShell. Look first with -DryRun (nothing is uploaded):
    powershell -ExecutionPolicy Bypass -File .\Sync-BPRT.ps1 -DryRun
    powershell -ExecutionPolicy Bypass -File .\Sync-BPRT.ps1
    It reads this server's manifest, hashes the local folder (C:\ProgramData\GEAerospace\MediaCenter\Cache\Deploy\Applications\BPRT unless -Source says otherwise), parse-checks the vendor scripts, and uploads the new and changed files with one sftp session: type the pxe password once.
  3. Come back here, open the new batch, Validate, read the changes, Activate.

No SSH from that PC? Run it with -ZipOut C:\Temp\bprt.zip and upload the zip below. A zip of the whole vendor folder works too, but is taken as the complete new folder: anything not in it is removed from BPRT (and kept in the archive). Uploads land in {{ incoming }}.

{# ---- Zip upload ---------------------------------------------------------- #}

Upload a zip instead

For a work PC without SSH: the output of Sync-BPRT.ps1 -ZipOut, or a zip of the whole vendor folder.
{% for z in zips %}
{{ z.name }} {{ size(z.size) }}
{% call ui.more(z.name) %}
{% endcall %}
{% endfor %} {% if zips %}
Uploaded zips, not unpacked yet.
{% endif %}
{# ---- Archives (folded) ---------------------------------------------------- #}

Previous BPRT folders

Each activation and rollback keeps the folder it replaced in BPRT-archive.
{{ ui.chip(archives | length ~ ' archive' ~ ('s' if archives | length != 1), 'neutral') }}
{% for a in archives %}
{{ a.name }} {{ a.packages | length }} MCL package{{ 's' if a.packages | length != 1 }}, {{ a.count }} files, own size {{ size(a.own_bytes) }} {% if a.missing_required %}{{ ui.chip('Required files missing', 'err') }}{% elif not a.packages %}{{ ui.chip('No packages', 'warn') }}{% else %}{{ a.modified | timestamp_fmt }}{% endif %}
{% if a.missing_required %}
Missing {{ a.missing_required | join(', ') }}
{% endif %}
Saved {{ a.modified | timestamp_fmt }}. {{ a.count }} files, {{ size(a.bytes) }}; {{ size(a.own_bytes) }} not shared with the live folder.
{% for p in a.packages %}
{{ fname(p) }}
{% else %}
No MCL packages.
{% endfor %}
{% call ui.more('archive ' ~ a.name) %}
{% endcall %}
{% else %}
None yet. The first activation archives the current folder here.
{% endfor %} {% if archives %}
Unchanged files are hard links shared with the live folder, so an archive only costs the files that differ ("own size").
{% endif %}
{# ---- History (folded) ----------------------------------------------------- #} {% if history %}

History

Activations and rollbacks, newest first.
{{ ui.chip(history | length, 'neutral') }}
{% for h in history %}
{{ h.at | local_dt }} {% if h.kind == 'rollback' %} Rolled back to {{ h.restored }} {% else %} Activated {{ h.batch }}: {{ h.counts.new or 0 }} new, {{ h.counts.changed or 0 }} changed, {{ h.counts.removed or 0 }} removed {% endif %}
{% if h.kind != 'rollback' %}
Activated {{ h.batch }}{% if h.sync and h.sync.computer %} from {{ h.sync.computer }}{% endif %}: {{ h.counts.new or 0 }} new, {{ h.counts.changed or 0 }} changed, {{ h.counts.removed or 0 }} removed.
{% for p in h.ppkg %}
{{ p.prefix }}: {{ p.old | map(attribute='version') | join(', ') or 'none' }} to {{ p.new | map(attribute='version') | join(', ') or 'none' }}
{% endfor %} {% endif %}
Previous folder kept as {{ h.archive }}.
{% endfor %}
{% endif %}
{% endif %} {% endblock %} {% block extra_scripts %} {% endblock %}