feat: initial daemon implementation
This commit is contained in:
parent
820208d079
commit
0314b5ca93
202
api/api.go
Normal file
202
api/api.go
Normal file
@ -0,0 +1,202 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
"code.dumpstack.io/tools/out-of-tree/artifact"
|
||||
"code.dumpstack.io/tools/out-of-tree/distro"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var ErrInvalid = errors.New("")
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusNew Status = "new"
|
||||
StatusWaiting Status = "waiting"
|
||||
StatusRunning Status = "running"
|
||||
StatusSuccess Status = "success"
|
||||
StatusFailure Status = "failure"
|
||||
)
|
||||
|
||||
type Command string
|
||||
|
||||
const (
|
||||
RawMode Command = "rawmode"
|
||||
|
||||
AddJob Command = "add_job"
|
||||
ListJobs Command = "list_jobs"
|
||||
JobLogs Command = "job_logs"
|
||||
JobStatus Command = "job_status"
|
||||
|
||||
AddRepo Command = "add_repo"
|
||||
ListRepos Command = "list_repos"
|
||||
|
||||
Kernels Command = "kernels"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
ID int64
|
||||
UUID string
|
||||
|
||||
RepoName string
|
||||
Commit string
|
||||
|
||||
Params string
|
||||
Artifact artifact.Artifact
|
||||
Target distro.KernelInfo
|
||||
|
||||
Status Status
|
||||
}
|
||||
|
||||
func (job *Job) GenUUID() {
|
||||
job.UUID = uuid.New().String()
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
ID int64
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
type JobLog struct {
|
||||
Name string
|
||||
Text string
|
||||
}
|
||||
|
||||
type Req struct {
|
||||
Command Command
|
||||
|
||||
Type string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (r *Req) SetData(data any) {
|
||||
r.Type = fmt.Sprintf("%v", reflect.TypeOf(data))
|
||||
r.Data = Marshal(data)
|
||||
}
|
||||
|
||||
func (r *Req) GetData(data any) (err error) {
|
||||
if len(r.Data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t := fmt.Sprintf("%v", reflect.TypeOf(data))
|
||||
if r.Type != t {
|
||||
err = fmt.Errorf("type mismatch (%v != %v)", r.Type, t)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("unmarshal %v", string(r.Data))
|
||||
err = json.Unmarshal(r.Data, &data)
|
||||
return
|
||||
}
|
||||
|
||||
func (r Req) Encode(conn net.Conn) {
|
||||
log.Trace().Msgf("encode %v", spew.Sdump(r))
|
||||
err := json.NewEncoder(conn).Encode(&r)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("encode %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Req) Decode(conn net.Conn) (err error) {
|
||||
err = json.NewDecoder(conn).Decode(r)
|
||||
return
|
||||
}
|
||||
|
||||
func (r Req) Marshal() (bytes []byte) {
|
||||
return Marshal(r)
|
||||
}
|
||||
|
||||
func (Req) Unmarshal(data []byte) (r Req, err error) {
|
||||
err = json.Unmarshal(data, &r)
|
||||
log.Trace().Msgf("unmarshal %v", spew.Sdump(r))
|
||||
return
|
||||
}
|
||||
|
||||
type Resp struct {
|
||||
UUID string
|
||||
|
||||
Error string
|
||||
|
||||
Err error `json:"-"`
|
||||
|
||||
Type string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func NewResp() (resp Resp) {
|
||||
resp.UUID = uuid.New().String()
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resp) SetData(data any) {
|
||||
r.Type = fmt.Sprintf("%v", reflect.TypeOf(data))
|
||||
r.Data = Marshal(data)
|
||||
}
|
||||
|
||||
func (r *Resp) GetData(data any) (err error) {
|
||||
if len(r.Data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t := fmt.Sprintf("%v", reflect.TypeOf(data))
|
||||
if r.Type != t {
|
||||
err = fmt.Errorf("type mismatch (%v != %v)", r.Type, t)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("unmarshal %v", string(r.Data))
|
||||
err = json.Unmarshal(r.Data, &data)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resp) Encode(conn net.Conn) {
|
||||
if r.Err != nil && r.Err != ErrInvalid && r.Error == "" {
|
||||
r.Error = fmt.Sprintf("%v", r.Err)
|
||||
}
|
||||
log.Trace().Msgf("encode %v", spew.Sdump(r))
|
||||
err := json.NewEncoder(conn).Encode(r)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("encode %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resp) Decode(conn net.Conn) (err error) {
|
||||
err = json.NewDecoder(conn).Decode(r)
|
||||
r.Err = ErrInvalid
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resp) Marshal() (bytes []byte) {
|
||||
if r.Err != nil && r.Err != ErrInvalid && r.Error == "" {
|
||||
r.Error = fmt.Sprintf("%v", r.Err)
|
||||
}
|
||||
|
||||
return Marshal(r)
|
||||
}
|
||||
|
||||
func (Resp) Unmarshal(data []byte) (r Resp, err error) {
|
||||
err = json.Unmarshal(data, &r)
|
||||
log.Trace().Msgf("unmarshal %v", spew.Sdump(r))
|
||||
r.Err = ErrInvalid
|
||||
return
|
||||
}
|
||||
|
||||
func Marshal(data any) (bytes []byte) {
|
||||
bytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("marshal %v", data)
|
||||
}
|
||||
log.Trace().Msgf("marshal %v", string(bytes))
|
||||
return
|
||||
}
|
47
api/api_test.go
Normal file
47
api/api_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReq(t *testing.T) {
|
||||
req := Req{}
|
||||
|
||||
req.Command = ListRepos
|
||||
req.SetData(Job{ID: 999, RepoName: "test"})
|
||||
|
||||
bytes := req.Marshal()
|
||||
|
||||
req2, err := Req{}.Unmarshal(bytes)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, req, req2)
|
||||
|
||||
job := Job{}
|
||||
err = req2.GetData(&job)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, req2.Type, "api.Job")
|
||||
}
|
||||
|
||||
func TestResp(t *testing.T) {
|
||||
resp := Resp{}
|
||||
|
||||
resp.Error = "abracadabra"
|
||||
resp.SetData([]Repo{Repo{}, Repo{}})
|
||||
|
||||
bytes := resp.Marshal()
|
||||
|
||||
resp2, err := Resp{}.Unmarshal(bytes)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, resp, resp2)
|
||||
|
||||
var repos []Repo
|
||||
err = resp2.GetData(&repos)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, resp2.Type, "[]api.Repo")
|
||||
}
|
436
artifact/artifact.go
Normal file
436
artifact/artifact.go
Normal file
@ -0,0 +1,436 @@
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/naoina/toml"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"code.dumpstack.io/tools/out-of-tree/config/dotfiles"
|
||||
"code.dumpstack.io/tools/out-of-tree/distro"
|
||||
"code.dumpstack.io/tools/out-of-tree/qemu"
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
// TODO
|
||||
// Version string
|
||||
// From string
|
||||
// To string
|
||||
|
||||
// prev. ReleaseMask
|
||||
Regex string
|
||||
ExcludeRegex string
|
||||
}
|
||||
|
||||
// Target defines the kernel
|
||||
type Target struct {
|
||||
Distro distro.Distro
|
||||
|
||||
Kernel Kernel
|
||||
}
|
||||
|
||||
// DockerName is returns stable name for docker container
|
||||
func (km Target) DockerName() string {
|
||||
distro := strings.ToLower(km.Distro.ID.String())
|
||||
release := strings.Replace(km.Distro.Release, ".", "__", -1)
|
||||
return fmt.Sprintf("out_of_tree_%s_%s", distro, release)
|
||||
}
|
||||
|
||||
// ArtifactType is the kernel module or exploit
|
||||
type ArtifactType int
|
||||
|
||||
const (
|
||||
// KernelModule is any kind of kernel module
|
||||
KernelModule ArtifactType = iota
|
||||
// KernelExploit is the privilege escalation exploit
|
||||
KernelExploit
|
||||
// Script for information gathering or automation
|
||||
Script
|
||||
)
|
||||
|
||||
func (at ArtifactType) String() string {
|
||||
return [...]string{"module", "exploit", "script"}[at]
|
||||
}
|
||||
|
||||
// UnmarshalTOML is for support github.com/naoina/toml
|
||||
func (at *ArtifactType) UnmarshalTOML(data []byte) (err error) {
|
||||
stype := strings.Trim(string(data), `"`)
|
||||
stypelower := strings.ToLower(stype)
|
||||
if strings.Contains(stypelower, "module") {
|
||||
*at = KernelModule
|
||||
} else if strings.Contains(stypelower, "exploit") {
|
||||
*at = KernelExploit
|
||||
} else if strings.Contains(stypelower, "script") {
|
||||
*at = Script
|
||||
} else {
|
||||
err = fmt.Errorf("type %s is unsupported", stype)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalTOML is for support github.com/naoina/toml
|
||||
func (at ArtifactType) MarshalTOML() (data []byte, err error) {
|
||||
s := ""
|
||||
switch at {
|
||||
case KernelModule:
|
||||
s = "module"
|
||||
case KernelExploit:
|
||||
s = "exploit"
|
||||
case Script:
|
||||
s = "script"
|
||||
default:
|
||||
err = fmt.Errorf("cannot marshal %d", at)
|
||||
}
|
||||
data = []byte(`"` + s + `"`)
|
||||
return
|
||||
}
|
||||
|
||||
// Duration type with toml unmarshalling support
|
||||
type Duration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
// UnmarshalTOML for Duration
|
||||
func (d *Duration) UnmarshalTOML(data []byte) (err error) {
|
||||
duration := strings.Replace(string(data), "\"", "", -1)
|
||||
d.Duration, err = time.ParseDuration(duration)
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalTOML for Duration
|
||||
func (d Duration) MarshalTOML() (data []byte, err error) {
|
||||
data = []byte(`"` + d.Duration.String() + `"`)
|
||||
return
|
||||
}
|
||||
|
||||
type PreloadModule struct {
|
||||
Repo string
|
||||
Path string
|
||||
TimeoutAfterLoad Duration
|
||||
}
|
||||
|
||||
// Extra test files to copy over
|
||||
type FileTransfer struct {
|
||||
User string
|
||||
Local string
|
||||
Remote string
|
||||
}
|
||||
|
||||
type Patch struct {
|
||||
Path string
|
||||
Source string
|
||||
Script string
|
||||
}
|
||||
|
||||
// Artifact is for .out-of-tree.toml
|
||||
type Artifact struct {
|
||||
Name string
|
||||
Type ArtifactType
|
||||
TestFiles []FileTransfer
|
||||
SourcePath string
|
||||
Targets []Target
|
||||
|
||||
Script string
|
||||
|
||||
Qemu struct {
|
||||
Cpus int
|
||||
Memory int
|
||||
Timeout Duration
|
||||
AfterStartTimeout Duration
|
||||
}
|
||||
|
||||
Docker struct {
|
||||
Timeout Duration
|
||||
}
|
||||
|
||||
Mitigations struct {
|
||||
DisableSmep bool
|
||||
DisableSmap bool
|
||||
DisableKaslr bool
|
||||
DisableKpti bool
|
||||
}
|
||||
|
||||
Patches []Patch
|
||||
|
||||
Make struct {
|
||||
Target string
|
||||
}
|
||||
|
||||
StandardModules bool
|
||||
|
||||
Preload []PreloadModule
|
||||
}
|
||||
|
||||
// Read is for read .out-of-tree.toml
|
||||
func (Artifact) Read(path string) (ka Artifact, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = toml.Unmarshal(buf, &ka)
|
||||
|
||||
if len(strings.Fields(ka.Name)) != 1 {
|
||||
err = errors.New("artifact name should not contain spaces")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ka Artifact) checkSupport(ki distro.KernelInfo, target Target) (
|
||||
supported bool, err error) {
|
||||
|
||||
if target.Distro.Release == "" {
|
||||
if ki.Distro.ID != target.Distro.ID {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !ki.Distro.Equal(target.Distro) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r, err := regexp.Compile(target.Kernel.Regex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exr, err := regexp.Compile(target.Kernel.ExcludeRegex)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !r.MatchString(ki.KernelRelease) {
|
||||
return
|
||||
}
|
||||
|
||||
if target.Kernel.ExcludeRegex != "" && exr.MatchString(ki.KernelRelease) {
|
||||
return
|
||||
}
|
||||
|
||||
supported = true
|
||||
return
|
||||
}
|
||||
|
||||
// Supported returns true if given kernel is supported by artifact
|
||||
func (ka Artifact) Supported(ki distro.KernelInfo) (supported bool, err error) {
|
||||
for _, km := range ka.Targets {
|
||||
supported, err = ka.checkSupport(ki, km)
|
||||
if supported {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ka Artifact) Process(slog zerolog.Logger, ki distro.KernelInfo,
|
||||
endless bool, cBinary,
|
||||
cEndlessStress string, cEndlessTimeout time.Duration,
|
||||
dump func(q *qemu.System, ka Artifact, ki distro.KernelInfo,
|
||||
result *Result)) {
|
||||
|
||||
slog.Info().Msg("start")
|
||||
testStart := time.Now()
|
||||
defer func() {
|
||||
slog.Debug().Str("test_duration",
|
||||
time.Since(testStart).String()).
|
||||
Msg("")
|
||||
}()
|
||||
|
||||
kernel := qemu.Kernel{KernelPath: ki.KernelPath, InitrdPath: ki.InitrdPath}
|
||||
q, err := qemu.NewSystem(qemu.X86x64, kernel, ki.RootFS)
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("qemu init")
|
||||
return
|
||||
}
|
||||
q.Log = slog
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 endless {
|
||||
q.Timeout = 0
|
||||
}
|
||||
|
||||
qemuStart := time.Now()
|
||||
|
||||
slog.Debug().Msgf("qemu start %v", qemuStart)
|
||||
err = q.Start()
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("qemu start")
|
||||
return
|
||||
}
|
||||
defer q.Stop()
|
||||
|
||||
slog.Debug().Msgf("wait %v", ka.Qemu.AfterStartTimeout)
|
||||
time.Sleep(ka.Qemu.AfterStartTimeout.Duration)
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Minute)
|
||||
for !q.Died {
|
||||
slog.Debug().Msg("still alive")
|
||||
time.Sleep(time.Minute)
|
||||
}
|
||||
}()
|
||||
|
||||
tmp, err := os.MkdirTemp(dotfiles.Dir("tmp"), "")
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("making tmp directory")
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
result := Result{}
|
||||
if !endless {
|
||||
defer dump(q, ka, ki, &result)
|
||||
}
|
||||
|
||||
var cTest string
|
||||
|
||||
if ka.Type == Script {
|
||||
result.Build.Ok = true
|
||||
cTest = ka.Script
|
||||
} else if cBinary == "" {
|
||||
// TODO: build should return structure
|
||||
start := time.Now()
|
||||
result.BuildDir, result.BuildArtifact, result.Build.Output, err =
|
||||
Build(slog, tmp, ka, ki, ka.Docker.Timeout.Duration)
|
||||
slog.Debug().Str("duration", time.Since(start).String()).
|
||||
Msg("build done")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("build")
|
||||
return
|
||||
}
|
||||
result.Build.Ok = true
|
||||
} else {
|
||||
result.BuildArtifact = cBinary
|
||||
result.Build.Ok = true
|
||||
}
|
||||
|
||||
if cTest == "" {
|
||||
cTest = result.BuildArtifact + "_test"
|
||||
if _, err := os.Stat(cTest); err != nil {
|
||||
slog.Debug().Msgf("%s does not exist", cTest)
|
||||
cTest = tmp + "/source/" + "test.sh"
|
||||
} else {
|
||||
slog.Debug().Msgf("%s exist", cTest)
|
||||
}
|
||||
}
|
||||
|
||||
if ka.Qemu.Timeout.Duration == 0 {
|
||||
ka.Qemu.Timeout.Duration = time.Minute
|
||||
}
|
||||
|
||||
err = q.WaitForSSH(ka.Qemu.Timeout.Duration)
|
||||
if err != nil {
|
||||
result.InternalError = err
|
||||
return
|
||||
}
|
||||
slog.Debug().Str("qemu_startup_duration",
|
||||
time.Since(qemuStart).String()).
|
||||
Msg("ssh is available")
|
||||
|
||||
remoteTest, err := copyTest(q, cTest, 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.Since(start).String()).
|
||||
Msg("copy standard modules")
|
||||
}
|
||||
|
||||
err = PreloadModules(q, ka, ki, ka.Docker.Timeout.Duration)
|
||||
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.Since(start).String()).
|
||||
Msgf("test completed (success: %v)", result.Test.Ok)
|
||||
|
||||
if !endless {
|
||||
return
|
||||
}
|
||||
|
||||
dump(q, ka, ki, &result)
|
||||
|
||||
if !result.Build.Ok || !result.Run.Ok || !result.Test.Ok {
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info().Msg("start endless tests")
|
||||
|
||||
if cEndlessStress != "" {
|
||||
slog.Debug().Msg("copy and run endless stress script")
|
||||
err = q.CopyAndRunAsync("root", cEndlessStress)
|
||||
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", cEndlessTimeout)
|
||||
time.Sleep(cEndlessTimeout)
|
||||
}
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
// 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 config
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"testing"
|
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a AGPLv3 license
|
||||
// (or later) that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
@ -15,13 +15,12 @@ import (
|
||||
"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/config/dotfiles"
|
||||
"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,
|
||||
func PreloadModules(q *qemu.System, ka Artifact, ki distro.KernelInfo,
|
||||
dockerTimeout time.Duration) (err error) {
|
||||
|
||||
for _, pm := range ka.Preload {
|
||||
@ -33,7 +32,7 @@ func preloadModules(q *qemu.System, ka config.Artifact, ki distro.KernelInfo,
|
||||
return
|
||||
}
|
||||
|
||||
func preload(q *qemu.System, ki distro.KernelInfo, pm config.PreloadModule,
|
||||
func preload(q *qemu.System, ki distro.KernelInfo, pm PreloadModule,
|
||||
dockerTimeout time.Duration) (err error) {
|
||||
|
||||
var workPath, cache string
|
||||
@ -46,7 +45,8 @@ func preload(q *qemu.System, ki distro.KernelInfo, pm config.PreloadModule,
|
||||
return
|
||||
}
|
||||
} else {
|
||||
errors.New("No repo/path in preload entry")
|
||||
err = errors.New("no repo/path in preload entry")
|
||||
return
|
||||
}
|
||||
|
||||
err = buildAndInsmod(workPath, q, ki, dockerTimeout, cache)
|
||||
@ -61,29 +61,29 @@ func preload(q *qemu.System, ki distro.KernelInfo, pm config.PreloadModule,
|
||||
func buildAndInsmod(workPath string, q *qemu.System, ki distro.KernelInfo,
|
||||
dockerTimeout time.Duration, cache string) (err error) {
|
||||
|
||||
tmp, err := fs.TempDir()
|
||||
tmp, err := tempDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
var artifact string
|
||||
if fs.PathExists(cache) {
|
||||
artifact = cache
|
||||
var af string
|
||||
if pathExists(cache) {
|
||||
af = cache
|
||||
} else {
|
||||
artifact, err = buildPreload(workPath, tmp, ki, dockerTimeout)
|
||||
af, err = buildPreload(workPath, tmp, ki, dockerTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if cache != "" {
|
||||
err = copyFile(artifact, cache)
|
||||
err = CopyFile(af, cache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output, err := q.CopyAndInsmod(artifact)
|
||||
output, err := q.CopyAndInsmod(af)
|
||||
if err != nil {
|
||||
log.Print(output)
|
||||
return
|
||||
@ -92,37 +92,48 @@ func buildAndInsmod(workPath string, q *qemu.System, ki distro.KernelInfo,
|
||||
}
|
||||
|
||||
func buildPreload(workPath, tmp string, ki distro.KernelInfo,
|
||||
dockerTimeout time.Duration) (artifact string, err error) {
|
||||
dockerTimeout time.Duration) (af string, err error) {
|
||||
|
||||
ka, err := config.ReadArtifactConfig(workPath + "/.out-of-tree.toml")
|
||||
ka, err := Artifact{}.Read(workPath + "/.out-of-tree.toml")
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("preload")
|
||||
}
|
||||
|
||||
ka.SourcePath = workPath
|
||||
|
||||
km := config.Target{
|
||||
km := Target{
|
||||
Distro: ki.Distro,
|
||||
Kernel: config.Kernel{Regex: ki.KernelRelease},
|
||||
Kernel: Kernel{Regex: ki.KernelRelease},
|
||||
}
|
||||
ka.Targets = []config.Target{km}
|
||||
ka.Targets = []Target{km}
|
||||
|
||||
if ka.Docker.Timeout.Duration != 0 {
|
||||
dockerTimeout = ka.Docker.Timeout.Duration
|
||||
}
|
||||
|
||||
_, artifact, _, err = build(log.Logger, tmp, ka, ki, dockerTimeout)
|
||||
_, af, _, err = Build(log.Logger, tmp, ka, ki, dockerTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
func pathExists(path string) bool {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func tempDir() (string, error) {
|
||||
return os.MkdirTemp(dotfiles.Dir("tmp"), "")
|
||||
}
|
||||
|
||||
func cloneOrPull(repo string, ki distro.KernelInfo) (workPath, cache string,
|
||||
err error) {
|
||||
|
||||
base := config.Dir("preload")
|
||||
base := dotfiles.Dir("preload")
|
||||
workPath = filepath.Join(base, "/repos/", sha1sum(repo))
|
||||
|
||||
var r *git.Repository
|
||||
if fs.PathExists(workPath) {
|
||||
if pathExists(workPath) {
|
||||
r, err = git.PlainOpen(workPath)
|
||||
if err != nil {
|
||||
return
|
377
artifact/process.go
Normal file
377
artifact/process.go
Normal file
@ -0,0 +1,377 @@
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/otiai10/copy"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"code.dumpstack.io/tools/out-of-tree/container"
|
||||
"code.dumpstack.io/tools/out-of-tree/distro"
|
||||
"code.dumpstack.io/tools/out-of-tree/qemu"
|
||||
)
|
||||
|
||||
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 {
|
||||
err = fmt.Errorf("%v %v output: %v", cmd, err, output)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func applyPatches(src string, ka 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 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 == 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 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 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
|
||||
}
|
||||
|
||||
type Result 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 copyArtifactAndTest(slog zerolog.Logger, q *qemu.System, ka Artifact,
|
||||
res *Result, 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 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 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 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 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 == 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 := os.ReadDir(ki.ModulesPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, de := range files {
|
||||
var fi fs.FileInfo
|
||||
fi, err = de.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(fi.Name(), "modules") {
|
||||
continue
|
||||
}
|
||||
err = q.CopyFile("root", ki.ModulesPath+"/"+fi.Name(), remotePath)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
262
client/client.go
Normal file
262
client/client.go
Normal file
@ -0,0 +1,262 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"code.dumpstack.io/tools/out-of-tree/api"
|
||||
"code.dumpstack.io/tools/out-of-tree/config/dotfiles"
|
||||
"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 Client struct {
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
func (c Client) client() *tls.Conn {
|
||||
if !fs.PathExists(dotfiles.File("daemon/cert.pem")) {
|
||||
log.Fatal().Msgf("no {cert,key}.pem at %s",
|
||||
dotfiles.Dir("daemon"))
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(
|
||||
dotfiles.File("daemon/cert.pem"),
|
||||
dotfiles.File("daemon/key.pem"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
cacert, err := os.ReadFile(dotfiles.File("daemon/cert.pem"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
certpool := x509.NewCertPool()
|
||||
certpool.AppendCertsFromPEM(cacert)
|
||||
|
||||
tlscfg := &tls.Config{
|
||||
RootCAs: certpool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", c.RemoteAddr, tlscfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
return conn // conn.Close()
|
||||
}
|
||||
|
||||
func (c Client) request(cmd api.Command, data any) (resp api.Resp, err error) {
|
||||
req := api.Req{Command: cmd}
|
||||
if data != nil {
|
||||
req.SetData(data)
|
||||
}
|
||||
|
||||
conn := c.client()
|
||||
defer conn.Close()
|
||||
|
||||
req.Encode(conn)
|
||||
|
||||
err = resp.Decode(conn)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("request %v", req)
|
||||
}
|
||||
|
||||
log.Debug().Msgf("resp: %v", resp)
|
||||
|
||||
if resp.Error != "" {
|
||||
err = errors.New(resp.Error)
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) Jobs() (jobs []api.Job, err error) {
|
||||
resp, _ := c.request(api.ListJobs, nil)
|
||||
|
||||
err = resp.GetData(&jobs)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) AddJob(job api.Job) (uuid string, err error) {
|
||||
resp, err := c.request(api.AddJob, &job)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = resp.GetData(&uuid)
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) Repos() (repos []api.Repo, err error) {
|
||||
resp, _ := c.request(api.ListRepos, nil)
|
||||
|
||||
log.Debug().Msgf("resp: %v", spew.Sdump(resp))
|
||||
|
||||
err = resp.GetData(&repos)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
tag string
|
||||
}
|
||||
|
||||
func (lw logWriter) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
log.Trace().Str("tag", lw.tag).Msgf("%v", strconv.Quote(string(p)))
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) handler(cConn net.Conn) {
|
||||
defer cConn.Close()
|
||||
|
||||
dConn := c.client()
|
||||
defer dConn.Close()
|
||||
|
||||
req := api.Req{Command: api.RawMode}
|
||||
req.Encode(dConn)
|
||||
|
||||
go io.Copy(cConn, io.TeeReader(dConn, logWriter{"recv"}))
|
||||
io.Copy(dConn, io.TeeReader(cConn, logWriter{"send"}))
|
||||
}
|
||||
|
||||
var ErrRepoNotFound = errors.New("repo not found")
|
||||
|
||||
// GetRepo virtual API call
|
||||
func (c Client) GetRepo(name string) (repo api.Repo, err error) {
|
||||
// TODO add API call
|
||||
|
||||
repos, err := c.Repos()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range repos {
|
||||
if r.Name == name {
|
||||
repo = r
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = ErrRepoNotFound
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) GitProxy(addr string, ready *sync.Mutex) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("git proxy listen")
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
log.Debug().Msgf("git proxy listen on %v", addr)
|
||||
|
||||
for {
|
||||
ready.Unlock()
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("accept")
|
||||
}
|
||||
log.Debug().Msgf("git proxy accept %s", conn.RemoteAddr())
|
||||
|
||||
go c.handler(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (c Client) PushRepo(repo api.Repo) (err error) {
|
||||
addr := qemu.GetFreeAddrPort()
|
||||
|
||||
ready := &sync.Mutex{}
|
||||
|
||||
ready.Lock()
|
||||
go c.GitProxy(addr, ready)
|
||||
|
||||
ready.Lock()
|
||||
|
||||
remote := fmt.Sprintf("git://%s/%s", addr, repo.Name)
|
||||
log.Debug().Msgf("git proxy remote: %v", remote)
|
||||
|
||||
raw, err := exec.Command("git", "--work-tree", repo.Path, "push", remote).
|
||||
CombinedOutput()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msgf("push repo %v\n%v", repo, string(raw))
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) AddRepo(repo api.Repo) (err error) {
|
||||
_, err = c.request(api.AddRepo, &repo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msgf("add repo %v", repo)
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) Kernels() (kernels []distro.KernelInfo, err error) {
|
||||
resp, err := c.request(api.Kernels, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = resp.GetData(&kernels)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
log.Info().Msgf("got %d kernels", len(kernels))
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) JobStatus(uuid string) (st api.Status, err error) {
|
||||
resp, err := c.request(api.JobStatus, &uuid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = resp.GetData(&st)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) JobLogs(uuid string) (logs []api.JobLog, err error) {
|
||||
resp, err := c.request(api.JobLogs, &uuid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = resp.GetData(&logs)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
120
cmd/daemon.go
Normal file
120
cmd/daemon.go
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright 2024 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"code.dumpstack.io/tools/out-of-tree/client"
|
||||
"code.dumpstack.io/tools/out-of-tree/daemon"
|
||||
)
|
||||
|
||||
type DaemonCmd struct {
|
||||
Addr string `default:":63527"`
|
||||
|
||||
Serve DaemonServeCmd `cmd:"" help:"start daemon"`
|
||||
|
||||
Job DaemonJobCmd `cmd:"" aliases:"jobs" help:"manage jobs"`
|
||||
Repo DaemonRepoCmd `cmd:"" aliases:"repos" help:"manage repositories"`
|
||||
}
|
||||
|
||||
type DaemonServeCmd struct{}
|
||||
|
||||
func (cmd *DaemonServeCmd) Run(dm *DaemonCmd, g *Globals) (err error) {
|
||||
d, err := daemon.Init(g.Config.Kernels)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
defer d.Kill()
|
||||
|
||||
go d.Daemon()
|
||||
d.Listen(dm.Addr)
|
||||
return
|
||||
}
|
||||
|
||||
type DaemonJobCmd struct {
|
||||