refactor: Wszystkie pakiety przeniesione do internal

This commit is contained in:
Rychliński Arkadiusz
2022-11-27 22:39:04 +01:00
parent b82a136fef
commit ac14310642
8 changed files with 9 additions and 9 deletions

75
internal/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
}

137
internal/cfg/params.go Normal file
View File

@ -0,0 +1,137 @@
package cfg
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"os"
)
const MULTISQLPASS = "MULTISQLPASS" // nazwa ziennej środowiskowej z hasłem
type Parameters struct {
ConfigFile string
Passfile string
OutDir string
SqlDir string
LogFile string
Verbose bool
AskPass bool
Version bool
}
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.StringVar(&params.LogFile, "log", "", "Plik, do którego zostanie dopisany log programu")
flag.BoolVar(&params.AskPass, "P", false, "Pytaj o hasło. Jeśli nie podane wymaga się hasła w ")
flag.BoolVar(&params.Version, "version", false, "Wypisuje wersję i kończy działanie")
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
Użycie w trybie szyfrowania:
multisql encrypt plik.wynikowy.zaszyfrowany plik.zródłowy
lub
multisql [-P] decrypt plik.źródłowy.zaszyfrowany [plik.wynikowy.jawny]
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.Fprintf(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ółach opisano go na https://www.postgresql.org/docs/current/libpq-pgpass.html.
Aktualnie obsługiwana jest jedynie zaszyforowana postać pliku. Przed użyciem należy
plik zaszyfrować:
multsql encrypt pgpass.encrypted pgpass
Przy użyciu (odszyfrowaniu) pliku, hasło jest pobierane ze zmiennej
środowiskowej %s lub z klawiatury, jeśli użyto opcji -P.
`, MULTISQLPASS)
}

132
internal/mgr/mgr.go Normal file
View File

@ -0,0 +1,132 @@
package mgr
import (
"fmt"
"log"
"os"
"path"
"sync"
"time"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"baal.ar76.eu/x/pub/multisql/internal/pass"
"baal.ar76.eu/x/pub/multisql/internal/psql"
)
type Manager struct {
config cfg.Config
runDir string
verbose bool
askPass bool
}
func Create(verbose bool, askPass bool, outdir string, config cfg.Config) (Manager, error) {
runDir := createRunDir(outdir)
manager := Manager{
config: config,
runDir: runDir,
verbose: verbose,
askPass: askPass,
}
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 {
log.Printf("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 passdb *pass.PassDb
if self.config.PassFile != "" {
password := pass.GetMasterPass(self.askPass)
passdb, err = pass.Load(self.config.PassFile, password)
if err != nil {
return err
}
}
var group sync.WaitGroup
for _, con := range self.config.Connections {
dbDir, err := self.createDirPerCon(con)
if err != nil {
log.Printf("Problem z utworzeniem katalogu wynikow dla połaczenie: %v", dbDir)
log.Printf("Pomijam połaczenie")
continue
}
group.Add(1)
stream := make(chan psql.Result)
go self.Logger(&group, stream, con)
sql := psql.Create(
passdb,
dbDir,
scripts,
con,
self.config.PsqlExec,
self.verbose,
)
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)
log.Printf("%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)
log.Printf("%s:%d:%s:%s Skrypt %s zakończony poprawnie\n", con.Host, con.Port, con.DbName, con.User, scr)
}
}
group.Done()
}

65
internal/pass/crypt.go Normal file
View File

@ -0,0 +1,65 @@
package pass
import (
"bytes"
"fmt"
"io"
"os"
"filippo.io/age"
"filippo.io/age/armor"
)
func DecryptFile(password []byte, file string) ([]byte, error) {
identity, err := age.NewScryptIdentity(string(password))
if err != nil {
return nil, fmt.Errorf("błąd przetwarzania hasła: %w", err)
}
fd, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("błąd otwarcia pliku: %w", err)
}
defer fd.Close()
in := armor.NewReader(fd)
r, err := age.Decrypt(in, identity)
if err != nil {
return nil, fmt.Errorf("błąd deszyfrowania #01: %w", err)
}
out := &bytes.Buffer{}
if _, err := io.Copy(out, r); err != nil {
return nil, fmt.Errorf("błąd deszyfrowania #02: %v", err)
}
return out.Bytes(), nil
}
func EncryptFile(password []byte, file string, data []byte) error {
recipient, err := age.NewScryptRecipient(string(password))
if err != nil {
return fmt.Errorf("błąd przetwarzania hasła: %w", err)
}
fd, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("błąd tworzenia pliku: %w", err)
}
defer fd.Close()
out := armor.NewWriter(fd) // armor
defer out.Close()
w, err := age.Encrypt(out, recipient)
if err != nil {
return fmt.Errorf("błąd szyfrowania #01: %w", err)
}
_, err = w.Write(data)
if err != nil {
return fmt.Errorf("błąd szyfrowania #02: %w", err)
}
err = w.Close()
if err != nil {
return fmt.Errorf("błąd szyfrowania #03: %w", err)
}
return nil
}

80
internal/pass/pass.go Normal file
View File

@ -0,0 +1,80 @@
package pass
import (
"bytes"
"fmt"
)
type pgpassrow struct {
hostname string
port string
database string
username string
password string
}
type PassDb struct {
db []pgpassrow
cache map[string]*pgpassrow
}
func Load(file string, master []byte) (*PassDb, error) {
data, err := DecryptFile(master, file)
if err != nil {
return nil, err
}
lines := bytes.Split(data, []byte{'\n'})
var rows []pgpassrow
for i, line := range lines {
line = bytes.TrimRight(line, "\n\r \t")
if len(line) == 0 || line[0] == byte('#') {
continue
}
fields := bytes.Split(line, []byte{':'})
if len(fields) != 5 {
return nil, fmt.Errorf("błąd w pliku hasłeł %s:%d: niewłaściwa liczba pól", file, i)
}
rows = append(rows, pgpassrow{
hostname: string(fields[0]),
port: string(fields[1]),
database: string(fields[2]),
username: string(fields[3]),
password: string(fields[4]),
})
}
return &PassDb{
db: rows,
cache: make(map[string]*pgpassrow),
}, nil
}
func match(value string, pattern string) bool {
return pattern == "*" || pattern == value
}
func (self *PassDb) FindPassword(host string, port uint, database string, user string) string {
myport := fmt.Sprintf("%d", port)
key := fmt.Sprintf("%s:%s:%s:%s", host, myport, database, user)
cached, ok := self.cache[key]
if ok {
return cached.password
}
// pierwszy pasujący do wzorca
for i := range self.db {
if match(host, self.db[i].hostname) &&
match(myport, self.db[i].port) &&
match(database, self.db[i].database) &&
match(user, self.db[i].username) {
self.cache[key] = &self.db[i]
return self.db[i].password
}
}
return ""
}

55
internal/pass/term.go Normal file
View File

@ -0,0 +1,55 @@
package pass
import (
"bytes"
"fmt"
"log"
"os"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"golang.org/x/term"
)
func GetMasterPass(askPass bool) []byte {
if askPass {
res, err := termGetPassword("Podaj hasło")
if err != nil {
log.Fatalln("Nie udało się wczytać hasła z konsoli")
}
return res
}
password := os.Getenv(cfg.MULTISQLPASS)
if password != "" {
return []byte(password)
}
log.Fatalf("Nieustalone hasło. Użyj flagi -P lub ustaw hasło w zmiennej %s", cfg.MULTISQLPASS)
return nil
}
func EnterMasterPass() []byte {
p1, err := termGetPassword("Podaj nowe hasło (min 8 znaków)")
if err != nil {
log.Fatalf("Błąd wczytywania hasła: %v", err)
}
if len(p1) < 8 {
log.Fatalf("podano zbyt krótkie hasło")
}
p2, err := termGetPassword("Powtórz nowe hasło")
if err != nil {
log.Fatalf("Błąd wczytywania hasła: %v", err)
}
if !bytes.Equal(p1, p2) {
log.Fatalf("podane hasła są różne")
}
return p1
}
func termGetPassword(prompt string) ([]byte, error) {
fmt.Fprintf(os.Stderr, "%s: ", prompt)
passwd, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(os.Stderr)
return passwd, err
}

121
internal/psql/psql.go Normal file
View File

@ -0,0 +1,121 @@
package psql
import (
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"baal.ar76.eu/x/pub/multisql/internal/cfg"
"baal.ar76.eu/x/pub/multisql/internal/pass"
)
type Instance struct {
outdir string
scripts []string
pasdb *pass.PassDb
psqlExe string
conn cfg.Connection
verbose bool
}
type Result struct {
Script string
Err error
}
func Create(passdb *pass.PassDb, outdir string, scripts []string, conn cfg.Connection, psqlExe string, verbose bool) (instnace Instance) {
instnace = Instance{
outdir: outdir,
scripts: scripts,
pasdb: passdb,
conn: conn,
psqlExe: psqlExe,
verbose: verbose,
}
return instnace
}
func (self Instance) Exec(result chan<- Result) {
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
}
password := self.pasdb.FindPassword(self.conn.Host, self.conn.Port, self.conn.DbName, self.conn.User)
err = self.cmdPsql(outdir, connString, scr, password)
result <- Result{
Script: scr,
Err: err,
}
}
close(result)
}
func (self Instance) cmdPsql(outdir string, connString string, script string, password 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 password != "" {
env := os.Environ()
env = append(env, "PGPASSWORD="+password)
cmd.Env = env
}
if self.verbose {
log.Printf("Uruchamiam %s", cmd.String())
}
err = cmd.Run()
if err != nil {
return fmt.Errorf("Błąd uruchamiania psql: %v", err)
}
return nil
}