Wersja inicjalna
This commit is contained in:
commit
3d21c4f1c2
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/output
|
||||||
|
/pgpass
|
||||||
|
|
||||||
|
#exec
|
||||||
|
/multisql
|
||||||
|
/multisql.exe
|
14
Makefile
Normal file
14
Makefile
Normal 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
75
cfg/config.go
Normal 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
112
cfg/params.go
Normal 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(¶ms.Verbose, "verbose", false, "Dodatkowe komunkaty diagnostyczne")
|
||||||
|
flag.StringVar(¶ms.ConfigFile, "config", "multisql.conf", "Plik konfiguracji")
|
||||||
|
flag.StringVar(¶ms.Passfile, "passfile", "", "Plik pgpass z hasłami do baz")
|
||||||
|
flag.StringVar(¶ms.OutDir, "outdir", "", "Katalog (istniejący i z prawem do zapisu), do którego generowane są wyniki")
|
||||||
|
flag.StringVar(¶ms.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 są obsługiwane.
|
||||||
|
Pliki wynikowe zapisywane są 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 są 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
46
cmd/multisql/multisql.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
117
mgr/mgr.go
Normal file
117
mgr/mgr.go
Normal 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
13
multisql.conf
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"Passfile": "pgpass",
|
||||||
|
"Defaults": {
|
||||||
|
"User": "ehubowner",
|
||||||
|
"Port": 5432,
|
||||||
|
"DbName": "ehub"
|
||||||
|
},
|
||||||
|
"Connections": [
|
||||||
|
{
|
||||||
|
"Host": "mort"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
125
psql/psql.go
Normal file
125
psql/psql.go
Normal 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
4
scripts/skrypt1.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
select current_user as myuser
|
||||||
|
\gset
|
||||||
|
|
||||||
|
\echo 'Jakiś wynik' :myuser
|
7
scripts/skrypt2.sql
Normal file
7
scripts/skrypt2.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
select true as warunek
|
||||||
|
\gset
|
||||||
|
|
||||||
|
\if :warunek
|
||||||
|
\o ekstra.txt
|
||||||
|
select 'Warunek został spełniony!!!';
|
||||||
|
\endif
|
Loading…
Reference in New Issue
Block a user