diff --git a/apt.go b/apt.go new file mode 100644 index 0000000..cf8a4fd --- /dev/null +++ b/apt.go @@ -0,0 +1,72 @@ +package zoopb + +import ( + "fmt" + + "go.wit.com/log" +) + +// init the installed package list +func (me *Machine) initPackages() { + // Get the list of installed packages for the detected distro + newP, err := getPackageList(me.Distro) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Print the installed packages and their versions + for pkg, version := range newP { + new1 := new(Package) + new1.Name = pkg + new1.Version = version + if me.Packages.Append(new1) { + // log.Info("added", new1.Name, "ok") + } else { + log.Info("added", new1.Name, "failed") + } + } + + log.Info(me.Hostname, "has distro", me.Distro, "with", me.Packages.Len(), "packages installed.") +} + +func (me *Machine) addNew(name string, version string) bool { + new1 := new(Package) + new1.Name = name + new1.Version = version + return me.Packages.Append(new1) +} + +func (me *Machine) updatePackages() string { + // Get the list of installed packages for the detected distro + newP, err := getPackageList(me.Distro) + if err != nil { + fmt.Println("Error:", err) + return fmt.Sprintln("getPackageList()", err) + } + + var newCounter, changeCounter int + // Print the installed packages and their versions + for pkg, version := range newP { + found := me.Packages.FindByName(pkg) + if found == nil { + log.Info("adding new", pkg, version) + me.addNew(pkg, version) + newCounter += 1 + } else { + found.Version = version + if me.Packages.Update(found) { + changeCounter += 1 + } + } + } + + footer := fmt.Sprintf("%s has distro %s with %d packages installed", me.Hostname, me.Distro, me.Packages.Len()) + if changeCounter != 0 { + footer += fmt.Sprintf(" (%d changed)", changeCounter) + } + if newCounter != 0 { + footer += fmt.Sprintf(" (%d new)", newCounter) + } + return footer +} diff --git a/apt_linux.go b/apt_linux.go new file mode 100644 index 0000000..3e71d1a --- /dev/null +++ b/apt_linux.go @@ -0,0 +1,72 @@ +package zoopb + +import ( + "bufio" + "fmt" + "os/exec" + "strings" + + "go.wit.com/log" +) + +// getPackageList returns the list of installed packages based on the distro +func getPackageList(distro string) (map[string]string, error) { + var cmd *exec.Cmd + + // Run the appropriate command based on the detected distribution + switch distro { + case "ubuntu", "debian": + return dpkgQuery() + case "fedora", "centos", "rhel": + cmd = exec.Command("rpm", "-qa") + case "arch", "manjaro": + cmd = exec.Command("pacman", "-Q") + default: + return nil, fmt.Errorf("unsupported distribution: %s", distro) + } + + // Capture the command's output + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error running command: %v", err) + } + + // todo: Split the output into lines and return + lines := strings.Split(string(output), "\n") + log.Info("output had", len(lines), "lines") + return nil, nil +} + +func dpkgQuery() (map[string]string, error) { + // Run the dpkg-query command to list installed packages and versions + cmd := exec.Command("dpkg-query", "-W", "-f=${Package} ${Version}\n") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + // Start the command execution + if err := cmd.Start(); err != nil { + return nil, err + } + defer cmd.Wait() + + // Create a map to store package names and versions + installedPackages := make(map[string]string) + + // Use a scanner to read the output of the command line by line + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + // Split each line into package name and version + parts := strings.SplitN(line, " ", 2) + if len(parts) == 2 { + packageName := parts[0] + version := parts[1] + installedPackages[packageName] = version + } + } + + // Return the map with package names and versions + return installedPackages, scanner.Err() +} diff --git a/config.go b/config.go index affe013..03c81c5 100644 --- a/config.go +++ b/config.go @@ -24,6 +24,29 @@ func (m *Machines) ConfigSave() error { configWrite(data) return nil } + +// when running on a single machine, save the file in forge/ +// as .pb +// write to ~/.config/forge/ unless ENV{FORGE_HOME} is set +func (m *Machine) ConfigSave() error { + if os.Getenv("FORGE_HOME") == "" { + homeDir, _ := os.UserHomeDir() + fullpath := filepath.Join(homeDir, ".config/forge") + os.Setenv("FORGE_HOME", fullpath) + } + data, err := m.Marshal() + if err != nil { + log.Info("proto.Marshal() failed len", len(data), err) + return err + } + + log.Info("ConfigSave() proto.Marshal() worked len", len(data)) + + hostname, _ := os.Hostname() + fname := hostname + ".pb" + return m.configWrite(fname, data) +} + func ConfigSaveRaw(data []byte) error { configWrite(data) return nil @@ -45,6 +68,30 @@ func (m *Machines) ConfigLoad() error { return nil } +func (m *Machine) ConfigLoad() error { + if m == nil { + return errors.New("It's not safe to run ConfigLoad() on a nil ?") + } + if os.Getenv("FORGE_HOME") == "" { + homeDir, _ := os.UserHomeDir() + fullpath := filepath.Join(homeDir, ".config/forge") + os.Setenv("FORGE_HOME", fullpath) + } + + hostname, _ := os.Hostname() + fname := hostname + ".pb" + + if data, err := m.loadFile(fname); err == nil { + if err = proto.Unmarshal(data, m); err != nil { + log.Warn("broken zookeeper.pb config file", fname) + return err + } + } else { + return err + } + return nil +} + func loadFile(filename string) ([]byte, error) { homeDir, err := os.UserHomeDir() p := filepath.Join(homeDir, ".config/zookeeper") @@ -57,6 +104,17 @@ func loadFile(filename string) ([]byte, error) { return data, nil } +func (m *Machine) loadFile(fname string) ([]byte, error) { + fullname := filepath.Join(os.Getenv("FORGE_HOME"), fname) + + data, err := os.ReadFile(fullname) + if err != nil { + // log.Info("open config file :", err) + return nil, err + } + return data, nil +} + func configWrite(data []byte) error { homeDir, err := os.UserHomeDir() p := filepath.Join(homeDir, ".config/zookeeper") @@ -70,3 +128,16 @@ func configWrite(data []byte) error { cfgfile.Write(data) return nil } + +func (m *Machine) configWrite(fname string, data []byte) error { + fullname := filepath.Join(os.Getenv("FORGE_HOME"), fname) + + cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE, 0666) + defer cfgfile.Close() + if err != nil { + log.Warn("open config file :", err) + return err + } + cfgfile.Write(data) + return nil +} diff --git a/distro.go b/distro.go new file mode 100644 index 0000000..bbbdda3 --- /dev/null +++ b/distro.go @@ -0,0 +1,61 @@ +// Copyright 2024 WIT.COM Inc. + +package zoopb + +// simple stab at making a human readable distro name +// this is for displaying in a table in the zookeeper app +// it's just so you can easily see what machines in your grid are +// doing what + +import ( + "bufio" + "fmt" + "os" + "runtime" + "strings" +) + +func initDistro() string { + switch runtime.GOOS { + case "windows": + return "windows" + case "macos": + return "macos" + case "linux": + // Detect the Linux distribution + distro := detectDistro() + if distro == "" { + fmt.Println("Unable to detect Linux distribution.") + distro = "fixme" + } + + fmt.Printf("Detected distribution: %s\n", distro) + return distro + default: + return runtime.GOOS + } +} + +// detectDistro returns the Linux distribution name (if possible) +func detectDistro() string { + // Check if we're on Linux + + // Try to read /etc/os-release to determine the distro + file, err := os.Open("/etc/os-release") + if err != nil { + return "" + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "ID=") { + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + return strings.Trim(parts[1], `"`) + } + } + } + return "" +} diff --git a/machine.proto b/machine.proto index 0926fa5..76c7aa2 100644 --- a/machine.proto +++ b/machine.proto @@ -12,6 +12,9 @@ message Machine { string distro = 4; Packages packages = 5; google.protobuf.Timestamp laststamp = 6; // the last time we heard anything from this machine + Packages installed = 7; // packages that are installed + Packages available = 8; // packages that are available + Packages wit = 9; // packages that are available from mirrors.wit.com } message Machines { diff --git a/package.proto b/package.proto index 53859a8..500db12 100644 --- a/package.proto +++ b/package.proto @@ -6,9 +6,11 @@ package gitpb; import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp message Package { - string name = 1; - string version = 2; - google.protobuf.Timestamp laststamp = 4; // the last time we heard anything from this droplet + string name = 1; // name: zookeeper-go + string version = 2; // version: 0.0.3 + google.protobuf.Timestamp laststamp = 3; // the last time this package was seen (used to timeout entries) + string srcPath = 4; // path to the sources (go.wit.com/apps/zookeeper) + bool installed = 5; // installed: true } message Packages {