Merge ~cgrabowski/maas:embed_run_scripts_in_hardware_sync into maas:master

Proposed by Christian Grabowski
Status: Rejected
Rejected by: Christian Grabowski
Proposed branch: ~cgrabowski/maas:embed_run_scripts_in_hardware_sync
Merge into: maas:master
Diff against target: 2163 lines (+1552/-104)
44 files modified
.gitignore (+1/-0)
src/host-info/Makefile (+12/-1)
src/host-info/cmd/hardware-sync/main.go (+160/-58)
src/host-info/go.mod (+1/-0)
src/host-info/go.sum (+2/-0)
src/host-info/pkg/scriptrunner/runner.go (+494/-0)
src/host-info/pkg/scriptrunner/runner_test.go (+164/-0)
src/host-info/scripts/50-maas-01-commissioning.py (+1/-0)
src/host-info/scripts/badblocks.py (+1/-0)
src/host-info/scripts/base-connectivity.sh (+1/-0)
src/host-info/scripts/bmc_config.py (+1/-0)
src/host-info/scripts/capture_lldpd.py (+1/-0)
src/host-info/scripts/dhcp_unconfigured_ifaces.py (+1/-0)
src/host-info/scripts/fio.py (+1/-0)
src/host-info/scripts/gateway-connectivity.sh (+115/-0)
src/host-info/scripts/install_lldpd.py (+1/-0)
src/host-info/scripts/internet-connectivity.sh (+98/-0)
src/host-info/scripts/maas-get-fruid-api-data.sh (+1/-0)
src/host-info/scripts/maas-kernel-cmdline.sh (+1/-0)
src/host-info/scripts/maas-list-modaliases.sh (+1/-0)
src/host-info/scripts/maas-lshw.sh (+1/-0)
src/host-info/scripts/maas-serial-ports.sh (+1/-0)
src/host-info/scripts/maas-support-info.sh (+1/-0)
src/host-info/scripts/maas_api_helper.py (+1/-0)
src/host-info/scripts/maas_run_scripts.py (+1/-0)
src/host-info/scripts/memtester.sh (+1/-0)
src/host-info/scripts/ntp.sh (+1/-0)
src/host-info/scripts/rack-controller-connectivity.sh (+99/-0)
src/host-info/scripts/seven_z.py (+1/-0)
src/host-info/scripts/smartctl.py (+1/-0)
src/host-info/scripts/stress-ng-cpu-long.sh (+1/-0)
src/host-info/scripts/stress-ng-cpu-short.sh (+1/-0)
src/host-info/scripts/stress-ng-memory-long.sh (+1/-0)
src/host-info/scripts/stress-ng-memory-short.sh (+1/-0)
src/metadataserver/api.py (+74/-0)
src/metadataserver/builtin_scripts/testing_scripts/badblocks.py (+3/-1)
src/metadataserver/builtin_scripts/testing_scripts/gateway-connectivity.sh (+1/-1)
src/metadataserver/templates/hardware_sync_service.template (+6/-1)
src/metadataserver/tests/test_api.py (+121/-0)
src/metadataserver/tests/test_vendor_data.py (+15/-3)
src/metadataserver/urls.py (+10/-1)
src/metadataserver/user_data/templates/snippets/maas_run_scripts.py (+90/-37)
src/metadataserver/user_data/templates/snippets/tests/test_maas_run_scripts.py (+44/-0)
src/metadataserver/vendor_data.py (+18/-1)
Reviewer Review Type Date Requested Status
Alberto Donato (community) Needs Information
MAAS Lander Approve
Review via email: mp+415278@code.launchpad.net

Commit message

add local scripts test

add runner tests

add script metadata endpoint

update script runner for local scripts

provide file extensions

add commissioning scripts

template in hardware-sync executable in systemd config

embed scripts as subcommands

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b embed_run_scripts_in_hardware_sync lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/11846/console
COMMIT: 634b95be2d5c07bf26583ccb74898f24cc0b8f21

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b embed_run_scripts_in_hardware_sync lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/11853/console
COMMIT: f685a1175007bdb4b5b9c99eb093931dcc43d24b

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b embed_run_scripts_in_hardware_sync lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/11854/console
COMMIT: 18a2ec8c7c3bcb36a0ab038a93643e1c77be4060

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b embed_run_scripts_in_hardware_sync lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: c007b066d7f8d3af9a0aa5449100c7f99b75b342

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b embed_run_scripts_in_hardware_sync lp:~cgrabowski/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: cbfdde8a2055195c1504f5196c7fdee7d27f3da5

review: Approve
Revision history for this message
Alberto Donato (ack) wrote :

While I think it's useful to move to a go binary to run maas scripts, I think we should avoid having two different ways to serve scripts that get run on a machine.

As a first step, it'd be useful to have something that works end-to-end using maas-run-scripts from the systemd timer, i.e. generating the config file and just downloading running it periodically.

Later we can replace the script with a go binary, which does the same thing but has no external dependencies, and make optimizations to reduce data transfer.
For instance, the binary could check a url on maas for the exact MAAS version/revision and if it doesn't match, download and re-execute itself.
We could also investigate pre-packaging builtin scripts as a tarball on release so that we serve it statically and only need to download custom scripts.

Whatever we do, we should try to use the same mechanisms and paths on commissioning, brownfield and hardware updates.

WDYT?

review: Needs Information
Revision history for this message
Christian Grabowski (cgrabowski) wrote :

> While I think it's useful to move to a go binary to run maas scripts, I think
> we should avoid having two different ways to serve scripts that get run on a
> machine.
>
> As a first step, it'd be useful to have something that works end-to-end using
> maas-run-scripts from the systemd timer, i.e. generating the config file and
> just downloading running it periodically.
>
> Later we can replace the script with a go binary, which does the same thing
> but has no external dependencies, and make optimizations to reduce data
> transfer.
> For instance, the binary could check a url on maas for the exact MAAS
> version/revision and if it doesn't match, download and re-execute itself.
> We could also investigate pre-packaging builtin scripts as a tarball on
> release so that we serve it statically and only need to download custom
> scripts.
>
> Whatever we do, we should try to use the same mechanisms and paths on
> commissioning, brownfield and hardware updates.
>
> WDYT?

Well the original plan was to use maas-run-scripts directly, so I'm not opposed to that. But it would require backing out a fair bit of work at this point.

As discussed on the spec, this already is supposed to replace brownfield (the redirect handler that was added will serve this instead), and it should replace commissioning and testing, but we decided that was out of scope for now, but we can certainly move towards this replacing those as well. So I think the concern of having two ways to run scripts on a machine is valid, but also one that is temporary.

I do like the idea of having a go binary directly replace maas-run-scripts rather than embedding it and executing it as a child proc, but I did think this would be a good first step, and then have maas-run-scripts call out to this binary for each subsequent script.

If having commissioning's way of running scripts being different for the current scope is a concern, I'm fine backing out the existing changes and closing this MP in favor of one that uses maas-run-scripts directly.

Revision history for this message
Alberto Donato (ack) wrote :

I do get the point about reducing dependencies on the running system and having a simpler way to deliver binaries, and I think we should eventually replace commissioning/brownfield scripts with a single go binary which fetches all required hardware info, fetches custom scripts and run those, and reports back everything.

This, though, is a much broader scope (it's a whole new feature), so I think that for now we should go for minimal change and just extend the current brownfield method to be run periodically.

Making the main binary a go binary that still uses maas-run-scripts doesn't really reduce our dependency on binaries from the system.

Revision history for this message
Christian Grabowski (cgrabowski) wrote :

Unmerged commits

cbfdde8... by Christian Grabowski

add local scripts test

9bcc406... by Christian Grabowski

add runner tests

d35a1dd... by Christian Grabowski

add script metadata endpoint

0609567... by Christian Grabowski

update script runner for local scripts

b8cce3f... by Christian Grabowski

provide file extensions

ad80ec4... by Christian Grabowski

add commissioning scripts

696c205... by Christian Grabowski

template in hardware-sync executable in systemd config

a90cce8... by Christian Grabowski

embed scripts as subcommands

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index dd00eca..45e89ae 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -47,6 +47,7 @@
6 /src/maasui/nodejs
7 /src/host-info/bin
8 /src/host-info/vendor
9+/src/host-info/pkg/scriptrunner/statik
10 /stage
11 /TAGS
12 /tags
13diff --git a/src/host-info/Makefile b/src/host-info/Makefile
14index d3a2b66..ba682cc 100644
15--- a/src/host-info/Makefile
16+++ b/src/host-info/Makefile
17@@ -21,13 +21,19 @@ HARDWARE_SYNC_DIR := $(CMD_DIR)/hardware-sync
18 VENDOR_DIR := $(PACKAGE_DIR)/vendor
19 GO_CACHE_DIR := $(shell [ -d $(HOME)/.cache ] && echo $(HOME)/.cache/go-cache || mktemp --tmpdir -d tmp.go-cacheXXX)
20
21+SCRIPTS_SRC_DIR := ./scripts/
22+SCRIPTS_OUT_DIR := ./pkg/scriptrunner/
23+
24 .DEFAULT_GOAL := build
25
26+$(SCRIPTS_OUT_DIR)statik/statik.go:
27+ statik -src=$(SCRIPTS_SRC_DIR) -dest=$(SCRIPTS_OUT_DIR) -f
28+
29 # XXX: Explicitly set GOCACHE to avoid situations where we can't mkdir $HOME/.cache (autopkgtest VM)
30 $(MACHINE_RESOURCES_BINARIES): vendor
31 GOCACHE=$(GO_CACHE_DIR) GOARCH=$(DEB_GO_ARCH_$(notdir $@)) go build -mod vendor -ldflags '-s -w' -o $@ $(PACKAGE_DIR)
32
33-$(HARDWARE_SYNC_BINARIES): vendor
34+$(HARDWARE_SYNC_BINARIES): vendor $(SCRIPTS_OUT_DIR)statik/statik.go
35 GOCACHE=$(GO_CACHE_DIR) GOARCH=$(DEB_GO_ARCH_$(notdir $@)) go build -mod vendor -ldflags '-s -w' -o $@ $(HARDWARE_SYNC_DIR)
36
37 build: $(MACHINE_RESOURCES_BINARIES) $(HARDWARE_SYNC_BINARIES)
38@@ -35,6 +41,7 @@ build: $(MACHINE_RESOURCES_BINARIES) $(HARDWARE_SYNC_BINARIES)
39
40 clean:
41 rm -rf $(BINDIR) $(VENDOR_DIR)
42+ rm -rf $(SCRIPTS_OUT_DIR)statik/
43 .PHONY: clean
44
45 format:
46@@ -53,3 +60,7 @@ update-deps:
47 go get -u all
48 go mod tidy
49 .PHONY: update-deps
50+
51+test:
52+ go test -v ./...
53+.PHONY: test
54diff --git a/src/host-info/cmd/hardware-sync/main.go b/src/host-info/cmd/hardware-sync/main.go
55index 9c5960e..97aa250 100644
56--- a/src/host-info/cmd/hardware-sync/main.go
57+++ b/src/host-info/cmd/hardware-sync/main.go
58@@ -4,86 +4,188 @@
59 package main
60
61 import (
62+ "context"
63 "encoding/json"
64- "flag"
65 "fmt"
66 "os"
67+ "os/signal"
68+ "syscall"
69
70 "host-info/pkg/info"
71+ "host-info/pkg/scriptrunner"
72 )
73
74 // MAASVersion corresponds with the running MAAS version hardware-sync reports to
75-var MAASVersion = "" // set at compile time
76+var (
77+ MAASVersion = "" // set at compile time
78
79-const TokenFormat = "'consumer-key:token-key:token-secret[:consumer_secret]'"
80+ HelpText = `maas-hardware-sync [-h | -v | --list | COMMAND] [COMMAND_ARGS]
81+maas-hardware-sync runs all commands necessary for reporting hardware info to MAAS.
82
83-type getMachineTokenOptions struct {
84- SystemId string
85- TokenFile string
86-}
87-
88-type reportResultsOptions struct {
89- Config string
90- MachineToken string
91- MetadataUrl string
92-}
93-
94-type cmdOptions struct {
95- Debug bool
96- MachineResourceRun bool
97- GetMachineToken getMachineTokenOptions
98- ReportResults reportResultsOptions
99-}
100+maas-hardware-sync -h: print help message
101+maas-hardware-sync -v: print MAAS version this binary is part of
102+maas-hardware-sync --list: list available commands
103+maas-hardware-sync COMMAND [COMMAND_ARGS]: run a command and pass it COMMAND_ARGS
104+`
105+)
106
107-func parseCmdLine() (opts cmdOptions) {
108- flag.BoolVar(&opts.Debug, "debub", false, "print verbose debug messages")
109- flag.BoolVar(&opts.MachineResourceRun, "machine-resources", false, "when set, will read machine resources and print the info to stdout")
110- flag.StringVar(&opts.GetMachineToken.SystemId, "system-id", "", "system ID for the machine to get credentials for")
111- flag.StringVar(&opts.GetMachineToken.TokenFile, "token-file", "", "path for the file to write the token to")
112- flag.StringVar(
113- &opts.ReportResults.Config,
114- "config",
115- "",
116- "cloud-init config with MAAS credentials and endpoint, (e.g. /etc/cloud/cloud.cfg.d/90_dpkg_local_cloud_config.cfg)",
117- )
118- flag.StringVar(
119- &opts.ReportResults.MachineToken,
120- "machine-token",
121- "",
122- fmt.Sprintf("Machine OAuth token, in the %s form", TokenFormat),
123- )
124- flag.StringVar(
125- &opts.ReportResults.MetadataUrl,
126- "metadata-url",
127- "",
128- "MAAS metadata URL",
129- )
130- flag.Parse()
131- return opts
132+func parseCmdLine() (cmd string, args []string) {
133+ if len(os.Args) < 2 {
134+ return "help", args
135+ }
136+ switch os.Args[1] {
137+ case "-h":
138+ return "help", args
139+ case "-v":
140+ return "version", args
141+ case "--list":
142+ return "list_cmds", args
143+ default:
144+ if len(os.Args) > 2 {
145+ args = os.Args[2:]
146+ }
147+ return os.Args[1], args
148+ }
149 }
150
151-func getMachineResources() error {
152+func getMachineResources() ([]byte, error) {
153 machineResources, err := info.GetInfo()
154 if err != nil {
155- return err
156+ return nil, err
157 }
158- encoder := json.NewEncoder(os.Stdout)
159- return encoder.Encode(machineResources)
160+ return json.Marshal(machineResources)
161+}
162+
163+func handleSignal(cancel context.CancelFunc) {
164+ sigC := make(chan os.Signal, 2)
165+ signal.Notify(sigC, syscall.SIGTERM, syscall.SIGINT)
166+ <-sigC
167+ cancel()
168 }
169
170 func run() int {
171- opts := parseCmdLine()
172+ bkg := context.Background()
173+ ctx, cancel := context.WithCancel(bkg)
174
175- if opts.MachineResourceRun {
176- err := getMachineResources()
177- if err != nil {
178- fmt.Printf("an error occurred while fetching machine resources: %s\n", err)
179- return 1
180- }
181- return 0
182+ go handleSignal(cancel)
183+
184+ var out, errOut []byte
185+
186+ cmd, args := parseCmdLine()
187+
188+ runner, err := scriptrunner.New()
189+ if err != nil {
190+ fmt.Printf("failed to initialize script runner: %s\n", err)
191+ return 1
192+ }
193+
194+ switch cmd {
195+ case "help":
196+ out = []byte(HelpText)
197+ case "version":
198+ out = []byte(MAASVersion)
199+ case "list_cmds":
200+ out = []byte(scriptrunner.ListCMDs())
201+ case scriptrunner.CMDGetMachineToken:
202+ out, errOut, err = runner.GetMachineToken(ctx, args)
203+ case scriptrunner.CMDRegisterMachine:
204+ out, errOut, err = runner.RegisterMachine(ctx, args)
205+ case scriptrunner.CMDReportResults:
206+ out, errOut, err = runner.ReportResults(ctx, args)
207+ case scriptrunner.CMDBMCConfig:
208+ out, errOut, err = runner.BMCConfig(ctx, args)
209+ case scriptrunner.CMDCaptureLLDPD:
210+ out, errOut, err = runner.CaptureLLDPD(ctx, args)
211+ case scriptrunner.CMDDHCPUnconfiguredIfaces:
212+ out, errOut, err = runner.DHCPUnconfiguredIfaces(ctx, args)
213+ case scriptrunner.CMDInstallLLDPD:
214+ out, errOut, err = runner.InstallLLDPD(ctx, args)
215+ case scriptrunner.CMDMAASGetFruidAPIData:
216+ out, errOut, err = runner.MAASGetFruidAPIData(ctx, args)
217+ case scriptrunner.CMDMAASKernelCMDLine:
218+ out, errOut, err = runner.MAASKernelCMDLine(ctx, args)
219+ case scriptrunner.CMDMAASListModaliases:
220+ out, errOut, err = runner.MAASListModaliases(ctx, args)
221+ case scriptrunner.CMDMAASLSHW:
222+ out, errOut, err = runner.MAASLshw(ctx, args)
223+ case scriptrunner.CMDMAASSerialPorts:
224+ out, errOut, err = runner.MAASSerialPorts(ctx, args)
225+ case scriptrunner.CMDMAASSupportInfo:
226+ out, errOut, err = runner.MAASSupportInfo(ctx, args)
227+ case scriptrunner.CMDMAASCommissioning:
228+ out, errOut, err = runner.MAASCommissioning(ctx, args)
229+ case scriptrunner.CMDBadBlocks:
230+ out, errOut, err = runner.BadBlocks(ctx, args)
231+ case scriptrunner.CMDBadBlocksDestructive:
232+ args = append([]string{
233+ "destructive",
234+ }, args...)
235+ out, errOut, err = runner.BadBlocks(ctx, args)
236+ case scriptrunner.CMDFio:
237+ out, errOut, err = runner.Fio(ctx, args)
238+ case scriptrunner.CMDGatewayConnectivity:
239+ out, errOut, err = runner.GatewayConnectivity(ctx, args)
240+ case scriptrunner.CMDInternetConnectivity:
241+ out, errOut, err = runner.InternetConnectivity(ctx, args)
242+ case scriptrunner.CMDMemTester:
243+ out, errOut, err = runner.MemTester(ctx, args)
244+ case scriptrunner.CMDNtp:
245+ out, errOut, err = runner.Ntp(ctx, args)
246+ case scriptrunner.CMDSevenZ:
247+ out, errOut, err = runner.SevenZ(ctx, args)
248+ case scriptrunner.CMDRackControllerConnectivity:
249+ out, errOut, err = runner.RackControllerConnectivity(ctx, args)
250+ case scriptrunner.CMDSmartctlConveyance:
251+ out, errOut, err = runner.Smartctl(ctx, append([]string{
252+ "--test",
253+ "conveyance",
254+ }, args...))
255+ case scriptrunner.CMDSmartctlLong:
256+ out, errOut, err = runner.Smartctl(ctx, append([]string{
257+ "--test",
258+ "long",
259+ }, args...))
260+ case scriptrunner.CMDSmartctlShort:
261+ out, errOut, err = runner.Smartctl(ctx, append([]string{
262+ "--test",
263+ "short",
264+ }, args...))
265+ case scriptrunner.CMDSmartctlValidate:
266+ out, errOut, err = runner.Smartctl(ctx, append([]string{
267+ "--test",
268+ "validate",
269+ }, args...))
270+ case scriptrunner.CMDStressNGCpuLong:
271+ out, errOut, err = runner.StressNGCpuLong(ctx, args)
272+ case scriptrunner.CMDStressNGCpuShort:
273+ out, errOut, err = runner.StressNGCpuShort(ctx, args)
274+ case scriptrunner.CMDStressNGMemLong:
275+ out, errOut, err = runner.StressNGMemLong(ctx, args)
276+ case scriptrunner.CMDStressNGMemShort:
277+ out, errOut, err = runner.StressNGMemShort(ctx, args)
278+ case "machine-resources":
279+ out, err = getMachineResources()
280+ case "40-maas-01-machine-resources":
281+ out, err = getMachineResources()
282+ default:
283+ // TODO handle custom scripts for commissioning
284+ err = fmt.Errorf("%s is an unknown command", cmd)
285+ }
286+ if err != nil {
287+ fmt.Printf("an error occured while executing '%s': %s\n", os.Args[1], err)
288+ return 1
289+ }
290+ _, err = os.Stdout.Write(out)
291+ if err != nil {
292+ fmt.Printf("error writing output: %s\n", err)
293+ return 1
294+ }
295+ _, err = os.Stderr.Write(errOut)
296+ if err != nil {
297+ fmt.Printf("error writing stderr: %s\n", err)
298+ return 1
299 }
300
301- // TODO embed scripts and call out to maas_run_scripts
302 return 0
303 }
304
305diff --git a/src/host-info/go.mod b/src/host-info/go.mod
306index 4b414a4..544b842 100644
307--- a/src/host-info/go.mod
308+++ b/src/host-info/go.mod
309@@ -12,6 +12,7 @@ require (
310 github.com/lxc/lxd v0.0.0-20210317085550-5d82899488db
311 github.com/pborman/uuid v1.2.1 // indirect
312 github.com/pkg/errors v0.9.1 // indirect
313+ github.com/rakyll/statik v0.1.7
314 github.com/stretchr/testify v1.7.0 // indirect
315 golang.org/x/sys v0.0.0-20210317091845-390168757d9c // indirect
316 gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect
317diff --git a/src/host-info/go.sum b/src/host-info/go.sum
318index 3b1758d..564b524 100644
319--- a/src/host-info/go.sum
320+++ b/src/host-info/go.sum
321@@ -29,6 +29,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
322 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
323 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
324 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
325+github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
326+github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
327 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
328 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
329 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
330diff --git a/src/host-info/pkg/scriptrunner/runner.go b/src/host-info/pkg/scriptrunner/runner.go
331new file mode 100644
332index 0000000..040141d
333--- /dev/null
334+++ b/src/host-info/pkg/scriptrunner/runner.go
335@@ -0,0 +1,494 @@
336+package scriptrunner
337+
338+import (
339+ "bytes"
340+ "context"
341+ "fmt"
342+ "html/template"
343+ "io"
344+ "io/ioutil"
345+ "net/http"
346+ "os/exec"
347+ "strings"
348+
349+ "github.com/rakyll/statik/fs"
350+
351+ _ "host-info/pkg/scriptrunner/statik"
352+)
353+
354+const (
355+ EnvBin = "/usr/bin/env"
356+ BashBin = "/bin/bash"
357+ Python3BinName = "python3"
358+
359+ MAASScriptsRunner = "/maas_run_scripts.py"
360+ MAASAPIHelper = "/maas_api_helper.py"
361+ BMCConfig = "/bmc_config.py"
362+ CaptureLLDPD = "/capture_lldpd.py"
363+ DHCPUnconfiguredIfaces = "/dhcp_unconfigured_ifaces.py"
364+ InstallLLDPD = "/install_lldpd.py"
365+ MAASGetFruidApiData = "/maas-get-fruid-api-data.sh"
366+ MAASKernelCMDLine = "/maas-kernel-cmdline.sh"
367+ MAASListModaliases = "/maas-list-modaliases.sh"
368+ MAASLSHW = "/maas-lshw.sh"
369+ MAASSerialPorts = "/maas-serial-ports.sh"
370+ MAASSupportInfo = "/maas-support-info.sh"
371+ MAASCommissioning = "/50-maas-01-commissioning.py"
372+ BadBlocks = "/badblocks.py"
373+ BaseConnectivity = "/base-connectivity.sh"
374+ Fio = "/fio.py"
375+ GatewayConnectivity = "/gateway-connectivity.sh"
376+ InternetConnectivity = "/internet-connectivity.sh"
377+ MemTester = "/memtester.sh"
378+ Ntp = "/ntp.sh"
379+ RackControllerConnectivity = "/rack-controller-connectivity.sh"
380+ SevenZ = "/seven_z.py"
381+ Smartctl = "/smartctl.py"
382+ StressNGCpuLong = "/stress-ng-cpu-long.sh"
383+ StressNGCpuShort = "/stress-ng-cpu-short.sh"
384+ StressNGMemLong = "/stress-ng-memory-long.sh"
385+ StressNGMemShort = "/stress-ng-memory-short.sh"
386+
387+ CMDGetMachineToken = "get-machine-token"
388+ CMDRegisterMachine = "register-machine"
389+ CMDReportResults = "report-results"
390+ CMDBMCConfig = "30-maas-01-bmc-config"
391+ CMDCaptureLLDPD = "maas-capture-lldpd"
392+ CMDDHCPUnconfiguredIfaces = "20-maas-02-dhcp-unconfigured-ifaces"
393+ CMDInstallLLDPD = "20-maas-01-install-lldpd"
394+ CMDMAASGetFruidAPIData = "maas-get-fruid-api-data"
395+ CMDMAASKernelCMDLine = "maas-kernel-cmdline"
396+ CMDMAASListModaliases = "maas-list-modaliases"
397+ CMDMAASLSHW = "maas-lshw"
398+ CMDMAASSerialPorts = "maas-serial-ports"
399+ CMDMAASSupportInfo = "maas-support-info"
400+ CMDMAASCommissioning = "50-maas-01-commissioning"
401+ CMDBadBlocks = "badblocks"
402+ CMDBadBlocksDestructive = "badblocks-destructive"
403+ CMDFio = "fio"
404+ CMDGatewayConnectivity = "gateway-connectivity"
405+ CMDInternetConnectivity = "internet-connectivity"
406+ CMDMemTester = "memtester"
407+ CMDNtp = "ntp"
408+ CMDSevenZ = "7z"
409+ CMDRackControllerConnectivity = "rack-controller-connectivity"
410+ CMDSmartctlConveyance = "smartctl-conveyance"
411+ CMDSmartctlLong = "smartctl-long"
412+ CMDSmartctlShort = "smartctl-short"
413+ CMDSmartctlValidate = "smartctl-validate"
414+ CMDStressNGCpuLong = "stress-ng-cpu-long"
415+ CMDStressNGCpuShort = "stress-ng-cpu-short"
416+ CMDStressNGMemLong = "stress-ng-memory-long"
417+ CMDStressNGMemShort = "stress-ng-memory-short"
418+)
419+
420+func ListCMDs() string {
421+ cmdList := []string{
422+ CMDGetMachineToken,
423+ CMDRegisterMachine,
424+ CMDReportResults,
425+ CMDBMCConfig,
426+ CMDCaptureLLDPD,
427+ CMDDHCPUnconfiguredIfaces,
428+ CMDInstallLLDPD,
429+ CMDMAASGetFruidAPIData,
430+ CMDMAASKernelCMDLine,
431+ CMDMAASListModaliases,
432+ CMDMAASLSHW,
433+ CMDMAASSerialPorts,
434+ CMDMAASSupportInfo,
435+ CMDMAASCommissioning,
436+ CMDBadBlocks,
437+ CMDBadBlocksDestructive,
438+ CMDFio,
439+ CMDGatewayConnectivity,
440+ CMDInternetConnectivity,
441+ CMDMemTester,
442+ CMDNtp,
443+ CMDSevenZ,
444+ CMDRackControllerConnectivity,
445+ CMDSmartctlConveyance,
446+ CMDSmartctlLong,
447+ CMDSmartctlShort,
448+ CMDSmartctlValidate,
449+ CMDStressNGCpuLong,
450+ CMDStressNGCpuShort,
451+ CMDStressNGMemLong,
452+ CMDStressNGMemShort,
453+ }
454+ return strings.Join(cmdList, "\n") + "\n"
455+}
456+
457+type ScriptRunner interface {
458+ Execute(ctx context.Context, args []string) ([]byte, []byte, error)
459+}
460+
461+func pipeScriptToProcess(script io.Reader, stdin io.WriteCloser) error {
462+ defer func() {
463+ stdin.Close()
464+ if scriptCloser, ok := script.(io.ReadCloser); ok {
465+ scriptCloser.Close()
466+ }
467+ }()
468+
469+ _, err := io.Copy(stdin, script)
470+ if err != nil {
471+ return fmt.Errorf("failed piping script to process: %w", err)
472+ }
473+ return nil
474+}
475+
476+func pipeStd(std io.Reader, stdC chan []byte) error {
477+ defer close(stdC)
478+
479+ var buf bytes.Buffer
480+ _, err := io.Copy(&buf, std)
481+ if err != nil {
482+ return fmt.Errorf("failed piping output to parent: %w", err)
483+ }
484+ stdC <- buf.Bytes()
485+ return nil
486+}
487+
488+type BashRunner struct {
489+ Env []string
490+ Script io.Reader
491+}
492+
493+func (b *BashRunner) Execute(ctx context.Context, args []string) ([]byte, []byte, error) {
494+ args = append([]string{"-s", "-"}, args...)
495+ cmd := exec.CommandContext(ctx, BashBin, args...)
496+
497+ var key string
498+ for i, e := range b.Env {
499+ if i%2 == 0 {
500+ key = e
501+ } else {
502+ cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, e))
503+ }
504+ }
505+
506+ stdin, err := cmd.StdinPipe()
507+ if err != nil {
508+ return nil, nil, fmt.Errorf("failed to fetch stdin pipe: %w", err)
509+ }
510+ stdout, err := cmd.StdoutPipe()
511+ if err != nil {
512+ return nil, nil, fmt.Errorf("failed to fetch stdout pipe: %w", err)
513+ }
514+ stderr, err := cmd.StderrPipe()
515+ if err != nil {
516+ return nil, nil, fmt.Errorf("failed to fetch stderr pipe: %w", err)
517+ }
518+
519+ go pipeScriptToProcess(b.Script, stdin)
520+
521+ stdoutC := make(chan []byte)
522+ stderrC := make(chan []byte)
523+ go pipeStd(stdout, stdoutC)
524+ go pipeStd(stderr, stderrC)
525+
526+ err = cmd.Run()
527+ out := <-stdoutC
528+ errOut := <-stderrC
529+ if err != nil {
530+ return nil, nil, fmt.Errorf(
531+ "failed to execute bash script: %w: %s\n----------------\n%s",
532+ err,
533+ string(out),
534+ string(errOut),
535+ )
536+ }
537+ return out, errOut, nil
538+}
539+
540+type Python3ScriptRunner struct {
541+ SubCommand string
542+ APIHelper io.ReadCloser
543+ Script io.Reader
544+}
545+
546+func (p *Python3ScriptRunner) Execute(ctx context.Context, args []string) ([]byte, []byte, error) {
547+ var cmd *exec.Cmd
548+ if len(p.SubCommand) > 0 {
549+ cmdArgs := append([]string{Python3BinName, "-", p.SubCommand}, args...)
550+ cmd = exec.CommandContext(ctx, EnvBin, cmdArgs...)
551+ } else {
552+ cmdArgs := append([]string{Python3BinName, "-"}, args...)
553+ cmd = exec.CommandContext(ctx, EnvBin, cmdArgs...)
554+ }
555+
556+ stdin, err := cmd.StdinPipe()
557+ if err != nil {
558+ return nil, nil, fmt.Errorf("failed to fetch stdin pipe: %w", err)
559+ }
560+ stdout, err := cmd.StdoutPipe()
561+ if err != nil {
562+ return nil, nil, fmt.Errorf("failed to fetch stdout pipe: %w", err)
563+ }
564+ stderr, err := cmd.StderrPipe()
565+ if err != nil {
566+ return nil, nil, fmt.Errorf("failed to fetch stderr pipe: %w", err)
567+ }
568+
569+ go func() {
570+ defer func() {
571+ stdin.Close()
572+ if p.APIHelper != nil {
573+ p.APIHelper.Close()
574+ }
575+ if script, ok := p.Script.(io.ReadCloser); ok {
576+ script.Close()
577+ }
578+ }()
579+
580+ if p.APIHelper != nil {
581+ io.Copy(stdin, p.APIHelper)
582+ }
583+ io.Copy(stdin, p.Script)
584+ }()
585+
586+ stdoutC := make(chan []byte)
587+ stderrC := make(chan []byte)
588+ go pipeStd(stdout, stdoutC)
589+ go pipeStd(stderr, stderrC)
590+
591+ err = cmd.Run()
592+ out := <-stdoutC
593+ errOut := <-stderrC
594+ if err != nil {
595+ return nil, nil, fmt.Errorf(
596+ "failed to execute python script: %w: %s\n----------------\n%s",
597+ err,
598+ string(out),
599+ string(errOut),
600+ )
601+ }
602+
603+ return out, errOut, nil
604+}
605+
606+type runScriptTemplateOptions struct {
607+ ImportAPIHelper bool
608+}
609+
610+type Runner struct {
611+ scripts http.FileSystem
612+}
613+
614+func New() (*Runner, error) {
615+ content, err := fs.New()
616+ if err != nil {
617+ return nil, err
618+ }
619+
620+ return &Runner{
621+ scripts: content,
622+ }, nil
623+}
624+
625+func (r *Runner) execMAASRunScripts(ctx context.Context, subcommand string, args []string) ([]byte, []byte, error) {
626+ script, err := r.scripts.Open(MAASScriptsRunner)
627+ if err != nil {
628+ return nil, nil, fmt.Errorf("failed to load script for '%s': %w", subcommand, err)
629+ }
630+ defer script.Close()
631+ tmpl := template.New("maas_run_scripts")
632+ scriptContents, err := ioutil.ReadAll(script)
633+ if err != nil {
634+ return nil, nil, err
635+ }
636+ tmpl, err = tmpl.Parse(string(scriptContents))
637+ if err != nil {
638+ return nil, nil, err
639+ }
640+ var runScript bytes.Buffer
641+ err = tmpl.Execute(&runScript, &runScriptTemplateOptions{ImportAPIHelper: false})
642+ if err != nil {
643+ return nil, nil, err
644+ }
645+ apiHelper, err := r.scripts.Open(MAASAPIHelper)
646+ if err != nil {
647+ return nil, nil, fmt.Errorf("failed to load maas_api_helper: %w", err)
648+ }
649+
650+ py := &Python3ScriptRunner{
651+ SubCommand: subcommand,
652+ APIHelper: apiHelper,
653+ Script: &runScript,
654+ }
655+ return py.Execute(ctx, append([]string{"--local-scripts"}, args...))
656+}
657+
658+func (r *Runner) basicPython3Script(ctx context.Context, script string, args []string) ([]byte, []byte, error) {
659+ content, err := r.scripts.Open(script)
660+ if err != nil {
661+ return nil, nil, fmt.Errorf("failed loading %s: %w", script, err)
662+ }
663+
664+ py := &Python3ScriptRunner{
665+ Script: content,
666+ }
667+ return py.Execute(ctx, args)
668+}
669+
670+func (r *Runner) basicBashScript(ctx context.Context, script string, args []string) ([]byte, []byte, error) {
671+ content, err := r.scripts.Open(script)
672+ if err != nil {
673+ return nil, nil, fmt.Errorf("failed loading %s: %w", script, err)
674+ }
675+
676+ sh := &BashRunner{
677+ Script: content,
678+ }
679+ return sh.Execute(ctx, args)
680+}
681+
682+type connectivityHelper struct {
683+ UseGoTemplate bool
684+ Template string
685+}
686+
687+func (r *Runner) connectivityTest(ctx context.Context, script string, args []string) ([]byte, []byte, error) {
688+ baseScript, err := r.scripts.Open(BaseConnectivity)
689+ if err != nil {
690+ return nil, nil, err
691+ }
692+ baseContent, err := ioutil.ReadAll(baseScript)
693+ if err != nil {
694+ return nil, nil, err
695+ }
696+ connScript, err := r.scripts.Open(script)
697+ if err != nil {
698+ return nil, nil, err
699+ }
700+ connContent, err := ioutil.ReadAll(connScript)
701+ tmpl := template.New("maas_connectivity")
702+ tmpl, err = tmpl.Parse(string(connContent))
703+ if err != nil {
704+ return nil, nil, err
705+ }
706+ var connTest bytes.Buffer
707+ err = tmpl.Execute(
708+ &connTest,
709+ connectivityHelper{
710+ UseGoTemplate: true,
711+ Template: string(baseContent),
712+ },
713+ )
714+ if err != nil {
715+ return nil, nil, err
716+ }
717+ sh := &BashRunner{
718+ Script: &connTest,
719+ }
720+ return sh.Execute(ctx, nil)
721+}
722+
723+func (r *Runner) GetMachineToken(ctx context.Context, args []string) ([]byte, []byte, error) {
724+ return r.execMAASRunScripts(ctx, CMDGetMachineToken, args)
725+}
726+
727+func (r *Runner) RegisterMachine(ctx context.Context, args []string) ([]byte, []byte, error) {
728+ return r.execMAASRunScripts(ctx, CMDRegisterMachine, args)
729+}
730+
731+func (r *Runner) ReportResults(ctx context.Context, args []string) ([]byte, []byte, error) {
732+ return r.execMAASRunScripts(ctx, CMDReportResults, args)
733+}
734+
735+func (r *Runner) BMCConfig(ctx context.Context, args []string) ([]byte, []byte, error) {
736+ return r.basicPython3Script(ctx, BMCConfig, args)
737+}
738+
739+func (r *Runner) CaptureLLDPD(ctx context.Context, args []string) ([]byte, []byte, error) {
740+ return r.basicPython3Script(ctx, CaptureLLDPD, args)
741+}
742+
743+func (r *Runner) DHCPUnconfiguredIfaces(ctx context.Context, args []string) ([]byte, []byte, error) {
744+ return r.basicPython3Script(ctx, DHCPUnconfiguredIfaces, args)
745+}
746+
747+func (r *Runner) InstallLLDPD(ctx context.Context, args []string) ([]byte, []byte, error) {
748+ return r.basicPython3Script(ctx, InstallLLDPD, args)
749+}
750+
751+func (r *Runner) MAASGetFruidAPIData(ctx context.Context, args []string) ([]byte, []byte, error) {
752+ return r.basicBashScript(ctx, MAASGetFruidApiData, args)
753+}
754+
755+func (r *Runner) MAASKernelCMDLine(ctx context.Context, args []string) ([]byte, []byte, error) {
756+ return r.basicBashScript(ctx, MAASKernelCMDLine, args)
757+}
758+
759+func (r *Runner) MAASListModaliases(ctx context.Context, args []string) ([]byte, []byte, error) {
760+ return r.basicBashScript(ctx, MAASListModaliases, args)
761+}
762+
763+func (r *Runner) MAASLshw(ctx context.Context, args []string) ([]byte, []byte, error) {
764+ return r.basicBashScript(ctx, MAASLSHW, args)
765+}
766+
767+func (r *Runner) MAASSerialPorts(ctx context.Context, args []string) ([]byte, []byte, error) {
768+ return r.basicBashScript(ctx, MAASSerialPorts, args)
769+}
770+
771+func (r *Runner) MAASSupportInfo(ctx context.Context, args []string) ([]byte, []byte, error) {
772+ return r.basicBashScript(ctx, MAASSupportInfo, args)
773+}
774+
775+func (r *Runner) MAASCommissioning(ctx context.Context, args []string) ([]byte, []byte, error) {
776+ return r.basicPython3Script(ctx, MAASCommissioning, args)
777+}
778+
779+func (r *Runner) BadBlocks(ctx context.Context, args []string) ([]byte, []byte, error) {
780+ return r.basicPython3Script(ctx, BadBlocks, args)
781+}
782+
783+func (r *Runner) Fio(ctx context.Context, args []string) ([]byte, []byte, error) {
784+ return r.basicPython3Script(ctx, Fio, args)
785+}
786+
787+func (r *Runner) GatewayConnectivity(ctx context.Context, args []string) ([]byte, []byte, error) {
788+ return r.connectivityTest(ctx, GatewayConnectivity, args)
789+}
790+
791+func (r *Runner) InternetConnectivity(ctx context.Context, args []string) ([]byte, []byte, error) {
792+ return r.connectivityTest(ctx, InternetConnectivity, args)
793+}
794+
795+func (r *Runner) MemTester(ctx context.Context, args []string) ([]byte, []byte, error) {
796+ return r.basicBashScript(ctx, MemTester, args)
797+}
798+
799+func (r *Runner) Ntp(ctx context.Context, args []string) ([]byte, []byte, error) {
800+ return r.basicBashScript(ctx, Ntp, args)
801+}
802+
803+func (r *Runner) RackControllerConnectivity(ctx context.Context, args []string) ([]byte, []byte, error) {
804+ return r.connectivityTest(ctx, RackControllerConnectivity, args)
805+}
806+
807+func (r *Runner) SevenZ(ctx context.Context, args []string) ([]byte, []byte, error) {
808+ return r.basicPython3Script(ctx, SevenZ, args)
809+}
810+
811+func (r *Runner) Smartctl(ctx context.Context, args []string) ([]byte, []byte, error) {
812+ return r.basicPython3Script(ctx, Smartctl, args)
813+}
814+
815+func (r *Runner) StressNGCpuLong(ctx context.Context, args []string) ([]byte, []byte, error) {
816+ return r.basicBashScript(ctx, StressNGCpuLong, args)
817+}
818+
819+func (r *Runner) StressNGCpuShort(ctx context.Context, args []string) ([]byte, []byte, error) {
820+ return r.basicBashScript(ctx, StressNGCpuShort, args)
821+}
822+
823+func (r *Runner) StressNGMemLong(ctx context.Context, args []string) ([]byte, []byte, error) {
824+ return r.basicBashScript(ctx, StressNGMemLong, args)
825+}
826+
827+func (r *Runner) StressNGMemShort(ctx context.Context, args []string) ([]byte, []byte, error) {
828+ return r.basicBashScript(ctx, StressNGMemShort, args)
829+}
830diff --git a/src/host-info/pkg/scriptrunner/runner_test.go b/src/host-info/pkg/scriptrunner/runner_test.go
831new file mode 100644
832index 0000000..f582817
833--- /dev/null
834+++ b/src/host-info/pkg/scriptrunner/runner_test.go
835@@ -0,0 +1,164 @@
836+package scriptrunner
837+
838+import (
839+ "bytes"
840+ "context"
841+ "os"
842+ "strings"
843+ "testing"
844+)
845+
846+const bashPrintInput = `#!/bin/bash
847+env
848+echo $@
849+`
850+
851+const bashExitOne = `#!/bin/bash
852+echo "error message" && exit 1
853+`
854+
855+const python3PrintInput = `import os
856+import sys
857+
858+print(os.environ.items())
859+print("\n".join(sys.argv))
860+`
861+
862+const python3ExitOne = `import os
863+
864+print("error message")
865+os.exit(1)
866+`
867+
868+func TestBashRunner(t *testing.T) {
869+ table := []struct {
870+ Name string
871+ Env []string
872+ Args []string
873+ }{
874+ {
875+ Name: "just_command_cleanly_executes",
876+ },
877+ {
878+ Name: "command_with_args",
879+ Args: []string{"--arg", "val"},
880+ },
881+ {
882+ Name: "command_with_env",
883+ Env: []string{"ENV_VAR", "val"},
884+ },
885+ {
886+ Name: "command_with_args_and_env",
887+ Env: []string{"ENV_VAR", "val"},
888+ Args: []string{"--args", "val"},
889+ },
890+ }
891+
892+ for _, tcase := range table {
893+ t.Run(tcase.Name, func(tt *testing.T) {
894+ runner := &BashRunner{
895+ Env: tcase.Env,
896+ Script: bytes.NewBuffer([]byte(bashPrintInput)),
897+ }
898+ stdout, _, err := runner.Execute(context.Background(), tcase.Args)
899+ if err != nil {
900+ tt.Fatal(err)
901+ }
902+ for _, envvar := range tcase.Env {
903+ if !strings.Contains(string(stdout), envvar) {
904+ tt.Errorf("%s does not contain %s", stdout, envvar)
905+ }
906+ }
907+ for _, arg := range tcase.Args {
908+ if !strings.Contains(string(stdout), arg) {
909+ tt.Errorf("%s does not contain %s", stdout, arg)
910+ }
911+ }
912+ })
913+ }
914+}
915+
916+func TestBashRunnerInheritsParentEnv(t *testing.T) {
917+ os.Setenv("TEST_VAR", "test_val")
918+ defer os.Setenv("TEST_VAR", "")
919+ runner := &BashRunner{
920+ Script: bytes.NewBuffer([]byte(bashPrintInput)),
921+ }
922+ stdout, _, err := runner.Execute(context.Background(), nil)
923+ if err != nil {
924+ t.Fatal(err)
925+ }
926+ if !strings.Contains(string(stdout), "TEST_VAR=test_val") {
927+ t.Fatalf("%s does not contain TEST_VAR=test_val", stdout)
928+ }
929+}
930+
931+func TestBashRunnerReturnsErrorWhenScriptExitsNon0(t *testing.T) {
932+ runner := &BashRunner{
933+ Script: bytes.NewBuffer([]byte(bashExitOne)),
934+ }
935+ _, _, err := runner.Execute(context.Background(), nil)
936+ if err == nil {
937+ t.Fatal("expected an error to be returned")
938+ }
939+}
940+
941+func TestPython3ScriptRunner(t *testing.T) {
942+ table := []struct {
943+ Name string
944+ Args []string
945+ }{
946+ {
947+ Name: "just_command_cleanly_execute",
948+ },
949+ {
950+ Name: "command_with_args",
951+ Args: []string{"--args", "val"},
952+ },
953+ }
954+ for _, tcase := range table {
955+ t.Run(tcase.Name, func(tt *testing.T) {
956+ runner := &Python3ScriptRunner{
957+ Script: bytes.NewBuffer([]byte(python3PrintInput)),
958+ }
959+ stdout, _, err := runner.Execute(context.Background(), tcase.Args)
960+ if err != nil {
961+ tt.Fatal(err)
962+ }
963+ for _, arg := range tcase.Args {
964+ if !strings.Contains(string(stdout), arg) {
965+ tt.Errorf("%s does not contain %s", stdout, arg)
966+ }
967+ }
968+ })
969+ }
970+
971+}
972+
973+func TestPython3RunnerInheritsParentEnv(t *testing.T) {
974+ os.Setenv("TEST_VAR", "test_val")
975+ defer os.Setenv("TEST_VAR", "")
976+ runner := &Python3ScriptRunner{
977+ Script: bytes.NewBuffer([]byte(python3PrintInput)),
978+ }
979+ stdout, _, err := runner.Execute(context.Background(), nil)
980+ if err != nil {
981+ t.Fatal(err)
982+ }
983+ if !strings.Contains(string(stdout), "TEST_VAR") {
984+ t.Fatalf("%s does not contain TEST_VAR", stdout)
985+ }
986+ if !strings.Contains(string(stdout), "test_val") {
987+ t.Fatalf("%s does not contain test_val", stdout)
988+ }
989+}
990+
991+func TestPython3RunnerReturnsErrorWhenScriptExitsNon0(t *testing.T) {
992+ runner := &Python3ScriptRunner{
993+ Script: bytes.NewBuffer([]byte(python3ExitOne)),
994+ }
995+ _, _, err := runner.Execute(context.Background(), nil)
996+ if err == nil {
997+ t.Fatal("expected an error to be returned")
998+ }
999+}
1000diff --git a/src/host-info/scripts/50-maas-01-commissioning.py b/src/host-info/scripts/50-maas-01-commissioning.py
1001new file mode 120000
1002index 0000000..aa47949
1003--- /dev/null
1004+++ b/src/host-info/scripts/50-maas-01-commissioning.py
1005@@ -0,0 +1 @@
1006+/home/christian/maas/src/provisioningserver/refresh/50-maas-01-commissioning
1007\ No newline at end of file
1008diff --git a/src/host-info/scripts/badblocks.py b/src/host-info/scripts/badblocks.py
1009new file mode 120000
1010index 0000000..1386716
1011--- /dev/null
1012+++ b/src/host-info/scripts/badblocks.py
1013@@ -0,0 +1 @@
1014+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/badblocks.py
1015\ No newline at end of file
1016diff --git a/src/host-info/scripts/base-connectivity.sh b/src/host-info/scripts/base-connectivity.sh
1017new file mode 120000
1018index 0000000..ef91a26
1019--- /dev/null
1020+++ b/src/host-info/scripts/base-connectivity.sh
1021@@ -0,0 +1 @@
1022+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/base-connectivity.sh
1023\ No newline at end of file
1024diff --git a/src/host-info/scripts/bmc_config.py b/src/host-info/scripts/bmc_config.py
1025new file mode 120000
1026index 0000000..eb6352e
1027--- /dev/null
1028+++ b/src/host-info/scripts/bmc_config.py
1029@@ -0,0 +1 @@
1030+/home/christian/maas/src/metadataserver/builtin_scripts/commissioning_scripts/bmc_config.py
1031\ No newline at end of file
1032diff --git a/src/host-info/scripts/capture_lldpd.py b/src/host-info/scripts/capture_lldpd.py
1033new file mode 120000
1034index 0000000..44218a5
1035--- /dev/null
1036+++ b/src/host-info/scripts/capture_lldpd.py
1037@@ -0,0 +1 @@
1038+/home/christian/maas/src/metadataserver/builtin_scripts/commissioning_scripts/capture_lldpd.py
1039\ No newline at end of file
1040diff --git a/src/host-info/scripts/dhcp_unconfigured_ifaces.py b/src/host-info/scripts/dhcp_unconfigured_ifaces.py
1041new file mode 120000
1042index 0000000..7510086
1043--- /dev/null
1044+++ b/src/host-info/scripts/dhcp_unconfigured_ifaces.py
1045@@ -0,0 +1 @@
1046+/home/christian/maas/src/metadataserver/builtin_scripts/commissioning_scripts/dhcp_unconfigured_ifaces.py
1047\ No newline at end of file
1048diff --git a/src/host-info/scripts/fio.py b/src/host-info/scripts/fio.py
1049new file mode 120000
1050index 0000000..e1ad126
1051--- /dev/null
1052+++ b/src/host-info/scripts/fio.py
1053@@ -0,0 +1 @@
1054+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/fio.py
1055\ No newline at end of file
1056diff --git a/src/host-info/scripts/gateway-connectivity.sh b/src/host-info/scripts/gateway-connectivity.sh
1057new file mode 100644
1058index 0000000..25cd583
1059--- /dev/null
1060+++ b/src/host-info/scripts/gateway-connectivity.sh
1061@@ -0,0 +1,115 @@
1062+#!/bin/bash
1063+#
1064+# gateway-connectivity - Check if an interface has access to the configured gateway.
1065+#
1066+# Author: Lee Trager <lee.trager@canonical.com>
1067+#
1068+# Copyright (C) 2019-2022 Canonical
1069+#
1070+# This program is free software: you can redistribute it and/or modify
1071+# it under the terms of the GNU Affero General Public License as
1072+# published by the Free Software Foundation, either version 3 of the
1073+# License, or (at your option) any later version.
1074+#
1075+# This program is distributed in the hope that it will be useful,
1076+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1077+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1078+# GNU Affero General Public License for more details.
1079+#
1080+# You should have received a copy of the GNU Affero General Public License
1081+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1082+#
1083+# --- Start MAAS 1.0 script metadata ---
1084+# name: gateway-connectivity
1085+# title: Gateway Connectivity Validation
1086+# description: Check if an interface has access to the configured gateway.
1087+# tags:
1088+# - network-validation
1089+# script_type: test
1090+# hardware_type: network
1091+# parallel: instance
1092+# parameters:
1093+# interface:
1094+# type: interface
1095+# apply_configured_networking: True
1096+# timeout: 00:05:00
1097+# --- End MAAS 1.0 script metadata ---
1098+
1099+{{.InjectFile}}
1100+
1101+OPTS=$(getopt -o 'i:h' --long 'interface:,help' -n 'gateway-connectivity' -- "$@")
1102+
1103+if [ $? -ne 0 ]; then
1104+ exit 1
1105+fi
1106+
1107+eval set -- "$OPTS"
1108+unset OPTS
1109+
1110+while true; do
1111+ case "$1" in
1112+ '-i'|'--interface')
1113+ INTERFACE=$2
1114+ shift 2
1115+ continue
1116+ ;;
1117+ '-h'|'--help')
1118+ echo "usage: gateway-connectivity [--interface INTERFACE]"
1119+ echo
1120+ echo "Check if an interface has access to the configured gateway."
1121+ echo
1122+ echo "optional arguments:"
1123+ echo " -h, --help Show this message"
1124+ echo " -i, --interface The interface to test gateway connectivity with."
1125+ echo " Default: Any interface"
1126+ exit 0
1127+ ;;
1128+ '--')
1129+ shift
1130+ break
1131+ ;;
1132+ *)
1133+ echo "Unknown argument $1!" >&2
1134+ exit 1
1135+ esac
1136+done
1137+
1138+
1139+if [ -n "$INTERFACE" ]; then
1140+ IP_ARGS="dev $INTERFACE"
1141+fi
1142+
1143+AWK_SCRIPT='
1144+{
1145+ for(i = 0; i < NF; i++) {
1146+ if($i == "via") {
1147+ print $(i + 1);
1148+ break;
1149+ }
1150+ }
1151+}
1152+'
1153+
1154+for gateway in $(ip route show $IP_ARGS | awk "$AWK_SCRIPT"); do
1155+ if [ -n "$gateways" ]; then
1156+ gateways="$gateways,$gateway"
1157+ else
1158+ gateways="$gateway"
1159+ fi
1160+done
1161+
1162+for gateway in $(ip -6 route show $IP_ARGS | awk "$AWK_SCRIPT"); do
1163+ if [ -n "$gateways" ]; then
1164+ gateways="$gateways,$gateway"
1165+ else
1166+ gateways="$gateway"
1167+ fi
1168+done
1169+
1170+if [ -z "$gateways" ]; then
1171+ echo "WARNING: No gateways configured, skipping test"
1172+ [ -n "$RESULT_PATH" ] && echo "{status: skipped}" >> $RESULT_PATH
1173+ exit 0
1174+fi
1175+
1176+test_interface "$INTERFACE" "$gateways"
1177diff --git a/src/host-info/scripts/install_lldpd.py b/src/host-info/scripts/install_lldpd.py
1178new file mode 120000
1179index 0000000..9d76c0b
1180--- /dev/null
1181+++ b/src/host-info/scripts/install_lldpd.py
1182@@ -0,0 +1 @@
1183+/home/christian/maas/src/metadataserver/builtin_scripts/commissioning_scripts/install_lldpd.py
1184\ No newline at end of file
1185diff --git a/src/host-info/scripts/internet-connectivity.sh b/src/host-info/scripts/internet-connectivity.sh
1186new file mode 100644
1187index 0000000..bcfd95b
1188--- /dev/null
1189+++ b/src/host-info/scripts/internet-connectivity.sh
1190@@ -0,0 +1,98 @@
1191+#!/bin/bash
1192+#
1193+# internet-connectivity - Check if an interface can access the specified URL(s).
1194+#
1195+# Author: Lee Trager <lee.trager@canonical.com>
1196+#
1197+# Copyright (C) 2017-2019 Canonical
1198+#
1199+# This program is free software: you can redistribute it and/or modify
1200+# it under the terms of the GNU Affero General Public License as
1201+# published by the Free Software Foundation, either version 3 of the
1202+# License, or (at your option) any later version.
1203+#
1204+# This program is distributed in the hope that it will be useful,
1205+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1206+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1207+# GNU Affero General Public License for more details.
1208+#
1209+# You should have received a copy of the GNU Affero General Public License
1210+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1211+#
1212+# --- Start MAAS 1.0 script metadata ---
1213+# name: internet-connectivity
1214+# title: Internet Connectivity Validation
1215+# description: Check if an interface can access the specified URL(s).
1216+# tags:
1217+# - internet
1218+# - network-validation
1219+# script_type: test
1220+# hardware_type: network
1221+# parallel: instance
1222+# parameters:
1223+# interface:
1224+# type: interface
1225+# url:
1226+# type: url
1227+# description: A comma separated list of URLs, IPs, or domains to test if
1228+# all the interfaces have access to. Any protocol supported
1229+# by curl is supported. If no protocol or icmp is given the
1230+# URL will be pinged.
1231+# default: https://connectivity-check.ubuntu.com
1232+# required: True
1233+# allow_list: True
1234+# apply_configured_networking: True
1235+# timeout: 00:05:00
1236+# --- End MAAS 1.0 script metadata ---
1237+
1238+{{.InjectFile}}
1239+
1240+OPTS=$(getopt -o 'i:u:h' --long 'interface:,url:,help' -n 'internet-connectivity' -- "$@")
1241+
1242+if [ $? -ne 0 ]; then
1243+ exit 1
1244+fi
1245+
1246+eval set -- "$OPTS"
1247+unset OPTS
1248+
1249+while true; do
1250+ case "$1" in
1251+ '-i'|'--interface')
1252+ INTERFACE=$2
1253+ shift 2
1254+ continue
1255+ ;;
1256+ '-u'|'--url')
1257+ URL=$2
1258+ shift 2
1259+ continue
1260+ ;;
1261+ '-h'|'--help')
1262+ echo "usage: internet-connectivity [--interface INTERFACE] [--url URL]"
1263+ echo
1264+ echo "Check if an interface can access the specified URL(s)."
1265+ echo
1266+ echo "optional arguments:"
1267+ echo " -h, --help Show this message"
1268+ echo " -i, --interface The interface to test internet connectivity with."
1269+ echo " Default: Any interface"
1270+ echo " -u, --url A URL or comma seperated list of URLs that should be accessible."
1271+ echo " Default: https://connectivity-check.ubuntu.com"
1272+ exit 0
1273+ ;;
1274+ '--')
1275+ shift
1276+ break
1277+ ;;
1278+ *)
1279+ echo "Unknown argument $1!" >&2
1280+ exit 1
1281+ esac
1282+done
1283+
1284+if [ -z "$URL" ]; then
1285+ URL="https://connectivity-check.ubuntu.com"
1286+fi
1287+
1288+test_interface "$INTERFACE" "$URL"
1289diff --git a/src/host-info/scripts/maas-get-fruid-api-data.sh b/src/host-info/scripts/maas-get-fruid-api-data.sh
1290new file mode 120000
1291index 0000000..a142ee1
1292--- /dev/null
1293+++ b/src/host-info/scripts/maas-get-fruid-api-data.sh
1294@@ -0,0 +1 @@
1295+/home/christian/maas/src/metadataserver/builtin_scripts/commissioning_scripts/maas-get-fruid-api-data
1296\ No newline at end of file
1297diff --git a/src/host-info/scripts/maas-kernel-cmdline.sh b/src/host-info/scripts/maas-kernel-cmdline.sh
1298new file mode 120000
1299index 0000000..8660214
1300--- /dev/null
1301+++ b/src/host-info/scripts/maas-kernel-cmdline.sh
1302@@ -0,0 +1 @@
1303+/home/christian/maas/src/metadataserver/builtin_scripts/commissioning_scripts/maas-kernel-cmdline
1304\ No newline at end of file
1305diff --git a/src/host-info/scripts/maas-list-modaliases.sh b/src/host-info/scripts/maas-list-modaliases.sh
1306new file mode 120000
1307index 0000000..c11273f
1308--- /dev/null
1309+++ b/src/host-info/scripts/maas-list-modaliases.sh
1310@@ -0,0 +1 @@
1311+/home/christian/maas/src/provisioningserver/refresh/maas-list-modaliases
1312\ No newline at end of file
1313diff --git a/src/host-info/scripts/maas-lshw.sh b/src/host-info/scripts/maas-lshw.sh
1314new file mode 120000
1315index 0000000..6d7fd41
1316--- /dev/null
1317+++ b/src/host-info/scripts/maas-lshw.sh
1318@@ -0,0 +1 @@
1319+/home/christian/maas/src/provisioningserver/refresh/maas-lshw
1320\ No newline at end of file
1321diff --git a/src/host-info/scripts/maas-serial-ports.sh b/src/host-info/scripts/maas-serial-ports.sh
1322new file mode 120000
1323index 0000000..593de7b
1324--- /dev/null
1325+++ b/src/host-info/scripts/maas-serial-ports.sh
1326@@ -0,0 +1 @@
1327+/home/christian/maas/src/provisioningserver/refresh/maas-serial-ports
1328\ No newline at end of file
1329diff --git a/src/host-info/scripts/maas-support-info.sh b/src/host-info/scripts/maas-support-info.sh
1330new file mode 120000
1331index 0000000..435a4b7
1332--- /dev/null
1333+++ b/src/host-info/scripts/maas-support-info.sh
1334@@ -0,0 +1 @@
1335+/home/christian/maas/src/provisioningserver/refresh/maas-support-info
1336\ No newline at end of file
1337diff --git a/src/host-info/scripts/maas_api_helper.py b/src/host-info/scripts/maas_api_helper.py
1338new file mode 120000
1339index 0000000..7763d27
1340--- /dev/null
1341+++ b/src/host-info/scripts/maas_api_helper.py
1342@@ -0,0 +1 @@
1343+/home/christian/maas/src/provisioningserver/refresh/maas_api_helper.py
1344\ No newline at end of file
1345diff --git a/src/host-info/scripts/maas_run_scripts.py b/src/host-info/scripts/maas_run_scripts.py
1346new file mode 120000
1347index 0000000..699c33e
1348--- /dev/null
1349+++ b/src/host-info/scripts/maas_run_scripts.py
1350@@ -0,0 +1 @@
1351+/home/christian/maas/src/metadataserver/user_data/templates/snippets/maas_run_scripts.py
1352\ No newline at end of file
1353diff --git a/src/host-info/scripts/memtester.sh b/src/host-info/scripts/memtester.sh
1354new file mode 120000
1355index 0000000..df3fe25
1356--- /dev/null
1357+++ b/src/host-info/scripts/memtester.sh
1358@@ -0,0 +1 @@
1359+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/memtester.sh
1360\ No newline at end of file
1361diff --git a/src/host-info/scripts/ntp.sh b/src/host-info/scripts/ntp.sh
1362new file mode 120000
1363index 0000000..5378686
1364--- /dev/null
1365+++ b/src/host-info/scripts/ntp.sh
1366@@ -0,0 +1 @@
1367+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/ntp.sh
1368\ No newline at end of file
1369diff --git a/src/host-info/scripts/rack-controller-connectivity.sh b/src/host-info/scripts/rack-controller-connectivity.sh
1370new file mode 100644
1371index 0000000..b9ab9b3
1372--- /dev/null
1373+++ b/src/host-info/scripts/rack-controller-connectivity.sh
1374@@ -0,0 +1,99 @@
1375+#!/bin/bash
1376+#
1377+# rack-controller-connectivity - Check if an interface has access to the booted rack controller.
1378+#
1379+# Author: Lee Trager <lee.trager@canonical.com>
1380+#
1381+# Copyright (C) 2019 Canonical
1382+#
1383+# This program is free software: you can redistribute it and/or modify
1384+# it under the terms of the GNU Affero General Public License as
1385+# published by the Free Software Foundation, either version 3 of the
1386+# License, or (at your option) any later version.
1387+#
1388+# This program is distributed in the hope that it will be useful,
1389+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1390+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1391+# GNU Affero General Public License for more details.
1392+#
1393+# You should have received a copy of the GNU Affero General Public License
1394+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1395+#
1396+# --- Start MAAS 1.0 script metadata ---
1397+# name: rack-controller-connectivity
1398+# title: Gateway Connectivity Validation
1399+# description: Check if an interface has access to the booted rack controller.
1400+# tags:
1401+# - network-validation
1402+# script_type: test
1403+# hardware_type: network
1404+# parallel: instance
1405+# parameters:
1406+# interface:
1407+# type: interface
1408+# apply_configured_networking: True
1409+# timeout: 00:05:00
1410+# --- End MAAS 1.0 script metadata ---
1411+
1412+{{.InjectFile}}
1413+
1414+OPTS=$(getopt -o 'i:h' --long 'interface:,help' -n 'rack-controller-connectivity' -- "$@")
1415+
1416+if [ $? -ne 0 ]; then
1417+ exit 1
1418+fi
1419+
1420+eval set -- "$OPTS"
1421+unset OPTS
1422+
1423+while true; do
1424+ case "$1" in
1425+ '-i'|'--interface')
1426+ INTERFACE=$2
1427+ shift 2
1428+ continue
1429+ ;;
1430+ '-h'|'--help')
1431+ echo "usage: rack-controller-connectivity [--interface INTERFACE]"
1432+ echo
1433+ echo "Check if an interface has access to the booted rack controller."
1434+ echo
1435+ echo "optional arguments:"
1436+ echo " -h, --help Show this message"
1437+ echo " -i, --interface The interface to test rack controller connectivity with."
1438+ echo " Default: Any interface"
1439+ exit 0
1440+ ;;
1441+ '--')
1442+ shift
1443+ break
1444+ ;;
1445+ *)
1446+ echo "Unknown argument $1!" >&2
1447+ exit 1
1448+ esac
1449+done
1450+
1451+# When booting into the ephemeral environment root is the retrieved from
1452+# the rack controller.
1453+for i in $(cat /proc/cmdline); do
1454+ arg=$(echo $i | cut -d '=' -f1)
1455+ if [ "$arg" == "root" ]; then
1456+ value=$(echo $i | cut -d '=' -f2-)
1457+ # MAAS normally specifies the file has "filetype:url"
1458+ filetype=$(echo $value | cut -d ':' -f1)
1459+ if [ "$filetype" == "squash" ]; then
1460+ url=$(echo $value | cut -d ':' -f2-)
1461+ else
1462+ url=$filetype
1463+ fi
1464+ break
1465+ fi
1466+done
1467+
1468+if [ -z "$url" ] || $(echo "$url" | grep -vq "://"); then
1469+ echo "ERROR: Unable to find rack controller URL!"
1470+ exit 1
1471+fi
1472+
1473+test_interface "$INTERFACE" "$url"
1474diff --git a/src/host-info/scripts/seven_z.py b/src/host-info/scripts/seven_z.py
1475new file mode 120000
1476index 0000000..a769a0e
1477--- /dev/null
1478+++ b/src/host-info/scripts/seven_z.py
1479@@ -0,0 +1 @@
1480+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/seven_z.py
1481\ No newline at end of file
1482diff --git a/src/host-info/scripts/smartctl.py b/src/host-info/scripts/smartctl.py
1483new file mode 120000
1484index 0000000..918166a
1485--- /dev/null
1486+++ b/src/host-info/scripts/smartctl.py
1487@@ -0,0 +1 @@
1488+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/smartctl.py
1489\ No newline at end of file
1490diff --git a/src/host-info/scripts/stress-ng-cpu-long.sh b/src/host-info/scripts/stress-ng-cpu-long.sh
1491new file mode 120000
1492index 0000000..bdb03ae
1493--- /dev/null
1494+++ b/src/host-info/scripts/stress-ng-cpu-long.sh
1495@@ -0,0 +1 @@
1496+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/stress-ng-cpu-long.sh
1497\ No newline at end of file
1498diff --git a/src/host-info/scripts/stress-ng-cpu-short.sh b/src/host-info/scripts/stress-ng-cpu-short.sh
1499new file mode 120000
1500index 0000000..1626d40
1501--- /dev/null
1502+++ b/src/host-info/scripts/stress-ng-cpu-short.sh
1503@@ -0,0 +1 @@
1504+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/stress-ng-cpu-short.sh
1505\ No newline at end of file
1506diff --git a/src/host-info/scripts/stress-ng-memory-long.sh b/src/host-info/scripts/stress-ng-memory-long.sh
1507new file mode 120000
1508index 0000000..911a85f
1509--- /dev/null
1510+++ b/src/host-info/scripts/stress-ng-memory-long.sh
1511@@ -0,0 +1 @@
1512+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/stress-ng-memory-long.sh
1513\ No newline at end of file
1514diff --git a/src/host-info/scripts/stress-ng-memory-short.sh b/src/host-info/scripts/stress-ng-memory-short.sh
1515new file mode 120000
1516index 0000000..2cc8c96
1517--- /dev/null
1518+++ b/src/host-info/scripts/stress-ng-memory-short.sh
1519@@ -0,0 +1 @@
1520+/home/christian/maas/src/metadataserver/builtin_scripts/testing_scripts/stress-ng-memory-short.sh
1521\ No newline at end of file
1522diff --git a/src/metadataserver/api.py b/src/metadataserver/api.py
1523index 787e628..add0920 100644
1524--- a/src/metadataserver/api.py
1525+++ b/src/metadataserver/api.py
1526@@ -1442,6 +1442,80 @@ class MAASScriptsHandler(OperationsHandler):
1527 )
1528
1529
1530+class MAASScriptsMetadataHandler(OperationsHandler):
1531+ def _get_script_meta_data(self, script_set, mtime, include_finshed=False):
1532+ if script_set is None:
1533+ return []
1534+
1535+ meta_data = []
1536+ for script_result in script_set:
1537+ # Don't rerun Scripts which have already run.
1538+ if (
1539+ not include_finshed
1540+ and script_result.status
1541+ not in SCRIPT_STATUS_RUNNING_OR_PENDING
1542+ ):
1543+ continue
1544+
1545+ md_item = {}
1546+ if script_result.script is None:
1547+ # Script was deleted by the user and it is not a builtin
1548+ # commissioning script. Don't expect a result.
1549+ script_result.delete()
1550+ continue
1551+
1552+ md_item = get_script_result_properties(script_result)
1553+ meta_data.append(md_item)
1554+ return meta_data
1555+
1556+ def read(self, request, version, mac=None):
1557+ node = get_queried_node(request)
1558+ mtime = time.time()
1559+ meta_data = {}
1560+
1561+ if (
1562+ node.status
1563+ in (
1564+ NODE_STATUS.COMMISSIONING,
1565+ NODE_STATUS.DEPLOYED,
1566+ NODE_STATUS.ENTERING_RESCUE_MODE,
1567+ NODE_STATUS.RESCUE_MODE,
1568+ )
1569+ and node.current_commissioning_script_set is not None
1570+ ):
1571+ # Prefetch all the data we need.
1572+ qs = node.current_commissioning_script_set.scriptresult_set
1573+ qs = qs.select_related("script", "script__script")
1574+ # After the script runner finishes sending all commissioning
1575+ # results it redownloads the script tar. It does this in-case
1576+ # a commissioning script discovers hardware associated with
1577+ # hardware identified in the for_hardware field of a script.
1578+ # select_for_hardware_scripts() processes the output of the
1579+ # builtin commissioning scripts and adds any associated script.
1580+ # This does not need to happen the first time the script runner
1581+ # downloads the tar as the region has not yet received new
1582+ # data.
1583+ for script_result in qs:
1584+ if script_result.status != SCRIPT_STATUS.PENDING:
1585+ script_set = node.current_commissioning_script_set
1586+ script_set.select_for_hardware_scripts()
1587+ break
1588+ meta_data["commissioning_scripts"] = self._get_script_meta_data(
1589+ node.current_commissioning_script_set, mtime
1590+ )
1591+
1592+ if node.current_testing_script_set is not None:
1593+ qs = node.current_testing_script_set.scriptresult_set
1594+ qs = qs.select_related("script", "script__script")
1595+ meta_data["testing_scripts"] = self._get_script_meta_data(
1596+ node.current_testing_script_set, mtime
1597+ )
1598+
1599+ return HttpResponse(
1600+ json.dumps({"1.0": meta_data}), content_type="application/json"
1601+ )
1602+
1603+
1604 class AnonMetaDataHandler(VersionIndexHandler):
1605 """Anonymous metadata."""
1606
1607diff --git a/src/metadataserver/builtin_scripts/testing_scripts/badblocks.py b/src/metadataserver/builtin_scripts/testing_scripts/badblocks.py
1608index f52f544..5df0f13 100755
1609--- a/src/metadataserver/builtin_scripts/testing_scripts/badblocks.py
1610+++ b/src/metadataserver/builtin_scripts/testing_scripts/badblocks.py
1611@@ -203,7 +203,9 @@ def run_badblocks(storage, destructive=False):
1612
1613 if __name__ == "__main__":
1614 # Determine if badblocks should run destructively from the script name.
1615- if "destructive" in sys.argv[0]:
1616+ if "destructive" in sys.argv[0] or (
1617+ len(sys.argv) >= 2 and sys.argv[1] == "destructive"
1618+ ):
1619 destructive = True
1620 else:
1621 destructive = False
1622diff --git a/src/metadataserver/builtin_scripts/testing_scripts/gateway-connectivity.sh b/src/metadataserver/builtin_scripts/testing_scripts/gateway-connectivity.sh
1623index 543d6a0..f6e8fd8 100644
1624--- a/src/metadataserver/builtin_scripts/testing_scripts/gateway-connectivity.sh
1625+++ b/src/metadataserver/builtin_scripts/testing_scripts/gateway-connectivity.sh
1626@@ -4,7 +4,7 @@
1627 #
1628 # Author: Lee Trager <lee.trager@canonical.com>
1629 #
1630-# Copyright (C) 2019 Canonical
1631+# Copyright (C) 2019-2022 Canonical
1632 #
1633 # This program is free software: you can redistribute it and/or modify
1634 # it under the terms of the GNU Affero General Public License as
1635diff --git a/src/metadataserver/templates/hardware_sync_service.template b/src/metadataserver/templates/hardware_sync_service.template
1636index 806bd42..bfc9e68 100644
1637--- a/src/metadataserver/templates/hardware_sync_service.template
1638+++ b/src/metadataserver/templates/hardware_sync_service.template
1639@@ -8,4 +8,9 @@ After=network.target
1640 type=oneshot
1641 ExecStartPre=/usr/bin/wget -O /usr/bin/maas-hardware-sync {{ maas_url }}/hardware-sync/{{ architecture }}
1642 ExecStartPre=/bin/chmod 0755 /usr/bin/maas-hardware-sync
1643-ExecStart=/bin/echo "TODO run hardware-sync commands"
1644+ExecStartPre=/usr/bin/maas-hardware-sync get-machine-token\
1645+ --maas-url '{{ maas_url }}'\
1646+ --admin_token '{{ admin_token }}'\
1647+ --system-id '{{ system_id }}'\
1648+ --token-file '{{ token_file_path }}'
1649+ExecStart=/usr/bin/maas-hardware-sync report-results --config '{{ token_file_path }}'
1650diff --git a/src/metadataserver/tests/test_api.py b/src/metadataserver/tests/test_api.py
1651index 8b8b5bd..e6a4fbb 100644
1652--- a/src/metadataserver/tests/test_api.py
1653+++ b/src/metadataserver/tests/test_api.py
1654@@ -4029,3 +4029,124 @@ class TestEnlistViews(MAASServerTestCase):
1655 response.content.decode(settings.DEFAULT_CHARSET).splitlines(),
1656 ContainsAll(("user-data", "meta-data")),
1657 )
1658+
1659+
1660+class TestMAASScriptsMetadataHandler(MAASServerTestCase):
1661+ """Tests for the script metadata"""
1662+
1663+ def setUp(self):
1664+ super().setUp()
1665+ load_builtin_scripts()
1666+
1667+ def test_node_with_builtin_script_set(self):
1668+ node = factory.make_Node(
1669+ status=NODE_STATUS.DEPLOYED, with_empty_script_sets=True
1670+ )
1671+ response = make_node_client(node=node).get(
1672+ reverse("maas-scripts-metadata", args=["latest"])
1673+ )
1674+ self.assertEqual(
1675+ (http.client.OK, "application/json"),
1676+ (response.status_code, response["Content-Type"]),
1677+ )
1678+ body = json.loads(response.content)
1679+ expected_commissioning_metadata = [
1680+ get_script_result_properties(script_result)
1681+ for script_result in node.current_commissioning_script_set
1682+ ]
1683+ self.assertCountEqual(
1684+ expected_commissioning_metadata,
1685+ body["1.0"]["commissioning_scripts"],
1686+ )
1687+
1688+ def test_node_with_custom_commissioning_script_set(self):
1689+ script_name = factory.make_name("script")
1690+ script = factory.make_Script(
1691+ name=script_name, script_type=SCRIPT_TYPE.COMMISSIONING
1692+ )
1693+ node = factory.make_Node(status=NODE_STATUS.COMMISSIONING)
1694+ node.current_commissioning_script_set = (
1695+ ScriptSet.objects.create_commissioning_script_set(node)
1696+ )
1697+ node.save()
1698+ response = make_node_client(node=node).get(
1699+ reverse("maas-scripts-metadata", args=["latest"])
1700+ )
1701+ self.assertEqual(
1702+ (http.client.OK, "application/json"),
1703+ (response.status_code, response["Content-Type"]),
1704+ )
1705+ body = json.loads(response.content)
1706+ expected_commissioning_metadata = [
1707+ get_script_result_properties(script_result)
1708+ for script_result in node.current_commissioning_script_set
1709+ ]
1710+ self.assertCountEqual(
1711+ expected_commissioning_metadata,
1712+ body["1.0"]["commissioning_scripts"],
1713+ )
1714+ self.assertIn(
1715+ script.name,
1716+ [s["name"] for s in body["1.0"]["commissioning_scripts"]],
1717+ )
1718+
1719+ def test_node_with_custom_testing_script_set(self):
1720+ script_name = factory.make_name("script")
1721+ script = factory.make_Script(
1722+ name=script_name, script_type=SCRIPT_TYPE.TESTING
1723+ )
1724+ node = factory.make_Node(status=NODE_STATUS.COMMISSIONING)
1725+ node.current_testing_script_set = (
1726+ ScriptSet.objects.create_testing_script_set(node, scripts=[script])
1727+ )
1728+ node.save()
1729+ response = make_node_client(node=node).get(
1730+ reverse("maas-scripts-metadata", args=["latest"])
1731+ )
1732+ self.assertEqual(
1733+ (http.client.OK, "application/json"),
1734+ (response.status_code, response["Content-Type"]),
1735+ )
1736+ body = json.loads(response.content)
1737+ expected_testing_metadata = [
1738+ get_script_result_properties(script_result)
1739+ for script_result in node.current_testing_script_set
1740+ ]
1741+ self.assertCountEqual(
1742+ expected_testing_metadata, body["1.0"]["testing_scripts"]
1743+ )
1744+ self.assertIn(
1745+ script.name,
1746+ [s["name"] for s in body["1.0"]["testing_scripts"]],
1747+ )
1748+
1749+ def test_deployed_machine_script_set(self):
1750+ script_name = factory.make_name("script")
1751+ script = factory.make_Script(
1752+ name=script_name, script_type=SCRIPT_TYPE.COMMISSIONING
1753+ )
1754+ machine = factory.make_Machine(status=NODE_STATUS.DEPLOYED)
1755+ machine.current_commissioning_script_set = (
1756+ ScriptSet.objects.create_deployed_machine_script_set(machine)
1757+ )
1758+ machine.save()
1759+ response = make_node_client(node=machine).get(
1760+ reverse("maas-scripts-metadata", args=["latest"])
1761+ )
1762+ self.assertEqual(
1763+ (http.client.OK, "application/json"),
1764+ (response.status_code, response["Content-Type"]),
1765+ )
1766+ body = json.loads(response.content)
1767+ expected_commissioning_metadata = [
1768+ get_script_result_properties(script_result)
1769+ for script_result in machine.current_commissioning_script_set
1770+ ]
1771+ self.assertCountEqual(
1772+ expected_commissioning_metadata,
1773+ body["1.0"]["commissioning_scripts"],
1774+ )
1775+ self.assertNotIn(
1776+ script.name,
1777+ [s["name"] for s in body["1.0"]["commissioning_scripts"]],
1778+ )
1779diff --git a/src/metadataserver/tests/test_vendor_data.py b/src/metadataserver/tests/test_vendor_data.py
1780index 6c4000a..7c32692 100644
1781--- a/src/metadataserver/tests/test_vendor_data.py
1782+++ b/src/metadataserver/tests/test_vendor_data.py
1783@@ -20,6 +20,7 @@ import yaml
1784
1785 from maasserver.enum import NODE_STATUS
1786 from maasserver.models import Config, ControllerInfo, NodeMetadata
1787+from maasserver.models.user import get_auth_tokens
1788 from maasserver.node_status import COMMISSIONING_LIKE_STATUSES
1789 from maasserver.server_address import get_maas_facing_server_host
1790 from maasserver.testing.factory import factory
1791@@ -39,6 +40,7 @@ from metadataserver.vendor_data import (
1792 generate_system_info,
1793 get_node_maas_url,
1794 get_vendor_data,
1795+ HARDWARE_SYNC_MACHINE_TOKEN_PATH,
1796 HARDWARE_SYNC_SERVICE_TEMPLATE,
1797 HARDWARE_SYNC_TIMER_TEMPLATE,
1798 )
1799@@ -710,16 +712,22 @@ class TestGenerateHardwareSyncSystemdConfiguration(MAASServerTestCase):
1800 self.assertRaises(StopIteration, next, config)
1801
1802 def test_returns_timer_and_service_when_node_enable_hw_sync_is_True(self):
1803+ self.maxDiff = None
1804+ user = factory.make_admin()
1805+ token = get_auth_tokens(user)[0]
1806 node = factory.make_Node(
1807 status=NODE_STATUS.DEPLOYING,
1808+ owner=user,
1809 enable_hw_sync=True,
1810 )
1811 config = generate_hardware_sync_systemd_configuration(node)
1812 expected_interval = Config.objects.get_configs(
1813 ["hardware_sync_interval"]
1814 )
1815-
1816- maas_url = get_node_maas_url(node)
1817+ expected_token = f"{token.consumer.key}:{token.key}:{token.secret}"
1818+ if token.consumer.secret:
1819+ expected_token += f":{token.consumer.secret}"
1820+ expected_maas_url = get_node_maas_url(node)
1821
1822 expected = (
1823 "write_files",
1824@@ -732,7 +740,11 @@ class TestGenerateHardwareSyncSystemdConfiguration(MAASServerTestCase):
1825 },
1826 {
1827 "content": self._get_service_template().substitute(
1828- maas_url=maas_url, architecture=node.architecture
1829+ architecture=node.architecture,
1830+ admin_token=expected_token,
1831+ maas_url=expected_maas_url,
1832+ system_id=node.system_id,
1833+ token_file_path=HARDWARE_SYNC_MACHINE_TOKEN_PATH,
1834 ),
1835 "path": "/lib/systemd/system/maas_hardware_sync.service",
1836 },
1837diff --git a/src/metadataserver/urls.py b/src/metadataserver/urls.py
1838index 0bd1f08..2e4d21d 100644
1839--- a/src/metadataserver/urls.py
1840+++ b/src/metadataserver/urls.py
1841@@ -17,6 +17,7 @@ from metadataserver.api import (
1842 EnlistVersionIndexHandler,
1843 IndexHandler,
1844 MAASScriptsHandler,
1845+ MAASScriptsMetadataHandler,
1846 MetaDataHandler,
1847 StatusHandler,
1848 UserDataHandler,
1849@@ -40,6 +41,9 @@ index_handler = OperationsResource(IndexHandler, authentication=api_auth)
1850 maas_scripts_handler = OperationsResource(
1851 MAASScriptsHandler, authentication=api_auth
1852 )
1853+maas_scripts_metadata_handler = OperationsResource(
1854+ MAASScriptsMetadataHandler, authentication=api_auth
1855+)
1856 commissioning_scripts_handler = OperationsResource(
1857 CommissioningScriptsHandler, authentication=api_auth
1858 )
1859@@ -90,11 +94,16 @@ node_patterns = [
1860 # definitive. maas-scripts is xz compressed while
1861 # maas-commissioning-scripts is not.
1862 url(
1863- r"^[/]*(?P<version>[^/]+)/maas-scripts",
1864+ r"^[/]*(?P<version>[^/]+)/maas-scripts(?!-metadata)",
1865 maas_scripts_handler,
1866 name="maas-scripts",
1867 ),
1868 url(
1869+ r"^[/]*(?P<version>[^/]+)/maas-scripts-metadata",
1870+ maas_scripts_metadata_handler,
1871+ name="maas-scripts-metadata",
1872+ ),
1873+ url(
1874 r"^[/]*(?P<version>[^/]+)/maas-commissioning-scripts",
1875 commissioning_scripts_handler,
1876 name="commissioning-scripts",
1877diff --git a/src/metadataserver/user_data/templates/snippets/maas_run_scripts.py b/src/metadataserver/user_data/templates/snippets/maas_run_scripts.py
1878index 09e8855..cfce8e9 100644
1879--- a/src/metadataserver/user_data/templates/snippets/maas_run_scripts.py
1880+++ b/src/metadataserver/user_data/templates/snippets/maas_run_scripts.py
1881@@ -26,6 +26,7 @@ import yaml
1882
1883 # imports from maas_api_helpers (only used in tests)
1884 # {% comment %}
1885+# {{if .ImportAPIHelper }}
1886 from snippets.maas_api_helper import (
1887 capture_script_output,
1888 Config,
1889@@ -38,6 +39,7 @@ from snippets.maas_api_helper import (
1890 signal,
1891 )
1892
1893+# {{end}}
1894 # {% endcomment %}
1895
1896
1897@@ -186,6 +188,15 @@ class Script:
1898 }
1899
1900
1901+LOCAL_SCRIPTS_RUNNER = "/usr/bin/maas-hardware-sync"
1902+
1903+
1904+class LocalScript(Script):
1905+ @property
1906+ def command(self):
1907+ return [LOCAL_SCRIPTS_RUNNER, self.name]
1908+
1909+
1910 def oauth_token(string):
1911 """Helper to use as type for OAuth token commandline args."""
1912 try:
1913@@ -222,6 +233,12 @@ def parse_args(args):
1914 action="store_true",
1915 default=False,
1916 )
1917+ parser.add_argument(
1918+ "--local-scripts",
1919+ type=bool,
1920+ default=False,
1921+ help="Path to locally installed scripts, if one exists",
1922+ )
1923 subparsers = parser.add_subparsers(
1924 metavar="ACTION",
1925 dest="action",
1926@@ -332,6 +349,23 @@ def fetch_scripts(maas_url, metadata_url, paths, credentials):
1927 ]
1928
1929
1930+def load_local_scripts(maas_url, metadata_url, paths, credentials):
1931+ res = geturl(
1932+ metadata_url + "maas-scripts-metadata",
1933+ credentials=credentials,
1934+ retry=False,
1935+ )
1936+ if res.status == http.client.NO_CONTENT:
1937+ raise ExitError("No script returned")
1938+
1939+ data = json.load(res)
1940+
1941+ return [
1942+ LocalScript(script_info, maas_url, paths)
1943+ for script_info in data["1.0"]["commissioning_scripts"]
1944+ ]
1945+
1946+
1947 def get_machine_token(maas_url, admin_token, system_id):
1948 """Return a dict with machine token and MAAS URL."""
1949 try:
1950@@ -370,6 +404,41 @@ def write_token(credentials, path=None):
1951 print(content)
1952
1953
1954+def _action_report_results_scripts(
1955+ ns, maas_url, metadata_url, paths, config, script
1956+):
1957+ print(
1958+ f"* Running '{script.name}'...",
1959+ end="\n" if ns.debug else " ",
1960+ )
1961+ result = script.run(console_output=ns.debug)
1962+ if ns.debug:
1963+ print(
1964+ f"* Finished running '{script.name}': ",
1965+ end=" ",
1966+ )
1967+ if result.exit_status == 0:
1968+ print("success")
1969+ else:
1970+ print(
1971+ "FAILED (status {result.exit_status}): {result.error}".format(
1972+ result=result
1973+ )
1974+ )
1975+ signal(
1976+ metadata_url,
1977+ config.credentials,
1978+ result.status,
1979+ error=result.error,
1980+ script_name=script.name,
1981+ script_result_id=script.info.get("script_result_id"),
1982+ files=result.result_files,
1983+ runtime=result.runtime,
1984+ exit_status=result.exit_status,
1985+ script_version_id=script.info.get("script_version_id"),
1986+ )
1987+
1988+
1989 def action_report_results(ns):
1990 config = get_config(ns)
1991 if not config.metadata_url:
1992@@ -381,46 +450,30 @@ def action_report_results(ns):
1993 maas_url = get_base_url(config.metadata_url)
1994 metadata_url = maas_url + "/MAAS/metadata/" + MD_VERSION + "/"
1995
1996- print(
1997- "* Fetching scripts from {url} to {dir}".format(
1998- url=metadata_url, dir=paths.scripts
1999- )
2000- )
2001- for script in fetch_scripts(
2002- maas_url, metadata_url, paths, config.credentials
2003- ):
2004- if not script.should_run():
2005- continue
2006- print(
2007- f"* Running '{script.name}'...",
2008- end="\n" if ns.debug else " ",
2009- )
2010- result = script.run(console_output=ns.debug)
2011- if ns.debug:
2012- print(
2013- f"* Finished running '{script.name}': ",
2014- end=" ",
2015+ if ns.local_scripts:
2016+ print("* Fetching script metadata from {url}".format(url=metadata_url))
2017+ for script in load_local_scripts(
2018+ maas_url, metadata_url, paths, config.credentials
2019+ ):
2020+ if not script.should_run():
2021+ continue
2022+ _action_report_results_scripts(
2023+ ns, maas_url, metadata_url, paths, config, script
2024 )
2025- if result.exit_status == 0:
2026- print("success")
2027- else:
2028- print(
2029- "FAILED (status {result.exit_status}): {result.error}".format(
2030- result=result
2031- )
2032+ else:
2033+ print(
2034+ "* Fetching scripts from {url} to {dir}".format(
2035+ url=metadata_url, dir=paths.scripts
2036 )
2037- signal(
2038- metadata_url,
2039- config.credentials,
2040- result.status,
2041- error=result.error,
2042- script_name=script.name,
2043- script_result_id=script.info.get("script_result_id"),
2044- files=result.result_files,
2045- runtime=result.runtime,
2046- exit_status=result.exit_status,
2047- script_version_id=script.info.get("script_version_id"),
2048 )
2049+ for script in fetch_scripts(
2050+ maas_url, metadata_url, paths, config.credentials
2051+ ):
2052+ if not script.should_run():
2053+ continue
2054+ _action_report_results_scripts(
2055+ ns, maas_url, metadata_url, paths, config, script
2056+ )
2057
2058
2059 def action_register_machine(ns):
2060diff --git a/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_scripts.py b/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_scripts.py
2061index ef0b3be..3823aa9 100644
2062--- a/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_scripts.py
2063+++ b/src/metadataserver/user_data/templates/snippets/tests/test_maas_run_scripts.py
2064@@ -13,6 +13,8 @@ from provisioningserver.refresh.maas_api_helper import Credentials
2065 from snippets import maas_run_scripts
2066 from snippets.maas_run_scripts import (
2067 get_config,
2068+ LOCAL_SCRIPTS_RUNNER,
2069+ LocalScript,
2070 main,
2071 parse_args,
2072 Script,
2073@@ -517,3 +519,45 @@ class TestWriteToken(MAASTestCase):
2074 yaml.safe_load(path),
2075 {"reporting": {"maas": token_info}},
2076 )
2077+
2078+
2079+class TestLocalScript(MAASTestCase):
2080+ def test_command(self):
2081+ info = {
2082+ "name": "myscript",
2083+ "path": "commissioning-scripts/myscript",
2084+ "timeout_seconds": 100,
2085+ }
2086+ paths = ScriptsPaths(base_path=Path("/base"))
2087+ script = LocalScript(info, "http://maas.example.com", paths)
2088+ self.assertCountEqual(
2089+ [LOCAL_SCRIPTS_RUNNER, script.name], script.command
2090+ )
2091+
2092+ def test_run(self):
2093+ fake_process = self.patch(
2094+ maas_run_scripts.subprocess, "Popen"
2095+ ).return_value
2096+ mock_capture_script_output = self.patch(
2097+ maas_run_scripts, "capture_script_output"
2098+ )
2099+ info = {
2100+ "name": "myscript",
2101+ "path": "commissioning-scripts/myscript",
2102+ "timeout_seconds": 100,
2103+ }
2104+ paths = ScriptsPaths(base_path=Path("/base"))
2105+ script = LocalScript(info, "http://maas.example.com", paths)
2106+ result = script.run()
2107+ self.assertEqual(result.exit_status, 0)
2108+ self.assertEqual(result.status, "WORKING")
2109+ self.assertIsNone(result.error)
2110+ self.assertGreater(result.runtime, 0.0)
2111+ mock_capture_script_output.assert_called_once_with(
2112+ fake_process,
2113+ script.combined_path,
2114+ script.stdout_path,
2115+ script.stderr_path,
2116+ timeout_seconds=100,
2117+ console_output=False,
2118+ )
2119diff --git a/src/metadataserver/vendor_data.py b/src/metadataserver/vendor_data.py
2120index 2d17e6a..39d93c0 100644
2121--- a/src/metadataserver/vendor_data.py
2122+++ b/src/metadataserver/vendor_data.py
2123@@ -18,6 +18,7 @@ import yaml
2124 from maasserver import ntp
2125 from maasserver.models import Config, NodeMetadata
2126 from maasserver.models.controllerinfo import get_target_version
2127+from maasserver.models.user import get_auth_tokens
2128 from maasserver.node_status import COMMISSIONING_LIKE_STATUSES
2129 from maasserver.permissions import NodePermission
2130 from maasserver.preseed import get_network_yaml_settings
2131@@ -33,6 +34,7 @@ VIRSH_PASSWORD_METADATA_KEY = "virsh_password"
2132
2133 HARDWARE_SYNC_TIMER_TEMPLATE = "hardware_sync_timer.template"
2134 HARDWARE_SYNC_SERVICE_TEMPLATE = "hardware_sync_service.template"
2135+HARDWARE_SYNC_MACHINE_TOKEN_PATH = "/tmp/mymachine-creds.yaml"
2136
2137
2138 def get_vendor_data(node, proxy):
2139@@ -389,8 +391,23 @@ def generate_hardware_sync_systemd_configuration(node):
2140 hardware_sync_timer = hardware_sync_timer_tmpl.substitute(
2141 hardware_sync_interval=hardware_sync_interval
2142 )
2143+
2144+ admin_token = ""
2145+ if node.owner is not None:
2146+ tokens = get_auth_tokens(node.owner)
2147+ if tokens:
2148+ admin_token = (
2149+ f"{tokens[0].consumer.key}:{tokens[0].key}:{tokens[0].secret}"
2150+ )
2151+ if tokens[0].consumer.secret:
2152+ admin_token += f":{tokens[0].consumer.secret}"
2153+
2154 hardware_sync_service = hardware_sync_service_tmpl.substitute(
2155- maas_url=maas_url, architecture=node.architecture
2156+ architecture=node.architecture,
2157+ admin_token=admin_token,
2158+ maas_url=maas_url,
2159+ system_id=node.system_id,
2160+ token_file_path=HARDWARE_SYNC_MACHINE_TOKEN_PATH,
2161 )
2162
2163 yield "write_files", [

Subscribers

People subscribed via source and target branches