package main import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "github.com/coreos/go-systemd/unit" "github.com/rivo/tview" "github.com/spf13/cobra" ) type CreateOptions struct { Type string Description string Exec string ExecStartPre string ExecStartPost string ExecReload string ExecStop string ExecStopPost string WorkingDirectory string RootDirectory string User string Group string Restart string RestartSec string TimeoutStartSec string TimeoutStopSec string After string WantedBy string } var ( createOpts = CreateOptions{} types = Strings{"simple", "forking", "oneshot", "dbus", "notify", "idle"} restarts = Strings{"no", "always", "on-success", "on-failure", "on-abnormal", "on-abort", "on-watchdog"} createCmd = &cobra.Command{ Use: "create [after] [wanted-by]", Short: "creates a new Unit file", Long: `The create command creates a new systemd Unit file`, RunE: func(cmd *cobra.Command, args []string) error { ts, err := targets() if err != nil { return fmt.Errorf("Can't find systemd targets: %s", err) } createOpts.Type = strings.ToLower(createOpts.Type) if !types.Contains(createOpts.Type) { return fmt.Errorf("No such service type: %s", createOpts.Type) } createOpts.Restart = strings.ToLower(createOpts.Restart) if !restarts.Contains(createOpts.Restart) { return fmt.Errorf("No such restart type: %s", createOpts.Restart) } switch len(args) { case 4: createOpts.WantedBy = args[3] fallthrough case 3: createOpts.After = args[2] fallthrough case 2: createOpts.Description = args[1] fallthrough case 1: createOpts.Exec = args[0] } if len(args) >= 2 { if err := validate(); err != nil { return err } return executeCreate() } app := tview.NewApplication() form := tview.NewForm() descriptionField := tview.NewInputField(). SetLabel("Description:"). SetText(createOpts.Description). SetFieldWidth(40). SetAcceptanceFunc(nil). SetChangedFunc(func(s string) { createOpts.Description = s }) form. AddDropDown("Type:", types, types.IndexOf(createOpts.Type), func(s string, i int) { createOpts.Type = s }). AddInputField("Exec on start:", createOpts.Exec, 40, nil, func(s string) { createOpts.Exec = s descriptionField.SetText(fmt.Sprintf("%s service", filepath.Base(createOpts.Exec))) }). AddFormItem(descriptionField). AddInputField("Exec on stop:", createOpts.ExecStop, 40, nil, func(s string) { createOpts.ExecStop = s }). AddInputField("Exec on reload:", createOpts.ExecReload, 40, nil, func(s string) { createOpts.ExecReload = s }). AddDropDown("Restarts on:", restarts, restarts.IndexOf(createOpts.Restart), func(s string, i int) { createOpts.Restart = s }). AddDropDown("Start after target:", ts.Strings(), Strings(ts.Strings()).IndexOf(createOpts.After), func(s string, i int) { createOpts.After = s }). AddDropDown("Wanted by target:", ts.Strings(), Strings(ts.Strings()).IndexOf(createOpts.WantedBy), func(s string, i int) { createOpts.WantedBy = s }) var apperr error form. AddButton("Create", func() { app.Stop() if err := validate(); err != nil { apperr = err } else { executeCreate() } }). AddButton("Cancel", func() { app.Stop() }) form.SetBorder(true).SetTitle("Create new service").SetTitleAlign(tview.AlignCenter) if err := app.SetRoot(form, true).Run(); err != nil { return err } return apperr }, } ) func validateExecutables(executable string, allowEmpty bool) (string, error) { executable = strings.TrimSpace(executable) if len(executable) == 0 { if allowEmpty { return executable, nil } return executable, fmt.Errorf("Need an executable to create a service for") } stat, err := os.Stat(executable) if os.IsNotExist(err) { return executable, fmt.Errorf("Could not find executable: %s is not a file", executable) } if stat.IsDir() { return executable, fmt.Errorf("Could not find executable: %s is a directory", executable) } if stat.Mode()&0111 == 0 { return executable, fmt.Errorf("%s is not executable", executable) } return executable, nil } func validate() error { ts, err := targets() if err != nil { return fmt.Errorf("Can't find systemd targets: %s", err) } // Executable checks createOpts.Exec, err = validateExecutables(createOpts.Exec, false) if err != nil { return err } createOpts.ExecReload, err = validateExecutables(createOpts.ExecReload, true) if err != nil { return err } createOpts.ExecStartPre, err = validateExecutables(createOpts.ExecStartPre, true) if err != nil { return err } createOpts.ExecStartPost, err = validateExecutables(createOpts.ExecStartPost, true) if err != nil { return err } createOpts.ExecStop, err = validateExecutables(createOpts.ExecStop, true) if err != nil { return err } createOpts.ExecStopPost, err = validateExecutables(createOpts.ExecStopPost, true) if err != nil { return err } // Description check if len(createOpts.Description) == 0 { return fmt.Errorf("Description for this service can't be empty") } // Target checks if len(createOpts.After) > 0 && !ts.Contains(createOpts.After) { return fmt.Errorf("Could not create service: no such target") } if len(createOpts.WantedBy) > 0 && !ts.Contains(createOpts.WantedBy) { return fmt.Errorf("Could not create service: no such target") } return nil } func executeCreate() error { u := []*unit.UnitOption{ &unit.UnitOption{"Unit", "Description", createOpts.Description}, &unit.UnitOption{"Unit", "After", createOpts.After}, &unit.UnitOption{"Service", "Type", createOpts.Type}, &unit.UnitOption{"Service", "WorkingDirectory", createOpts.WorkingDirectory}, &unit.UnitOption{"Service", "RootDirectory", createOpts.RootDirectory}, &unit.UnitOption{"Service", "ExecStart", createOpts.Exec}, &unit.UnitOption{"Service", "ExecStartPre", createOpts.ExecStartPre}, &unit.UnitOption{"Service", "ExecStartPost", createOpts.ExecStartPost}, &unit.UnitOption{"Service", "ExecReload", createOpts.ExecReload}, &unit.UnitOption{"Service", "ExecStop", createOpts.ExecStop}, &unit.UnitOption{"Service", "ExecStopPost", createOpts.ExecStopPost}, &unit.UnitOption{"Service", "User", createOpts.User}, &unit.UnitOption{"Service", "Group", createOpts.Group}, &unit.UnitOption{"Service", "Restart", createOpts.Restart}, &unit.UnitOption{"Service", "RestartSec", createOpts.RestartSec}, &unit.UnitOption{"Service", "TimeoutStartSec", createOpts.TimeoutStartSec}, &unit.UnitOption{"Service", "TimeoutStopSec", createOpts.TimeoutStopSec}, &unit.UnitOption{"Install", "WantedBy", createOpts.WantedBy}, } r := unit.Serialize(stripEmptyOptions(u)) b, err := ioutil.ReadAll(r) if err != nil { return fmt.Errorf("Encountered error while reading output: %v", err) } filename := filepath.Base(createOpts.Exec) + ".service" f, err := os.Create(filename) if err != nil { return fmt.Errorf("Could not create file: %s", err) } defer f.Close() _, err = f.Write(b) if err != nil { return fmt.Errorf("Could not write to file: %s", err) } fmt.Printf("Generated Unit file: %s\n%s\n", filename, b) return nil } func init() { createCmd.PersistentFlags().StringVarP(&createOpts.Type, "type", "t", "simple", "Type of service (simple, forking, oneshot, dbus, notify or idle)") createCmd.PersistentFlags().StringVar(&createOpts.ExecStartPre, "execstartpre", "", "Executable to run before the service starts") createCmd.PersistentFlags().StringVar(&createOpts.ExecStartPost, "execstartpost", "", "Executable to run after the service started") createCmd.PersistentFlags().StringVar(&createOpts.ExecReload, "execreload", "", "Executable to run to reload the service") createCmd.PersistentFlags().StringVar(&createOpts.ExecStop, "execstop", "", "Executable to run to stop the service") createCmd.PersistentFlags().StringVar(&createOpts.ExecStopPost, "execstoppost", "", "Executable to run after the service stopped") createCmd.PersistentFlags().StringVarP(&createOpts.WorkingDirectory, "workingdir", "w", "", "Working-directory of the service") createCmd.PersistentFlags().StringVar(&createOpts.RootDirectory, "rootdir", "", "Root-directory of the service") createCmd.PersistentFlags().StringVarP(&createOpts.User, "user", "u", "root", "User to run service as") createCmd.PersistentFlags().StringVarP(&createOpts.Group, "group", "g", "root", "Group to run service as") createCmd.PersistentFlags().StringVarP(&createOpts.Restart, "restart", "r", "on-failure", "When to restart (no, always, on-success, on-failure, on-abnormal, on-abort or on-watchdog)") createCmd.PersistentFlags().StringVarP(&createOpts.RestartSec, "restartsec", "s", "", "How many seconds between restarts") createCmd.PersistentFlags().StringVar(&createOpts.TimeoutStartSec, "timeoutstartsec", "", "How many seconds to wait for a startup") createCmd.PersistentFlags().StringVar(&createOpts.TimeoutStopSec, "timeoutstopsec", "", "How many seconds to wait when stoping a service") RootCmd.AddCommand(createCmd) }