package ec2macosinit import ( "bytes" "fmt" "io" "os" "os/exec" "os/user" "strconv" "strings" "syscall" "time" ) // ioReadCloserToString converts an io.ReadCloser to a string. func ioReadCloserToString(iorc io.ReadCloser) (str string, err error) { buf := new(bytes.Buffer) _, err = buf.ReadFrom(iorc) if err != nil { return "", err } return buf.String(), nil } // commandOutput wraps the output from an exec command as strings. type commandOutput struct { stdout string stderr string } // executeCommand executes the command and returns stdout and stderr as strings. func executeCommand(c []string, runAsUser string, envVars []string) (output commandOutput, err error) { // Separate name and args, plus catch a few error cases var name string var args []string if len(c) > 1 { name = c[0] args = c[1:] } else if len(c) == 1 { name = c[0] if name == "" { // Empty string case ("") return commandOutput{}, fmt.Errorf("ec2macosinit: must provide a command") } } else { // Empty struct case ([]string{}) return commandOutput{}, fmt.Errorf("ec2macosinit: must provide a command") } // Set command and create output buffers cmd := exec.Command(name, args...) var stdoutb, stderrb bytes.Buffer cmd.Stdout = &stdoutb cmd.Stderr = &stderrb // Set runAsUser, if defined, otherwise will run as root if runAsUser != "" { uid, gid, err := getUIDandGID(runAsUser) if err != nil { return commandOutput{}, fmt.Errorf("ec2macosinit: error looking up user: %s\n", err) } cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} } // Append environment variables cmd.Env = os.Environ() cmd.Env = append(cmd.Env, envVars...) // Run command err = cmd.Run() if err != nil { return commandOutput{stdout: stdoutb.String(), stderr: stderrb.String()}, err } return commandOutput{stdout: stdoutb.String(), stderr: stderrb.String()}, nil } // getUIDandGID takes a username and returns the uid and gid for that user. // While testing UID/GID lookup for a user, it was found that the user.Lookup() function does not always return // information for a new user on first boot. In the case that user.Lookup() fails, we try dscacheutil, which has a // higher success rate. If that fails, we return an error. Any successful case returns the UID and GID as ints. func getUIDandGID(username string) (uid int, gid int, err error) { var uidstr, gidstr string // Preference is user.Lookup(), if it works u, lookuperr := user.Lookup(username) if lookuperr != nil { // user.Lookup() has failed, second try by checking the DS cache out, cmderr := executeCommand([]string{"dscacheutil", "-q", "user", "-a", "name", username}, "", []string{}) if cmderr != nil { // dscacheutil has failed with an error return 0, 0, fmt.Errorf("ec2macosinit: error while looking up user %s: \n"+ "user.Lookup() error: %s \ndscacheutil error: %s\ndscacheutil stderr: %s\n", username, lookuperr, cmderr, out.stderr) } // Check length of stdout - dscacheutil returns nothing if user is not found if len(out.stdout) > 0 { // dscacheutil has returned something // Command output from dscacheutil should look like: // name: ec2-user // password: ******** // uid: 501 // gid: 20 // dir: /Users/ec2-user // shell: /bin/bash // gecos: ec2-user dsSplit := strings.Split(out.stdout, "\n") // split on newline to separate uid and gid for _, e := range dsSplit { eSplit := strings.Fields(e) // split into fields to separate tag with id // Find UID and GID and set them if strings.HasPrefix(e, "uid") { if len(eSplit) != 2 { // dscacheutil has returned some sort of weird output that can't be split return 0, 0, fmt.Errorf("ec2macosinit: error while splitting dscacheutil uid output for user %s: %s\n"+ "user.Lookup() error: %s \ndscacheutil error: %s\ndscacheutil stderr: %s\n", username, out.stdout, lookuperr, cmderr, out.stderr) } uidstr = eSplit[1] } else if strings.HasPrefix(e, "gid") { if len(eSplit) != 2 { // dscacheutil has returned some sort of weird output that can't be split return 0, 0, fmt.Errorf("ec2macosinit: error while splitting dscacheutil gid output for user %s: %s\n"+ "user.Lookup() error: %s \ndscacheutil error: %s\ndscacheutil stderr: %s\n", username, out.stdout, lookuperr, cmderr, out.stderr) } gidstr = eSplit[1] } } } else { // dscacheutil has returned nothing, user is not found return 0, 0, fmt.Errorf("ec2macosinit: user %s not found: \n"+ "user.Lookup() error: %s \ndscacheutil error: %s\ndscacheutil stderr: %s\n", username, lookuperr, cmderr, out.stderr) } } else { // user.Lookup() was successful, use the returned UID/GID uidstr = u.Uid gidstr = u.Gid } // Convert UID and GID to int uid, err = strconv.Atoi(uidstr) if err != nil { return 0, 0, fmt.Errorf("ec2macosinit: error while converting UID to int: %s\n", err) } gid, err = strconv.Atoi(gidstr) if err != nil { return 0, 0, fmt.Errorf("ec2macosinit: error while converting GID to int: %s\n", err) } return uid, gid, nil } // userExists takes a username and returns whether or not the user exists on the system. func userExists(username string) (exists bool, err error) { out, err := executeCommand([]string{"dscacheutil", "-q", "user", "-a", "name", username}, "", []string{}) if err != nil { return false, fmt.Errorf("ec2macosinit: error while checking dscacheutil for user %s: %s\n", username, err) } // If dscacheutil output containing the username, the user exists if strings.Contains(out.stdout, username) { return true, nil } // No output means the user does not exist return false, nil } // retry is an extremely simple retry function which waits a specified duration on error and retries. func retry(attempts int, sleep time.Duration, f func() error) (err error) { for i := 0; ; i++ { err = f() if err == nil { return } if i >= (attempts - 1) { break } time.Sleep(sleep) } return fmt.Errorf("after %d attempts, last error: %s", attempts, err) } // getOSProductVersion uses the sysctl command to retrieve the product version number from the kernel func getOSProductVersion() (version string, err error) { cmdGetProductVersion := []string{"sysctl", "-n", "kern.osproductversion"} output, err := executeCommand(cmdGetProductVersion, "", []string{}) if err != nil { return version, fmt.Errorf("ec2macosinit: error getting kernel state for product version: %s", err) } // Remove any extra space characters from the output to leave only the product version number version = strings.TrimSpace(output.stdout) return version, nil }