Skip to content

Notifications: name the image, and tell a capture from a deploy - #21

Merged
mastacontrola merged 1 commit into
mainfrom
imaging-notification-detail
Aug 19, 2026
Merged

Notifications: name the image, and tell a capture from a deploy#21
mastacontrola merged 1 commit into
mainfrom
imaging-notification-detail

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

The other half of FOGProject/fogproject#1202; the core half is FOGProject/fogproject#1205. Neither depends on the other landing first.

What changed upstream

Core now fires HOST_IMAGEUP_COMPLETE for a capture, HOST_IMAGE_COMPLETE for a deploy and HOST_IMAGE_FAIL when imaging did not finish — and it sends the image and the failure reason alongside the host name. Before that, two of those three names had no caller anywhere in FOG, so listeners registered for them had never run.

What these listeners did with it

Nothing, because they could not. Every one of the six registered HOST_IMAGEUP_COMPLETE or HOST_IMAGE_FAIL and then ignored which event had arrived:

public function onEvent($event, $data)
{
    self::$message = 'This host has finished imaging.';
    self::$shortdesc = 'Imaging Complete';
    parent::onEvent($event, $data);
}

So a finished capture said "This host has finished imaging", identically to a deploy, and a failure said "This host has failed to image" with no hint as to why.

before after
deploy Host lab01 completed imaging. Host lab01 finished deploying image Win11-Lab.
capture Host lab01 completed imaging. Host lab01 finished capturing image Win11-Lab.
failure Host lab01 imaging failed. Host lab01 failed imaging Win11-Lab: Failed to update host

Defensive about every added key

A plugin release is not tied to a FOG release, so all six have to keep working against a server whose core still sends nothing but HostName:

$image = (string) ($data['ImageName'] ?? '');
if ('' === $image) {
    $image = _('an unnamed image');
}

Reading a missing key directly would turn the notification into a PHP warning — in an event that only fires when something has already gone wrong. That is also why fog_min is untouched: these work on any 1.6, they just say more on a server that has taken #1205.

Translation

The image name is substituted outside _(), not inside it — a msgid that mixes literal text with a variable extracts nothing and never translates, silently and permanently. Positional specifiers (%1$s) so a translator can reorder the sentence.

The ntfy and pushbullet listeners compose the finished string themselves rather than handing their base class a bare literal, because the substitution has to happen after translation. The base's _() then finds no entry and passes it through unchanged.

Verification

tests/imaging-notification-detail.test.php — 42 checks across the six files, source-level for the same reason group-tab-permissions.test.php is: these classes extend the plugin's own Event base, which extends FOG's, so none of them loads without a booted FOG, a session and a database.

It pins the defensive reads, the capture/deploy split, the registrations, and the gettext shape — that last one because this repository has no gettext gate of its own; fogproject's only scans fogproject.

Mutation-verified:

drop the ImageName default    -> reads ImageName without a default
drop the capture branch       -> does not distinguish a capture from a deploy
interpolate inside _()        -> interpolates a variable inside a double-quoted _()
drop the Reason default       -> does not report why imaging failed

Full suite: 7 passed, 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_013mJVe4CpK3rRbi9H5GubXd

The other half of fogproject#1202. Core now fires HOST_IMAGEUP_COMPLETE for a
capture, HOST_IMAGE_COMPLETE for a deploy and HOST_IMAGE_FAIL when imaging
did not finish, and it sends the image and the failure reason along with the
host name. All three of those events reached listeners that could not say
anything useful with them.

Every one of these six listeners registered HOST_IMAGEUP_COMPLETE or
HOST_IMAGE_FAIL and then ignored which event had arrived, because until now
neither name was ever fired. So a finished capture said "This host has
finished imaging", identically to a deploy, and a failure said "This host has
failed to image" with no hint as to why. They now say which image, and for a
failure, what FOG rejected.

Deliberately defensive about every added key. A plugin release is not tied to
a FOG release, so these have to keep working against a server whose core
still sends nothing but HostName -- reading a missing key directly would turn
the notification into a PHP warning, in an event that only fires when
something has already gone wrong. That is also why fog_min is untouched.

The image name is substituted outside _(), not inside it: a msgid that mixes
literal text with a variable extracts nothing and never translates. This
repository has no gettext gate of its own, so tests/imaging-notification-
detail.test.php pins that shape along with the defensive reads and the
capture/deploy split. Mutation-verified.

Co-authored-by: Claude <noreply@anthropic.com>
mastacontrola added a commit to FOGProject/fogproject that referenced this pull request Aug 19, 2026
Closes the half of #1202 that lives in core. The plugin half is [FOGProject/fog-plugins#21](FOGProject/fog-plugins#21); neither depends on the other landing first.

Three defects, all in `TaskQueue::checkout()`'s single unconditional `notify()`.

## 1. `HOST_IMAGEUP_COMPLETE` had no caller anywhere in the tree

All three bundled notification plugins register a listener for it, on the same class as `HOST_IMAGE_COMPLETE`:

```php
self::$EventManager->register('HOST_IMAGE_COMPLETE', $this)
                   ->register('HOST_IMAGEUP_COMPLETE', $this);
```

So the name plainly means "a capture finished". Nothing ever fired it, so a capture announced itself with the *deploy* name and nothing listening could tell "an image finished uploading" from "a machine finished being imaged". `checkout()` already branches on `isDeploy()`/`isCapture()` forty lines above — it knew which it was and threw the answer away.

## 2. `HOST_IMAGE_FAIL` had no caller either

Its listeners have never run on any server. It now fires from `checkout()`'s catch, which is the one place in core that knows imaging ran and FOG then failed to record it — the host update, the task save, the task log or the imaging log.

**This is not the same as "the deploy failed."** FOS's `handleError()` prints to the console and `exit 1`s; it reports nothing to the server, so a genuine imaging failure — bad image, mount failure, partition error — is still invisible to FOG and the task just sits in Progress. Closing that needs a failure-report endpoint plus a FOS change, which is a separate decision and is written up in the issue.

## 3. The notification did not check the task was imaging

`checkout()` is reached from `Post_Wipe.php` as well as `Post_Stage2/3.php`, so **wiping a disk sent "This host has finished imaging."**

## The payload

`HostName` is kept exactly as it was — it is the only key any current listener reads, in core, in the bundled plugins, and in third-party plugins nobody here can inspect. `Host`, `Task`, `Image`, `ImageName`, `TaskType` are added alongside it, plus `Reason` on the failure path. Additive, so **no listener needs editing.**

No references in the payload: `notify()` hands `onEvent()` a copy and discards the result, so `&` would imply a mutability it does not have. That distinction is the one ADR 0017 draws between `notify()` and `processEvent()`.

## Blast radius

One behaviour change worth naming: **a capture now fires `HOST_IMAGEUP_COMPLETE` instead of `HOST_IMAGE_COMPLETE`.** A third-party listener registered only on the deploy name stops hearing captures. That is the documented intent of having two names, and all three bundled plugins already register both, but it is a real change and not a pure fix.

The other two changes can only reduce wrong notifications: a wipe stops claiming to be imaging, and a failure that previously notified nothing now notifies something.

No route change, so no OpenAPI change. No schema change.

## Verification

`tests/imaging-notify-events.test.php`, no database. Mutation-verified — each of these fails the suite:

```
M1 drop the imagingTask guard   -> a non-imaging task still notifies HOST_IMAGE_COMPLETE
M2 always the deploy name       -> a completed capture does not notify HOST_IMAGEUP_COMPLETE
M3 drop the failure branch      -> a failed imaging task does not notify HOST_IMAGE_FAIL
M4 catch no longer notifies     -> checkout() no longer announces a failed imaging task
M5 drop ImageName               -> the payload does not carry ImageName
```

Full suite: `67 passed, 0 failed`.

## Downstream

No change to `Route::$validClasses`, so no FogApi sync needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013mJVe4CpK3rRbi9H5GubXd
@mastacontrola
mastacontrola merged commit c0edd09 into main Aug 19, 2026
2 checks passed
@mastacontrola
mastacontrola deleted the imaging-notification-detail branch August 19, 2026 10:51
mastacontrola added a commit to FOGProject/fogproject that referenced this pull request Aug 19, 2026
TaskError has been sending ImageName and Reason on HOST_IMAGE_FAIL since the
FOS reporting work landed -- Reason being the flattened, MAX_REASON-bounded
opening of whatever FOS actually reported. Both bundled listeners ignored all
of it and pushed the fixed string "This host has failed to image", which tells
an admin nothing the task list did not already show. Confirmed live on 1.6
before this: a report whose stored row read "fog.download: failed to restore
partition 2 / partclone.ntfs: /dev/sda2 is busy / ..." produced a push saying
only "Failed".

So the whole point of storing the trace -- somebody seeing it -- stopped at
the server.

    Host lab01 failed imaging Win11-Lab: fog.download: failed to restore
    partition 2 partclone.ntfs: /dev/sda2 is busy ...

Every added key is read defensively. These events fire only when something has
already gone wrong, and a web tree can be older than what writes the payload,
so a bare $data['Reason'] would turn the notification into a PHP warning at
the worst possible moment. Both keys fall back to a translated placeholder
rather than an empty slot, so an older server says "failed imaging an unnamed
image: no reason was reported" instead of "failed imaging : ".

The substitution happens OUTSIDE _(), with positional specifiers. A msgid
built at runtime matches no catalog entry and never translates, silently and
permanently; %1$s so a translator can reorder the sentence. Slack's old form
had 'Host: %s ' outside the call and translated only the tail, which is not a
sentence anyone could translate -- that is now one whole msgid.

Ported from FOGProject/fog-plugins#21, which did the same for the 1.6 line
where these plugins now live. ntfy has no listener on this branch, so this is
two files rather than three.

tests/imaging-failure-reason.test.php pins the reads, the defaults, the
fallbacks and the gettext shape -- source-level, because these classes extend
the plugin's Event base, which extends FOG's, so neither loads without a
booted FOG, a session and a database.

Six mutations, all killed, each confirmed to have actually applied:

    stop reading Reason              -> never reads Reason
    read Reason with no default      -> read without a default
    interpolate inside _()           -> msgid built at runtime
    drop the missing-reason default  -> renders with an empty slot
    bare %s instead of %1$s          -> translator cannot reorder
    stop naming the image            -> never names the image

Full suite: 15 passed, 0 failed.

Co-authored-by: JJ Fullmer <7743340+darksidemilk@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
mastacontrola added a commit to FOGProject/fogproject that referenced this pull request Aug 19, 2026
v1.6.11 carries FOGProject/fog-plugins#21, which was merged the morning after
v1.6.10 was cut and so has never reached a server.

That change matters more than its own PR made it sound, because the FOS
reporting work landed in between. #1206/#1211/#1217/#1223 give a failed task a
stored, multi-line report of what FOS actually said, and TaskError sends the
flattened opening of it as HOST_IMAGE_FAIL's Reason -- but every bundled
listener on v1.6.10 ignores that key and pushes the fixed string "This host
has failed to image".

Confirmed live before cutting the release. A report whose stored row read

    fog.download: failed to restore partition 2
      partclone.ntfs: /dev/sda2 is busy
      ERROR: win11-split part 2 checksum mismatch
      exit code 1 (fog.download)

pushed, in its entirety: "fos-deploy-test Failed" / "This host has failed to
image". So the whole point of storing the trace stopped at the server.

One line, because the sha256 is fetched from the release alongside the
tarball rather than pinned here; bin/fetch-plugins.sh reads this constant and
verifies what it downloads against it. Verified end to end on this pin:
"Fetching plugins v1.6.11" -> "Plugins at v1.6.11", and all three imagefail
listeners in the fetched tree read Reason.

The 1.5 line keeps its plugins in-tree and has no pin, so its equivalent is a
code change: #1226.

Co-authored-by: JJ Fullmer <7743340+darksidemilk@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant