//nolint:exhaustivestruct,gochecknoglobals package prepare import ( "fmt" "io/ioutil" "os" "os/exec" "os/user" "strings" "text/template" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" assets "go.xsfx.dev/schnutibox/assets/prepare" ) const ( mopidyGroup = "audio" mopidyUser = "mopidy" serviceFileName = "schnutibox.service" serviceLocation = "/etc/systemd/system" timesyncGroup = "systemd-timesync" timesyncUser = "systemd-timesync" schnutiboxUser = "schnutibox" schnutboxConfigDir = "/etc/schnutibox" upmpdcliUser = "upmpdcli" upmpdcliGroup = "nogroup" snapserverUser = "snapserver" snapserverGroup = "snapserver" snapclientUser = "snapclient" snapclientGroup = "snapclient" ) // Cfg represents the structured data for the schnutibox config file. var Cfg = struct { RFIDReader string ReadOnly bool Spotify bool SpotifyClientID string SpotifyClientSecret string SpotifyPassword string SpotifyUsername string StopID string System string }{} // boxService creates a systemd service for schnutibox. func boxService(filename string, enable bool) error { logger := log.With().Str("stage", "BoxService").Logger() if err := createUser(); err != nil { return fmt.Errorf("could not create user: %w", err) } // Create config dir. if err := os.MkdirAll(schnutboxConfigDir, os.ModePerm); err != nil { return fmt.Errorf("could not create config dir: %w", err) } schnutiboxService, err := assets.Files.ReadFile("files/schnutibox.service") if err != nil { return fmt.Errorf("could not get service file: %w", err) } //nolint:gosec if err := ioutil.WriteFile(filename, schnutiboxService, 0o644); err != nil { return fmt.Errorf("could not write service file: %w", err) } if enable { cmd := exec.Command("systemctl", "daemon-reload") logger.Info().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not reload service files: %w", err) } cmd = exec.Command("systemctl", "enable", "schnutibox.service") logger.Info().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not enable service: %w", err) } } return nil } func ntp() error { logger := log.With().Str("stage", "NTP").Logger() cmd := exec.Command("apt-get", "install", "-y", "ntp", "ntpdate") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Info().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not install ntp: %w", err) } ntpService, err := assets.Files.ReadFile("files/ntp.service") if err != nil { return fmt.Errorf("could not get ntp service file: %w", err) } // nolint:gosec if err := ioutil.WriteFile("/etc/systemd/system/ntp.service", ntpService, 0o644); err != nil { return fmt.Errorf("could not copy ntp service file: %w", err) } cmd = exec.Command("systemctl", "daemon-reload") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Info().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not reload systemd service files: %w", err) } return nil } // fstab creates a fstab for a read-only system. // nolint:funlen,cyclop func fstab(system string) error { logger := log.With().Str("stage", "Fstab").Logger() logger.Debug().Str("system", system).Msg("ignoring for now") // Getting timesync user and group informations. timesyncUser, err := user.Lookup(timesyncUser) if err != nil { return fmt.Errorf("could not lookup timesync user: %w", err) } timesyncGroup, err := user.LookupGroup(timesyncGroup) if err != nil { return fmt.Errorf("could not lookup timesync group: %w", err) } logger.Debug().Str("uid", timesyncUser.Uid).Str("gid", timesyncGroup.Gid).Msg("timesyncd") // Getting mopidy user and group informations. mopidyUser, err := user.Lookup(mopidyUser) if err != nil { return fmt.Errorf("could not lookup mopidy user: %w", err) } mopidyGroup, err := user.LookupGroup(mopidyGroup) if err != nil { return fmt.Errorf("could not lookup mopidy group: %w", err) } logger.Debug().Str("uid", mopidyUser.Uid).Str("gid", mopidyGroup.Gid).Msg("mopidy") // Getting upmpd user and group informations. upmpdcliUser, err := user.Lookup(upmpdcliUser) if err != nil { return fmt.Errorf("could not lookup upmpdcli user: %w", err) } upmpdcliGroup, err := user.LookupGroup(upmpdcliGroup) if err != nil { return fmt.Errorf("could not lookup upmpdcli group: %w", err) } logger.Debug().Str("uid", upmpdcliUser.Uid).Str("gid", upmpdcliGroup.Gid).Msg("upmpdcli") // Getting snapserver user and group informations. snapserverUser, err := user.Lookup(snapserverUser) if err != nil { return fmt.Errorf("could not lookup snapserver user: %w", err) } snapserverGroup, err := user.LookupGroup(snapserverGroup) if err != nil { return fmt.Errorf("could not lookup snapserver group: %w", err) } logger.Debug().Str("uid", snapserverUser.Uid).Str("gid", snapserverGroup.Gid).Msg("snapserver") snapclientUser, err := user.Lookup(snapclientUser) if err != nil { return fmt.Errorf("could not lookup snapclient user: %w", err) } snapclientGroup, err := user.LookupGroup(snapclientGroup) if err != nil { return fmt.Errorf("could not lookup snapclient group: %w", err) } logger.Debug().Str("uid", snapclientUser.Uid).Str("gid", snapclientGroup.Gid).Msg("snapclient") // Chose the right template. // In future it should be a switch statement. tmpl, err := assets.Templates.ReadFile("templates/fstab.raspbian.tmpl") if err != nil { return fmt.Errorf("could not get fstab template: %w", err) } // Parse template. t := template.Must(template.New("fstab").Parse(string(tmpl))) // Open fstab. f, err := os.Create("/etc/fstab") if err != nil { return fmt.Errorf("could not create file to write: %w", err) } defer f.Close() // Create and write. if err := t.Execute(f, struct { TimesyncUID string TimesyncGID string MopidyUID string MopidyGID string UpmpdcliUID string UpmpdcliGID string SnapserverUID string SnapserverGID string SnapclientUID string SnapclientGID string }{ timesyncUser.Uid, timesyncGroup.Gid, mopidyUser.Uid, mopidyGroup.Gid, upmpdcliUser.Uid, upmpdcliGroup.Gid, snapserverUser.Uid, snapserverGroup.Gid, snapclientUser.Uid, snapclientGroup.Gid, }); err != nil { return fmt.Errorf("could not write templated fstab: %w", err) } return nil } // removePkgs removes not needed software in read-only mode. func removePkgs(system string) error { logger := log.With().Str("stage", "RemovePkgs").Logger() if system != "raspbian" { logger.Info().Msg("nothing to do") return nil } pkgs := []string{ "cron", "logrotate", "triggerhappy", "dphys-swapfile", "fake-hwclock", "samba-common", } for _, i := range pkgs { logger.Debug().Str("pkg", i).Msg("remove package") cmd := exec.Command("apt-get", "remove", "-y", i) logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not remove pkg: %w", err) } } cmd := exec.Command("apt-get", "autoremove", "--purge", "-y") logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not do an autoremove: %w", err) } return nil } func udevRules() error { logger := log.With().Str("stage", "CreateUDEVrules").Logger() logger.Info().Msg("writing udev rule file") // Parse template. tmpl, err := assets.Templates.ReadFile("templates/50-neuftech.rules.tmpl") if err != nil { return fmt.Errorf("could not get udev rules file: %w", err) } t := template.Must(template.New("udev").Parse(string(tmpl))) // Open file. f, err := os.Create("/etc/udev/rules.d/50-neuftech.rules") if err != nil { return fmt.Errorf("could not create file to write: %w", err) } defer f.Close() // Create and write. if err := t.Execute(f, struct { SchnutiboxGroup string }{ schnutiboxUser, }); err != nil { return fmt.Errorf("could not write templated udev rules: %w", err) } return nil } // createUser creates schnutibox system user and group. func createUser() error { logger := log.With().Str("stage", "CreateUser").Logger() cmd := exec.Command("adduser", "--system", "--group", "--no-create-home", schnutiboxUser) logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not create user: %w", err) } return nil } // symlinks creates all needed symlinks. func symlinks(system string) error { logger := log.With().Str("stage", "Symlinks").Logger() logger.Debug().Str("system", system).Msg("ignoring for now") links := []struct { symlink string dest string }{ { "/var/lib/dhcp", "/tmp/dhcp", }, { "/var/spool", "/tmp/spool", }, { "/var/lock", "/tmp/lock", }, { "/etc/resolv.conf", "/tmp/resolv.conf", }, } removeFiles := []string{ "/var/lib/dhcp", "/var/spool", "/var/lock", "/etc/resolv.conf", } for _, i := range removeFiles { logger.Debug().Str("item", i).Msg("remove file/directory") if err := os.RemoveAll(i); err != nil { return fmt.Errorf("could not remove: %w", err) } } for _, i := range links { logger.Debug().Str("symlink", i.symlink).Str("dest", i.dest).Msg("linking") dest, err := os.Readlink(i.symlink) if err == nil { if dest == i.dest { logger.Debug().Str("dest", dest).Str("expected dest", i.dest).Msg("matches") continue } } if err := os.Symlink(i.dest, i.symlink); err != nil { return fmt.Errorf("could not create symlink: %w", err) } } return nil } // cmdlineTxt modifies the /boot/cmdline.txt. func cmdlineTxt() error { // Read. oldLine, err := ioutil.ReadFile("/boot/cmdline.txt") if err != nil { return fmt.Errorf("could not read cmdline.txt: %w", err) } newLine := strings.TrimSuffix(string(oldLine), "\n") + " " + "fastboot" + " " + "noswap" // Write. // nolint:gosec if err := ioutil.WriteFile("/boot/cmdline.txt", []byte(newLine), 0o644); err != nil { return fmt.Errorf("could not write cmdline.txt: %w", err) } return nil } // readOnly executes stuff if a read-only system is wanted. func readOnly(system string) error { if err := removePkgs(system); err != nil { return fmt.Errorf("could not remove pkgs: %w", err) } if err := symlinks(system); err != nil { return fmt.Errorf("could not create symlinks: %w", err) } if err := fstab(system); err != nil { return fmt.Errorf("could not create fstab: %w", err) } if err := cmdlineTxt(); err != nil { return fmt.Errorf("could not modify cmdline.txt: %w", err) } return nil } // mopidy setups mopidy. //nolint:funlen,cyclop func mopidy() error { logger := log.With().Str("stage", "Mopidy").Logger() // GPG Key. cmd := exec.Command("/bin/sh", "-c", "wget -q -O - https://apt.mopidy.com/mopidy.gpg | apt-key add -") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not add mopidy key: %w", err) } // Repo. cmd = exec.Command( "/bin/sh", "-c", "wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not download apt repo: %w", err) } // Update. cmd = exec.Command("apt-get", "update") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not update apt: %w", err) } // Install. cmd = exec.Command( "apt-get", "install", "-y", "libgstreamer-plugins-bad1.0", "mopidy", "mopidy-alsamixer", "mopidy-mpd", "mopidy-spotify", "python3-pip", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not install mopidy: %w", err) } // Extensions. cmd = exec.Command( "pip3", "install", "--upgrade", "Mopidy-YouTube", "requests>=2.22", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not install extensions: %w", err) } // Enable service. cmd = exec.Command("systemctl", "enable", "mopidy.service") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not enable mopidy service: %w", err) } // Config. if Cfg.SpotifyUsername != "" && Cfg.SpotifyPassword != "" && Cfg.SpotifyClientID != "" && Cfg.SpotifyClientSecret != "" { Cfg.Spotify = true } tmpl, err := assets.Templates.ReadFile("templates/mopidy.conf.tmpl") if err != nil { return fmt.Errorf("could not get mopidy.conf: %w", err) } t := template.Must(template.New("mopidyConf").Parse(string(tmpl))) f, err := os.Create("/etc/mopidy/mopidy.conf") if err != nil { return fmt.Errorf("could not create file to write: %w", err) } defer f.Close() if err := t.Execute(f, Cfg); err != nil { return fmt.Errorf("could not write compiled mopidy config: %w", err) } return nil } // Upmpdcli setups upmpdcli. //nolint:funlen func upmpdcli() error { logger := log.With().Str("stage", "Upmpdcli").Logger() // GPG Key. cmd := exec.Command( "/bin/sh", "-c", "wget https://www.lesbonscomptes.com/pages/lesbonscomptes.gpg -O /usr/share/keyrings/lesbonscomptes.gpg", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not add upmpdcli key: %w", err) } // Repo. cmd = exec.Command( "curl", "-o", "/etc/apt/sources.list.d/upmpdcli.list", "https://www.lesbonscomptes.com/upmpdcli/pages/upmpdcli-rbuster.list", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not add sources list for upmpdcli: %w", err) } // Update. cmd = exec.Command("apt-get", "update") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not update apt: %w", err) } // Install. cmd = exec.Command( "apt-get", "install", "-y", "upmpdcli", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not install mopidy: %w", err) } // Enable service. cmd = exec.Command("systemctl", "enable", "upmpdcli.service") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not enable upmpdcli service: %w", err) } // Create config. upmpdcliConf, err := assets.Files.ReadFile("files/upmpdcli.conf") if err != nil { return fmt.Errorf("could not get upmpdcli.conf: %w", err) } // nolint:gosec if err := ioutil.WriteFile("/etc/upmpdcli.conf", upmpdcliConf, 0o644); err != nil { return fmt.Errorf("could not copy upmpdcli config: %w", err) } return nil } // nolint:funlen func snapcast() error { logger := log.With().Str("stage", "snapcast").Logger() // Download deb. cmd := exec.Command( "wget", "https://github.com/badaix/snapcast/releases/download/v0.24.0/snapclient_0.24.0-1_without-pulse_armhf.deb", "-O", "/tmp/snapclient.deb", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not download snapclient deb: %w", err) } // Install deb cmd = exec.Command( "/bin/sh", "-c", "dpkg -i /tmp/snapclient.deb; apt --fix-broken install -y; rm /tmp/snapclient.deb", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not install snapclient deb: %w", err) } // Download deb. cmd = exec.Command( "wget", "https://github.com/badaix/snapcast/releases/download/v0.24.0/snapserver_0.24.0-1_armhf.deb", "-O", "/tmp/snapserver.deb", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not download snapserver deb: %w", err) } // Install deb cmd = exec.Command( "/bin/sh", "-c", "dpkg -i /tmp/snapserver.deb; apt --fix-broken install -y; rm /tmp/snapserver.deb", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logger.Debug().Str("cmd", cmd.String()).Msg("running") if err := cmd.Run(); err != nil { return fmt.Errorf("could not install snapserver deb: %w", err) } return nil } func schnutiboxConfig() error { logger := log.With().Str("stage", "schnutiboxConfig").Logger() logger.Info().Msg("writing schnutibox config") // Parse template. tmpl, err := assets.Templates.ReadFile("templates/schnutibox.yml.tmpl") if err != nil { return fmt.Errorf("could not get template: %w", err) } t := template.Must(template.New("config").Parse(string(tmpl))) // Open file. f, err := os.Create("/etc/schnutibox/schnutibox.yml") if err != nil { return fmt.Errorf("could not create file to write: %w", err) } defer f.Close() // Create and write. if err := t.Execute(f, Cfg); err != nil { return fmt.Errorf("could not write templated udev rules: %w", err) } return nil } func Run(cmd *cobra.Command, args []string) { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) // Install schnutibox service. if err := boxService(serviceLocation+"/"+serviceFileName, true); err != nil { log.Fatal().Err(err).Msg("could not create schnutibox service") } // Create schnutibox config. if err := schnutiboxConfig(); err != nil { log.Fatal().Err(err).Msg("could not create schnutibox config.") } // Install udev file. if err := udevRules(); err != nil { log.Fatal().Err(err).Msg("could not install udev rules") } // Setup NTP. if err := ntp(); err != nil { log.Fatal().Err(err).Msg("could not setup ntp") } // Setup mopidy. if err := mopidy(); err != nil { log.Fatal().Err(err).Msg("could not setup mopidy") } // Setup upmpdcli. if err := upmpdcli(); err != nil { log.Fatal().Err(err).Msg("could not setup upmpdcli") } // Setup snapcast. if err := snapcast(); err != nil { log.Fatal().Err(err).Msg("could not setup snapclient") } // Making system read-only. if Cfg.ReadOnly { if err := readOnly(Cfg.System); err != nil { log.Fatal().Err(err).Msg("could not make system read-only") } } }