Hi folks,

The 2026.07 tag has been pushed to master on gitlab.com/lava/lava.
.deb packages have been built in GitLab CI and are published at

  https://apt.lavasoftware.org/release

Docker images for amd64 and arm64 have been built in GitLab CI and
are available from

  https://registry.gitlab.com/

and

  https://hub.docker.com/u/lavasoftware


Changes in this release
==================

# Database migation

3 database migrations are applied by this release:

* lava_scheduler_app.0065_alter_devicetype_health_frequency_default
* lava_scheduler_app.0066_blacklist_to_jsonfield
* lava_scheduler_app.0067_testjob_requested_device_and_worker

These migrations should not create any downtime, as they can run while
the instance is running. However, if you are running a large LAVA
deployment, be aware that 0066 rewrites the notification table in place,
so it needs temporary disk space of about that table's size and the table
is unavailable while it runs. 0067 may briefly stall the scheduler. The
new columns themselves are added instantly, but the indexes that come
with them block writes to the job table until the migration commits.
Generally this is pretty quick, a few seconds even on millions of jobs.

# Device-types

## New device-types

* stm32wba65i-dk1
* imx952-frdm

## QEMU: overlay drive interface

The `qemu` and `kvm` templates were attaching the `lava-guest.qcow2` test
overlay to an IDE drive (`if=ide`), which only works on x86 machines. On other
architectures QEMU rejected the drive, so only boot-only tests could run.

The interface is now selected from the architecture:

* `ide` is kept on x86 for backward compatibility
* `virtio` is used everywhere else (aarch64, riscv, ppc, ...).

# Breaking changes

## qdl: `overlay_path` removed

`overlay_path` became a no-op in the `qdl` deploy action after the
`CompressOverlay` rework, and has been removed from the deploy schema. Jobs
still setting it will now fail validation and must simply drop the key:

```yaml
- deploy:
    to: qdl
    rootfs_image: "disk-sdcard.img2"
    qcomflash:
      url: 'https://example.com/qcomflash.tar.gz'
      apply-overlay: true
```

# Server

## Target a specific device or worker

A job can now request one exact device or restrict scheduling to the devices
attached to one worker. The device must belong to the requested `device_type`,
and the worker must exist on the instance:

```yaml
device_type: qemu
device: qemu-01
worker: worker-01
```

When `device` is used the scheduler will only run the job on that device, and
will not fall back to another device of the same type. Likewise `worker`
restricts the job to devices of the requested type attached to that worker. If
both are given, the selected device must also be attached to the selected
worker.

For multinode jobs, `device` and `worker` are specified inside each
`lava-multinode` role. A role-level `device` constraint requires that role to
use `count: 1`.

## Require login on specific paths

Allow requiring login for specific URL paths.

`REQUIRE_LOGIN` gates a whole instance, which is too broad for otherwise public
instances. The query UI, for example, is open to anonymous users and can
trigger expensive database queries, making large instances easy to overload
with indiscriminate crawlers.

The new `REQUIRE_LOGIN_PATHS` setting takes a list of URL path prefixes,
relative to the mount point. Each prefix covers itself and everything below it:

```yaml
REQUIRE_LOGIN_PATHS: ["results/query", "results/chart"]
```

Requests under a configured prefix get the same enforcement as `REQUIRE_LOGIN`,
including the standard exemptions and API token passthrough; everything else
stays open. An invalid configuration raises `ImproperlyConfigured` at startup.

## Custom default health-check frequency

Device-types were always created with a routine health-check frequency of 24
hours. Admins can now change that default for an instance:

```yaml
HEALTH_FREQUENCY_HOURS: 168
```

The value is validated when the application starts: non-integer or
non-positive values are rejected with `ImproperlyConfigured`.

## Secrets in multinode jobs

A top level `secrets` section was silently dropped when submitting a multinode
job. It is now copied to every child job of the multinode job, like the other
shared sections.

## Notification blacklist stored as JSON

`Notification.blacklist` moved from a PostgreSQL-only `ArrayField` to a
`JSONField` so that it works on every database backend. A data migration
converts the existing `varchar[]` column to `jsonb` in place, preserving the
stored values. The conversion only runs on PostgreSQL instances where the
column is still an array, and is a no-op on fresh installs and on SQLite.

## Bug fixes

* **Login redirects**: `LOGIN_URL` is now computed with the mount point prefix,
  and `LOGIN_PATH` was replaced by a `login_path()` classmethod reading the
  current setting at call time. With `MOUNT_POINT=lava/`, the login page
  exemption check was failing and redirects were landing outside the instance.
* **Compressed logs**: gunicorn used the `sendfile` kernel call when the file
  object of a `FileResponse` exposes `fileno()`. For LZMA files this returned
  the underlying file descriptor, so responses contained truncated, still
  compressed data. The LZMA `fileno()` is now overridden to raise
  `io.UnsupportedOperation`.
* **Empty settings file**: `yaml.safe_load()` returns `None` for an empty or
  comment-only file, and calling `.items()` on it crashed LAVA at startup. An
  empty `settings.conf`, `settings.yaml` or `settings.d/*.yaml` now simply
  contributes no setting.
* **ConfigFile**: the parser has been reworked on top of `shlex` instead of
  regexes, fixing a file handle leak, restoring key validation and reporting
  line numbers starting at 1.

# Dispatcher

## user_commands: lists of commands

`do` and `undo` in `user_commands` accepted a single command only, unlike
`power_on_command` and `power_off_command`. Both keys now also accept a list of
commands, run in the order they are listed:

```jinja
{% set user_commands = {'set_boot_to_qspi': {'do': ['webrelay --host 192.168.0.15 --port 1234 --relay 1 on',
                                                    'webrelay --host 192.168.0.15 --port 1234 --relay 2 on'],
                                             'undo': ['webrelay --host 192.168.0.15 --port 1234 --relay 2 off',
                                                      'webrelay --host 192.168.0.15 --port 1234 --relay 1 off']}} %}
```

Empty commands (and lists containing an empty command) are rejected at
validation time instead of silently running nothing. The device schema has been
aligned with the implementation.

## Devices always powered off

The power cleanup now runs even when `CLEANUP_TIMEOUT` has already been used up
by the previous cleanup steps, so boards are no longer left powered on after a
job.

## Faster ext4 overlay

Two improvements to the `debugfs` based overlay introduced in 2026.05:

* Only the parent directory is resolved for non-directory tar members, the
  basename being appended afterwards. Directory components are cached while
  each file leaf is unique, so every file previously cost two uncacheable
  `debugfs` stat calls. Building a large overlay took ~40 seconds for ~11
  seconds of actual writes.
* Partition copies use a large `dd` block size.

Behaviour change: a file whose destination is an existing symlink in the image
now replaces it instead of writing through it, which matches `tar` and
`libguestfs` `tar_in` semantics.

## Bug fixes

* **vexpress**: most exceptions raised while deploying are now reported as
  `InfrastructureError` rather than job failures.
* **Overlay environment**: a debug message is logged when
  `LAVA_DEVICE_HOSTNAME` or `LAVA_DEVICE_TYPE` are not exported because the
  device configuration lacks the `hostname` or `devicetype` field.
* **Device-type templates**: variables unused by any base template have been
  removed.

# Packaging

## sysusers.d

The Debian packages now ship `sysusers.d` configuration files instead of
creating users from hand-written maintainer scripts. This declarative
configuration also allows `/usr`-only image based systems to be created, and
`/etc` to be recreated on boot after a factory reset. It does not add a hard
dependency on systemd: debhelper generates the dependencies so that non-systemd
and non-Linux Debian builds keep working.

## SQLite for local development

`DATABASE_URL` is now supported in the development settings and the model no
longer uses PostgreSQL-specific fields, so a development instance can run on
SQLite. The uv based development workflow is documented.

# CI

## pre-commit

The CI now runs a `pre-commit` job in the analyze stage, and most of the checks
have been moved to hooks: `ruff` (check and format), `codespell`, `pyupgrade`,
`django-upgrade`, `bandit`, `pylint`, `mypy` and the standard file checks
(large files, merge conflicts, private keys, TOML and YAML syntax, ...). The
`pyrefly` and `ty` hooks are enabled experimentally.

The now redundant `codespell`, `pylint`, `ruff` and SAST/`semgrep` CI jobs and
their runner scripts have been removed, and `black` and `isort` have been
dropped from the development dependencies in favour of `ruff`.

## Supply chain

An SBOM is generated and scanned for vulnerabilities in CI, and the `uv.lock`
file has been refreshed (closes CVE-2026-32274).

## Type checking

`lava_dispatcher/action.py` is now type checked. This required using
`isinstance` checks in `Action.parsed_command`, waiting on the
`subprocess.Popen` object rather than on `PexpectPopenSpawn`, and using
`subprocess.run` in `Action.run_command`. The asymmetric `errors`
getter/setter of `Action` and `Protocol` has been replaced by an `errors_add`
method, since type checkers require a getter and a setter to use the same type.


Rgds

--
Rémi Duraffort
Principal Tech Lead
LAVA Tech Lead
Automation Software Team
Linaro