refactor: move commands to cmd/
This commit is contained in:
		
							
								
								
									
										61
									
								
								cmd/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								cmd/container.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| // Copyright 2023 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/rs/zerolog/log" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/container" | ||||
| ) | ||||
|  | ||||
| type ContainerCmd struct { | ||||
| 	Filter string `help:"filter by name"` | ||||
|  | ||||
| 	List    ContainerListCmd    `cmd:"" help:"list containers"` | ||||
| 	Cleanup ContainerCleanupCmd `cmd:"" help:"cleanup containers"` | ||||
| } | ||||
|  | ||||
| func (cmd ContainerCmd) Containers() (names []string) { | ||||
| 	images, err := container.Images() | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err).Msg("") | ||||
| 	} | ||||
|  | ||||
| 	for _, img := range images { | ||||
| 		if cmd.Filter != "" && !strings.Contains(img.Name, cmd.Filter) { | ||||
| 			continue | ||||
| 		} | ||||
| 		names = append(names, img.Name) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type ContainerListCmd struct{} | ||||
|  | ||||
| func (cmd ContainerListCmd) Run(containerCmd *ContainerCmd) (err error) { | ||||
| 	for _, name := range containerCmd.Containers() { | ||||
| 		fmt.Println(name) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type ContainerCleanupCmd struct{} | ||||
|  | ||||
| func (cmd ContainerCleanupCmd) Run(containerCmd *ContainerCmd) (err error) { | ||||
| 	var output []byte | ||||
| 	for _, name := range containerCmd.Containers() { | ||||
| 		output, err = exec.Command(container.Runtime, "image", "rm", name). | ||||
| 			CombinedOutput() | ||||
| 		if err != nil { | ||||
| 			log.Error().Err(err).Str("output", string(output)).Msg("") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										380
									
								
								cmd/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										380
									
								
								cmd/db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,380 @@ | ||||
| // Copyright 2019 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/qemu" | ||||
| ) | ||||
|  | ||||
| // Change on ANY database update | ||||
| const currentDatabaseVersion = 3 | ||||
|  | ||||
| const versionField = "db_version" | ||||
|  | ||||
| type logEntry struct { | ||||
| 	ID        int | ||||
| 	Tag       string | ||||
| 	Timestamp time.Time | ||||
|  | ||||
| 	qemu.System | ||||
| 	config.Artifact | ||||
| 	distro.KernelInfo | ||||
| 	phasesResult | ||||
| } | ||||
|  | ||||
| func createLogTable(db *sql.DB) (err error) { | ||||
| 	_, err = db.Exec(` | ||||
| 	CREATE TABLE IF NOT EXISTS log ( | ||||
| 		id		INTEGER PRIMARY KEY, | ||||
| 		time		DATETIME DEFAULT CURRENT_TIMESTAMP, | ||||
| 		tag		TEXT, | ||||
|  | ||||
| 		name		TEXT, | ||||
| 		type		TEXT, | ||||
|  | ||||
| 		distro_type	TEXT, | ||||
| 		distro_release	TEXT, | ||||
| 		kernel_release	TEXT, | ||||
|  | ||||
| 		internal_err	TEXT, | ||||
|  | ||||
| 		build_output	TEXT, | ||||
| 		build_ok	BOOLEAN, | ||||
|  | ||||
| 		run_output	TEXT, | ||||
| 		run_ok		BOOLEAN, | ||||
|  | ||||
| 		test_output	TEXT, | ||||
| 		test_ok		BOOLEAN, | ||||
|  | ||||
| 		qemu_stdout	TEXT, | ||||
| 		qemu_stderr	TEXT, | ||||
|  | ||||
| 		kernel_panic	BOOLEAN, | ||||
| 		timeout_kill	BOOLEAN | ||||
| 	)`) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func createMetadataTable(db *sql.DB) (err error) { | ||||
| 	_, err = db.Exec(` | ||||
| 	CREATE TABLE IF NOT EXISTS metadata ( | ||||
| 		id	INTEGER PRIMARY KEY, | ||||
| 		key	TEXT UNIQUE, | ||||
| 		value	TEXT | ||||
| 	)`) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func metaChkValue(db *sql.DB, key string) (exist bool, err error) { | ||||
| 	sql := "SELECT EXISTS(SELECT id FROM metadata WHERE key = $1)" | ||||
| 	stmt, err := db.Prepare(sql) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	err = stmt.QueryRow(key).Scan(&exist) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func metaGetValue(db *sql.DB, key string) (value string, err error) { | ||||
| 	stmt, err := db.Prepare("SELECT value FROM metadata " + | ||||
| 		"WHERE key = $1") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	err = stmt.QueryRow(key).Scan(&value) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func metaSetValue(db *sql.DB, key, value string) (err error) { | ||||
| 	stmt, err := db.Prepare("INSERT OR REPLACE INTO metadata " + | ||||
| 		"(key, value) VALUES ($1, $2)") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	_, err = stmt.Exec(key, value) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func getVersion(db *sql.DB) (version int, err error) { | ||||
| 	s, err := metaGetValue(db, versionField) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	version, err = strconv.Atoi(s) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func addToLog(db *sql.DB, q *qemu.System, ka config.Artifact, | ||||
| 	ki distro.KernelInfo, res *phasesResult, tag string) (err error) { | ||||
|  | ||||
| 	stmt, err := db.Prepare("INSERT INTO log (name, type, tag, " + | ||||
| 		"distro_type, distro_release, kernel_release, " + | ||||
| 		"internal_err, " + | ||||
| 		"build_output, build_ok, " + | ||||
| 		"run_output, run_ok, " + | ||||
| 		"test_output, test_ok, " + | ||||
| 		"qemu_stdout, qemu_stderr, " + | ||||
| 		"kernel_panic, timeout_kill) " + | ||||
| 		"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, " + | ||||
| 		"$10, $11, $12, $13, $14, $15, $16, $17);") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	_, err = stmt.Exec( | ||||
| 		ka.Name, ka.Type, tag, | ||||
| 		ki.Distro.ID, ki.Distro.Release, ki.KernelRelease, | ||||
| 		res.InternalErrorString, | ||||
| 		res.Build.Output, res.Build.Ok, | ||||
| 		res.Run.Output, res.Run.Ok, | ||||
| 		res.Test.Output, res.Test.Ok, | ||||
| 		q.Stdout, q.Stderr, | ||||
| 		q.KernelPanic, q.KilledByTimeout, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func getAllLogs(db *sql.DB, tag string, num int) (les []logEntry, err error) { | ||||
| 	stmt, err := db.Prepare("SELECT id, time, name, type, tag, " + | ||||
| 		"distro_type, distro_release, kernel_release, " + | ||||
| 		"internal_err, " + | ||||
| 		"build_ok, run_ok, test_ok, kernel_panic, " + | ||||
| 		"timeout_kill FROM log ORDER BY datetime(time) DESC " + | ||||
| 		"LIMIT $1") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	rows, err := stmt.Query(num) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
|  | ||||
| 	for rows.Next() { | ||||
| 		var internalErr sql.NullString | ||||
| 		le := logEntry{} | ||||
| 		err = rows.Scan(&le.ID, &le.Timestamp, | ||||
| 			&le.Name, &le.Type, &le.Tag, | ||||
| 			&le.Distro.ID, &le.Distro.Release, &le.KernelRelease, | ||||
| 			&internalErr, | ||||
| 			&le.Build.Ok, &le.Run.Ok, &le.Test.Ok, | ||||
| 			&le.KernelPanic, &le.KilledByTimeout, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		le.InternalErrorString = internalErr.String | ||||
|  | ||||
| 		if tag == "" || tag == le.Tag { | ||||
| 			les = append(les, le) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func getAllArtifactLogs(db *sql.DB, tag string, num int, ka config.Artifact) ( | ||||
| 	les []logEntry, err error) { | ||||
|  | ||||
| 	stmt, err := db.Prepare("SELECT id, time, name, type, tag, " + | ||||
| 		"distro_type, distro_release, kernel_release, " + | ||||
| 		"internal_err, " + | ||||
| 		"build_ok, run_ok, test_ok, kernel_panic, " + | ||||
| 		"timeout_kill FROM log WHERE name=$1 AND type=$2 " + | ||||
| 		"ORDER BY datetime(time) DESC LIMIT $3") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	rows, err := stmt.Query(ka.Name, ka.Type, num) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
|  | ||||
| 	for rows.Next() { | ||||
| 		var internalErr sql.NullString | ||||
| 		le := logEntry{} | ||||
| 		err = rows.Scan(&le.ID, &le.Timestamp, | ||||
| 			&le.Name, &le.Type, &le.Tag, | ||||
| 			&le.Distro.ID, &le.Distro.Release, &le.KernelRelease, | ||||
| 			&internalErr, | ||||
| 			&le.Build.Ok, &le.Run.Ok, &le.Test.Ok, | ||||
| 			&le.KernelPanic, &le.KilledByTimeout, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		le.InternalErrorString = internalErr.String | ||||
|  | ||||
| 		if tag == "" || tag == le.Tag { | ||||
| 			les = append(les, le) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func getLogByID(db *sql.DB, id int) (le logEntry, err error) { | ||||
| 	stmt, err := db.Prepare("SELECT id, time, name, type, tag, " + | ||||
| 		"distro_type, distro_release, kernel_release, " + | ||||
| 		"internal_err, " + | ||||
| 		"build_ok, run_ok, test_ok, " + | ||||
| 		"build_output, run_output, test_output, " + | ||||
| 		"qemu_stdout, qemu_stderr, " + | ||||
| 		"kernel_panic, timeout_kill " + | ||||
| 		"FROM log WHERE id=$1") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer stmt.Close() | ||||
|  | ||||
| 	var internalErr sql.NullString | ||||
| 	err = stmt.QueryRow(id).Scan(&le.ID, &le.Timestamp, | ||||
| 		&le.Name, &le.Type, &le.Tag, | ||||
| 		&le.Distro.ID, &le.Distro.Release, &le.KernelRelease, | ||||
| 		&internalErr, | ||||
| 		&le.Build.Ok, &le.Run.Ok, &le.Test.Ok, | ||||
| 		&le.Build.Output, &le.Run.Output, &le.Test.Output, | ||||
| 		&le.Stdout, &le.Stderr, | ||||
| 		&le.KernelPanic, &le.KilledByTimeout, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	le.InternalErrorString = internalErr.String | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func getLastLog(db *sql.DB) (le logEntry, err error) { | ||||
| 	var internalErr sql.NullString | ||||
| 	err = db.QueryRow("SELECT MAX(id), time, name, type, tag, "+ | ||||
| 		"distro_type, distro_release, kernel_release, "+ | ||||
| 		"internal_err, "+ | ||||
| 		"build_ok, run_ok, test_ok, "+ | ||||
| 		"build_output, run_output, test_output, "+ | ||||
| 		"qemu_stdout, qemu_stderr, "+ | ||||
| 		"kernel_panic, timeout_kill "+ | ||||
| 		"FROM log").Scan(&le.ID, &le.Timestamp, | ||||
| 		&le.Name, &le.Type, &le.Tag, | ||||
| 		&le.Distro.ID, &le.Distro.Release, &le.KernelRelease, | ||||
| 		&internalErr, | ||||
| 		&le.Build.Ok, &le.Run.Ok, &le.Test.Ok, | ||||
| 		&le.Build.Output, &le.Run.Output, &le.Test.Output, | ||||
| 		&le.Stdout, &le.Stderr, | ||||
| 		&le.KernelPanic, &le.KilledByTimeout, | ||||
| 	) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	le.InternalErrorString = internalErr.String | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func createSchema(db *sql.DB) (err error) { | ||||
| 	err = createMetadataTable(db) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = createLogTable(db) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func openDatabase(path string) (db *sql.DB, err error) { | ||||
| 	db, err = sql.Open("sqlite3", path) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	db.SetMaxOpenConns(1) | ||||
|  | ||||
| 	exists, _ := metaChkValue(db, versionField) | ||||
| 	if !exists { | ||||
| 		err = createSchema(db) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err = metaSetValue(db, versionField, | ||||
| 			strconv.Itoa(currentDatabaseVersion)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	version, err := getVersion(db) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if version == 1 { | ||||
| 		_, err = db.Exec(`ALTER TABLE log ADD tag TEXT`) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err = metaSetValue(db, versionField, "2") | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		version = 2 | ||||
|  | ||||
| 	} else if version == 2 { | ||||
| 		_, err = db.Exec(`ALTER TABLE log ADD internal_err TEXT`) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err = metaSetValue(db, versionField, "3") | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		version = 3 | ||||
| 	} | ||||
|  | ||||
| 	if version != currentDatabaseVersion { | ||||
| 		err = fmt.Errorf("Database is not supported (%d instead of %d)", | ||||
| 			version, currentDatabaseVersion) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										286
									
								
								cmd/debug.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								cmd/debug.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| // Copyright 2018 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"gopkg.in/logrusorgru/aurora.v2" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/fs" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/qemu" | ||||
| ) | ||||
|  | ||||
| type DebugCmd struct { | ||||
| 	Kernel string `help:"regexp (first match)" required:""` | ||||
| 	Gdb    string `help:"gdb listen address" default:"tcp::1234"` | ||||
|  | ||||
| 	SshAddr string `help:"ssh address to listen" default:"127.0.0.1"` | ||||
| 	SshPort int    `help:"ssh port to listen" default:"50022"` | ||||
|  | ||||
| 	ArtifactConfig string `help:"path to artifact config" type:"path"` | ||||
|  | ||||
| 	Kaslr bool `help:"Enable KASLR"` | ||||
| 	Smep  bool `help:"Enable SMEP"` | ||||
| 	Smap  bool `help:"Enable SMAP"` | ||||
| 	Kpti  bool `help:"Enable KPTI"` | ||||
|  | ||||
| 	NoKaslr bool `help:"Disable KASLR"` | ||||
| 	NoSmep  bool `help:"Disable SMEP"` | ||||
| 	NoSmap  bool `help:"Disable SMAP"` | ||||
| 	NoKpti  bool `help:"Disable KPTI"` | ||||
| } | ||||
|  | ||||
| // TODO: merge with pew.go | ||||
| func (cmd *DebugCmd) Run(g *Globals) (err error) { | ||||
| 	kcfg, err := config.ReadKernelConfig(g.Config.Kernels) | ||||
| 	if err != nil { | ||||
| 		log.Print(err) | ||||
| 	} | ||||
|  | ||||
| 	var configPath string | ||||
| 	if cmd.ArtifactConfig == "" { | ||||
| 		configPath = g.WorkDir + "/.out-of-tree.toml" | ||||
| 	} else { | ||||
| 		configPath = cmd.ArtifactConfig | ||||
| 	} | ||||
| 	ka, err := config.ReadArtifactConfig(configPath) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ka.SourcePath == "" { | ||||
| 		ka.SourcePath = g.WorkDir | ||||
| 	} | ||||
|  | ||||
| 	ki, err := firstSupported(kcfg, ka, cmd.Kernel) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	kernel := qemu.Kernel{KernelPath: ki.KernelPath, InitrdPath: ki.InitrdPath} | ||||
| 	q, err := qemu.NewSystem(qemu.X86x64, kernel, ki.RootFS) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = q.SetSSHAddrPort(cmd.SshAddr, cmd.SshPort) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ka.Qemu.Cpus != 0 { | ||||
| 		q.Cpus = ka.Qemu.Cpus | ||||
| 	} | ||||
| 	if ka.Qemu.Memory != 0 { | ||||
| 		q.Memory = ka.Qemu.Memory | ||||
| 	} | ||||
|  | ||||
| 	if ka.Docker.Timeout.Duration != 0 { | ||||
| 		g.Config.Docker.Timeout.Duration = ka.Docker.Timeout.Duration | ||||
| 	} | ||||
|  | ||||
| 	q.SetKASLR(false) // set KASLR to false by default because of gdb | ||||
| 	q.SetSMEP(!ka.Mitigations.DisableSmep) | ||||
| 	q.SetSMAP(!ka.Mitigations.DisableSmap) | ||||
| 	q.SetKPTI(!ka.Mitigations.DisableKpti) | ||||
|  | ||||
| 	if cmd.Kaslr { | ||||
| 		q.SetKASLR(true) | ||||
| 	} else if cmd.NoKaslr { | ||||
| 		q.SetKASLR(false) | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Smep { | ||||
| 		q.SetSMEP(true) | ||||
| 	} else if cmd.NoSmep { | ||||
| 		q.SetSMEP(false) | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Smap { | ||||
| 		q.SetSMAP(true) | ||||
| 	} else if cmd.NoSmap { | ||||
| 		q.SetSMAP(false) | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Kpti { | ||||
| 		q.SetKPTI(true) | ||||
| 	} else if cmd.NoKpti { | ||||
| 		q.SetKPTI(false) | ||||
| 	} | ||||
|  | ||||
| 	redgreen := func(name string, enabled bool) aurora.Value { | ||||
| 		if enabled { | ||||
| 			return aurora.BgGreen(aurora.Black(name)) | ||||
| 		} | ||||
|  | ||||
| 		return aurora.BgRed(aurora.White(name)) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("[*] %s %s %s %s\n", | ||||
| 		redgreen("KASLR", q.GetKASLR()), | ||||
| 		redgreen("SMEP", q.GetSMEP()), | ||||
| 		redgreen("SMAP", q.GetSMAP()), | ||||
| 		redgreen("KPTI", q.GetKPTI())) | ||||
|  | ||||
| 	fmt.Printf("[*] SMP: %d CPUs\n", q.Cpus) | ||||
| 	fmt.Printf("[*] Memory: %d MB\n", q.Memory) | ||||
|  | ||||
| 	q.Debug(cmd.Gdb) | ||||
| 	coloredGdbAddress := aurora.BgGreen(aurora.Black(cmd.Gdb)) | ||||
| 	fmt.Printf("[*] gdb is listening on %s\n", coloredGdbAddress) | ||||
|  | ||||
| 	err = q.Start() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer q.Stop() | ||||
|  | ||||
| 	tmp, err := fs.TempDir() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmp) | ||||
|  | ||||
| 	err = q.WaitForSSH(time.Minute) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ka.StandardModules { | ||||
| 		// Module depends on one of the standard modules | ||||
| 		err = copyStandardModules(q, ki) | ||||
| 		if err != nil { | ||||
| 			log.Print(err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = preloadModules(q, ka, ki, g.Config.Docker.Timeout.Duration) | ||||
| 	if err != nil { | ||||
| 		log.Print(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var buildDir, outFile, output, remoteFile string | ||||
|  | ||||
| 	if ka.Type == config.Script { | ||||
| 		err = q.CopyFile("root", ka.Script, ka.Script) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		buildDir, outFile, output, err = build(log.Logger, tmp, ka, ki, g.Config.Docker.Timeout.Duration) | ||||
| 		if err != nil { | ||||
| 			log.Print(err, output) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		remoteFile = "/tmp/" + strings.Replace(ka.Name, " ", "_", -1) | ||||
| 		if ka.Type == config.KernelModule { | ||||
| 			remoteFile += ".ko" | ||||
| 		} | ||||
|  | ||||
| 		err = q.CopyFile("user", outFile, remoteFile) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Copy all test files to the remote machine | ||||
| 	for _, f := range ka.TestFiles { | ||||
| 		if f.Local[0] != '/' { | ||||
| 			if buildDir != "" { | ||||
| 				f.Local = buildDir + "/" + f.Local | ||||
| 			} | ||||
| 		} | ||||
| 		err = q.CopyFile(f.User, f.Local, f.Remote) | ||||
| 		if err != nil { | ||||
| 			log.Print("error copy err:", err, f.Local, f.Remote) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	coloredRemoteFile := aurora.BgGreen(aurora.Black(remoteFile)) | ||||
| 	fmt.Printf("[*] build result copied to %s\n", coloredRemoteFile) | ||||
|  | ||||
| 	fmt.Printf("\n%s\n", q.GetSSHCommand()) | ||||
| 	fmt.Printf("gdb %s -ex 'target remote %s'\n\n", ki.VmlinuxPath, cmd.Gdb) | ||||
|  | ||||
| 	// TODO set substitute-path /build/.../linux-... /path/to/linux-source | ||||
|  | ||||
| 	err = interactive(q) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func firstSupported(kcfg config.KernelConfig, ka config.Artifact, | ||||
| 	kernel string) (ki distro.KernelInfo, err error) { | ||||
|  | ||||
| 	km, err := kernelMask(kernel) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ka.Targets = []config.Target{km} | ||||
|  | ||||
| 	for _, ki = range kcfg.Kernels { | ||||
| 		var supported bool | ||||
| 		supported, err = ka.Supported(ki) | ||||
| 		if err != nil || supported { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = errors.New("No supported kernel found") | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func handleLine(q *qemu.System) (err error) { | ||||
| 	fmt.Print("out-of-tree> ") | ||||
| 	rawLine := "help" | ||||
| 	fmt.Scanf("%s", &rawLine) | ||||
| 	params := strings.Fields(rawLine) | ||||
| 	cmd := params[0] | ||||
|  | ||||
| 	switch cmd { | ||||
| 	case "h", "help": | ||||
| 		fmt.Printf("help\t: print this help message\n") | ||||
| 		fmt.Printf("log\t: print qemu log\n") | ||||
| 		fmt.Printf("clog\t: print qemu log and cleanup buffer\n") | ||||
| 		fmt.Printf("cleanup\t: cleanup qemu log buffer\n") | ||||
| 		fmt.Printf("ssh\t: print arguments to ssh command\n") | ||||
| 		fmt.Printf("quit\t: quit\n") | ||||
| 	case "l", "log": | ||||
| 		fmt.Println(q.Stdout) | ||||
| 	case "cl", "clog": | ||||
| 		fmt.Println(q.Stdout) | ||||
| 		q.Stdout = "" | ||||
| 	case "c", "cleanup": | ||||
| 		q.Stdout = "" | ||||
| 	case "s", "ssh": | ||||
| 		fmt.Println(q.GetSSHCommand()) | ||||
| 	case "q", "quit": | ||||
| 		return errors.New("end of session") | ||||
| 	default: | ||||
| 		fmt.Println("No such command") | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func interactive(q *qemu.System) (err error) { | ||||
| 	for { | ||||
| 		err = handleLine(q) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										216
									
								
								cmd/distro.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								cmd/distro.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/cavaliergopher/grab/v3" | ||||
| 	"github.com/davecgh/go-spew/spew" | ||||
| 	"github.com/remeh/sizedwaitgroup" | ||||
| 	"github.com/rs/zerolog/log" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/cache" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro/debian" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro/debian/snapshot" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/fs" | ||||
| ) | ||||
|  | ||||
| type DistroCmd struct { | ||||
| 	List DistroListCmd `cmd:"" help:"list available distros"` | ||||
|  | ||||
| 	Debian DebianCmd `cmd:"" hidden:""` | ||||
| } | ||||
|  | ||||
| type DebianCmd struct { | ||||
| 	Cache DebianCacheCmd `cmd:"" help:"populate cache"` | ||||
| 	Fetch DebianFetchCmd `cmd:"" help:"download deb packages"` | ||||
|  | ||||
| 	Limit int    `help:"limit amount of kernels to fetch"` | ||||
| 	Regex string `help:"match deb pkg names by regex" default:".*"` | ||||
| } | ||||
|  | ||||
| type DebianCacheCmd struct { | ||||
| 	Path          string `help:"path to cache"` | ||||
| 	Refetch       int    `help:"days before refetch versions without deb package" default:"7"` | ||||
| 	UpdateRelease bool   `help:"update release data"` | ||||
| 	UpdateKbuild  bool   `help:"update kbuild package"` | ||||
| 	Dump          bool   `help:"dump cache"` | ||||
| } | ||||
|  | ||||
| func (cmd *DebianCacheCmd) Run(dcmd *DebianCmd) (err error) { | ||||
| 	if cmd.Path != "" { | ||||
| 		debian.CachePath = cmd.Path | ||||
| 	} | ||||
| 	debian.RefetchDays = cmd.Refetch | ||||
|  | ||||
| 	log.Info().Msg("Fetching kernels...") | ||||
|  | ||||
| 	if dcmd.Limit == 0 { | ||||
| 		dcmd.Limit = math.MaxInt32 | ||||
| 	} | ||||
|  | ||||
| 	mode := debian.NoMode | ||||
| 	if cmd.UpdateRelease { | ||||
| 		mode |= debian.UpdateRelease | ||||
| 	} | ||||
| 	if cmd.UpdateKbuild { | ||||
| 		mode |= debian.UpdateKbuild | ||||
| 	} | ||||
|  | ||||
| 	kernels, err := debian.GetKernelsWithLimit(dcmd.Limit, mode) | ||||
| 	if err != nil { | ||||
| 		log.Error().Err(err).Msg("") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Dump { | ||||
| 		re, err := regexp.Compile(dcmd.Regex) | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Err(err).Msg("regex") | ||||
| 		} | ||||
|  | ||||
| 		for _, kernel := range kernels { | ||||
| 			if !re.MatchString(kernel.Image.Deb.Name) { | ||||
| 				continue | ||||
| 			} | ||||
| 			fmt.Println(spew.Sdump(kernel)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	log.Info().Msg("Success") | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type DebianFetchCmd struct { | ||||
| 	Path         string `help:"path to download directory" type:"existingdir" default:"./"` | ||||
| 	IgnoreMirror bool   `help:"ignore check if packages on the mirror"` | ||||
|  | ||||
| 	Max int `help:"do not download more than X" default:"100500"` | ||||
|  | ||||
| 	Threads int `help:"parallel download threads" default:"8"` | ||||
|  | ||||
| 	Timeout time.Duration `help:"timeout for each download" default:"1m"` | ||||
|  | ||||
| 	swg        sizedwaitgroup.SizedWaitGroup | ||||
| 	hasResults bool | ||||
| } | ||||
|  | ||||
| func (cmd *DebianFetchCmd) fetch(pkg snapshot.Package) { | ||||
| 	flog := log.With(). | ||||
| 		Str("pkg", pkg.Deb.Name). | ||||
| 		Logger() | ||||
|  | ||||
| 	defer cmd.swg.Done() | ||||
|  | ||||
| 	if !cmd.IgnoreMirror { | ||||
| 		flog.Debug().Msg("check mirror") | ||||
| 		found, _ := cache.PackageURL(distro.Debian, pkg.Deb.URL) | ||||
| 		if found { | ||||
| 			flog.Debug().Msg("found on the mirror") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	target := filepath.Join(cmd.Path, filepath.Base(pkg.Deb.URL)) | ||||
|  | ||||
| 	if fs.PathExists(target) { | ||||
| 		flog.Debug().Msg("already exists") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tmp, err := os.MkdirTemp(cmd.Path, "tmp-") | ||||
| 	if err != nil { | ||||
| 		flog.Fatal().Err(err).Msg("mkdir") | ||||
| 		return | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmp) | ||||
|  | ||||
| 	flog.Info().Msg("fetch") | ||||
| 	flog.Debug().Msg(pkg.Deb.URL) | ||||
|  | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), cmd.Timeout) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	req, err := grab.NewRequest(tmp, pkg.Deb.URL) | ||||
| 	if err != nil { | ||||
| 		flog.Warn().Err(err).Msg("cannot create request") | ||||
| 		return | ||||
| 	} | ||||
| 	req = req.WithContext(ctx) | ||||
|  | ||||
| 	resp := grab.DefaultClient.Do(req) | ||||
| 	if err := resp.Err(); err != nil { | ||||
| 		flog.Warn().Err(err).Msg("request cancelled") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = os.Rename(resp.Filename, target) | ||||
| 	if err != nil { | ||||
| 		flog.Fatal().Err(err).Msg("mv") | ||||
| 	} | ||||
|  | ||||
| 	cmd.hasResults = true | ||||
| 	cmd.Max-- | ||||
| } | ||||
|  | ||||
| func (cmd *DebianFetchCmd) Run(dcmd *DebianCmd) (err error) { | ||||
| 	re, err := regexp.Compile(dcmd.Regex) | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err).Msg("regex") | ||||
| 	} | ||||
|  | ||||
| 	log.Info().Msg("will not download packages that exist on the mirror") | ||||
| 	log.Info().Msg("use --ignore-mirror if you really need it") | ||||
|  | ||||
| 	if dcmd.Limit == 0 { | ||||
| 		dcmd.Limit = math.MaxInt32 | ||||
| 	} | ||||
|  | ||||
| 	kernels, err := debian.GetKernelsWithLimit(dcmd.Limit, debian.NoMode) | ||||
| 	if err != nil { | ||||
| 		log.Error().Err(err).Msg("") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var packages []snapshot.Package | ||||
| 	for _, kernel := range kernels { | ||||
| 		for _, pkg := range kernel.Packages() { | ||||
| 			if !re.MatchString(pkg.Deb.Name) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			packages = append(packages, pkg) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	cmd.swg = sizedwaitgroup.New(cmd.Threads) | ||||
| 	for _, pkg := range packages { | ||||
| 		if cmd.Max <= 0 { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		cmd.swg.Add() | ||||
| 		go cmd.fetch(pkg) | ||||
| 	} | ||||
| 	cmd.swg.Wait() | ||||
|  | ||||
| 	if !cmd.hasResults { | ||||
| 		log.Fatal().Msg("no packages found to download") | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type DistroListCmd struct{} | ||||
|  | ||||
| func (cmd *DistroListCmd) Run() (err error) { | ||||
| 	for _, d := range distro.List() { | ||||
| 		fmt.Println(d.ID, d.Release) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										57
									
								
								cmd/gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								cmd/gen.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| // Copyright 2018 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/naoina/toml" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| ) | ||||
|  | ||||
| type GenCmd struct { | ||||
| 	Type string `enum:"module,exploit" required:"" help:"module/exploit"` | ||||
| } | ||||
|  | ||||
| func (cmd *GenCmd) Run(g *Globals) (err error) { | ||||
| 	switch cmd.Type { | ||||
| 	case "module": | ||||
| 		err = genConfig(config.KernelModule) | ||||
| 	case "exploit": | ||||
| 		err = genConfig(config.KernelExploit) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func genConfig(at config.ArtifactType) (err error) { | ||||
| 	a := config.Artifact{ | ||||
| 		Name: "Put name here", | ||||
| 		Type: at, | ||||
| 	} | ||||
| 	a.Targets = append(a.Targets, config.Target{ | ||||
| 		Distro: distro.Distro{ID: distro.Ubuntu, Release: "18.04"}, | ||||
| 		Kernel: config.Kernel{Regex: ".*"}, | ||||
| 	}) | ||||
| 	a.Targets = append(a.Targets, config.Target{ | ||||
| 		Distro: distro.Distro{ID: distro.Debian, Release: "8"}, | ||||
| 		Kernel: config.Kernel{Regex: ".*"}, | ||||
| 	}) | ||||
| 	a.Preload = append(a.Preload, config.PreloadModule{ | ||||
| 		Repo: "Repo name (e.g. https://github.com/openwall/lkrg)", | ||||
| 	}) | ||||
| 	a.Patches = append(a.Patches, config.Patch{ | ||||
| 		Path: "/path/to/profiling.patch", | ||||
| 	}) | ||||
|  | ||||
| 	buf, err := toml.Marshal(&a) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	fmt.Print(string(buf)) | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										15
									
								
								cmd/globals.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cmd/globals.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"net/url" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| ) | ||||
|  | ||||
| type Globals struct { | ||||
| 	Config config.OutOfTree `help:"path to out-of-tree configuration" default:"~/.out-of-tree/out-of-tree.toml"` | ||||
|  | ||||
| 	WorkDir string `help:"path to work directory" default:"./" type:"path"` | ||||
|  | ||||
| 	CacheURL url.URL | ||||
| } | ||||
							
								
								
									
										113
									
								
								cmd/images.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								cmd/images.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| // Copyright 2023 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/fs" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/qemu" | ||||
| ) | ||||
|  | ||||
| type ImageCmd struct { | ||||
| 	List ImageListCmd `cmd:"" help:"list images"` | ||||
| 	Edit ImageEditCmd `cmd:"" help:"edit image"` | ||||
| } | ||||
|  | ||||
| type ImageListCmd struct{} | ||||
|  | ||||
| func (cmd *ImageListCmd) Run(g *Globals) (err error) { | ||||
| 	entries, err := os.ReadDir(config.Dir("images")) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, e := range entries { | ||||
| 		fmt.Println(e.Name()) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type ImageEditCmd struct { | ||||
| 	Name   string `help:"image name" required:""` | ||||
| 	DryRun bool   `help:"do nothing, just print commands"` | ||||
| } | ||||
|  | ||||
| func (cmd *ImageEditCmd) Run(g *Globals) (err error) { | ||||
| 	image := filepath.Join(config.Dir("images"), cmd.Name) | ||||
| 	if !fs.PathExists(image) { | ||||
| 		fmt.Println("image does not exist") | ||||
| 	} | ||||
|  | ||||
| 	kcfg, err := config.ReadKernelConfig(g.Config.Kernels) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(kcfg.Kernels) == 0 { | ||||
| 		return errors.New("No kernels found") | ||||
| 	} | ||||
|  | ||||
| 	ki := distro.KernelInfo{} | ||||
| 	for _, k := range kcfg.Kernels { | ||||
| 		if k.RootFS == image { | ||||
| 			ki = k | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	kernel := qemu.Kernel{ | ||||
| 		KernelPath: ki.KernelPath, | ||||
| 		InitrdPath: ki.InitrdPath, | ||||
| 	} | ||||
|  | ||||
| 	q, err := qemu.NewSystem(qemu.X86x64, kernel, ki.RootFS) | ||||
|  | ||||
| 	q.Mutable = true | ||||
|  | ||||
| 	if cmd.DryRun { | ||||
| 		s := q.Executable() | ||||
| 		for _, arg := range q.Args() { | ||||
| 			if strings.Contains(arg, " ") || | ||||
| 				strings.Contains(arg, ",") { | ||||
|  | ||||
| 				s += fmt.Sprintf(` "%s"`, arg) | ||||
| 			} else { | ||||
| 				s += fmt.Sprintf(" %s", arg) | ||||
| 			} | ||||
| 		} | ||||
| 		fmt.Println(s) | ||||
| 		fmt.Println(q.GetSSHCommand()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = q.Start() | ||||
| 	if err != nil { | ||||
| 		fmt.Println("Qemu start error:", err) | ||||
| 		return | ||||
| 	} | ||||
| 	defer q.Stop() | ||||
|  | ||||
| 	fmt.Print("ssh command:\n\n\t") | ||||
| 	fmt.Println(q.GetSSHCommand()) | ||||
|  | ||||
| 	fmt.Print("\npress enter to stop") | ||||
| 	fmt.Scanln() | ||||
|  | ||||
| 	q.Command("root", "poweroff") | ||||
|  | ||||
| 	for !q.Died { | ||||
| 		time.Sleep(time.Second) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										387
									
								
								cmd/kernel.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								cmd/kernel.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,387 @@ | ||||
| // Copyright 2023 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/naoina/toml" | ||||
| 	"github.com/remeh/sizedwaitgroup" | ||||
| 	"github.com/rs/zerolog/log" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/container" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/kernel" | ||||
| ) | ||||
|  | ||||
| type KernelCmd struct { | ||||
| 	NoDownload bool `help:"do not download qemu image while kernel generation"` | ||||
| 	UseHost    bool `help:"also use host kernels"` | ||||
| 	Force      bool `help:"force reinstall kernel"` | ||||
| 	NoHeaders  bool `help:"do not install kernel headers"` | ||||
| 	Shuffle    bool `help:"randomize kernels installation order"` | ||||
| 	Retries    int  `help:"amount of tries for each kernel" default:"2"` | ||||
| 	Threads    int  `help:"threads for parallel installation" default:"1"` | ||||
| 	Update     bool `help:"update container"` | ||||
| 	Max        int  `help:"maximum kernels to download" default:"100500"` | ||||
| 	NoPrune    bool `help:"do not remove dangling or unused images from local storage after build"` | ||||
|  | ||||
| 	ContainerTimeout time.Duration `help:"container timeout"` | ||||
|  | ||||
| 	List        KernelListCmd        `cmd:"" help:"list kernels"` | ||||
| 	ListRemote  KernelListRemoteCmd  `cmd:"" help:"list remote kernels"` | ||||
| 	Autogen     KernelAutogenCmd     `cmd:"" help:"generate kernels based on the current config"` | ||||
| 	Genall      KernelGenallCmd      `cmd:"" help:"generate all kernels for distro"` | ||||
| 	Install     KernelInstallCmd     `cmd:"" help:"install specific kernel"` | ||||
| 	ConfigRegen KernelConfigRegenCmd `cmd:"" help:"regenerate config"` | ||||
|  | ||||
| 	shutdown bool | ||||
| 	kcfg     config.KernelConfig | ||||
|  | ||||
| 	stats struct { | ||||
| 		overall int | ||||
| 		success int | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (cmd KernelCmd) UpdateConfig() (err error) { | ||||
| 	if cmd.stats.success != cmd.stats.overall { | ||||
| 		log.Warn().Msgf("%d kernels failed to install", | ||||
| 			cmd.stats.overall-cmd.stats.success) | ||||
| 	} | ||||
|  | ||||
| 	log.Info().Msgf("updating kernels.toml") | ||||
| 	kcfg := config.KernelConfig{} | ||||
|  | ||||
| 	if cmd.UseHost { | ||||
| 		// Get host kernels | ||||
| 		kcfg.Kernels, err = kernel.GenHostKernels(!cmd.NoDownload) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, dist := range distro.List() { | ||||
| 		var kernels []distro.KernelInfo | ||||
| 		kernels, err = dist.Kernels() | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		kcfg.Kernels = append(kcfg.Kernels, kernels...) | ||||
| 	} | ||||
|  | ||||
| 	buf, err := toml.Marshal(&kcfg) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = os.WriteFile(config.File("kernels.toml"), buf, os.ModePerm) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	log.Info().Msgf("kernels.toml successfully updated") | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (cmd *KernelCmd) GenKernel(km config.Target, pkg string) { | ||||
| 	flog := log.With(). | ||||
| 		Str("kernel", pkg). | ||||
| 		Str("distro", km.Distro.String()). | ||||
| 		Logger() | ||||
|  | ||||
| 	reinstall := false | ||||
| 	for _, kinfo := range cmd.kcfg.Kernels { | ||||
| 		if !km.Distro.Equal(kinfo.Distro) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		var found bool | ||||
| 		if kinfo.Distro.ID == distro.Debian { // FIXME | ||||
| 			found = pkg == kinfo.Package | ||||
| 		} else if kinfo.Distro.ID == distro.OpenSUSE { | ||||
| 			found = strings.Contains(pkg, kinfo.KernelRelease) | ||||
| 		} else { | ||||
| 			found = strings.Contains(pkg, kinfo.KernelVersion) | ||||
| 		} | ||||
|  | ||||
| 		if found { | ||||
| 			if !cmd.Force { | ||||
| 				flog.Info().Msg("already installed") | ||||
| 				return | ||||
| 			} | ||||
| 			reinstall = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if reinstall { | ||||
| 		flog.Info().Msg("reinstall") | ||||
| 	} else { | ||||
| 		flog.Info().Msg("install") | ||||
| 	} | ||||
|  | ||||
| 	cmd.stats.overall += 1 | ||||
|  | ||||
| 	var attempt int | ||||
| 	for { | ||||
| 		attempt++ | ||||
|  | ||||
| 		if cmd.shutdown { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err := km.Distro.Install(pkg, !cmd.NoHeaders) | ||||
| 		if err == nil { | ||||
| 			cmd.stats.success += 1 | ||||
| 			flog.Info().Msg("success") | ||||
| 			break | ||||
| 		} else if attempt >= cmd.Retries { | ||||
| 			flog.Error().Err(err).Msg("install kernel") | ||||
| 			flog.Debug().Msg("skip") | ||||
| 			break | ||||
| 		} else { | ||||
| 			flog.Warn().Err(err).Msg("install kernel") | ||||
| 			time.Sleep(time.Second) | ||||
| 			flog.Info().Msg("retry") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (cmd *KernelCmd) Generate(g *Globals, km config.Target) (err error) { | ||||
| 	if cmd.Update { | ||||
| 		container.UseCache = false | ||||
| 	} | ||||
| 	if cmd.NoPrune { | ||||
| 		container.Prune = false | ||||
| 	} | ||||
|  | ||||
| 	cmd.kcfg, err = config.ReadKernelConfig(g.Config.Kernels) | ||||
| 	if err != nil { | ||||
| 		log.Debug().Err(err).Msg("read kernels config") | ||||
| 	} | ||||
|  | ||||
| 	container.Commands = g.Config.Docker.Commands | ||||
| 	container.Registry = g.Config.Docker.Registry | ||||
| 	container.Timeout = g.Config.Docker.Timeout.Duration | ||||
| 	if cmd.ContainerTimeout != 0 { | ||||
| 		container.Timeout = cmd.ContainerTimeout | ||||
| 	} | ||||
|  | ||||
| 	log.Info().Msgf("Generating for target %v", km) | ||||
|  | ||||
| 	_, err = kernel.GenRootfsImage(km.Distro.RootFS(), !cmd.NoDownload) | ||||
| 	if err != nil || cmd.shutdown { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	pkgs, err := kernel.MatchPackages(km) | ||||
| 	if err != nil || cmd.shutdown { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Shuffle { | ||||
| 		pkgs = kernel.ShuffleStrings(pkgs) | ||||
| 	} | ||||
|  | ||||
| 	swg := sizedwaitgroup.New(cmd.Threads) | ||||
|  | ||||
| 	for i, pkg := range pkgs { | ||||
| 		if cmd.shutdown { | ||||
| 			err = nil | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		swg.Add() | ||||
|  | ||||
| 		if cmd.shutdown { | ||||
| 			err = nil | ||||
| 			swg.Done() | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if cmd.stats.success >= cmd.Max { | ||||
| 			log.Print("Max is reached") | ||||
| 			swg.Done() | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		log.Info().Msgf("%d/%d %s", i+1, len(pkgs), pkg) | ||||
|  | ||||
| 		go func(p string) { | ||||
| 			defer swg.Done() | ||||
| 			cmd.GenKernel(km, p) | ||||
| 		}(pkg) | ||||
| 	} | ||||
| 	swg.Wait() | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type KernelListCmd struct{} | ||||
|  | ||||
| func (cmd *KernelListCmd) Run(g *Globals) (err error) { | ||||
| 	kcfg, err := config.ReadKernelConfig(g.Config.Kernels) | ||||
| 	if err != nil { | ||||
| 		log.Debug().Err(err).Msg("read kernel config") | ||||
| 	} | ||||
|  | ||||
| 	if len(kcfg.Kernels) == 0 { | ||||
| 		return errors.New("No kernels found") | ||||
| 	} | ||||
|  | ||||
| 	for _, k := range kcfg.Kernels { | ||||
| 		fmt.Println(k.Distro.ID, k.Distro.Release, k.KernelRelease) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type KernelListRemoteCmd struct { | ||||
| 	Distro string `required:"" help:"distribution"` | ||||
| 	Ver    string `help:"distro version"` | ||||
| } | ||||
|  | ||||
| func (cmd *KernelListRemoteCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { | ||||
| 	if kernelCmd.Update { | ||||
| 		container.UseCache = false | ||||
| 	} | ||||
| 	if kernelCmd.NoPrune { | ||||
| 		container.Prune = false | ||||
| 	} | ||||
|  | ||||
| 	distroType, err := distro.NewID(cmd.Distro) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	km := config.Target{ | ||||
| 		Distro: distro.Distro{ID: distroType, Release: cmd.Ver}, | ||||
| 		Kernel: config.Kernel{Regex: ".*"}, | ||||
| 	} | ||||
|  | ||||
| 	_, err = kernel.GenRootfsImage(km.Distro.RootFS(), false) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	container.Registry = g.Config.Docker.Registry | ||||
| 	container.Commands = g.Config.Docker.Commands | ||||
|  | ||||
| 	pkgs, err := kernel.MatchPackages(km) | ||||
| 	// error check skipped on purpose | ||||
|  | ||||
| 	for _, k := range pkgs { | ||||
| 		fmt.Println(k) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type KernelAutogenCmd struct{} | ||||
|  | ||||
| func (cmd *KernelAutogenCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { | ||||
| 	ka, err := config.ReadArtifactConfig(g.WorkDir + "/.out-of-tree.toml") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	kernel.SetSigintHandler(&kernelCmd.shutdown) | ||||
|  | ||||
| 	for _, sk := range ka.Targets { | ||||
| 		if sk.Distro.Release == "" { | ||||
| 			err = errors.New("Please set distro_release") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err = kernelCmd.Generate(g, sk) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if kernelCmd.shutdown { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return kernelCmd.UpdateConfig() | ||||
| } | ||||
|  | ||||
| type KernelGenallCmd struct { | ||||
| 	Distro string `help:"distribution"` | ||||
| 	Ver    string `help:"distro version"` | ||||
| } | ||||
|  | ||||
| func (cmd *KernelGenallCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { | ||||
| 	distroType, err := distro.NewID(cmd.Distro) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	kernel.SetSigintHandler(&kernelCmd.shutdown) | ||||
|  | ||||
| 	for _, dist := range distro.List() { | ||||
| 		if kernelCmd.shutdown { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		if distroType != distro.None && distroType != dist.ID { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if cmd.Ver != "" && dist.Release != cmd.Ver { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		target := config.Target{ | ||||
| 			Distro: dist, | ||||
| 			Kernel: config.Kernel{Regex: ".*"}, | ||||
| 		} | ||||
|  | ||||
| 		err = kernelCmd.Generate(g, target) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return kernelCmd.UpdateConfig() | ||||
| } | ||||
|  | ||||
| type KernelInstallCmd struct { | ||||
| 	Distro string `required:"" help:"distribution"` | ||||
| 	Ver    string `required:"" help:"distro version"` | ||||
| 	Kernel string `required:"" help:"kernel release mask"` | ||||
| } | ||||
|  | ||||
| func (cmd *KernelInstallCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { | ||||
| 	distroType, err := distro.NewID(cmd.Distro) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	kernel.SetSigintHandler(&kernelCmd.shutdown) | ||||
|  | ||||
| 	km := config.Target{ | ||||
| 		Distro: distro.Distro{ID: distroType, Release: cmd.Ver}, | ||||
| 		Kernel: config.Kernel{Regex: cmd.Kernel}, | ||||
| 	} | ||||
| 	err = kernelCmd.Generate(g, km) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	return kernelCmd.UpdateConfig() | ||||
| } | ||||
|  | ||||
| type KernelConfigRegenCmd struct{} | ||||
|  | ||||
| func (cmd *KernelConfigRegenCmd) Run(kernelCmd *KernelCmd, g *Globals) (err error) { | ||||
| 	return kernelCmd.UpdateConfig() | ||||
| } | ||||
							
								
								
									
										322
									
								
								cmd/log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								cmd/log.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,322 @@ | ||||
| // Copyright 2023 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/olekukonko/tablewriter" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"gopkg.in/logrusorgru/aurora.v2" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| ) | ||||
|  | ||||
| type LogCmd struct { | ||||
| 	Query    LogQueryCmd    `cmd:"" help:"query logs"` | ||||
| 	Dump     LogDumpCmd     `cmd:"" help:"show all info for log entry with ID"` | ||||
| 	Json     LogJsonCmd     `cmd:"" help:"generate json statistics"` | ||||
| 	Markdown LogMarkdownCmd `cmd:"" help:"generate markdown statistics"` | ||||
| } | ||||
|  | ||||
| type LogQueryCmd struct { | ||||
| 	Num  int    `help:"how much lines" default:"50"` | ||||
| 	Rate bool   `help:"show artifact success rate"` | ||||
| 	Tag  string `help:"filter tag"` | ||||
| } | ||||
|  | ||||
| func (cmd *LogQueryCmd) Run(g *Globals) (err error) { | ||||
| 	db, err := openDatabase(g.Config.Database) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer db.Close() | ||||
|  | ||||
| 	var les []logEntry | ||||
|  | ||||
| 	ka, kaErr := config.ReadArtifactConfig(g.WorkDir + "/.out-of-tree.toml") | ||||
| 	if kaErr == nil { | ||||
| 		log.Print(".out-of-tree.toml found, filter by artifact name") | ||||
| 		les, err = getAllArtifactLogs(db, cmd.Tag, cmd.Num, ka) | ||||
| 	} else { | ||||
| 		les, err = getAllLogs(db, cmd.Tag, cmd.Num) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	s := "\nS" | ||||
| 	if cmd.Rate { | ||||
| 		if kaErr != nil { | ||||
| 			err = kaErr | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		s = fmt.Sprintf("{[%s] %s} Overall s", ka.Type, ka.Name) | ||||
|  | ||||
| 		les, err = getAllArtifactLogs(db, cmd.Tag, math.MaxInt64, ka) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		for _, l := range les { | ||||
| 			logLogEntry(l) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	success := 0 | ||||
| 	for _, l := range les { | ||||
| 		if l.Test.Ok { | ||||
| 			success++ | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	overall := float64(success) / float64(len(les)) | ||||
| 	fmt.Printf("%success rate: %.04f (~%.0f%%)\n", | ||||
| 		s, overall, overall*100) | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type LogDumpCmd struct { | ||||
| 	ID int `help:"id" default:"-1"` | ||||
| } | ||||
|  | ||||
| func (cmd *LogDumpCmd) Run(g *Globals) (err error) { | ||||
| 	db, err := openDatabase(g.Config.Database) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer db.Close() | ||||
|  | ||||
| 	var l logEntry | ||||
| 	if cmd.ID > 0 { | ||||
| 		l, err = getLogByID(db, cmd.ID) | ||||
| 	} else { | ||||
| 		l, err = getLastLog(db) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println("ID:", l.ID) | ||||
| 	fmt.Println("Date:", l.Timestamp.Format("2006-01-02 15:04")) | ||||
| 	fmt.Println("Tag:", l.Tag) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	fmt.Println("Type:", l.Type.String()) | ||||
| 	fmt.Println("Name:", l.Name) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	fmt.Println("Distro:", l.Distro.ID.String(), l.Distro.Release) | ||||
| 	fmt.Println("Kernel:", l.KernelRelease) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	fmt.Println("Build ok:", l.Build.Ok) | ||||
| 	if l.Type == config.KernelModule { | ||||
| 		fmt.Println("Insmod ok:", l.Run.Ok) | ||||
| 	} | ||||
| 	fmt.Println("Test ok:", l.Test.Ok) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	fmt.Printf("Build output:\n%s\n", l.Build.Output) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	if l.Type == config.KernelModule { | ||||
| 		fmt.Printf("Insmod output:\n%s\n", l.Run.Output) | ||||
| 		fmt.Println() | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("Test output:\n%s\n", l.Test.Output) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	fmt.Printf("Qemu stdout:\n%s\n", l.Stdout) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	fmt.Printf("Qemu stderr:\n%s\n", l.Stderr) | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type LogJsonCmd struct { | ||||
| 	Tag string `required:"" help:"filter tag"` | ||||
| } | ||||
|  | ||||
| func (cmd *LogJsonCmd) Run(g *Globals) (err error) { | ||||
| 	db, err := openDatabase(g.Config.Database) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer db.Close() | ||||
|  | ||||
| 	distros, err := getStats(db, g.WorkDir, cmd.Tag) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	bytes, err := json.Marshal(&distros) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println(string(bytes)) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type LogMarkdownCmd struct { | ||||
| 	Tag string `required:"" help:"filter tag"` | ||||
| } | ||||
|  | ||||
| func (cmd *LogMarkdownCmd) Run(g *Globals) (err error) { | ||||
| 	db, err := openDatabase(g.Config.Database) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer db.Close() | ||||
|  | ||||
| 	distros, err := getStats(db, g.WorkDir, cmd.Tag) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	table := tablewriter.NewWriter(os.Stdout) | ||||
| 	table.SetHeader([]string{"Distro", "Release", "Kernel", "Reliability"}) | ||||
| 	table.SetBorders(tablewriter.Border{ | ||||
| 		Left: true, Top: false, Right: true, Bottom: false}) | ||||
| 	table.SetCenterSeparator("|") | ||||
|  | ||||
| 	for distro, releases := range distros { | ||||
| 		for release, kernels := range releases { | ||||
| 			for kernel, stats := range kernels { | ||||
| 				all := float64(stats.All) | ||||
| 				ok := float64(stats.TestOK) | ||||
| 				r := fmt.Sprintf("%6.02f%%", (ok/all)*100) | ||||
| 				table.Append([]string{distro, release, kernel, r}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	table.Render() | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func center(s string, w int) string { | ||||
| 	return fmt.Sprintf("%[1]*s", -w, fmt.Sprintf("%[1]*s", (w+len(s))/2, s)) | ||||
| } | ||||
|  | ||||
| func genOkFailCentered(name string, ok bool) (aurv aurora.Value) { | ||||
| 	name = center(name, 10) | ||||
| 	if ok { | ||||
| 		aurv = aurora.BgGreen(aurora.Black(name)) | ||||
| 	} else { | ||||
| 		aurv = aurora.BgRed(aurora.White(aurora.Bold(name))) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func logLogEntry(l logEntry) { | ||||
| 	distroInfo := fmt.Sprintf("%s-%s {%s}", l.Distro.ID, | ||||
| 		l.Distro.Release, l.KernelRelease) | ||||
|  | ||||
| 	artifactInfo := fmt.Sprintf("{[%s] %s}", l.Type, l.Name) | ||||
|  | ||||
| 	timestamp := l.Timestamp.Format("2006-01-02 15:04") | ||||
|  | ||||
| 	var status aurora.Value | ||||
| 	if l.InternalErrorString != "" { | ||||
| 		status = genOkFailCentered("INTERNAL", false) | ||||
| 	} else if l.Type == config.KernelExploit { | ||||
| 		if l.Build.Ok { | ||||
| 			status = genOkFailCentered("LPE", l.Test.Ok) | ||||
| 		} else { | ||||
| 			status = genOkFailCentered("BUILD", l.Build.Ok) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if l.Build.Ok { | ||||
| 			if l.Run.Ok { | ||||
| 				status = genOkFailCentered("TEST", l.Test.Ok) | ||||
| 			} else { | ||||
| 				status = genOkFailCentered("INSMOD", l.Run.Ok) | ||||
| 			} | ||||
| 		} else { | ||||
| 			status = genOkFailCentered("BUILD", l.Build.Ok) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	additional := "" | ||||
| 	if l.KernelPanic { | ||||
| 		additional = "(panic)" | ||||
| 	} else if l.KilledByTimeout { | ||||
| 		additional = "(timeout)" | ||||
| 	} | ||||
|  | ||||
| 	colored := aurora.Sprintf("[%4d %4s] [%s] %s    %-70s: %s %s", | ||||
| 		l.ID, l.Tag, timestamp, artifactInfo, distroInfo, status, | ||||
| 		additional) | ||||
|  | ||||
| 	fmt.Println(colored) | ||||
| } | ||||
|  | ||||
| type runstat struct { | ||||
| 	All, BuildOK, RunOK, TestOK, Timeout, Panic int | ||||
| } | ||||
|  | ||||
| func getStats(db *sql.DB, path, tag string) ( | ||||
| 	distros map[string]map[string]map[string]runstat, err error) { | ||||
|  | ||||
| 	var les []logEntry | ||||
|  | ||||
| 	ka, kaErr := config.ReadArtifactConfig(path + "/.out-of-tree.toml") | ||||
| 	if kaErr == nil { | ||||
| 		les, err = getAllArtifactLogs(db, tag, -1, ka) | ||||
| 	} else { | ||||
| 		les, err = getAllLogs(db, tag, -1) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	distros = make(map[string]map[string]map[string]runstat) | ||||
|  | ||||
| 	for _, l := range les { | ||||
| 		_, ok := distros[l.Distro.ID.String()] | ||||
| 		if !ok { | ||||
| 			distros[l.Distro.ID.String()] = make(map[string]map[string]runstat) | ||||
| 		} | ||||
|  | ||||
| 		_, ok = distros[l.Distro.ID.String()][l.Distro.Release] | ||||
| 		if !ok { | ||||
| 			distros[l.Distro.ID.String()][l.Distro.Release] = make(map[string]runstat) | ||||
| 		} | ||||
|  | ||||
| 		rs := distros[l.Distro.ID.String()][l.Distro.Release][l.KernelRelease] | ||||
|  | ||||
| 		rs.All++ | ||||
| 		if l.Build.Ok { | ||||
| 			rs.BuildOK++ | ||||
| 		} | ||||
| 		if l.Run.Ok { | ||||
| 			rs.RunOK++ | ||||
| 		} | ||||
| 		if l.Test.Ok { | ||||
| 			rs.TestOK++ | ||||
| 		} | ||||
| 		if l.KernelPanic { | ||||
| 			rs.Panic++ | ||||
| 		} | ||||
| 		if l.KilledByTimeout { | ||||
| 			rs.Timeout++ | ||||
| 		} | ||||
|  | ||||
| 		distros[l.Distro.ID.String()][l.Distro.Release][l.KernelRelease] = rs | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										88
									
								
								cmd/pack.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								cmd/pack.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // Copyright 2023 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/fs" | ||||
|  | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
|  | ||||
| type PackCmd struct { | ||||
| 	Autogen     bool  `help:"kernel autogeneration"` | ||||
| 	UseHost     bool  `help:"also use host kernels"` | ||||
| 	NoDownload  bool  `help:"do not download qemu image while kernel generation"` | ||||
| 	ExploitRuns int64 `default:"4" help:"amount of runs of each exploit"` | ||||
| 	KernelRuns  int64 `default:"1" help:"amount of runs of each kernel"` | ||||
| 	Max         int   `help:"download random kernels from set defined by regex in release_mask, but no more than X for each of release_mask" default:"1"` | ||||
|  | ||||
| 	Threads int `help:"threads" default:"4"` | ||||
|  | ||||
| 	Tag string `help:"filter tag"` | ||||
|  | ||||
| 	Timeout       time.Duration `help:"timeout after tool will not spawn new tests"` | ||||
| 	QemuTimeout   time.Duration `help:"timeout for qemu"` | ||||
| 	DockerTimeout time.Duration `help:"timeout for docker"` | ||||
| } | ||||
|  | ||||
| func (cmd *PackCmd) Run(g *Globals) (err error) { | ||||
| 	tag := fmt.Sprintf("pack_run_%d", time.Now().Unix()) | ||||
| 	log.Print("Tag:", tag) | ||||
|  | ||||
| 	files, err := ioutil.ReadDir(g.WorkDir) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, f := range files { | ||||
| 		workPath := g.WorkDir + "/" + f.Name() | ||||
|  | ||||
| 		if !fs.PathExists(workPath + "/.out-of-tree.toml") { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if cmd.Autogen { | ||||
| 			autogen := KernelAutogenCmd{} | ||||
| 			err = autogen.Run( | ||||
| 				&KernelCmd{ | ||||
| 					NoDownload: cmd.NoDownload, | ||||
| 					UseHost:    cmd.UseHost, | ||||
| 					Max:        cmd.Max, | ||||
| 				}, | ||||
| 				&Globals{ | ||||
| 					Config:  g.Config, | ||||
| 					WorkDir: workPath, | ||||
| 				}, | ||||
| 			) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		log.Print(f.Name()) | ||||
|  | ||||
| 		pew := PewCmd{ | ||||
| 			Max:           cmd.KernelRuns, | ||||
| 			Runs:          cmd.ExploitRuns, | ||||
| 			Threads:       cmd.Threads, | ||||
| 			Tag:           tag, | ||||
| 			Timeout:       cmd.Timeout, | ||||
| 			QemuTimeout:   cmd.QemuTimeout, | ||||
| 			DockerTimeout: cmd.DockerTimeout, | ||||
| 			Dist:          pathDevNull, | ||||
| 		} | ||||
|  | ||||
| 		pew.Run(&Globals{ | ||||
| 			Config:  g.Config, | ||||
| 			WorkDir: workPath, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										966
									
								
								cmd/pew.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										966
									
								
								cmd/pew.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,966 @@ | ||||
| // Copyright 2023 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"math/rand" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/otiai10/copy" | ||||
| 	"github.com/remeh/sizedwaitgroup" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"gopkg.in/logrusorgru/aurora.v2" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/container" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/fs" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/qemu" | ||||
| ) | ||||
|  | ||||
| type LevelWriter struct { | ||||
| 	io.Writer | ||||
| 	Level zerolog.Level | ||||
| } | ||||
|  | ||||
| func (lw *LevelWriter) WriteLevel(l zerolog.Level, p []byte) (n int, err error) { | ||||
| 	if l >= lw.Level { | ||||
| 		return lw.Writer.Write(p) | ||||
| 	} | ||||
| 	return len(p), nil | ||||
| } | ||||
|  | ||||
| var ConsoleWriter, FileWriter LevelWriter | ||||
|  | ||||
| var LogLevel zerolog.Level | ||||
|  | ||||
| type PewCmd struct { | ||||
| 	Max     int64         `help:"test no more than X kernels" default:"100500"` | ||||
| 	Runs    int64         `help:"runs per each kernel" default:"1"` | ||||
| 	Kernel  string        `help:"override kernel regex"` | ||||
| 	RootFS  string        `help:"override rootfs image" type:"existingfile"` | ||||
| 	Guess   bool          `help:"try all defined kernels"` | ||||
| 	Shuffle bool          `help:"randomize kernels test order"` | ||||
| 	Binary  string        `help:"use binary, do not build"` | ||||
| 	Test    string        `help:"override path for test"` | ||||
| 	Dist    string        `help:"build result path" default:"/dev/null"` | ||||
| 	Threads int           `help:"threads" default:"1"` | ||||
| 	Tag     string        `help:"log tagging"` | ||||
| 	Timeout time.Duration `help:"timeout after tool will not spawn new tests"` | ||||
|  | ||||
| 	ArtifactConfig string `help:"path to artifact config" type:"path"` | ||||
|  | ||||
| 	QemuTimeout           time.Duration `help:"timeout for qemu"` | ||||
| 	QemuAfterStartTimeout time.Duration `help:"timeout after starting of the qemu vm before tests"` | ||||
| 	DockerTimeout         time.Duration `help:"timeout for docker"` | ||||
|  | ||||
| 	Threshold             float64 `help:"reliablity threshold for exit code" default:"1.00"` | ||||
| 	IncludeInternalErrors bool    `help:"count internal errors as part of the success rate"` | ||||
|  | ||||
| 	Endless        bool          `help:"endless tests"` | ||||
| 	EndlessTimeout time.Duration `help:"timeout between tests" default:"1m"` | ||||
| 	EndlessStress  string        `help:"endless stress script" type:"existingfile"` | ||||
|  | ||||
| 	db              *sql.DB | ||||
| 	kcfg            config.KernelConfig | ||||
| 	timeoutDeadline time.Time | ||||
| } | ||||
|  | ||||
| func (cmd *PewCmd) Run(g *Globals) (err error) { | ||||
| 	cmd.kcfg, err = config.ReadKernelConfig(g.Config.Kernels) | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err).Msg("read kernels config") | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Timeout != 0 { | ||||
| 		log.Info().Msgf("Set global timeout to %s", cmd.Timeout) | ||||
| 		cmd.timeoutDeadline = time.Now().Add(cmd.Timeout) | ||||
| 	} | ||||
|  | ||||
| 	cmd.db, err = openDatabase(g.Config.Database) | ||||
| 	if err != nil { | ||||
| 		log.Fatal().Err(err). | ||||
| 			Msgf("Cannot open database %s", g.Config.Database) | ||||
| 	} | ||||
| 	defer cmd.db.Close() | ||||
|  | ||||
| 	var configPath string | ||||
| 	if cmd.ArtifactConfig == "" { | ||||
| 		configPath = g.WorkDir + "/.out-of-tree.toml" | ||||
| 	} else { | ||||
| 		configPath = cmd.ArtifactConfig | ||||
| 	} | ||||
| 	ka, err := config.ReadArtifactConfig(configPath) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(ka.Targets) == 0 || cmd.Guess { | ||||
| 		log.Debug().Msg("will use all available targets") | ||||
|  | ||||
| 		for _, dist := range distro.List() { | ||||
| 			ka.Targets = append(ka.Targets, config.Target{ | ||||
| 				Distro: dist, | ||||
| 				Kernel: config.Kernel{ | ||||
| 					Regex: ".*", | ||||
| 				}, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if ka.SourcePath == "" { | ||||
| 		ka.SourcePath = g.WorkDir | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Kernel != "" { | ||||
| 		var km config.Target | ||||
| 		km, err = kernelMask(cmd.Kernel) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		ka.Targets = []config.Target{km} | ||||
| 	} | ||||
|  | ||||
| 	if cmd.QemuTimeout != 0 { | ||||
| 		log.Info().Msgf("Set qemu timeout to %s", cmd.QemuTimeout) | ||||
| 	} else { | ||||
| 		cmd.QemuTimeout = g.Config.Qemu.Timeout.Duration | ||||
| 	} | ||||
|  | ||||
| 	if cmd.DockerTimeout != 0 { | ||||
| 		log.Info().Msgf("Set docker timeout to %s", cmd.DockerTimeout) | ||||
| 	} else { | ||||
| 		cmd.DockerTimeout = g.Config.Docker.Timeout.Duration | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Tag == "" { | ||||
| 		cmd.Tag = fmt.Sprintf("%d", time.Now().Unix()) | ||||
| 	} | ||||
| 	log.Info().Str("tag", cmd.Tag).Msg("") | ||||
|  | ||||
| 	err = cmd.performCI(ka) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if state.InternalErrors > 0 { | ||||
| 		s := "not counted towards success rate" | ||||
| 		if cmd.IncludeInternalErrors { | ||||
| 			s = "included in success rate" | ||||
| 		} | ||||
| 		log.Warn().Msgf("%d internal errors "+ | ||||
| 			"(%s)", state.InternalErrors, s) | ||||
| 	} | ||||
|  | ||||
| 	if cmd.IncludeInternalErrors { | ||||
| 		state.Overall += float64(state.InternalErrors) | ||||
| 	} | ||||
|  | ||||
| 	msg := fmt.Sprintf("Success rate: %.02f (%d/%d), Threshold: %.02f", | ||||
| 		successRate(state), | ||||
| 		int(state.Success), int(state.Overall), | ||||
| 		cmd.Threshold) | ||||
|  | ||||
| 	if successRate(state) < cmd.Threshold { | ||||
| 		log.Error().Msg(msg) | ||||
| 		err = errors.New("reliability threshold not met") | ||||
| 	} else { | ||||
| 		log.Info().Msg(msg) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type runstate struct { | ||||
| 	Overall, Success float64 | ||||
| 	InternalErrors   int | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	state runstate | ||||
| ) | ||||
|  | ||||
| func successRate(state runstate) float64 { | ||||
| 	return state.Success / state.Overall | ||||
| } | ||||
|  | ||||
| const pathDevNull = "/dev/null" | ||||
|  | ||||
| func sh(workdir, command string) (output string, err error) { | ||||
| 	flog := log.With(). | ||||
| 		Str("workdir", workdir). | ||||
| 		Str("command", command). | ||||
| 		Logger() | ||||
|  | ||||
| 	cmd := exec.Command("sh", "-c", "cd "+workdir+" && "+command) | ||||
|  | ||||
| 	flog.Debug().Msgf("%v", cmd) | ||||
|  | ||||
| 	stdout, err := cmd.StdoutPipe() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	cmd.Stderr = cmd.Stdout | ||||
|  | ||||
| 	err = cmd.Start() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		scanner := bufio.NewScanner(stdout) | ||||
| 		for scanner.Scan() { | ||||
| 			m := scanner.Text() | ||||
| 			output += m + "\n" | ||||
| 			flog.Trace().Str("stdout", m).Msg("") | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	err = cmd.Wait() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		e := fmt.Sprintf("%v %v output: %v", cmd, err, output) | ||||
| 		err = errors.New(e) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func applyPatches(src string, ka config.Artifact) (err error) { | ||||
| 	for i, patch := range ka.Patches { | ||||
| 		name := fmt.Sprintf("patch_%02d", i) | ||||
|  | ||||
| 		path := src + "/" + name + ".diff" | ||||
| 		if patch.Source != "" && patch.Path != "" { | ||||
| 			err = errors.New("path and source are mutually exclusive") | ||||
| 			return | ||||
| 		} else if patch.Source != "" { | ||||
| 			err = os.WriteFile(path, []byte(patch.Source), 0644) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} else if patch.Path != "" { | ||||
| 			err = copy.Copy(patch.Path, path) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if patch.Source != "" || patch.Path != "" { | ||||
| 			_, err = sh(src, "patch < "+path) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if patch.Script != "" { | ||||
| 			script := src + "/" + name + ".sh" | ||||
| 			err = os.WriteFile(script, []byte(patch.Script), 0755) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 			_, err = sh(src, script) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func build(flog zerolog.Logger, tmp string, ka config.Artifact, | ||||
| 	ki distro.KernelInfo, dockerTimeout time.Duration) ( | ||||
| 	outdir, outpath, output string, err error) { | ||||
|  | ||||
| 	target := strings.Replace(ka.Name, " ", "_", -1) | ||||
| 	if target == "" { | ||||
| 		target = fmt.Sprintf("%d", rand.Int()) | ||||
| 	} | ||||
|  | ||||
| 	outdir = tmp + "/source" | ||||
|  | ||||
| 	err = copy.Copy(ka.SourcePath, outdir) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = applyPatches(outdir, ka) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	outpath = outdir + "/" + target | ||||
| 	if ka.Type == config.KernelModule { | ||||
| 		outpath += ".ko" | ||||
| 	} | ||||
|  | ||||
| 	if ki.KernelVersion == "" { | ||||
| 		ki.KernelVersion = ki.KernelRelease | ||||
| 	} | ||||
|  | ||||
| 	kernel := "/lib/modules/" + ki.KernelVersion + "/build" | ||||
| 	if ki.KernelSource != "" { | ||||
| 		kernel = ki.KernelSource | ||||
| 	} | ||||
|  | ||||
| 	buildCommand := "make KERNEL=" + kernel + " TARGET=" + target | ||||
| 	if ka.Make.Target != "" { | ||||
| 		buildCommand += " " + ka.Make.Target | ||||
| 	} | ||||
|  | ||||
| 	if ki.ContainerName != "" { | ||||
| 		var c container.Container | ||||
| 		container.Timeout = dockerTimeout | ||||
| 		c, err = container.NewFromKernelInfo(ki) | ||||
| 		c.Log = flog | ||||
| 		if err != nil { | ||||
| 			log.Fatal().Err(err).Msg("container creation failure") | ||||
| 		} | ||||
|  | ||||
| 		output, err = c.Run(outdir, []string{ | ||||
| 			buildCommand + " && chmod -R 777 /work", | ||||
| 		}) | ||||
| 	} else { | ||||
| 		cmd := exec.Command("bash", "-c", "cd "+outdir+" && "+ | ||||
| 			buildCommand) | ||||
|  | ||||
| 		log.Debug().Msgf("%v", cmd) | ||||
|  | ||||
| 		timer := time.AfterFunc(dockerTimeout, func() { | ||||
| 			cmd.Process.Kill() | ||||
| 		}) | ||||
| 		defer timer.Stop() | ||||
|  | ||||
| 		var raw []byte | ||||
| 		raw, err = cmd.CombinedOutput() | ||||
| 		if err != nil { | ||||
| 			e := fmt.Sprintf("error `%v` for cmd `%v` with output `%v`", | ||||
| 				err, buildCommand, string(raw)) | ||||
| 			err = errors.New(e) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		output = string(raw) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func runScript(q *qemu.System, script string) (output string, err error) { | ||||
| 	return q.Command("root", script) | ||||
| } | ||||
|  | ||||
| func testKernelModule(q *qemu.System, ka config.Artifact, | ||||
| 	test string) (output string, err error) { | ||||
|  | ||||
| 	output, err = q.Command("root", test) | ||||
| 	// TODO generic checks for WARNING's and so on | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func testKernelExploit(q *qemu.System, ka config.Artifact, | ||||
| 	test, exploit string) (output string, err error) { | ||||
|  | ||||
| 	output, err = q.Command("user", "chmod +x "+exploit) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	randFilePath := fmt.Sprintf("/root/%d", rand.Int()) | ||||
|  | ||||
| 	cmd := fmt.Sprintf("%s %s %s", test, exploit, randFilePath) | ||||
| 	output, err = q.Command("user", cmd) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_, err = q.Command("root", "stat "+randFilePath) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func genOkFail(name string, ok bool) (aurv aurora.Value) { | ||||
| 	s := " " + name | ||||
| 	if name == "" { | ||||
| 		s = "" | ||||
| 	} | ||||
| 	if ok { | ||||
| 		s += " SUCCESS " | ||||
| 		aurv = aurora.BgGreen(aurora.Black(s)) | ||||
| 	} else { | ||||
| 		s += " FAILURE " | ||||
| 		aurv = aurora.BgRed(aurora.White(aurora.Bold(s))) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| type phasesResult struct { | ||||
| 	BuildDir         string | ||||
| 	BuildArtifact    string | ||||
| 	Build, Run, Test struct { | ||||
| 		Output string | ||||
| 		Ok     bool | ||||
| 	} | ||||
|  | ||||
| 	InternalError       error | ||||
| 	InternalErrorString string | ||||
| } | ||||
|  | ||||
| func copyFile(sourcePath, destinationPath string) (err error) { | ||||
| 	sourceFile, err := os.Open(sourcePath) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer sourceFile.Close() | ||||
|  | ||||
| 	destinationFile, err := os.Create(destinationPath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if _, err := io.Copy(destinationFile, sourceFile); err != nil { | ||||
| 		destinationFile.Close() | ||||
| 		return err | ||||
| 	} | ||||
| 	return destinationFile.Close() | ||||
| } | ||||
|  | ||||
| func dumpResult(q *qemu.System, ka config.Artifact, ki distro.KernelInfo, | ||||
| 	res *phasesResult, dist, tag, binary string, db *sql.DB) { | ||||
|  | ||||
| 	// TODO refactor | ||||
|  | ||||
| 	if res.InternalError != nil { | ||||
| 		q.Log.Warn().Err(res.InternalError). | ||||
| 			Str("panic", fmt.Sprintf("%v", q.KernelPanic)). | ||||
| 			Str("timeout", fmt.Sprintf("%v", q.KilledByTimeout)). | ||||
| 			Msg("internal") | ||||
| 		res.InternalErrorString = res.InternalError.Error() | ||||
| 		state.InternalErrors += 1 | ||||
| 	} else { | ||||
| 		colored := "" | ||||
|  | ||||
| 		state.Overall += 1 | ||||
|  | ||||
| 		if res.Test.Ok { | ||||
| 			state.Success += 1 | ||||
| 		} | ||||
|  | ||||
| 		switch ka.Type { | ||||
| 		case config.KernelExploit: | ||||
| 			colored = aurora.Sprintf("%s %s", | ||||
| 				genOkFail("BUILD", res.Build.Ok), | ||||
| 				genOkFail("LPE", res.Test.Ok)) | ||||
| 		case config.KernelModule: | ||||
| 			colored = aurora.Sprintf("%s %s %s", | ||||
| 				genOkFail("BUILD", res.Build.Ok), | ||||
| 				genOkFail("INSMOD", res.Run.Ok), | ||||
| 				genOkFail("TEST", res.Test.Ok)) | ||||
| 		case config.Script: | ||||
| 			colored = aurora.Sprintf("%s", | ||||
| 				genOkFail("", res.Test.Ok)) | ||||
| 		} | ||||
|  | ||||
| 		additional := "" | ||||
| 		if q.KernelPanic { | ||||
| 			additional = "(panic)" | ||||
| 		} else if q.KilledByTimeout { | ||||
| 			additional = "(timeout)" | ||||
| 		} | ||||
|  | ||||
| 		if additional != "" { | ||||
| 			q.Log.Info().Msgf("%v %v", colored, additional) | ||||
| 		} else { | ||||
| 			q.Log.Info().Msgf("%v", colored) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err := addToLog(db, q, ka, ki, res, tag) | ||||
| 	if err != nil { | ||||
| 		q.Log.Warn().Err(err).Msgf("[db] addToLog (%v)", ka) | ||||
| 	} | ||||
|  | ||||
| 	if binary == "" && dist != pathDevNull { | ||||
| 		err = os.MkdirAll(dist, os.ModePerm) | ||||
| 		if err != nil { | ||||
| 			log.Warn().Err(err).Msgf("os.MkdirAll (%v)", ka) | ||||
| 		} | ||||
|  | ||||
| 		path := fmt.Sprintf("%s/%s-%s-%s", dist, ki.Distro.ID, | ||||
| 			ki.Distro.Release, ki.KernelRelease) | ||||
| 		if ka.Type != config.KernelExploit { | ||||
| 			path += ".ko" | ||||
| 		} | ||||
|  | ||||
| 		err = copyFile(res.BuildArtifact, path) | ||||
| 		if err != nil { | ||||
| 			log.Warn().Err(err).Msgf("copy file (%v)", ka) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func copyArtifactAndTest(slog zerolog.Logger, q *qemu.System, ka config.Artifact, | ||||
| 	res *phasesResult, remoteTest string) (err error) { | ||||
|  | ||||
| 	// Copy all test files to the remote machine | ||||
| 	for _, f := range ka.TestFiles { | ||||
| 		if f.Local[0] != '/' { | ||||
| 			if res.BuildDir != "" { | ||||
| 				f.Local = res.BuildDir + "/" + f.Local | ||||
| 			} | ||||
| 		} | ||||
| 		err = q.CopyFile(f.User, f.Local, f.Remote) | ||||
| 		if err != nil { | ||||
| 			res.InternalError = err | ||||
| 			slog.Error().Err(err).Msg("copy test file") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch ka.Type { | ||||
| 	case config.KernelModule: | ||||
| 		res.Run.Output, err = q.CopyAndInsmod(res.BuildArtifact) | ||||
| 		if err != nil { | ||||
| 			slog.Error().Err(err).Msg(res.Run.Output) | ||||
| 			// TODO errors.As | ||||
| 			if strings.Contains(err.Error(), "connection refused") { | ||||
| 				res.InternalError = err | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		res.Run.Ok = true | ||||
|  | ||||
| 		res.Test.Output, err = testKernelModule(q, ka, remoteTest) | ||||
| 		if err != nil { | ||||
| 			slog.Error().Err(err).Msg(res.Test.Output) | ||||
| 			return | ||||
| 		} | ||||
| 		res.Test.Ok = true | ||||
| 	case config.KernelExploit: | ||||
| 		remoteExploit := fmt.Sprintf("/tmp/exploit_%d", rand.Int()) | ||||
| 		err = q.CopyFile("user", res.BuildArtifact, remoteExploit) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		res.Test.Output, err = testKernelExploit(q, ka, remoteTest, | ||||
| 			remoteExploit) | ||||
| 		if err != nil { | ||||
| 			slog.Error().Err(err).Msg(res.Test.Output) | ||||
| 			return | ||||
| 		} | ||||
| 		res.Run.Ok = true // does not really used | ||||
| 		res.Test.Ok = true | ||||
| 	case config.Script: | ||||
| 		res.Test.Output, err = runScript(q, remoteTest) | ||||
| 		if err != nil { | ||||
| 			slog.Error().Err(err).Msg(res.Test.Output) | ||||
| 			return | ||||
| 		} | ||||
| 		slog.Info().Msgf("\n%v\n", res.Test.Output) | ||||
| 		res.Run.Ok = true | ||||
| 		res.Test.Ok = true | ||||
| 	default: | ||||
| 		slog.Fatal().Msg("Unsupported artifact type") | ||||
| 	} | ||||
|  | ||||
| 	_, err = q.Command("root", "echo") | ||||
| 	if err != nil { | ||||
| 		slog.Error().Err(err).Msg("after-test ssh reconnect") | ||||
| 		res.Test.Ok = false | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func copyTest(q *qemu.System, testPath string, ka config.Artifact) ( | ||||
| 	remoteTest string, err error) { | ||||
|  | ||||
| 	remoteTest = fmt.Sprintf("/tmp/test_%d", rand.Int()) | ||||
| 	err = q.CopyFile("user", testPath, remoteTest) | ||||
| 	if err != nil { | ||||
| 		if ka.Type == config.KernelExploit { | ||||
| 			q.Command("user", | ||||
| 				"echo -e '#!/bin/sh\necho touch $2 | $1' "+ | ||||
| 					"> "+remoteTest+ | ||||
| 					" && chmod +x "+remoteTest) | ||||
| 		} else { | ||||
| 			q.Command("user", "echo '#!/bin/sh' "+ | ||||
| 				"> "+remoteTest+" && chmod +x "+remoteTest) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	_, err = q.Command("root", "chmod +x "+remoteTest) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func copyStandardModules(q *qemu.System, ki distro.KernelInfo) (err error) { | ||||
| 	_, err = q.Command("root", "mkdir -p /lib/modules/"+ki.KernelVersion) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	remotePath := "/lib/modules/" + ki.KernelVersion + "/" | ||||
|  | ||||
| 	err = q.CopyDirectory("root", ki.ModulesPath+"/kernel", remotePath+"/kernel") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	files, err := ioutil.ReadDir(ki.ModulesPath) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, f := range files { | ||||
| 		if f.Mode()&os.ModeSymlink == os.ModeSymlink { | ||||
| 			continue | ||||
| 		} | ||||
| 		if !strings.HasPrefix(f.Name(), "modules") { | ||||
| 			continue | ||||
| 		} | ||||
| 		err = q.CopyFile("root", ki.ModulesPath+"/"+f.Name(), remotePath) | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (cmd PewCmd) testArtifact(swg *sizedwaitgroup.SizedWaitGroup, | ||||
| 	ka config.Artifact, ki distro.KernelInfo) { | ||||
|  | ||||
| 	defer swg.Done() | ||||
|  | ||||
| 	logdir := "logs/" + cmd.Tag | ||||
| 	err := os.MkdirAll(logdir, os.ModePerm) | ||||
| 	if err != nil { | ||||
| 		log.Error().Err(err).Msgf("mkdir %s", logdir) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	logfile := fmt.Sprintf("logs/%s/%s-%s-%s.log", | ||||
| 		cmd.Tag, | ||||
| 		ki.Distro.ID.String(), | ||||
| 		ki.Distro.Release, | ||||
| 		ki.KernelRelease, | ||||
| 	) | ||||
| 	f, err := os.Create(logfile) | ||||
| 	if err != nil { | ||||
| 		log.Error().Err(err).Msgf("create %s", logfile) | ||||
| 		return | ||||
| 	} | ||||
| 	defer f.Close() | ||||
|  | ||||
| 	slog := zerolog.New(zerolog.MultiLevelWriter( | ||||
| 		&ConsoleWriter, | ||||
| 		&FileWriter, | ||||
| 		&zerolog.ConsoleWriter{ | ||||
| 			Out: f, | ||||
| 			FieldsExclude: []string{ | ||||
| 				"distro_release", | ||||
| 				"distro_type", | ||||
| 				"kernel", | ||||
| 			}, | ||||
| 			NoColor: true, | ||||
| 		}, | ||||
| 	)) | ||||
|  | ||||
| 	switch LogLevel { | ||||
| 	case zerolog.TraceLevel, zerolog.DebugLevel: | ||||
| 		slog = slog.With().Caller().Logger() | ||||
| 	} | ||||
|  | ||||
| 	slog = slog.With().Timestamp(). | ||||
| 		Str("distro_type", ki.Distro.ID.String()). | ||||
| 		Str("distro_release", ki.Distro.Release). | ||||
| 		Str("kernel", ki.KernelRelease). | ||||
| 		Logger() | ||||
|  | ||||
| 	slog.Info().Msg("start") | ||||
| 	testStart := time.Now() | ||||
| 	defer func() { | ||||
| 		slog.Debug().Str("test_duration", | ||||
| 			time.Now().Sub(testStart).String()). | ||||
| 			Msg("") | ||||
| 	}() | ||||
|  | ||||
| 	kernel := qemu.Kernel{KernelPath: ki.KernelPath, InitrdPath: ki.InitrdPath} | ||||
| 	if cmd.RootFS != "" { | ||||
| 		ki.RootFS = cmd.RootFS | ||||
| 	} | ||||
| 	q, err := qemu.NewSystem(qemu.X86x64, kernel, ki.RootFS) | ||||
| 	if err != nil { | ||||
| 		slog.Error().Err(err).Msg("qemu init") | ||||
| 		return | ||||
| 	} | ||||
| 	q.Log = slog | ||||
|  | ||||
| 	q.Timeout = cmd.QemuTimeout | ||||
|  | ||||
| 	if ka.Qemu.Timeout.Duration != 0 { | ||||
| 		q.Timeout = ka.Qemu.Timeout.Duration | ||||
| 	} | ||||
| 	if ka.Qemu.Cpus != 0 { | ||||
| 		q.Cpus = ka.Qemu.Cpus | ||||
| 	} | ||||
| 	if ka.Qemu.Memory != 0 { | ||||
| 		q.Memory = ka.Qemu.Memory | ||||
| 	} | ||||
|  | ||||
| 	if ka.Docker.Timeout.Duration != 0 { | ||||
| 		cmd.DockerTimeout = ka.Docker.Timeout.Duration | ||||
| 	} | ||||
|  | ||||
| 	q.SetKASLR(!ka.Mitigations.DisableKaslr) | ||||
| 	q.SetSMEP(!ka.Mitigations.DisableSmep) | ||||
| 	q.SetSMAP(!ka.Mitigations.DisableSmap) | ||||
| 	q.SetKPTI(!ka.Mitigations.DisableKpti) | ||||
|  | ||||
| 	if ki.CPU.Model != "" { | ||||
| 		q.CPU.Model = ki.CPU.Model | ||||
| 	} | ||||
|  | ||||
| 	if len(ki.CPU.Flags) != 0 { | ||||
| 		q.CPU.Flags = ki.CPU.Flags | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Endless { | ||||
| 		q.Timeout = 0 | ||||
| 	} | ||||
|  | ||||
| 	qemuStart := time.Now() | ||||
| 	err = q.Start() | ||||
| 	if err != nil { | ||||
| 		slog.Error().Err(err).Msg("qemu start") | ||||
| 		return | ||||
| 	} | ||||
| 	defer q.Stop() | ||||
|  | ||||
| 	slog.Debug().Msgf("wait %v", cmd.QemuAfterStartTimeout) | ||||
| 	time.Sleep(cmd.QemuAfterStartTimeout) | ||||
|  | ||||
| 	go func() { | ||||
| 		time.Sleep(time.Minute) | ||||
| 		for !q.Died { | ||||
| 			slog.Debug().Msg("still alive") | ||||
| 			time.Sleep(time.Minute) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	tmp, err := fs.TempDir() | ||||
| 	if err != nil { | ||||
| 		slog.Error().Err(err).Msg("making tmp directory") | ||||
| 		return | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmp) | ||||
|  | ||||
| 	result := phasesResult{} | ||||
| 	if !cmd.Endless { | ||||
| 		defer dumpResult(q, ka, ki, &result, cmd.Dist, cmd.Tag, cmd.Binary, cmd.db) | ||||
| 	} | ||||
|  | ||||
| 	if ka.Type == config.Script { | ||||
| 		result.Build.Ok = true | ||||
| 		cmd.Test = ka.Script | ||||
| 	} else if cmd.Binary == "" { | ||||
| 		// TODO: build should return structure | ||||
| 		start := time.Now() | ||||
| 		result.BuildDir, result.BuildArtifact, result.Build.Output, err = | ||||
| 			build(slog, tmp, ka, ki, cmd.DockerTimeout) | ||||
| 		slog.Debug().Str("duration", time.Now().Sub(start).String()). | ||||
| 			Msg("build done") | ||||
| 		if err != nil { | ||||
| 			log.Error().Err(err).Msg("build") | ||||
| 			return | ||||
| 		} | ||||
| 		result.Build.Ok = true | ||||
| 	} else { | ||||
| 		result.BuildArtifact = cmd.Binary | ||||
| 		result.Build.Ok = true | ||||
| 	} | ||||
|  | ||||
| 	if cmd.Test == "" { | ||||
| 		cmd.Test = result.BuildArtifact + "_test" | ||||
| 		if !fs.PathExists(cmd.Test) { | ||||
| 			slog.Debug().Msgf("%s does not exist", cmd.Test) | ||||
| 			cmd.Test = tmp + "/source/" + "test.sh" | ||||
| 		} else { | ||||
| 			slog.Debug().Msgf("%s exist", cmd.Test) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = q.WaitForSSH(cmd.QemuTimeout) | ||||
| 	if err != nil { | ||||
| 		result.InternalError = err | ||||
| 		return | ||||
| 	} | ||||
| 	slog.Debug().Str("qemu_startup_duration", | ||||
| 		time.Now().Sub(qemuStart).String()). | ||||
| 		Msg("ssh is available") | ||||
|  | ||||
| 	remoteTest, err := copyTest(q, cmd.Test, ka) | ||||
| 	if err != nil { | ||||
| 		result.InternalError = err | ||||
| 		slog.Error().Err(err).Msg("copy test script") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ka.StandardModules { | ||||
| 		// Module depends on one of the standard modules | ||||
| 		start := time.Now() | ||||
| 		err = copyStandardModules(q, ki) | ||||
| 		if err != nil { | ||||
| 			result.InternalError = err | ||||
| 			slog.Error().Err(err).Msg("copy standard modules") | ||||
| 			return | ||||
| 		} | ||||
| 		slog.Debug().Str("duration", time.Now().Sub(start).String()). | ||||
| 			Msg("copy standard modules") | ||||
| 	} | ||||
|  | ||||
| 	err = preloadModules(q, ka, ki, cmd.DockerTimeout) | ||||
| 	if err != nil { | ||||
| 		result.InternalError = err | ||||
| 		slog.Error().Err(err).Msg("preload modules") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	start := time.Now() | ||||
| 	copyArtifactAndTest(slog, q, ka, &result, remoteTest) | ||||
| 	slog.Debug().Str("duration", time.Now().Sub(start).String()). | ||||
| 		Msgf("test completed (success: %v)", result.Test.Ok) | ||||
|  | ||||
| 	if !cmd.Endless { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	dumpResult(q, ka, ki, &result, cmd.Dist, cmd.Tag, cmd.Binary, cmd.db) | ||||
|  | ||||
| 	if !result.Build.Ok || !result.Run.Ok || !result.Test.Ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	slog.Info().Msg("start endless tests") | ||||
|  | ||||
| 	if cmd.EndlessStress != "" { | ||||
| 		slog.Debug().Msg("copy and run endless stress script") | ||||
| 		err = q.CopyAndRunAsync("root", cmd.EndlessStress) | ||||
| 		if err != nil { | ||||
| 			q.Stop() | ||||
| 			f.Sync() | ||||
| 			slog.Fatal().Err(err).Msg("cannot copy/run stress") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for { | ||||
| 		output, err := q.Command("root", remoteTest) | ||||
| 		if err != nil { | ||||
| 			q.Stop() | ||||
| 			f.Sync() | ||||
| 			slog.Fatal().Err(err).Msg(output) | ||||
| 			return | ||||
| 		} | ||||
| 		slog.Debug().Msg(output) | ||||
|  | ||||
| 		slog.Info().Msg("test success") | ||||
|  | ||||
| 		slog.Debug().Msgf("wait %v", cmd.EndlessTimeout) | ||||
| 		time.Sleep(cmd.EndlessTimeout) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func shuffleKernels(a []distro.KernelInfo) []distro.KernelInfo { | ||||
| 	// Fisher–Yates shuffle | ||||
| 	for i := len(a) - 1; i > 0; i-- { | ||||
| 		j := rand.Intn(i + 1) | ||||
| 		a[i], a[j] = a[j], a[i] | ||||
| 	} | ||||
| 	return a | ||||
| } | ||||
|  | ||||
| func (cmd PewCmd) performCI(ka config.Artifact) (err error) { | ||||
| 	found := false | ||||
| 	max := cmd.Max | ||||
|  | ||||
| 	threadCounter := 0 | ||||
|  | ||||
| 	swg := sizedwaitgroup.New(cmd.Threads) | ||||
| 	if cmd.Shuffle { | ||||
| 		cmd.kcfg.Kernels = shuffleKernels(cmd.kcfg.Kernels) | ||||
| 	} | ||||
| 	for _, kernel := range cmd.kcfg.Kernels { | ||||
| 		if max <= 0 { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		var supported bool | ||||
| 		supported, err = ka.Supported(kernel) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if kernel.Blocklisted { | ||||
| 			log.Debug().Str("kernel", kernel.KernelVersion). | ||||
| 				Msgf("skip (blocklisted)") | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if supported { | ||||
| 			found = true | ||||
| 			max-- | ||||
| 			for i := int64(0); i < cmd.Runs; i++ { | ||||
| 				if !cmd.timeoutDeadline.IsZero() && | ||||
| 					time.Now().After(cmd.timeoutDeadline) { | ||||
|  | ||||
| 					break | ||||
| 				} | ||||
| 				swg.Add() | ||||
| 				if threadCounter < cmd.Threads { | ||||
| 					time.Sleep(time.Second) | ||||
| 					threadCounter++ | ||||
| 				} | ||||
| 				go cmd.testArtifact(&swg, ka, kernel) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	swg.Wait() | ||||
|  | ||||
| 	if !found { | ||||
| 		err = errors.New("No supported kernels found") | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func kernelMask(kernel string) (km config.Target, err error) { | ||||
| 	parts := strings.Split(kernel, ":") | ||||
| 	if len(parts) != 2 { | ||||
| 		err = errors.New("Kernel is not 'distroType:regex'") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	dt, err := distro.NewID(parts[0]) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	km = config.Target{ | ||||
| 		Distro: distro.Distro{ID: dt}, | ||||
| 		Kernel: config.Kernel{Regex: parts[1]}, | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										164
									
								
								cmd/preload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								cmd/preload.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| // Copyright 2020 Mikhail Klementev. All rights reserved. | ||||
| // Use of this source code is governed by a AGPLv3 license | ||||
| // (or later) that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"crypto/sha1" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/go-git/go-git/v5" | ||||
| 	"github.com/rs/zerolog/log" | ||||
|  | ||||
| 	"code.dumpstack.io/tools/out-of-tree/config" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/distro" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/fs" | ||||
| 	"code.dumpstack.io/tools/out-of-tree/qemu" | ||||
| ) | ||||
|  | ||||
| func preloadModules(q *qemu.System, ka config.Artifact, ki distro.KernelInfo, | ||||
| 	dockerTimeout time.Duration) (err error) { | ||||
|  | ||||
| 	for _, pm := range ka.Preload { | ||||
| 		err = preload(q, ki, pm, dockerTimeout) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func preload(q *qemu.System, ki distro.KernelInfo, pm config.PreloadModule, | ||||
| 	dockerTimeout time.Duration) (err error) { | ||||
|  | ||||
| 	var workPath, cache string | ||||
| 	if pm.Path != "" { | ||||
| 		log.Print("Use non-git path for preload module (no cache)") | ||||
| 		workPath = pm.Path | ||||
| 	} else if pm.Repo != "" { | ||||
| 		workPath, cache, err = cloneOrPull(pm.Repo, ki) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		errors.New("No repo/path in preload entry") | ||||
| 	} | ||||
|  | ||||
| 	err = buildAndInsmod(workPath, q, ki, dockerTimeout, cache) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	time.Sleep(pm.TimeoutAfterLoad.Duration) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func buildAndInsmod(workPath string, q *qemu.System, ki distro.KernelInfo, | ||||
| 	dockerTimeout time.Duration, cache string) (err error) { | ||||
|  | ||||
| 	tmp, err := fs.TempDir() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmp) | ||||
|  | ||||
| 	var artifact string | ||||
| 	if fs.PathExists(cache) { | ||||
| 		artifact = cache | ||||
| 	} else { | ||||
| 		artifact, err = buildPreload(workPath, tmp, ki, dockerTimeout) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if cache != "" { | ||||
| 			err = copyFile(artifact, cache) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	output, err := q.CopyAndInsmod(artifact) | ||||
| 	if err != nil { | ||||
| 		log.Print(output) | ||||
| 		return | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func buildPreload(workPath, tmp string, ki distro.KernelInfo, | ||||
| 	dockerTimeout time.Duration) (artifact string, err error) { | ||||
|  | ||||
| 	ka, err := config.ReadArtifactConfig(workPath + "/.out-of-tree.toml") | ||||
| 	if err != nil { | ||||
| 		log.Warn().Err(err).Msg("preload") | ||||
| 	} | ||||
|  | ||||
| 	ka.SourcePath = workPath | ||||
|  | ||||
| 	km := config.Target{ | ||||
| 		Distro: ki.Distro, | ||||
| 		Kernel: config.Kernel{Regex: ki.KernelRelease}, | ||||
| 	} | ||||
| 	ka.Targets = []config.Target{km} | ||||
|  | ||||
| 	if ka.Docker.Timeout.Duration != 0 { | ||||
| 		dockerTimeout = ka.Docker.Timeout.Duration | ||||
| 	} | ||||
|  | ||||
| 	_, artifact, _, err = build(log.Logger, tmp, ka, ki, dockerTimeout) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func cloneOrPull(repo string, ki distro.KernelInfo) (workPath, cache string, | ||||
| 	err error) { | ||||
|  | ||||
| 	base := config.Dir("preload") | ||||
| 	workPath = filepath.Join(base, "/repos/", sha1sum(repo)) | ||||
|  | ||||
| 	var r *git.Repository | ||||
| 	if fs.PathExists(workPath) { | ||||
| 		r, err = git.PlainOpen(workPath) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var w *git.Worktree | ||||
| 		w, err = r.Worktree() | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		err = w.Pull(&git.PullOptions{}) | ||||
| 		if err != nil && err != git.NoErrAlreadyUpToDate { | ||||
| 			log.Print(repo, "pull error:", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		r, err = git.PlainClone(workPath, false, &git.CloneOptions{URL: repo}) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ref, err := r.Head() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	cachedir := filepath.Join(base, "/cache/") | ||||
| 	os.MkdirAll(cachedir, 0700) | ||||
|  | ||||
| 	filename := sha1sum(repo + ki.KernelPath + ref.Hash().String()) | ||||
| 	cache = filepath.Join(cachedir, filename) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func sha1sum(data string) string { | ||||
| 	h := sha1.Sum([]byte(data)) | ||||
| 	return hex.EncodeToString(h[:]) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user