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