Wersja inicjalna

This commit is contained in:
Arkadiusz Rychliński 2022-11-08 11:16:29 +01:00
commit 3d21c4f1c2
11 changed files with 522 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/output
/pgpass
#exec
/multisql
/multisql.exe

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
.PHONY: multisql winCrossCompile clean
multisql:
go build ./cmd/multisql
winCrossCompile:
GOOS=windows go build ./cmd/multisql
all: multisql winCrossCompile
rmOutput:
-rm -rf output/*
clean: rmOutput
-rm multisql multisql.exe

75
cfg/config.go Normal file
View File

@ -0,0 +1,75 @@
package cfg
import (
"encoding/json"
"fmt"
"os"
)
type Connection struct {
Host string `json:"Host,omitempty"`
User string `json:"User,omitempty"`
Port uint `json:"Port,omitempty"`
DbName string `json:"DbName,omitempty"`
}
type Config struct {
PassFile string `json:"Passfile"`
PsqlExec string `json:"PsqlExec"`
SqlDir string `json:"SqlDir"`
Defaults Connection `json:"Defaults"`
Connections []Connection `json:"Connections"`
}
func GetConfig(params Parameters) (config Config, err error) {
bytes, err := os.ReadFile(params.ConfigFile)
if err != nil {
return config, fmt.Errorf("błąd odczytu pliku konfiguracji: %w", err)
}
err = json.Unmarshal(bytes, &config)
if err != nil {
return config, fmt.Errorf("błąd parsowania pliku konfiguracji: %w", err)
}
if config.PassFile == "" && params.Passfile != "" {
config.PassFile = params.Passfile
}
if config.PassFile == "" {
return config, fmt.Errorf("nie podano Passfile (dodaj w konfiguracji lub użyj flag --passfile)")
}
if config.SqlDir == "" && params.SqlDir != "" {
config.SqlDir = params.SqlDir
}
err = fixConnections(&config)
return config, err
}
func fixConnections(config *Config) error {
def := config.Defaults
for i := range config.Connections {
con := &config.Connections[i]
if con.DbName == "" {
con.DbName = def.DbName
}
if con.User == "" {
con.User = def.User
}
if con.Host == "" {
con.Host = def.Host
}
if con.Port == 0 {
if def.Port != 0 {
con.Port = def.Port
} else {
con.Port = 5432 // domyślny dla PostgreSQL
}
}
if con.DbName == "" || con.Host == "" || con.User == "" {
return fmt.Errorf("opis połączenia z pozycji %d jest niekompletny: %#v", i+1, con)
}
}
return nil
}

112
cfg/params.go Normal file
View File

@ -0,0 +1,112 @@
package cfg
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"os"
)
type Parameters struct {
Verbose bool
ConfigFile string
Passfile string
OutDir string
SqlDir string
}
var params Parameters
func init() {
flag.BoolVar(&params.Verbose, "verbose", false, "Dodatkowe komunkaty diagnostyczne")
flag.StringVar(&params.ConfigFile, "config", "multisql.conf", "Plik konfiguracji")
flag.StringVar(&params.Passfile, "passfile", "", "Plik pgpass z hasłami do baz")
flag.StringVar(&params.OutDir, "outdir", "", "Katalog (istniejący i z prawem do zapisu), do którego generowane są wyniki")
flag.StringVar(&params.SqlDir, "sqldir", "scripts", "Katalog, w którym znajdują się skrypty do uruchomienia")
flag.Usage = printUsage
flag.Parse()
}
func GetParams() Parameters {
return params
}
func printUsage() {
fmt.Fprintf(os.Stderr,
`
multisql - uruchamia zestaw skryptów na skonfigurowanej liście baz danych.
Program, dla każdej z baz wskazanych w konfiguracji, uruchamia kolejno każdy
z plików sql wskazanych parametrem lub w pliku konfiguracji poprzez
uruchomienie narzędzia psql:
psql -f skrypt.sql -w -L log.txt _szczegóły_połaczenia_ 2>stderr.txt > stdout.txt
Skrypty mogą mieć dowolne rozszerzenia ale podkatalogi nie obsługiwane.
Pliki wynikowe zapisywane do katalogu wskazanego w outdir w strukturze o postaci
output:
-> data-godzina-pid
-> host-port-baza-user
-> skrypt.sql
-> log.txt
-> stdout.txt
-> stderr.txt
Użycie:
multisql -outdir /tmp -sqldir /data/skrypty -passfile ./hasla
Opis flag:
`,
)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr,
`
Plik passfile ma standardowy format .pgpass PostgreSQLa.
Plik multisql.conf ma format JSON, np:
> `,
)
c := Config{
PassFile: "mypgpass",
PsqlExec: "/usr/local/bin/psql",
SqlDir: "/data/skrypty-sql",
Defaults: Connection{
Port: 5433,
User: "myapp",
DbName: "moja-db",
},
Connections: []Connection{
{
Host: "10.20.30.01",
},
{
Host: "10.20.30.02",
DbName: "innadb",
User: "innyuser",
},
},
}
b, _ := json.Marshal(&c)
var out bytes.Buffer
_ = json.Indent(&out, b, "> ", "\t")
_, _ = out.WriteTo(os.Stderr)
fmt.Fprintln(os.Stderr, `
PsqlExec jest opcjonalny - jesli nie zostanie podany wyszukuje się programu psql.exe w PATH.
Każde połączenie w tablicy Connections może zawierać Host, Port, User i DbName.
Brakujące wartości uzupełniane z sekcji Defaults (która ma taki sam
format jak obiekt w Connections).
Parametry mają pierwszeństwo przed wartościami w konfiguracji.
Format pliku passfile jest następujący:
hostname:port:database:username:password
W szczególach opisano go na https://www.postgresql.org/docs/current/libpq-pgpass.html.`)
}

46
cmd/multisql/multisql.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"fmt"
"multisql/cfg"
"multisql/mgr"
"os"
)
func main() {
params := cfg.GetParams()
config, err := cfg.GetConfig(params)
if err != nil {
fmt.Fprintf(os.Stderr, "Błąd z konfiguracją: %v\n", err)
os.Exit(1)
}
if params.OutDir == "" {
params.OutDir = "."
} else {
fs, err := os.Stat(params.OutDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Podany katalog wyników nie jest prawidłowy: %v\n", err)
os.Exit(1)
}
if !fs.IsDir() {
fmt.Fprintf(os.Stderr, "Ścieżka %s nie wskazuje na poprawny katalog\n", params.OutDir)
}
}
manager, err := mgr.Create(params.Verbose, params.OutDir, config)
if err != nil {
fmt.Fprintf(os.Stderr, "Nie można wystartować operacji: %v\n", err)
os.Exit(1)
}
err = manager.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Błąd przetwarzania: %v\n", err)
os.Exit(1)
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module multisql
go 1.19

117
mgr/mgr.go Normal file
View File

@ -0,0 +1,117 @@
package mgr
import (
"fmt"
"multisql/cfg"
"multisql/psql"
"os"
"path"
"sync"
"time"
)
type Manager struct {
config cfg.Config
runDir string
verbose bool
}
func Create(verbose bool, outdir string, config cfg.Config) (Manager, error) {
runDir := createRunDir(outdir)
manager := Manager{
config: config,
runDir: runDir,
verbose: verbose,
}
return manager, nil
}
func createRunDir(out string) string {
t := time.Now()
dataPart := t.Format("2006-01-02T15_04_05")
newDir := fmt.Sprintf("%s-%d", dataPart, os.Getpid())
dir := path.Join(out, newDir)
return dir
}
func (self Manager) GetScripts() ([]string, error) {
entries, err := os.ReadDir(self.config.SqlDir)
if err != nil {
return nil, fmt.Errorf("błąd wczytywania skryptów: %w", err)
}
scripts := make([]string, 0, len(entries))
for _, e := range entries {
if e.IsDir() {
continue
}
scrpt := path.Join(self.config.SqlDir, e.Name())
_, err := os.Stat(scrpt)
if err != nil {
fmt.Fprintf(os.Stderr, "Skrypt zostanie pominięty z powodu błedu: %v\n", err)
continue
}
scripts = append(scripts, scrpt)
}
return scripts, err
}
func (self Manager) Run() error {
err := os.Mkdir(self.runDir, 0o755)
if err != nil {
return fmt.Errorf("błąd tworzenia katalogu z wynikami: %w", err)
}
scripts, err := self.GetScripts()
if err != nil {
return err
}
var group sync.WaitGroup
for _, con := range self.config.Connections {
dbDir, err := self.createDirPerCon(con)
if err != nil {
fmt.Fprintf(os.Stderr, "Problem z utworzeniem katalogu wynikow dla połaczenie: %v", dbDir)
fmt.Fprintf(os.Stderr, "Pomijam połaczenie")
continue
}
group.Add(1)
stream := make(chan psql.Result)
go self.Logger(&group, stream, con)
sql := psql.Create(
self.config.PassFile,
dbDir,
scripts,
con,
self.config.PsqlExec,
)
go sql.Exec(stream)
}
group.Wait()
return nil
}
func (self Manager) createDirPerCon(con cfg.Connection) (string, error) {
dbDir := fmt.Sprintf("%s_%d_%s_%s", con.Host, con.Port, con.DbName, con.User)
dir := path.Join(self.runDir, dbDir)
err := os.Mkdir(dir, 0o755)
return dir, err
}
func (self Manager) Logger(group *sync.WaitGroup, stream <-chan psql.Result, con cfg.Connection) {
for event := range stream {
if event.Err != nil {
scr := path.Base(event.Script)
fmt.Fprintf(os.Stderr, "%s:%d:%s:%s Skrypt: %s Błąd: %v\n", con.Host, con.Port, con.DbName, con.User, scr, event.Err)
} else if self.verbose {
scr := path.Base(event.Script)
fmt.Fprintf(os.Stderr, "%s:%d:%s:%s Skrypt %s zakończony poprawnie\n", con.Host, con.Port, con.DbName, con.User, scr)
}
}
group.Done()
}

13
multisql.conf Normal file
View File

@ -0,0 +1,13 @@
{
"Passfile": "pgpass",
"Defaults": {
"User": "ehubowner",
"Port": 5432,
"DbName": "ehub"
},
"Connections": [
{
"Host": "mort"
}
]
}

125
psql/psql.go Normal file
View File

@ -0,0 +1,125 @@
package psql
import (
"fmt"
"multisql/cfg"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
)
type Instance struct {
outdir string
scripts []string
passfile string
psqlExe string
conn cfg.Connection
}
type Result struct {
Script string
Err error
}
func Create(passfile string, outdir string, scripts []string, conn cfg.Connection, psqlExe string) (instnace Instance) {
instnace = Instance{
outdir: outdir,
scripts: scripts,
passfile: passfile,
conn: conn,
psqlExe: psqlExe,
}
return instnace
}
func (self Instance) Exec(result chan<- Result) {
absPassFile := ""
if self.passfile != "" {
var err error
absPassFile, err = filepath.Abs(self.passfile)
if err != nil {
result <- Result{
Script: "*",
Err: fmt.Errorf("problem ze zbudowaniem ściężki do pliku haseł: %w", err),
}
return
}
}
connString := fmt.Sprintf("host=%s port=%d dbname=%s user=%s", self.conn.Host, self.conn.Port, self.conn.DbName, self.conn.User)
for _, scr := range self.scripts {
srcName := path.Base(scr)
outdir := path.Join(self.outdir, srcName)
err := os.Mkdir(outdir, 0o775)
if err != nil {
result <- Result{
Script: scr,
Err: err,
}
continue
}
err = self.cmdPsql(outdir, connString, scr, absPassFile)
fmt.Println()
result <- Result{
Script: scr,
Err: err,
}
}
close(result)
}
func (self Instance) cmdPsql(outdir string, connString string, script string, passfile string) error {
absScript, err := filepath.Abs(script)
if err != nil {
return fmt.Errorf("problem ze zbudowaniem ściezki do skryptu: %w", err)
}
psqlExe := "psql"
if runtime.GOOS == "windows" {
psqlExe = "psql.exe"
}
if self.psqlExe != "" {
psqlExe = self.psqlExe
}
stdoutFile := path.Join(outdir, "stdout.txt")
stderrFile := path.Join(outdir, "stderr.txt")
logFile := "log.txt"
fhOut, err := os.Create(stdoutFile)
if err != nil {
return fmt.Errorf("Problem utworzenie pliku wyników: %v", err)
}
defer fhOut.Close()
fhErr, err := os.Create(stderrFile)
if err != nil {
return fmt.Errorf("Problem utworzenie pliku błędów: %v", err)
}
defer fhErr.Close()
cmd := exec.Command(psqlExe,
"-w",
"-f", absScript,
"-L", logFile,
connString,
)
cmd.Stderr = fhErr
cmd.Stdout = fhOut
cmd.Dir = outdir
if passfile != "" {
env := os.Environ()
env = append(env, "PGPASSFILE=" + passfile)
cmd.Env = env
}
fmt.Println("Uruchamiam", cmd.String())
err = cmd.Run()
if err != nil {
return fmt.Errorf("Błąd uruchamiania psql: %v", err)
}
return nil
}

4
scripts/skrypt1.sql Normal file
View File

@ -0,0 +1,4 @@
select current_user as myuser
\gset
\echo 'Jakiś wynik' :myuser

7
scripts/skrypt2.sql Normal file
View File

@ -0,0 +1,7 @@
select true as warunek
\gset
\if :warunek
\o ekstra.txt
select 'Warunek został spełniony!!!';
\endif