Skip to content

Commit 689fbc4

Browse files
committed
feat(image test): run LISA test suites locally via 'image test'
Previously LISA-type suites were rejected by 'azldev image test' and only served as metadata for external orchestration. This adds a local runner that generates a LISA runbook from a suite's configured test cases, boots the image in a QEMU VM, and runs the tests. - Extend LisaConfig with a framework git source, test-cases, pip pre-install/extras, and extra-args; validate that the framework ref is a full commit SHA and that test-cases is non-empty. - Generate a qemu-platform runbook that inlines the image path and an ephemeral admin key, with keep_environment=no so VMs are torn down. - Auto-generate and clean up an ephemeral admin SSH key pair per run. - Require the image to be qcow2 (reject other formats) before booting. - Regenerate CLI docs, JSON schema, and scenario snapshots.
1 parent 4042732 commit 689fbc4

13 files changed

Lines changed: 1182 additions & 32 deletions

docs/user/reference/cli/azldev_image_test.md

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/user/reference/config/test-suites.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Test suite names must be simple identifiers (no path separators, traversal segme
1111
| Description | `description` | string | No | Human-readable description of the test suite |
1212
| Type | `type` | string | Yes | Test framework to use: `"pytest"` or `"lisa"`. |
1313
| Pytest | `pytest` | table | When `type = "pytest"` | Pytest-specific configuration (see below) |
14+
| Lisa | `lisa` | table | Optional (required to run locally) | LISA-specific configuration (see below). May be omitted for metadata-only suites; required to run the suite locally via `azldev image test`. |
1415

1516
Test suites are referenced by images through the [`[images.<name>.tests]`](images.md#image-tests) subtable. Each image can reference one or more test suites by name.
1617

@@ -48,7 +49,34 @@ The following placeholders may appear in `extra-args` and are substituted at run
4849
| `{capabilities}` | Comma-separated list of capability names enabled on the image |
4950

5051

51-
## Examples
52+
## LISA Suite Config
53+
54+
The `[test-suites.<name>.lisa]` subtable is optional. A `type = "lisa"` suite without it is a metadata-only suite (usable by external orchestration but not runnable locally). To run the suite locally via `azldev image test`, the `[test-suites.<name>.lisa]` subtable must be provided.
55+
56+
When run locally, `azldev` executes the suite as follows: it clones the LISA framework at a pinned commit, creates (or reuses) a Python virtual environment and installs the framework into it, generates a runbook from the configured `test-cases`, and boots the image in a QEMU VM to run those cases. VMs are torn down after the run.
57+
58+
The image under test must already be in **qcow2** format; other formats are rejected. Each run generates and later removes an ephemeral admin SSH key pair for VM access.
59+
60+
| Field | TOML Key | Type | Required | Description |
61+
|-------|----------|------|----------|-------------|
62+
| Framework | `framework` | table | Yes | Git source for the LISA framework (see below). |
63+
| Test cases | `test-cases` | array of strings | Yes | LISA test case names to run. They are joined with `\|` into the criteria of the generated runbook. Must be non-empty. |
64+
| Pip pre-install | `pip-pre-install` | array of strings | No | Pip packages installed into the venv *before* the framework, to override framework version pins that conflict with the local environment (e.g., a system-matching `libvirt-python`). |
65+
| Pip extras | `pip-extras` | array of strings | No | Pip extras installed from the framework package, appended as `pip install -e ".[extra1,extra2]"`. |
66+
| Extra args | `extra-args` | array of strings | No | Additional arguments passed to the `lisa` CLI verbatim, after placeholder substitution. See [Placeholders](#placeholders). The generated runbook is always passed via `-r`. |
67+
68+
### Framework git source
69+
70+
The `[test-suites.<name>.lisa.framework]` subtable pins the LISA framework repository.
71+
72+
| Field | TOML Key | Type | Required | Description |
73+
|-------|----------|------|----------|-------------|
74+
| Git URL | `git-url` | string | Yes | URL of the LISA framework git repository. |
75+
| Ref | `ref` | string | Yes | Full 40-character hex commit SHA to check out. Branch names and tags are rejected. |
76+
77+
The framework checkout is keyed by ref, so updating `ref` clones the new revision. If a pinned ref cannot be checked out in an existing checkout (e.g., it was updated to a newer commit), `azldev` re-clones the repository automatically.
78+
79+
5280

5381
### Basic pytest suite
5482

@@ -100,14 +128,24 @@ type = "pytest"
100128
test-paths = ["/opt/preinstalled-tests/test_*.py"]
101129
```
102130

103-
### LISA suite (external)
131+
### LISA suite
104132

105-
LISA test suites are metadata-only — they are not executed by `azldev` but are used by external orchestration systems.
133+
LISA suites are executed locally by `azldev`, which generates a runbook from `test-cases` and boots the image in a QEMU VM. The image must be in qcow2 format.
106134

107135
```toml
108136
[test-suites.vm-integration]
109137
description = "VM integration tests using LISA"
110138
type = "lisa"
139+
140+
[test-suites.vm-integration.lisa]
141+
test-cases = ["verify_cpu_count", "verify_grub"]
142+
# Optional: override a framework version pin and pass extra CLI args to LISA.
143+
pip-pre-install = ["libvirt-python==9.0.0"]
144+
extra-args = ["-v"]
145+
146+
[test-suites.vm-integration.lisa.framework]
147+
git-url = "https://github.com/microsoft/lisa.git"
148+
ref = "abcdef0123456789abcdef0123456789abcdef01" # full 40-char commit SHA
111149
```
112150

113151
### Referencing test suites from an image
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package image
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
"gopkg.in/yaml.v3"
11+
)
12+
13+
// The following structs model the subset of a LISA runbook that azldev generates. The
14+
// YAML field names are dictated by LISA's runbook schema (snake_case), which we do not
15+
// control.
16+
//
17+
18+
type lisaRunbook struct {
19+
Name string `yaml:"name"`
20+
Include []lisaInclude `yaml:"include"`
21+
Testcase []lisaTestcase `yaml:"testcase"`
22+
Notifier []lisaNotifier `yaml:"notifier"`
23+
Platform []lisaPlatform `yaml:"platform"`
24+
}
25+
26+
type lisaInclude struct {
27+
Path string `yaml:"path"`
28+
}
29+
30+
type lisaTestcase struct {
31+
Criteria lisaCriteria `yaml:"criteria"`
32+
}
33+
34+
type lisaCriteria struct {
35+
Name string `yaml:"name"`
36+
}
37+
38+
type lisaNotifier struct {
39+
Type string `yaml:"type"`
40+
}
41+
42+
//nolint:tagliatelle // External schema (LISA runbook) dictates the field names.
43+
type lisaPlatform struct {
44+
Type string `yaml:"type"`
45+
AdminPrivateKeyFile string `yaml:"admin_private_key_file"`
46+
KeepEnvironment string `yaml:"keep_environment"`
47+
Qemu lisaPlatformQemu `yaml:"qemu"`
48+
Requirement lisaPlatformReqRoot `yaml:"requirement"`
49+
}
50+
51+
//nolint:tagliatelle // External schema (LISA runbook) dictates the field names.
52+
type lisaPlatformQemu struct {
53+
NetworkBootTimeout int `yaml:"network_boot_timeout"`
54+
}
55+
56+
type lisaPlatformReqRoot struct {
57+
Qemu lisaPlatformReqQemu `yaml:"qemu"`
58+
}
59+
60+
type lisaPlatformReqQemu struct {
61+
Qcow2 string `yaml:"qcow2"`
62+
}
63+
64+
const (
65+
// runbookTierIncludePath is the path (relative to the generated runbook) to the shared
66+
// tier definitions in the LISA tree. LISA resolves includes relative to the runbook file's
67+
// directory, so this resolves correctly only when the generated runbook is written at the
68+
// framework repo root (see writeGeneratedRunbook).
69+
runbookTierIncludePath = "lisa/microsoft/runbook/tiers/tier.yml"
70+
// runbookBootTimeoutSeconds is the QEMU network boot timeout used in the generated runbook.
71+
runbookBootTimeoutSeconds = 300
72+
)
73+
74+
// generateRunbookYAML builds a LISA runbook that runs the given test cases on a QEMU VM
75+
// booted from imagePath, authenticating with adminKeyPath. All values (image path, admin
76+
// key path) are inlined as concrete values. keep_environment is "no" so LISA tears down the
77+
// VM environment after the run.
78+
func generateRunbookYAML(suiteName string, testCases []string, imagePath, adminKeyPath string) ([]byte, error) {
79+
runbook := lisaRunbook{
80+
Name: suiteName,
81+
Include: []lisaInclude{{Path: runbookTierIncludePath}},
82+
Testcase: []lisaTestcase{
83+
{Criteria: lisaCriteria{Name: strings.Join(testCases, "|")}},
84+
},
85+
Notifier: []lisaNotifier{{Type: "html"}},
86+
Platform: []lisaPlatform{
87+
{
88+
Type: "qemu",
89+
AdminPrivateKeyFile: adminKeyPath,
90+
KeepEnvironment: "no",
91+
Qemu: lisaPlatformQemu{NetworkBootTimeout: runbookBootTimeoutSeconds},
92+
Requirement: lisaPlatformReqRoot{
93+
Qemu: lisaPlatformReqQemu{
94+
Qcow2: imagePath,
95+
},
96+
},
97+
},
98+
},
99+
}
100+
101+
data, err := yaml.Marshal(&runbook)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to marshal generated LISA runbook:\n%w", err)
104+
}
105+
106+
return data, nil
107+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package image
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
"gopkg.in/yaml.v3"
12+
)
13+
14+
func TestGenerateRunbookYAML(t *testing.T) {
15+
data, err := generateRunbookYAML(
16+
"lisa-qemu",
17+
[]string{"verify_cpu_count", "verify_grub"},
18+
"/abs/image.qcow2",
19+
"/home/user/.ssh/id_rsa",
20+
)
21+
require.NoError(t, err)
22+
23+
var runbook lisaRunbook
24+
require.NoError(t, yaml.Unmarshal(data, &runbook))
25+
26+
assert.Equal(t, "lisa-qemu", runbook.Name)
27+
require.Len(t, runbook.Include, 1)
28+
assert.Equal(t, "lisa/microsoft/runbook/tiers/tier.yml", runbook.Include[0].Path)
29+
30+
require.Len(t, runbook.Testcase, 1)
31+
assert.Equal(t, "verify_cpu_count|verify_grub", runbook.Testcase[0].Criteria.Name)
32+
33+
require.Len(t, runbook.Notifier, 1)
34+
assert.Equal(t, "html", runbook.Notifier[0].Type)
35+
36+
require.Len(t, runbook.Platform, 1)
37+
platform := runbook.Platform[0]
38+
assert.Equal(t, "qemu", platform.Type)
39+
assert.Equal(t, "/home/user/.ssh/id_rsa", platform.AdminPrivateKeyFile)
40+
assert.Equal(t, "no", platform.KeepEnvironment)
41+
assert.Equal(t, runbookBootTimeoutSeconds, platform.Qemu.NetworkBootTimeout)
42+
assert.Equal(t, "/abs/image.qcow2", platform.Requirement.Qemu.Qcow2)
43+
}
44+
45+
func TestGenerateRunbookYAML_SingleTestCase(t *testing.T) {
46+
data, err := generateRunbookYAML("solo", []string{"verify_grub"}, "img", "key")
47+
require.NoError(t, err)
48+
49+
var runbook lisaRunbook
50+
require.NoError(t, yaml.Unmarshal(data, &runbook))
51+
52+
require.Len(t, runbook.Testcase, 1)
53+
assert.Equal(t, "verify_grub", runbook.Testcase[0].Criteria.Name)
54+
}

0 commit comments

Comments
 (0)