{% extends "base.html" %} {% block title %}BPRT Sync - PXE Server 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_badge(v, lg=false) -%} {% set cls = 'badge badge-lg' if lg else 'badge' %} {% if not v %}Not validated {% elif v.stale %}Changed since validation {% elif v.state == 'valid' %}Valid {% else %}{{ v.errors | length }} error{{ 's' if v.errors | length != 1 }}{% 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 %}

{% if b.sync %}From {{ b.sync.computer or 'unknown PC' }}{% if b.sync.user %} ({{ b.sync.user }}){% endif %}{% if b.sync.created %}, {{ b.sync.created | local_dt }}{% endif %}. {% if b.sync.source %}Source {{ b.sync.source }}.{% endif %} {{ b.sync.listed }} vendor files listed, {{ b.files }} uploaded ({{ size(b.bytes) }}).{% endif %} Folder {{ incoming }}/{{ b.name }}.

{% 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 %}
Errors {{ res.errors | length }}

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.

{% endif %} {% if res.stale %}
{{ res.stale }}: validate again before activating.
{% endif %} {% if res.warnings %}
Warnings {{ res.warnings | length }}
{% endif %} {% if res.counts %}
New files
{{ res.counts.new }}
Changed
{{ res.counts.changed }}
Removed
{{ res.counts.removed }}
Unchanged
{{ res.counts.unchanged }}
MCL packages
{% if res.ppkg %}
{% for p in res.ppkg %} {% endfor %}
Tenant / purposeLive nowAfter activation
{{ p.prefix }} {% for o in p.old %}
{{ o.version }} {{ fname(o.name) }}
{% else %}none{% endfor %}
to {% for n in p.new %}
{{ n.version }} {{ fname(n.name) }}
{% else %}none (removed){% endfor %}
{% else %}

No package changes.

{% endif %}
{% if res.zips or res.bom_fix %}
Large files and fixes
{% if res.zips %}
{% for z in res.zips %}{% endfor %}
ZipLive sizeNew size
{{ z.path }}{{ size(z.old_size) if z.old_size is not none else 'new' }}{{ size(z.new_size) }}
{% endif %} {% 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):

{% 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 }} {{ rows | length }}
    {% 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) }}

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.

Validated {{ res.validated | local_dt }}.
{% endif %}
{% else %} {# ======================= overview ======================================= #}

_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.

{{ imaging_alert(imaging) }} {{ job_panel(job) }} {# ---- Live folder --------------------------------------------------------- #}
Live BPRT
{% if not live.exists %}
{{ live.path }} does not exist.
{% else %}

{{ live.path }}: {{ live.count }} files, {{ size(live.bytes) }}{% if live.newest %}, newest file {{ live.newest | timestamp_fmt }}{% endif %}.

{% 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.missing_required %}
Missing files the bulk-enroll flow calls by path: {% for r in live.missing_required %}{{ r }}{% if not loop.last %}, {% endif %}{% endfor %}.
{% endif %} {% 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
{% if packages %}
{% for p in packages %} {% endfor %}
Tenant / purposeFileVersionSize
{{ p.prefix or '-' }}{% if p.conflict %} Duplicate{% endif %} {{ fname(p.filename) }} {{ p.version or '?' }} {{ size(p.size) }}
{% else %}

No packages.

{% endif %}
A single package can also be replaced on Enrollment Packages.
Site files (kept on every sync)
{% for o in live.overlay %} {% endfor %}
{{ o.name }} {% if o.ok %}Present {% elif o.present %}Vendor copy {% else %}Missing{% endif %} {% if o.vendor_copy is defined %}{% if o.vendor_copy %}vendor dialog kept{% else %}no vendor dialog{% endif %}{% endif %}
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 {{ batches | length }}
{% if batches %}
{% for b in batches %} {% endfor %}
BatchFromUploadedStatus
{% if b.uploading %}{{ b.name }}{% else %}{{ b.name }}{% endif %} {% if b.sync %}{{ b.sync.computer or '-' }}{% if b.sync.created %}
{{ b.sync.created | local_dt }}
{% endif %}{% else %}-{% endif %}
{{ b.files }} file{{ 's' if b.files != 1 }}, {{ size(b.bytes) }} {% if b.uploading %}Uploading {% elif b.sync_error %}Bad sync.json {% elif not b.sync %}No sync.json {% else %}{{ val_badge(b.validation) }}{% endif %} {% if not b.uploading and b.sync %} {{ 'Review' if b.validation else 'Open' }} {% if not b.validation %}
{% endif %} {% endif %}
{% else %}

Nothing waiting. Run Sync-BPRT.ps1 on the work PC (below), or upload a zip.

{% endif %}
{# ---- How to sync --------------------------------------------------------- #}
Sync from the work PC
  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
{% if zips %}
{% for z in zips %} {% endfor %}
Uploaded zip, not unpackedSize
{{ z.name }}{{ size(z.size) }}
{% endif %}
{# ---- Archives ------------------------------------------------------------ #}
Previous BPRT folders {{ archives | length }}
{% if archives %}

Each activation and rollback keeps the folder it replaced in BPRT-archive. Unchanged files are hard links shared with the live folder, so an archive only costs the files that differ ("own size").

{% for a in archives %} {% endfor %}
ArchiveMCL packagesFilesOwn size
{{ a.name }}
{{ a.modified | timestamp_fmt }}
{% for p in a.packages %}
{{ fname(p) }}
{% else %}none{% endfor %} {% if a.missing_required %}
missing {{ a.missing_required | join(', ') }}
{% endif %}
{{ a.count }}, {{ size(a.bytes) }} {{ size(a.own_bytes) }}
{% else %}

None yet. The first activation archives the current folder here.

{% endif %}
{% if history %}
History
{% for h in history %} {% endfor %}
WhenWhatPrevious folder
{{ h.at | local_dt }} {% if h.kind == 'rollback' %}Rolled back to {{ h.restored }} {% else %}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 %}
{{ h.archive }}
{% endif %} {% endif %}
{% endblock %} {% block extra_scripts %} {% endblock %}