From 3d21c4f1c20f2777fa03e14745d259ce49e7347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Rychli=C5=84ski?= Date: Tue, 8 Nov 2022 11:16:29 +0100 Subject: [PATCH] Wersja inicjalna --- .gitignore | 6 ++ Makefile | 14 +++++ cfg/config.go | 75 +++++++++++++++++++++++ cfg/params.go | 112 +++++++++++++++++++++++++++++++++++ cmd/multisql/multisql.go | 46 ++++++++++++++ go.mod | 3 + mgr/mgr.go | 117 ++++++++++++++++++++++++++++++++++++ multisql.conf | 13 ++++ psql/psql.go | 125 +++++++++++++++++++++++++++++++++++++++ scripts/skrypt1.sql | 4 ++ scripts/skrypt2.sql | 7 +++ 11 files changed, 522 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cfg/config.go create mode 100644 cfg/params.go create mode 100644 cmd/multisql/multisql.go create mode 100644 go.mod create mode 100644 mgr/mgr.go create mode 100644 multisql.conf create mode 100644 psql/psql.go create mode 100644 scripts/skrypt1.sql create mode 100644 scripts/skrypt2.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..004263b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/output +/pgpass + +#exec +/multisql +/multisql.exe diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..16b1bcc --- /dev/null +++ b/Makefile @@ -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 + diff --git a/cfg/config.go b/cfg/config.go new file mode 100644 index 0000000..94fb609 --- /dev/null +++ b/cfg/config.go @@ -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 +} diff --git a/cfg/params.go b/cfg/params.go new file mode 100644 index 0000000..06acd42 --- /dev/null +++ b/cfg/params.go @@ -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.`) +} diff --git a/cmd/multisql/multisql.go b/cmd/multisql/multisql.go new file mode 100644 index 0000000..696b642 --- /dev/null +++ b/cmd/multisql/multisql.go @@ -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) + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..720da0b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module multisql + +go 1.19 diff --git a/mgr/mgr.go b/mgr/mgr.go new file mode 100644 index 0000000..9f56aa2 --- /dev/null +++ b/mgr/mgr.go @@ -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() +} diff --git a/multisql.conf b/multisql.conf new file mode 100644 index 0000000..25031e9 --- /dev/null +++ b/multisql.conf @@ -0,0 +1,13 @@ +{ + "Passfile": "pgpass", + "Defaults": { + "User": "ehubowner", + "Port": 5432, + "DbName": "ehub" + }, + "Connections": [ + { + "Host": "mort" + } + ] +} diff --git a/psql/psql.go b/psql/psql.go new file mode 100644 index 0000000..fc08e2e --- /dev/null +++ b/psql/psql.go @@ -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 +} diff --git a/scripts/skrypt1.sql b/scripts/skrypt1.sql new file mode 100644 index 0000000..0d8507c --- /dev/null +++ b/scripts/skrypt1.sql @@ -0,0 +1,4 @@ +select current_user as myuser +\gset + +\echo 'Jakiś wynik' :myuser diff --git a/scripts/skrypt2.sql b/scripts/skrypt2.sql new file mode 100644 index 0000000..65f9b45 --- /dev/null +++ b/scripts/skrypt2.sql @@ -0,0 +1,7 @@ +select true as warunek +\gset + +\if :warunek +\o ekstra.txt +select 'Warunek został spełniony!!!'; +\endif