commit b514cc36fbcc187bf0d85aa2cf55c2f49d085328 Author: Voomra Date: Mon Dec 23 01:55:48 2024 +0300 first commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fda30d9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*.{sh,yml}] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true + +[*.sh] +indent_size = 4 +max_line_length = 80 + +[*.yml] +indent_size = 2 +max_line_length = 80 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e673575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +target/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2140301 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +#!make + +# ldflags: +# -s: Убирает информацию о символах (например, дебаг-символы). +# -w: Убирает информацию о отладке, включая таблицу строк и информацию о файлах. + +build: + GOOS=linux go build -ldflags="-s -w" -buildmode=exe -o target/playbookctl main.go \ + && strip target/playbookctl + +clean: + rm -rf target/* diff --git a/cmd/backup.go b/cmd/backup.go new file mode 100644 index 0000000..64d5e47 --- /dev/null +++ b/cmd/backup.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "path/filepath" + "playbookctl/internal/space_worker" + "playbookctl/internal/types" + "playbookctl/internal/utils/logger" +) + +func NewCommandBackup() *cobra.Command { + backupCmd := &cobra.Command{ + Use: "backup [role1, role2, ...]", + Short: "выполнить резервное копирование пространства", + Args: backupCheckArgsE, + ValidArgsFunction: ArgRoleCompletion, + RunE: backupRunE, + } + + backupCmd.Flags().Uint8Var(&flagAnsibleVerbose, "ansible-verbose", 0, "ansible verbose mode (0-3)") + backupCmd.Flags().StringVar(&flagAnsibleBin, "ansible-bin", "/usr/bin/ansible-playbook", "путь к ansible-playbook") + backupCmd.Flags().BoolVar(&flagGenOnly, "generate-only", false, "только сгенерировать исполняемую команду") + backupCmd.Flags().StringVar(&flagTargetHost, "target", "", "имя целевого хоста") + + if err := backupCmd.MarkFlagFilename("ansible-bin"); err != nil { + fmt.Println(err) + os.Exit(1) + } + + return backupCmd +} + +func backupCheckArgsE(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + playbook, err := types.ReadPlaybook(workDir) + if err != nil { + return err + } + + for _, roleName := range args { + if !containsInSlice(roleName, playbook.Roles) { + return fmt.Errorf("роли \"%s\" не существует или она не добавлена в playbook.yml", roleName) + } + } + + return nil +} + +func backupRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + spaceWorker.AnsibleBin = flagAnsibleBin + spaceWorker.AnsibleVerbose = flagAnsibleVerbose + return spaceWorker.Backup(flagGenOnly, flagTargetHost, args...) +} diff --git a/cmd/hosts.go b/cmd/hosts.go new file mode 100644 index 0000000..fc777b9 --- /dev/null +++ b/cmd/hosts.go @@ -0,0 +1,181 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "path/filepath" + "playbookctl/internal/space_worker" + "playbookctl/internal/types" + "playbookctl/internal/utils/logger" + "strconv" + "strings" +) + +func NewCommandHosts() *cobra.Command { + hostsCmd := &cobra.Command{ + Use: "hosts", + Short: "работа с хостами", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + hostsCmd.AddCommand(newCommandHostsList()) + hostsCmd.AddCommand(newCommandHostsAdd()) + hostsCmd.AddCommand(newCommandHostsRemove()) + hostsCmd.AddCommand(newCommandHostsDefault()) + + return hostsCmd +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandHostsList() *cobra.Command { + listCmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "список хостов", + RunE: hostsListRunE, + } + + return listCmd +} + +func hostsListRunE(_ *cobra.Command, _ []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.ListHosts() +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandHostsAdd() *cobra.Command { + addCmd := &cobra.Command{ + Use: "add [user]", + Aliases: []string{"new"}, + Short: "добавить новый хост", + Args: cobra.MinimumNArgs(2), + RunE: hostsAddRunE, + } + + return addCmd +} + +func hostsAddRunE(_ *cobra.Command, args []string) error { + var ( + name string + host string + port uint16 + user string + ) + + name = args[0] + + if idx := strings.Index(args[1], ":"); idx > 0 { + split := strings.Split(args[1], ":") + host = split[0] + + parsedInt, err := strconv.ParseInt(split[1], 0, 16) + if err != nil { + return err + } + port = uint16(parsedInt) + } else { + host = args[1] + port = 22 + } + + if len(args) == 2 { + user = "root" + } else if len(args) == 3 { + user = args[2] + } + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.HostsAdd(name, host, port, user) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandHostsRemove() *cobra.Command { + listCmd := &cobra.Command{ + Use: "remove ", + Aliases: []string{"delete", "del", "rm"}, + Short: "удалить хост", + Args: cobra.ExactArgs(1), + ValidArgsFunction: hostsRemoveValidateArgs, + RunE: hostsRemoveRunE, + } + + return listCmd +} + +func hostsRemoveValidateArgs(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + exitApp := func(err error) { + fmt.Println(err) + os.Exit(1) + } + + var suggestions []string + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + exitApp(err) + } + + hosts, err := types.ReadHosts(workDir) + if err != nil { + exitApp(err) + } + + for host := range *hosts { + if toComplete == "" || len(host) >= len(toComplete) && host[:len(toComplete)] == toComplete { + suggestions = append(suggestions, host) + } + } + + return suggestions, cobra.ShellCompDirectiveDefault +} + +func hostsRemoveRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.HostRemove(args[0]) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandHostsDefault() *cobra.Command { + command := &cobra.Command{ + Use: "set-default", + Short: "установка хоста по-умолчанию", + Args: cobra.ExactArgs(1), + ValidArgsFunction: ArgHostCompletion, + RunE: hostsDefaultRunE, + } + + return command +} + +func hostsDefaultRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.SetDefaultHost(args[0]) +} diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 0000000..bab01c2 --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "path/filepath" + "playbookctl/internal/space_worker" + "playbookctl/internal/types" + "playbookctl/internal/utils/logger" +) + +func NewCommandInstall() *cobra.Command { + installCmd := &cobra.Command{ + Use: "install [role1, role2, ...]", + Short: "применить настройки пространства к серверу", + Args: installCheckArgsE, + ValidArgsFunction: ArgRoleCompletion, + RunE: installRunE, + } + + installCmd.Flags().Uint8Var(&flagAnsibleVerbose, "ansible-verbose", 0, "ansible verbose mode (0-3)") + installCmd.Flags().StringVar(&flagAnsibleBin, "ansible-bin", "/usr/bin/ansible-playbook", "путь к ansible-playbook") + installCmd.Flags().BoolVar(&flagGenOnly, "generate-only", false, "только сгенерировать исполняемую команду") + installCmd.Flags().StringVar(&flagTargetHost, "target", "", "имя целевого хоста") + + if err := installCmd.MarkFlagFilename("ansible-bin"); err != nil { + fmt.Println(err) + os.Exit(1) + } + + return installCmd +} + +func installCheckArgsE(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + playbook, err := types.ReadPlaybook(workDir) + if err != nil { + return err + } + + for _, roleName := range args { + if !containsInSlice(roleName, playbook.Roles) { + return fmt.Errorf("роли \"%s\" не существует или она не добавлена в playbook.yml", roleName) + } + } + + return nil +} + +func installRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + spaceWorker.AnsibleBin = flagAnsibleBin + spaceWorker.AnsibleVerbose = flagAnsibleVerbose + return spaceWorker.Install(flagGenOnly, flagTargetHost, args...) +} + +func containsInSlice(element string, slice []string) bool { + for _, item := range slice { + if item == element { + return true + } + } + + return false +} diff --git a/cmd/restore.go b/cmd/restore.go new file mode 100644 index 0000000..270e090 --- /dev/null +++ b/cmd/restore.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "path/filepath" + "playbookctl/internal/space_worker" + "playbookctl/internal/types" + "playbookctl/internal/utils/logger" +) + +var ( + flagRestoreTimestamp string + flagRestoreInventory string +) + +func NewCommandRestore() *cobra.Command { + restoreCmd := &cobra.Command{ + Use: "restore [role1, role2, ...]", + Short: "восстановить резервную копированию пространства", + Args: restoreCheckArgsE, + ValidArgsFunction: ArgRoleCompletion, + RunE: restoreRunE, + } + + restoreCmd.Flags().Uint8Var(&flagAnsibleVerbose, "ansible-verbose", 0, "ansible verbose mode (0-3)") + restoreCmd.Flags().StringVar(&flagAnsibleBin, "ansible-bin", "/usr/bin/ansible-playbook", "путь к ansible-playbook") + restoreCmd.Flags().BoolVar(&flagGenOnly, "generate-only", false, "только сгенерировать исполняемую команду") + restoreCmd.Flags().StringVar(&flagTargetHost, "target", "", "имя целевого хоста") + restoreCmd.Flags().StringVar(&flagRestoreTimestamp, "timestamp", "latest", "выбор времени бекапа") + restoreCmd.Flags().StringVar(&flagRestoreInventory, "inventory", "", "выбор хост бекапа") + + if err := restoreCmd.MarkFlagFilename("ansible-bin"); err != nil { + fmt.Println(err) + os.Exit(1) + } + + return restoreCmd +} + +func restoreCheckArgsE(_ *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + playbook, err := types.ReadPlaybook(workDir) + if err != nil { + return err + } + + for _, roleName := range args { + if !containsInSlice(roleName, playbook.Roles) { + return fmt.Errorf("роли \"%s\" не существует или она не добавлена в playbook.yml", roleName) + } + } + + return nil +} + +func restoreRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := space_worker.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + spaceWorker.AnsibleBin = flagAnsibleBin + spaceWorker.AnsibleVerbose = flagAnsibleVerbose + return spaceWorker.Restore(flagTargetHost, flagRestoreTimestamp, flagRestoreInventory, args...) +} diff --git a/cmd/role.go b/cmd/role.go new file mode 100644 index 0000000..1dd61db --- /dev/null +++ b/cmd/role.go @@ -0,0 +1,130 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "path/filepath" + sw "playbookctl/internal/space_worker" + "playbookctl/internal/utils/logger" +) + +var ( + flagBackupAdd bool +) + +func NewCommandRole() *cobra.Command { + roleCmd := &cobra.Command{ + Use: "role", + Short: "работа с ролями в пространстве", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + roleCmd.AddCommand(newCommandRoleCreate()) + roleCmd.AddCommand(newCommandRoleRemove()) + roleCmd.AddCommand(newCommandRoleList()) + roleCmd.AddCommand(newCommandRoleModify()) + + return roleCmd +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandRoleCreate() *cobra.Command { + createCmd := &cobra.Command{ + Use: "create ", + Aliases: []string{"new"}, + Short: "создать роль в пространстве", + Args: cobra.ExactArgs(1), + RunE: roleCreateRunE, + } + + return createCmd +} + +func roleCreateRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := sw.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.CreateRole(args[0]) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandRoleRemove() *cobra.Command { + createCmd := &cobra.Command{ + Use: "remove ", + Aliases: []string{"delete", "rm", "del"}, + Short: "удаляет роль из пространства", + Args: cobra.ExactArgs(1), + ValidArgsFunction: ArgRoleCompletion, + RunE: roleRemoveRunE, + } + + return createCmd +} + +func roleRemoveRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := sw.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.RemoveRole(args[0]) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandRoleList() *cobra.Command { + listCmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "список ролей в пространстве", + RunE: roleListRunE, + } + + return listCmd +} + +func roleListRunE(_ *cobra.Command, _ []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + spaceWorker := sw.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + return spaceWorker.ListRoles() +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +func newCommandRoleModify() *cobra.Command { + modifyCmd := &cobra.Command{ + Use: "modify ", + Short: "модифицировать роль", + Args: cobra.ExactArgs(1), + ValidArgsFunction: ArgRoleCompletion, + RunE: roleModifyRunE, + } + + modifyCmd.Flags().BoolVar(&flagBackupAdd, "backup-add", false, "добавить backup функцию") + + return modifyCmd +} + +func roleModifyRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceWorker := sw.NewSpaceWorker(logger.LogVerbose(flagVerbose), workDir) + if flagBackupAdd { + return spaceWorker.ModifyRoleBackupAdd(args[0]) + } + + return nil +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..378b34c --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" +) + +var ( + flagVerbose uint8 + flagWorkdir string +) + +var ( + flagAnsibleVerbose uint8 + flagAnsibleBin string + flagGenOnly bool + flagTargetHost string +) + +var rootCmd = &cobra.Command{ + Use: "playbookctl", + Short: "Ansible Playbook Dedic2 Control", + Version: "4.0", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, +} + +func init() { + rootCmd.PersistentFlags().Uint8Var(&flagVerbose, "verbose", 1, "verbose mode (0-3)") + rootCmd.PersistentFlags().StringVar(&flagWorkdir, "workdir", ".", "рабочая директория") + + if err := rootCmd.MarkPersistentFlagDirname("workdir"); err != nil { + fmt.Println(err) + os.Exit(1) + } + + rootCmd.AddCommand(NewCommandSpace()) + rootCmd.AddCommand(NewCommandRole()) + rootCmd.AddCommand(NewCommandInstall()) + rootCmd.AddCommand(NewCommandHosts()) + rootCmd.AddCommand(NewCommandBackup()) + rootCmd.AddCommand(NewCommandRestore()) +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/space.go b/cmd/space.go new file mode 100644 index 0000000..6bd7a6c --- /dev/null +++ b/cmd/space.go @@ -0,0 +1,70 @@ +package cmd + +import ( + _ "embed" + "errors" + "github.com/spf13/cobra" + "path/filepath" + sc "playbookctl/internal/space_creator" + "playbookctl/internal/utils/logger" +) + +var ( + flagName string + flagHost string + flagPort uint16 + flagUser string +) + +func NewCommandSpace() *cobra.Command { + spaceCmd := &cobra.Command{ + Use: "space", + Short: "работа с пространствами", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + spaceCmd.AddCommand(newCommandSpaceCreate()) + + return spaceCmd +} + +func newCommandSpaceCreate() *cobra.Command { + createCmd := &cobra.Command{ + Use: "create ", + Aliases: []string{"new"}, + Short: "создать пространство", + Args: spaceCreateCheckArgsE, + RunE: spaceCreateRunE, + } + + createCmd.Flags().StringVar(&flagName, "name", "default", "псевдоним сервера") + createCmd.Flags().StringVar(&flagHost, "host", "127.0.0.1", "SSH адрес") + createCmd.Flags().Uint16Var(&flagPort, "port", 22, "SSH порт") + createCmd.Flags().StringVar(&flagUser, "user", "root", "SSH пользователь") + + return createCmd +} + +func spaceCreateCheckArgsE(_ *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("должен быть указан один аргумент") + } + return nil +} + +func spaceCreateRunE(_ *cobra.Command, args []string) error { + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + return err + } + + spaceCreator := sc.NewSpaceCreator(logger.LogVerbose(flagVerbose), workDir) + return spaceCreator.CreateSpace(args[0], &sc.ServerProps{ + Name: flagName, + Host: flagHost, + Port: flagPort, + User: flagUser, + }) +} diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..826ad1f --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "path/filepath" + "playbookctl/internal/types" +) + +func ArgRoleCompletion(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + exitApp := func(err error) { + fmt.Println(err) + os.Exit(1) + } + + var suggestions []string + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + exitApp(err) + } + + playbook, err := types.ReadPlaybook(workDir) + if err != nil { + exitApp(err) + } + + for _, role := range playbook.Roles { + if toComplete == "" || len(role) >= len(toComplete) && role[:len(toComplete)] == toComplete { + suggestions = append(suggestions, role) + } + } + + return suggestions, cobra.ShellCompDirectiveDefault +} + +func ArgHostCompletion(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + exitApp := func(err error) { + fmt.Println(err) + os.Exit(1) + } + + var suggestions []string + + workDir, err := filepath.Abs(flagWorkdir) + if err != nil { + exitApp(err) + } + + hosts, err := types.ReadHosts(workDir) + if err != nil { + exitApp(err) + } + + for host := range *hosts { + if toComplete == "" || len(host) >= len(toComplete) && host[:len(toComplete)] == toComplete { + suggestions = append(suggestions, host) + } + } + + return suggestions, cobra.ShellCompDirectiveDefault +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fa85df1 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module playbookctl + +go 1.21 + +require ( + github.com/spf13/cobra v1.8.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b17a5f8 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/space_creator/space_creator.go b/internal/space_creator/space_creator.go new file mode 100644 index 0000000..e62cac3 --- /dev/null +++ b/internal/space_creator/space_creator.go @@ -0,0 +1,154 @@ +package space_creator + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "playbookctl/internal/types" + "playbookctl/internal/utils" + "playbookctl/internal/utils/logger" +) + +//goland:noinspection SpellCheckingInspection +var ( + //go:embed static/gitignore.txt + staticGitIgnore []byte + + //go:embed static/editorconfig.txt + staticEditorConfig []byte + + //go:embed static/dd.datetime.sh + staticLibDatetime []byte + + //go:embed static/dd.restore.sh + staticLibRestore []byte + + //go:embed static/dd.sethostname.sh + staticLibSetHostname []byte +) + +type ServerProps struct { + Name string + Host string + Port uint16 + User string +} + +type SpaceCreator struct { + log *logger.Logger + + // Рабочая папка + workDir string +} + +func NewSpaceCreator(verbose logger.LogVerbose, workDir string) *SpaceCreator { + return &SpaceCreator{ + log: &logger.Logger{Verbose: verbose}, + workDir: workDir, + } +} + +// CreateSpace Создать пространство +func (app *SpaceCreator) CreateSpace(name string, props *ServerProps) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + app.log.Debug(fmt.Sprintf("space name: %s", name)) + app.log.Debug(fmt.Sprintf("server props: %+v", props)) + + spacePath := filepath.Join(app.workDir, name) + + app.log.Trace(fmt.Sprintf("try create folder '%s'", name)) + if err := os.Mkdir(spacePath, 0755); os.IsExist(err) { + return fmt.Errorf("Папка \"%s\" уже существует\n", name) + } + + for _, dir := range []string{"roles", "library", "vars"} { + app.log.Trace(fmt.Sprintf("try create folder '%s/%s'", name, dir)) + if err := os.Mkdir(filepath.Join(spacePath, dir), 0755); err != nil { + return err + } + } + + type StaticPair struct { + Name string + Data []byte + } + + { // root dir + app.log.Trace("generate hosts.yaml") + if err := generateHosts(spacePath, props); err != nil { + return err + } + + app.log.Trace("generate playbook.yml") + if err := generatePlaybook(spacePath); err != nil { + return err + } + + app.log.Trace("generate default.host.txt") + if err := utils.WriteStringFile(filepath.Join(spacePath, "default.host.txt"), props.Name); err != nil { + return err + } + + for _, pair := range []StaticPair{ + {".gitignore", staticGitIgnore}, + {".editorconfig", staticEditorConfig}, + } { + app.log.Trace(fmt.Sprintf("export %s", pair.Name)) + if err := utils.SaveStaticFile(filepath.Join(spacePath, pair.Name), pair.Data); err != nil { + return err + } + } + } + + { // library dir + for _, pair := range []StaticPair{ + {"dd.datetime.sh", staticLibDatetime}, + {"dd.restore.sh", staticLibRestore}, + {"dd.sethostname.sh", staticLibSetHostname}, + } { + app.log.Trace(fmt.Sprintf("export library/%s", pair.Name)) + if err := utils.SaveStaticFile(filepath.Join(spacePath, "library", pair.Name), pair.Data); err != nil { + return err + } + } + } + + { // vars dir + app.log.Trace(fmt.Sprintf("create vars/%s.vars.yml", props.Name)) + err := utils.WriteEmptyYaml(filepath.Join(spacePath, "vars", fmt.Sprintf("%s.vars.yml", props.Name))) + if err != nil { + return err + } + } + + app.log.Info(fmt.Sprintf("Пространство \"%s\" создано.", name)) + return nil +} + +func generateHosts(spacePath string, props *ServerProps) error { + hosts := types.THosts{ + props.Name: types.THostProps{ + Host: props.Host, + Port: props.Port, + User: props.User, + Interpreter: types.DefaultInterpreter, + }, + } + + return types.WriteHosts(spacePath, &hosts) +} + +func generatePlaybook(spacePath string) error { + playbook := types.Playbook{ + Hosts: "all", + GatherFacts: true, + PreTasks: []types.Task{{ + Name: "Include vars", + IncludeVars: "vars/{{ inventory_hostname }}.vars.yml", + }}, + Roles: []string{}, + } + + return types.WritePlaybook(spacePath, &playbook) +} diff --git a/internal/space_creator/static/dd.datetime.sh b/internal/space_creator/static/dd.datetime.sh new file mode 100644 index 0000000..09b6c57 --- /dev/null +++ b/internal/space_creator/static/dd.datetime.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# vi: set tabstop=4 shiftwidth=4 noexpandtab : +#------------------------------------------------# +# Модуль получения текущего даты-времени. +# +# . . . . . . . . . . . . . . . . . . . . . . . . +# - dd.datetime: +# register: current_datetime +#------------------------------------------------# +source $1 +set -euo pipefail +L_CHANGED=false + +## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + +__curr_date=$(date +%Y%m%d_%H%M%S) + +## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + +echo "{ \"changed\": ${L_CHANGED}, \"datetime\": \"$__curr_date\" }" diff --git a/internal/space_creator/static/dd.restore.sh b/internal/space_creator/static/dd.restore.sh new file mode 100644 index 0000000..18eec20 --- /dev/null +++ b/internal/space_creator/static/dd.restore.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# vi: set tabstop=4 shiftwidth=4 noexpandtab : +#------------------------------------------------# +# Модуль получения даты последнего бэкапа, если +# в качесте даты указано "latest". Иначе, +# возвращает переданный параметр. +# +# . . . . . . . . . . . . . . . . . . . . . . . . +# - dd.restore: +# role_path: "{{ role_path }}" +# inventory: name_of_inventory +# datetime: 20240412_231753 +# register: res_dd_restore +#------------------------------------------------# +source $1 +set -euo pipefail +L_CHANGED=false + +## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + +if [[ "$datetime" == "latest" ]]; then + cd "$role_path/backups/$inventory" + datetime=$(ls -1t | head -1) +fi + +## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + +echo "{ \"changed\": $L_CHANGED, \"inventory\": \"$inventory\", \"datetime\": \"$datetime\" }" diff --git a/internal/space_creator/static/dd.sethostname.sh b/internal/space_creator/static/dd.sethostname.sh new file mode 100644 index 0000000..b829bfc --- /dev/null +++ b/internal/space_creator/static/dd.sethostname.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# vi: set tabstop=4 shiftwidth=4 noexpandtab : +#------------------------------------------------# +# Модуль настройки hostname. +# +# В отличии от стокового модуля "hostname", +# данный модуль так же патчит файл `/etc/hosts`. +# . . . . . . . . . . . . . . . . . . . . . . . . +# - dd.sethostname: +# name: my-virtual-machine +#------------------------------------------------# +source $1 +set -euo pipefail + +__curr_hostname="$(hostname)" +__changed=false + +if [[ "$__curr_hostname" != "$name" ]]; then + hostnamectl set-hostname "$name" + perl - "$__curr_hostname" "$name" <<'EOP' +#!perl +use strict; +use warnings; +use Data::Dumper qw(Dumper); + +my ($oldhost, $newhost) = @ARGV; +my %hosts; + +open(FILE_HOSTS, "<", "/etc/hosts") || die $!; +while() { + chomp; + if (/^#/ || /^\s*$/) { + next; + } + my @pair = split(/\s+/, $_, 2); + my @values = split(/\s+/, $pair[1]); + $hosts{$pair[0]} = [@values]; +} +close(FILE_HOSTS); + +while (my ($key, $value) = each(%hosts)) { + my $i = 0; + foreach my $host (@{ $value }) { + if ($host eq $oldhost) { + @{ $value }[$i] = $newhost; + $hosts{$key} = [@{ $value }[$i]]; + } + $i = $i + 1; + } +} + +open(FILE_HOSTS, ">", "/etc/hosts") || die $!; +foreach my $key (sort(keys(%hosts))) { + print FILE_HOSTS "$key @{$hosts{$key}}\n"; +} +close(FILE_HOSTS); +EOP + __changed=true +fi + +## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## + +echo "{ \"changed\": $__changed }" diff --git a/internal/space_creator/static/editorconfig.txt b/internal/space_creator/static/editorconfig.txt new file mode 100644 index 0000000..2da2168 --- /dev/null +++ b/internal/space_creator/static/editorconfig.txt @@ -0,0 +1,15 @@ +root = true + +[*.{sh,yml}] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true + +[*.sh] +indent_size = 4 +max_line_length = 80 + +[*.yml] +indent_size = 2 +max_line_length = 80 \ No newline at end of file diff --git a/internal/space_creator/static/gitignore.txt b/internal/space_creator/static/gitignore.txt new file mode 100644 index 0000000..c43ba85 --- /dev/null +++ b/internal/space_creator/static/gitignore.txt @@ -0,0 +1,5 @@ +# Jet Brains IDEA +/.idea/ + +# Other +*.bak \ No newline at end of file diff --git a/internal/space_worker/backup.go b/internal/space_worker/backup.go new file mode 100644 index 0000000..27a71ab --- /dev/null +++ b/internal/space_worker/backup.go @@ -0,0 +1,103 @@ +package space_worker + +import ( + "fmt" + "os" + "os/exec" + "playbookctl/internal/utils" + "strings" + "time" +) + +func (app *SpaceWorker) Backup(generateOnly bool, targetHost string, roles ...string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + var host string + if targetHost != "" { + host = targetHost + } else { + var err error + host, err = app.GetDefaultHost() + if err != nil { + return err + } + } + + postBackupFile := fmt.Sprintf("/tmp/%s", time.Now().Format("20060102_150405")) + + ansibleArgs, err := app.setupBackupAnsibleArgs(app.AnsibleVerbose, host, roles, postBackupFile) + if err != nil { + return err + } + + command := exec.Command(app.AnsibleBin, ansibleArgs...) + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + + app.log.Info(fmt.Sprintf("Target Host: %s", host)) + + if generateOnly { + fmt.Println(strings.Join(command.Args, " ")) + return nil + } + + app.log.Debug(fmt.Sprintf("ansible command line: %s", command.Args)) + _ = command.Run() + + app.log.Info("Download Files") + + command = exec.Command("/bin/bash", "-c", postBackupFile) + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + _ = command.Run() + + if err = utils.RemoveFile(postBackupFile); err != nil { + return err + } + + command = exec.Command("/bin/bash", "-c", postBackupFile+"_clean") + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + _ = command.Run() + + if err = utils.RemoveFile(postBackupFile + "_clean"); err != nil { + return err + } + + return nil +} + +func (app *SpaceWorker) setupBackupAnsibleArgs(verbose uint8, targetHost string, roles []string, postBackupFile string) ([]string, error) { + var ansibleArgs []string + + switch verbose { + case 1: + ansibleArgs = append(ansibleArgs, "-v") + case 2: + ansibleArgs = append(ansibleArgs, "-vv") + case 3: + ansibleArgs = append(ansibleArgs, "-vvv") + } + + ansibleArgs = append(ansibleArgs, "-i", "hosts.yml") + ansibleArgs = append(ansibleArgs, "-l", targetHost) + + if len(roles) > 0 { + for _, role := range roles { + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_backup_%s=true", role)) + } + } else { + ansibleArgs = append(ansibleArgs, "--extra-vars", "dd_backup=true") + } + + ansibleArgs = append(ansibleArgs, "--extra-vars", "dd_postbackup=true") + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_postbackup_file=%s", postBackupFile)) + + return append(ansibleArgs, "playbook.yml"), nil +} diff --git a/internal/space_worker/host.go b/internal/space_worker/host.go new file mode 100644 index 0000000..8ff7ec2 --- /dev/null +++ b/internal/space_worker/host.go @@ -0,0 +1,119 @@ +package space_worker + +import ( + "fmt" + "path/filepath" + "playbookctl/internal/types" + "playbookctl/internal/utils" +) + +func (app *SpaceWorker) ListHosts() error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + app.log.Trace("try read hosts.yml") + hosts, err := types.ReadHosts(app.workDir) + if err != nil { + return err + } + + app.log.Trace("try read default.host.txt") + defaultHost, err := utils.ReadStringFile(filepath.Join(app.workDir, "default.host.txt")) + if err != nil { + return err + } + + fmt.Println("Хосты:") + for serverName, props := range *hosts { + defaultMarker := "" + if serverName == defaultHost { + defaultMarker = " *" + } + fmt.Printf(" - %s [%s:%d] (%s)%s\n", serverName, props.Host, props.Port, props.User, defaultMarker) + } + + return nil +} + +func (app *SpaceWorker) HostsAdd(name string, host string, port uint16, user string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + app.log.Trace("try read hosts.yml") + hosts, err := types.ReadHosts(app.workDir) + if err != nil { + return err + } + + app.log.Trace("append new host to hosts.yml") + (*hosts)[name] = types.THostProps{ + Host: host, + Port: port, + User: user, + Interpreter: types.DefaultInterpreter, + } + + app.log.Trace("write hosts.yml") + if err := types.WriteHosts(app.workDir, hosts); err != nil { + return err + } + + app.log.Trace(fmt.Sprintf("create vars/%s.vars.yml", name)) + err = utils.WriteEmptyYaml(filepath.Join(app.workDir, "vars", fmt.Sprintf("%s.vars.yml", name))) + if err != nil { + return err + } + + app.log.Info("Новый хост добавлен") + return nil +} + +func (app *SpaceWorker) HostRemove(name string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + app.log.Trace("try read hosts.yml") + hosts, err := types.ReadHosts(app.workDir) + if err != nil { + return err + } + + app.log.Trace("remove host from hosts.yml") + delete(*hosts, name) + + app.log.Trace("write hosts.yml") + if err := types.WriteHosts(app.workDir, hosts); err != nil { + return err + } + + app.log.Trace(fmt.Sprintf("remove vars/%s.vars.yml", name)) + err = utils.RemoveFile(filepath.Join(app.workDir, "vars", fmt.Sprintf("%s.vars.yml", name))) + if err != nil { + return err + } + + app.log.Info("Хост удалён") + return nil +} + +func (app *SpaceWorker) SetDefaultHost(name string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + app.log.Trace("try write default.host.txt") + err := utils.WriteStringFile(filepath.Join(app.workDir, "default.host.txt"), name) + if err != nil { + return err + } + + app.log.Info(fmt.Sprintf("Хост '%s' установлен по-умолчанию", name)) + return nil +} + +func (app *SpaceWorker) GetDefaultHost() (string, error) { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + app.log.Trace("try read default.host.txt") + value, err := utils.ReadStringFile(filepath.Join(app.workDir, "default.host.txt")) + if err != nil { + return "", err + } + + return value, nil +} diff --git a/internal/space_worker/install.go b/internal/space_worker/install.go new file mode 100644 index 0000000..c0a0c0e --- /dev/null +++ b/internal/space_worker/install.go @@ -0,0 +1,71 @@ +package space_worker + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func (app *SpaceWorker) Install(generateOnly bool, targetHost string, roles ...string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + var host string + if targetHost != "" { + host = targetHost + } else { + var err error + host, err = app.GetDefaultHost() + if err != nil { + return err + } + } + + ansibleArgs, err := app.setupInstallAnsibleArgs(app.AnsibleVerbose, host, roles) + if err != nil { + return err + } + + command := exec.Command(app.AnsibleBin, ansibleArgs...) + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + + app.log.Info(fmt.Sprintf("Target Host: %s", host)) + + if generateOnly { + fmt.Println(strings.Join(command.Args, " ")) + } else { + app.log.Debug(fmt.Sprintf("ansible command line: %s", command.Args)) + _ = command.Run() + } + + return nil +} + +func (app *SpaceWorker) setupInstallAnsibleArgs(verbose uint8, targetHost string, roles []string) ([]string, error) { + var ansibleArgs []string + + switch verbose { + case 1: + ansibleArgs = append(ansibleArgs, "-v") + case 2: + ansibleArgs = append(ansibleArgs, "-vv") + case 3: + ansibleArgs = append(ansibleArgs, "-vvv") + } + + ansibleArgs = append(ansibleArgs, "-i", "hosts.yml") + ansibleArgs = append(ansibleArgs, "-l", targetHost) + + if len(roles) > 0 { + for _, role := range roles { + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_install_%s=true", role)) + } + } else { + ansibleArgs = append(ansibleArgs, "--extra-vars", "dd_install=true") + } + + return append(ansibleArgs, "playbook.yml"), nil +} diff --git a/internal/space_worker/restore.go b/internal/space_worker/restore.go new file mode 100644 index 0000000..eb447ec --- /dev/null +++ b/internal/space_worker/restore.go @@ -0,0 +1,139 @@ +package space_worker + +import ( + "fmt" + "os" + "os/exec" + "playbookctl/internal/utils" + "time" +) + +func (app *SpaceWorker) Restore(targetHost string, timestamp string, inventory string, roles ...string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + var host string + if targetHost != "" { + host = targetHost + } else { + var err error + host, err = app.GetDefaultHost() + if err != nil { + return err + } + } + + preRestoreFile := fmt.Sprintf("/tmp/%s", time.Now().Format("20060102_150405")) + + ansibleArgs, err := app.setupPreRestoreAnsibleArgs(app.AnsibleVerbose, host, roles, preRestoreFile, timestamp, inventory) + if err != nil { + return err + } + + command := exec.Command(app.AnsibleBin, ansibleArgs...) + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + + app.log.Info(fmt.Sprintf("Target Host: %s", host)) + + app.log.Info("Pre restore Ansible") + _ = command.Run() + + app.log.Info("Send files") + command = exec.Command("/bin/bash", "-c", preRestoreFile) + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + _ = command.Run() + + app.log.Info("Restore Ansible") + ansibleArgs, err = app.setupRestoreAnsibleArgs(app.AnsibleVerbose, host, roles, preRestoreFile, timestamp, inventory) + if err != nil { + return err + } + + command = exec.Command(app.AnsibleBin, ansibleArgs...) + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + _ = command.Run() + + app.log.Info("Clean temp files") + command = exec.Command("/bin/bash", "-c", preRestoreFile+"_clean") + command.Dir = app.workDir + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + _ = command.Run() + + if err = utils.RemoveFile(preRestoreFile); err != nil { + return err + } + if err = utils.RemoveFile(preRestoreFile + "_clean"); err != nil { + return err + } + + return nil +} + +func (app *SpaceWorker) setupPreRestoreAnsibleArgs(verbose uint8, targetHost string, roles []string, preRestoreFile string, timestamp string, inventory string) ([]string, error) { + var ansibleArgs []string + + switch verbose { + case 1: + ansibleArgs = append(ansibleArgs, "-v") + case 2: + ansibleArgs = append(ansibleArgs, "-vv") + case 3: + ansibleArgs = append(ansibleArgs, "-vvv") + } + + ansibleArgs = append(ansibleArgs, "-i", "hosts.yml") + ansibleArgs = append(ansibleArgs, "-l", targetHost) + + if len(roles) > 0 { + for _, role := range roles { + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_prerestore_%s=true", role)) + } + } else { + ansibleArgs = append(ansibleArgs, "--extra-vars", "dd_prerestore=true") + } + + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_restore_datetime=%s", timestamp)) + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_restore_inventory=%s", inventory)) + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_prerestore_file=%s", preRestoreFile)) + + return append(ansibleArgs, "playbook.yml"), nil +} + +func (app *SpaceWorker) setupRestoreAnsibleArgs(verbose uint8, targetHost string, roles []string, preRestoreFile string, timestamp string, inventory string) ([]string, error) { + var ansibleArgs []string + + switch verbose { + case 1: + ansibleArgs = append(ansibleArgs, "-v") + case 2: + ansibleArgs = append(ansibleArgs, "-vv") + case 3: + ansibleArgs = append(ansibleArgs, "-vvv") + } + + ansibleArgs = append(ansibleArgs, "-i", "hosts.yml") + ansibleArgs = append(ansibleArgs, "-l", targetHost) + + if len(roles) > 0 { + for _, role := range roles { + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_restore_%s=true", role)) + } + } else { + ansibleArgs = append(ansibleArgs, "--extra-vars", "dd_restore=true") + } + + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_restore_datetime=%s", timestamp)) + ansibleArgs = append(ansibleArgs, "--extra-vars", fmt.Sprintf("dd_restore_inventory=%s", inventory)) + + return append(ansibleArgs, "playbook.yml"), nil +} diff --git a/internal/space_worker/role.go b/internal/space_worker/role.go new file mode 100644 index 0000000..9672952 --- /dev/null +++ b/internal/space_worker/role.go @@ -0,0 +1,202 @@ +package space_worker + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "playbookctl/internal/types" + "playbookctl/internal/utils" + "slices" +) + +func (app *SpaceWorker) CreateRole(name string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + app.log.Debug(fmt.Sprintf("role name: %s", name)) + + rolePath := filepath.Join(app.workDir, "roles", name) + app.log.Debug(fmt.Sprintf("rolePath: %s", rolePath)) + + app.log.Trace(fmt.Sprintf("try create '%s' dir", name)) + if err := os.Mkdir(rolePath, 0755); os.IsExist(err) { + app.log.Warn(fmt.Sprintf("Папка \"%s\" уже существует", name)) + return nil + } + + for _, dir := range []string{"defaults", "files", "tasks", "templates", "vars"} { + app.log.Trace(fmt.Sprintf("try create %s/%s dir", name, dir)) + if err := utils.CreateDir(filepath.Join(rolePath, dir)); err != nil { + return err + } + } + + for _, dir := range []string{"defaults", "vars"} { + app.log.Trace(fmt.Sprintf("try create %s/%s/main.yml", name, dir)) + if err := utils.WriteEmptyYaml(filepath.Join(rolePath, dir, "main.yml")); err != nil { + return err + } + } + + { // tasks dir + app.log.Trace("generate main task") + if err := generateMainTask(name, rolePath, false); err != nil { + return err + } + + app.log.Trace("create install task") + if err := utils.WriteEmptyYaml(filepath.Join(rolePath, "tasks", "install.yml")); err != nil { + return err + } + } + + app.log.Trace("generate readme") + if err := generateReadme(name, rolePath); err != nil { + return err + } + + app.log.Trace("read playbook.yml") + playbook, err := types.ReadPlaybook(app.workDir) + if err != nil { + return err + } + + app.log.Trace("append role to playbook.yml") + playbook.Roles = append(playbook.Roles, name) + + app.log.Trace("write playbook.yml") + if err := types.WritePlaybook(app.workDir, playbook); err != nil { + return err + } + + app.log.Info(fmt.Sprintf("Роль %s создана", name)) + return nil +} + +func generateMainTask(name string, path string, withBackup bool) error { + var mainTask []types.MainTask + + if withBackup { + mainTask = make([]types.MainTask, 4) + } else { + mainTask = make([]types.MainTask, 1) + } + + mainTask[0] = types.MainTask{ + IncludeTasks: "install.yml", + When: fmt.Sprintf("(dd_install is defined and dd_install) "+ + "or (dd_install_%s is defined and dd_install_%s)", name, name), + } + + if withBackup { + mainTask[1] = types.MainTask{ + IncludeTasks: "backup.yml", + When: fmt.Sprintf("(dd_backup is defined and dd_backup) "+ + "or (dd_backup_%s is defined and dd_backup_%s)", name, name), + } + + mainTask[2] = types.MainTask{ + IncludeTasks: "pre-restore.yml", + When: fmt.Sprintf("(dd_prerestore is defined and dd_prerestore) "+ + "or (dd_prerestore_%s is defined and dd_prerestore_%s)", name, name), + } + + mainTask[3] = types.MainTask{ + IncludeTasks: "restore.yml", + When: fmt.Sprintf("(dd_restore is defined and dd_restore) "+ + "or (dd_restore_%s is defined and dd_restore_%s)", name, name), + } + } + + return types.WriteMainTask(path, &mainTask) +} + +func generateReadme(name string, path string) error { + buffer := &bytes.Buffer{} + + buffer.WriteString(fmt.Sprintf("# %s\n\n", name)) + buffer.Write(staticReadme) + + return os.WriteFile(filepath.Join(path, "README.MD"), buffer.Bytes(), 0644) +} + +func (app SpaceWorker) RemoveRole(name string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + app.log.Debug(fmt.Sprintf("role name: %s", name)) + + rolePath := filepath.Join(app.workDir, "roles", name) + app.log.Debug(fmt.Sprintf("rolePath: %s", rolePath)) + + app.log.Trace(fmt.Sprintf("try remove '%s' dir", name)) + if err := os.RemoveAll(rolePath); err != nil { + return err + } + + app.log.Trace("read playbook.yml") + playbook, err := types.ReadPlaybook(app.workDir) + if err != nil { + return err + } + + app.log.Trace("remove role from playbook.yml") + idx := slices.Index(playbook.Roles, name) + if idx >= 0 { + playbook.Roles = slices.Delete(playbook.Roles, idx, idx+1) + } + + app.log.Trace("write playbook.yml") + if err := types.WritePlaybook(app.workDir, playbook); err != nil { + return err + } + + app.log.Info(fmt.Sprintf("Роль %s удалена", name)) + return nil +} + +func (app *SpaceWorker) ListRoles() error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + + app.log.Trace("try read playbook.yml") + playbook, err := types.ReadPlaybook(app.workDir) + if err != nil { + return err + } + + fmt.Println("Доступные роли:") + for _, roleName := range playbook.Roles { + fmt.Printf(" - %s\n", roleName) + } + + return nil +} + +func (app *SpaceWorker) ModifyRoleBackupAdd(name string) error { + app.log.Debug(fmt.Sprintf("workDir: %s", app.workDir)) + app.log.Debug(fmt.Sprintf("role name: %s", name)) + + rolePath := filepath.Join(app.workDir, "roles", name) + app.log.Debug(fmt.Sprintf("rolePath: %s", rolePath)) + + for _, file := range []string{"backup.yml", "pre-restore.yml", "restore.yml"} { + app.log.Trace(fmt.Sprintf("try create %s", file)) + if err := utils.WriteEmptyYaml(filepath.Join(rolePath, "tasks", file)); err != nil { + return err + } + } + + app.log.Trace("re-generate main task") + if err := generateMainTask(name, rolePath, true); err != nil { + return err + } + + app.log.Trace("try create backups dir") + if err := utils.CreateDirIfNotExists(filepath.Join(rolePath, "backups")); err != nil { + return err + } + + app.log.Trace("create .gitignore") + if err := utils.WriteStringFile(filepath.Join(rolePath, ".gitignore"), "backups/"); err != nil { + return err + } + + return nil +} diff --git a/internal/space_worker/space_worker.go b/internal/space_worker/space_worker.go new file mode 100644 index 0000000..426fc81 --- /dev/null +++ b/internal/space_worker/space_worker.go @@ -0,0 +1,28 @@ +package space_worker + +import ( + _ "embed" + "playbookctl/internal/utils/logger" +) + +var ( + //go:embed static/README.MD + staticReadme []byte +) + +type SpaceWorker struct { + log *logger.Logger + + // Рабочая папка + workDir string + + AnsibleBin string + AnsibleVerbose uint8 +} + +func NewSpaceWorker(verbose logger.LogVerbose, workDir string) *SpaceWorker { + return &SpaceWorker{ + log: &logger.Logger{Verbose: verbose}, + workDir: workDir, + } +} diff --git a/internal/space_worker/static/README.MD b/internal/space_worker/static/README.MD new file mode 100644 index 0000000..dd51a1f --- /dev/null +++ b/internal/space_worker/static/README.MD @@ -0,0 +1,4 @@ +## Переменные + +| Name | Description | Default | +|------|-------------|---------| \ No newline at end of file diff --git a/internal/types/hosts.go b/internal/types/hosts.go new file mode 100644 index 0000000..f4512b2 --- /dev/null +++ b/internal/types/hosts.go @@ -0,0 +1,48 @@ +package types + +import ( + "gopkg.in/yaml.v3" + "os" + "path/filepath" + "playbookctl/internal/utils" +) + +var DefaultInterpreter = "/usr/bin/python3" + +type THostProps struct { + Host string `yaml:"ansible_host"` + Port uint16 `yaml:"ansible_port"` + User string `yaml:"ansible_user"` + Interpreter string `yaml:"ansible_python_interpreter"` +} + +type THosts map[string]THostProps + +type tUngrouped struct { + Hosts THosts `yaml:"hosts"` +} + +type tHostsConfig struct { + Ungrouped tUngrouped `yaml:"ungrouped"` +} + +func ReadHosts(workDir string) (*THosts, error) { + var hostsConf tHostsConfig + { + bb, err := os.ReadFile(filepath.Join(workDir, "hosts.yml")) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(bb, &hostsConf); err != nil { + return nil, err + } + } + + return &hostsConf.Ungrouped.Hosts, nil +} + +func WriteHosts(workDir string, hosts *THosts) error { + hostsConf := tHostsConfig{Ungrouped: tUngrouped{Hosts: *hosts}} + return utils.WriteYaml(&hostsConf, filepath.Join(workDir, "hosts.yml")) +} diff --git a/internal/types/main_task.go b/internal/types/main_task.go new file mode 100644 index 0000000..2d0e1d0 --- /dev/null +++ b/internal/types/main_task.go @@ -0,0 +1,15 @@ +package types + +import ( + "path/filepath" + "playbookctl/internal/utils" +) + +type MainTask struct { + IncludeTasks string `yaml:"include_tasks"` + When string `yaml:"when"` +} + +func WriteMainTask(roleDir string, mainTask *[]MainTask) error { + return utils.WriteYaml(mainTask, filepath.Join(roleDir, "tasks", "main.yml")) +} diff --git a/internal/types/playbook.go b/internal/types/playbook.go new file mode 100644 index 0000000..bede5ed --- /dev/null +++ b/internal/types/playbook.go @@ -0,0 +1,40 @@ +package types + +import ( + "gopkg.in/yaml.v3" + "os" + "path/filepath" + "playbookctl/internal/utils" +) + +type Task struct { + Name string `yaml:"name"` + IncludeVars string `yaml:"include_vars"` +} + +type Playbook struct { + Hosts string `yaml:"hosts"` + GatherFacts bool `yaml:"gather_facts"` + PreTasks []Task `yaml:"pre_tasks"` + Roles []string `yaml:"roles"` +} + +func ReadPlaybook(workDir string) (*Playbook, error) { + var playbook []Playbook + { + bb, err := os.ReadFile(filepath.Join(workDir, "playbook.yml")) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(bb, &playbook); err != nil { + return nil, err + } + } + + return &playbook[0], nil +} + +func WritePlaybook(workDir string, playbook *Playbook) error { + return utils.WriteYaml([]*Playbook{playbook}, filepath.Join(workDir, "playbook.yml")) +} diff --git a/internal/utils/filesystem.go b/internal/utils/filesystem.go new file mode 100644 index 0000000..338960e --- /dev/null +++ b/internal/utils/filesystem.go @@ -0,0 +1,60 @@ +package utils + +import ( + "bytes" + "os" +) + +func SaveStaticFile(path string, data []byte) error { + return os.WriteFile(path, data, 0644) +} + +func CreateDir(path string) error { + return os.Mkdir(path, 0755) +} + +func CreateDirIfNotExists(path string) error { + exists, err := IsExistsDir(path) + if err != nil { + return err + } + + if !exists { + if err = CreateDir(path); err != nil { + return err + } + } + + return nil +} + +func IsExistsDir(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +func RemoveFile(path string) error { + return os.Remove(path) +} + +func WriteStringFile(path string, data string) error { + buffer := &bytes.Buffer{} + buffer.WriteString(data) + + return os.WriteFile(path, buffer.Bytes(), 0644) +} + +func ReadStringFile(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/internal/utils/logger/logger.go b/internal/utils/logger/logger.go new file mode 100644 index 0000000..1639071 --- /dev/null +++ b/internal/utils/logger/logger.go @@ -0,0 +1,39 @@ +package logger + +import "fmt" + +type LogVerbose uint8 + +const ( + Warning LogVerbose = 0 + Info = 1 + Debug = 2 + Trace = 3 +) + +type Logger struct { + // Уровень логирования + Verbose LogVerbose +} + +func (log *Logger) Warn(message string) { + fmt.Printf("[warning] %s\n", message) +} + +func (log *Logger) Info(message string) { + if log.Verbose >= Info { + fmt.Printf("[info] %s\n", message) + } +} + +func (log *Logger) Debug(message string) { + if log.Verbose >= Debug { + fmt.Printf("[debug] %s\n", message) + } +} + +func (log *Logger) Trace(message string) { + if log.Verbose >= Trace { + fmt.Printf("[trace] %s\n", message) + } +} diff --git a/internal/utils/yaml_utils.go b/internal/utils/yaml_utils.go new file mode 100644 index 0000000..c41d9a5 --- /dev/null +++ b/internal/utils/yaml_utils.go @@ -0,0 +1,36 @@ +package utils + +import ( + "bytes" + "gopkg.in/yaml.v3" + "os" +) + +func YamlHeader() string { + return "# vi: set tabstop=2 shiftwidth=2 expandtab :\n---\n" +} + +func WriteYaml(data interface{}, yamlFile string) error { + buffer := &bytes.Buffer{} + buffer.WriteString(YamlHeader()) + + yamlEncoder := yaml.NewEncoder(buffer) + yamlEncoder.SetIndent(2) + + if err := yamlEncoder.Encode(data); err != nil { + return err + } + + if err := os.WriteFile(yamlFile, buffer.Bytes(), 0644); err != nil { + return err + } + + return nil +} + +func WriteEmptyYaml(yamlFile string) error { + buffer := &bytes.Buffer{} + buffer.WriteString(YamlHeader()) + + return os.WriteFile(yamlFile, buffer.Bytes(), 0644) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9dccfe8 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "playbookctl/cmd" + +func main() { + cmd.Execute() +}