Skip to content

Commit

Permalink
Merge pull request #4 from open-oni/feature/ensure-awardee
Browse files Browse the repository at this point in the history
Feature/ensure awardee
  • Loading branch information
jechols authored Nov 5, 2024
2 parents 8829b3f + b1ad058 commit b169fcd
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.go diff=golang
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export BA_BIND=":2222"
export BATCH_SOURCE="/mnt/news/production-batches"
export ONI_LOCATION="/opt/openoni/"
export HOST_KEY_FILE="/etc/oni-agent"
export DB_CONNECTION="user:password@tcp(127.0.0.1:3306)/databasename"
make
./bin/agent
```
Expand Down
81 changes: 57 additions & 24 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"database/sql"
"encoding/pem"
"errors"
"fmt"
Expand All @@ -13,6 +14,7 @@ import (
"sync/atomic"

gliderssh "github.com/gliderlabs/ssh"
_ "github.com/go-sql-driver/mysql"
"github.com/open-oni/oni-agent/internal/queue"
"github.com/open-oni/oni-agent/internal/version"
"golang.org/x/crypto/ssh"
Expand All @@ -35,48 +37,78 @@ var HostKeySigner ssh.Signer
// background jobs, providing status of existing jobs, etc.
var JobRunner *queue.Queue

// dbPool is our single DB connection shared app-wide
var dbPool *sql.DB

func getEnvironment() {
var errList []error
var err error

BABind = os.Getenv("BA_BIND")
if BABind == "" {
slog.Error("BA_BIND must be set")
os.Exit(1)
errList = append(errList, errors.New("BA_BIND must be set"))
}

ONILocation = os.Getenv("ONI_LOCATION")

var info, err = os.Stat(ONILocation)
if err == nil {
if !info.IsDir() {
err = errors.New("not a valid directory")
if ONILocation == "" {
errList = append(errList, errors.New("ONI_LOCATION must be set"))
} else {
var info, err = os.Stat(ONILocation)
if err == nil {
if !info.IsDir() {
err = errors.New("not a valid directory")
}
}
if err != nil {
errList = append(errList, fmt.Errorf("Invalid setting for ONI_LOCATION: %w", err))
}
}
if err != nil {
slog.Error("Invalid setting for ONI_LOCATION", "error", err)
os.Exit(1)
}

BatchSource = os.Getenv("BATCH_SOURCE")
info, err = os.Stat(BatchSource)
if err == nil {
if !info.IsDir() {
err = errors.New("not a valid directory")
if BatchSource == "" {
errList = append(errList, errors.New("BATCH_SOURCE must be set"))
} else {
var info, err = os.Stat(BatchSource)
if err == nil {
if !info.IsDir() {
err = errors.New("not a valid directory")
}
}
if err != nil {
errList = append(errList, fmt.Errorf("Invalid setting for BATCH_SOURCE: %w", err))
}
}
if err != nil {
slog.Error("Invalid setting for BATCH_SOURCE", "error", err)
os.Exit(1)
}

var fname = os.Getenv("HOST_KEY_FILE")
if fname == "" {
slog.Error("HOST_KEY_FILE must be set")
os.Exit(1)
errList = append(errList, errors.New("HOST_KEY_FILE must be set"))
} else {
HostKeySigner, err = readKey(fname)
if err != nil {
errList = append(errList, fmt.Errorf("HOST_KEY_FILE is invalid or cannot be read: %w", err))
}
}
HostKeySigner, err = readKey(fname)
if err != nil {
slog.Error("HOST_KEY_FILE is invalid or cannot be read", "error", err)

var connect = os.Getenv("DB_CONNECTION")
if connect == "" {
errList = append(errList, errors.New(`DB_CONNECTION must be set (e.g., "user:pass@tcp(127.0.0.1:3306)/dbname")`))
} else {
dbPool, err = sql.Open("mysql", connect)
if err != nil {
errList = append(errList, fmt.Errorf(`DB_CONNECTION is invalid: %w`, err))
}
}

if len(errList) > 0 {
for _, err := range errList {
fmt.Fprintf(os.Stderr, " - %s\n", err)
}
os.Exit(1)
}

dbPool.SetConnMaxLifetime(0)
dbPool.SetMaxIdleConns(3)
dbPool.SetMaxOpenConns(3)
}

func readKey(keyfile string) (ssh.Signer, error) {
Expand Down Expand Up @@ -155,6 +187,7 @@ func main() {
trapIntTerm(func() {
cancel()
srv.Close()
dbPool.Close()
})
go JobRunner.Wait(ctx)

Expand Down
90 changes: 79 additions & 11 deletions cmd/agent/session.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"database/sql"
"encoding/json"
"fmt"
"log/slog"
Expand Down Expand Up @@ -62,44 +63,55 @@ func (s session) respond(st Status, msg string, data H) {
}

func (s session) handle() {
var cmds = s.Command()
if len(cmds) == 0 {
var parts = s.Command()
if len(parts) == 0 {
s.respond(StatusError, "no command specified", nil)
return
}

var command = cmds[0]
var command, args = parts[0], parts[1:]
switch command {
case "version":
s.respond(StatusSuccess, "", H{"version": version.Version})

case "job-status":
if len(cmds) != 2 {
if len(args) != 1 {
s.respond(StatusError, "You must supply a job ID", nil)
return
}
s.getJobStatus(cmds[1])
s.getJobStatus(args[0])

case "job-logs":
if len(cmds) != 2 {
if len(args) != 1 {
s.respond(StatusError, "You must supply a job ID", nil)
return
}
s.getJobLogs(cmds[1])
s.getJobLogs(args[0])

case "load-batch":
if len(cmds) != 2 {
if len(args) != 1 {
s.respond(StatusError, fmt.Sprintf("%q requires exactly one batch name", command), nil)
return
}
s.loadBatch(cmds[1])
s.loadBatch(args[0])

case "purge-batch":
if len(cmds) != 2 {
if len(args) != 1 {
s.respond(StatusError, fmt.Sprintf("%q requires exactly one batch name", command), nil)
return
}
s.purgeBatch(cmds[1])
s.purgeBatch(args[0])

case "ensure-awardee":
if len(args) < 1 || len(args) > 2 {
s.respond(StatusError, fmt.Sprintf("%q requires one or two args: MARC org code and awardee name. Name is required if the awardee is to be auto-created.", command), nil)
return
}

if len(args) == 1 {
args = []string{args[0], ""}
}
s.ensureAwardee(args[0], args[1])

default:
s.respond(StatusError, fmt.Sprintf("%q is not a valid command name", command), nil)
Expand Down Expand Up @@ -185,6 +197,62 @@ func (s session) queueJob(command string, args ...string) {
s.respond(StatusSuccess, "Job added to queue", H{"job": H{"id": id}})
}

func (s session) ensureAwardee(code string, name string) {
var rows, err = dbPool.Query("SELECT COUNT(*) FROM core_awardee WHERE org_code = ?", code)
if err != nil {
s.respond(StatusError, "Unable to query database", H{"error": err.Error()})
return
}
defer rows.Close()

// What does it mean if there's no error reported, but no count returned?
if !rows.Next() {
s.respond(StatusError, "Unable to count awardees in database", H{"error": "no rows returned by SQL COUNT()"})
return
}

var count int
err = rows.Scan(&count)
if err != nil {
s.respond(StatusError, "Unable to count awardees in database", H{"error": err.Error()})
return
}

// We really only care that there's at least one row. If there are dupes,
// that's out of scope to deal with, and technically not an error in terms of
// what we need.
if count > 0 {
s.respond(StatusSuccess, "Awardee already exists", nil)
return
}

// No rows, no error: if a name was given, create the awardee, otherwise abort
if name == "" {
s.respond(StatusError, "Unable to create awardee", H{"error": "awardee name must be given to auto-create awardees", "org_code": code, "name": name})
return
}

var result sql.Result
result, err = dbPool.Exec("INSERT INTO core_awardee (`org_code`, `name`, `created`) VALUES(?, ?, NOW())", code, name)
if err != nil {
s.respond(StatusError, "Unable to create awardee", H{"error": err.Error(), "org_code": code, "name": name})
return
}
var n int64
n, err = result.RowsAffected()
if err != nil {
s.respond(StatusError, "Unable to read result of INSERT", H{"error": err.Error(), "org_code": code, "name": name})
return
}
if n != 1 {
s.respond(StatusError, "Unable to create awardee", H{"error": "No rows created", "org_code": code, "name": name})
return
}

s.respond(StatusSuccess, "Awardee created", nil)
return
}

// close terminates the session, always with a status of 0: Go ssh clients
// return an error if the request is anything but successful, so the caller has
// to parse the status instead.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ go 1.22.5
require github.com/gliderlabs/ssh v0.3.7

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sys v0.25.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
Expand Down
3 changes: 2 additions & 1 deletion oni-agent.service
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

[Service]
Environment="BA_BIND=:2222"
Environment="BATCH_SOURCE=/mnt/news/production-batches"
Environment="ONI_LOCATION=/opt/openoni/"
Environment="BATCH_SOURCE=/mnt/news/production-batches"
Environment="HOST_KEY_FILE=/etc/oni-agent"
Environment="DB_CONNECTION=user:password@tcp(127.0.0.1:3306)/databasename"
Type=simple
ExecStart=/usr/local/oni-agent/agent
SyslogIdentifier=oni-agent
Expand Down

0 comments on commit b169fcd

Please sign in to comment.