Marvin Steadfast
2ad0221259
All checks were successful
continuous-integration/drone/push Build is passing
654 lines
16 KiB
Go
654 lines
16 KiB
Go
//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"
|
|
"go.xsfx.dev/schnutibox/assets"
|
|
)
|
|
|
|
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"
|
|
)
|
|
|
|
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.Assets.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.Assets.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
|
|
func Fstab(system string) error {
|
|
logger := log.With().Str("stage", "Fstab").Logger()
|
|
|
|
// 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")
|
|
|
|
// Chose the right template.
|
|
// In future it should be a switch statement.
|
|
tmpl, err := assets.Assets.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
|
|
}{
|
|
timesyncUser.Uid,
|
|
timesyncGroup.Gid,
|
|
mopidyUser.Uid,
|
|
mopidyGroup.Gid,
|
|
upmpdcliUser.Uid,
|
|
upmpdcliGroup.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 CreateUDEVrules() error {
|
|
logger := log.With().Str("stage", "CreateUDEVrules").Logger()
|
|
logger.Info().Msg("writing udev rule file")
|
|
|
|
// Parse template.
|
|
tmpl, err := assets.Assets.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
|
|
}
|
|
|
|
// CreateSymlinks creates all needed symlinks.
|
|
func CreateSymlinks(system string) error {
|
|
logger := log.With().Str("stage", "Symlinks").Logger()
|
|
|
|
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
|
|
}
|
|
|
|
// makeReadOnly executes stuff if a read-only system is wanted.
|
|
func makeReadOnly(system string) error {
|
|
if err := RemovePkgs(system); err != nil {
|
|
return fmt.Errorf("could not remove pkgs: %w", err)
|
|
}
|
|
|
|
if err := CreateSymlinks(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",
|
|
"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.Assets.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.Assets.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
|
|
}
|
|
|
|
func SchnutiboxConfig() error {
|
|
logger := log.With().Str("stage", "Upmpdcli").Logger()
|
|
logger.Info().Msg("writing schnutibox config")
|
|
|
|
// Parse template.
|
|
tmpl, err := assets.Assets.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 := CreateUDEVrules(); 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")
|
|
}
|
|
|
|
// Making system read-only.
|
|
if Cfg.ReadOnly {
|
|
if err := makeReadOnly(Cfg.System); err != nil {
|
|
log.Fatal().Err(err).Msg("could not make system read-only")
|
|
}
|
|
}
|
|
}
|