diff --git a/instance.go b/instance.go index 0ad4519..ecad4f0 100644 --- a/instance.go +++ b/instance.go @@ -59,8 +59,9 @@ const ( var ( // ErrLoadFailed is an error signifying that the loading of the source code failed - ErrLoadFailed = errors.New("load did not appear to finish successfully") - getQlist = qlist + ErrLoadFailed = errors.New("load did not appear to finish successfully") + getQlist = qlist + parameterReader = fileParameterReader ) // An Instance represents an instance of Caché/Ensemble/Iris on the current system. @@ -91,7 +92,29 @@ type Instance struct { // Update will query the the underlying instance and update the Instance fields with its current state. // It returns any error encountered. func (i *Instance) Update() error { - q, err := getQlist(i.Name) + procAttr, err := i.managerSysProc() + if err != nil { + return err + } + + // if we didn't get a manager proc, try to update without it to find the manager + if procAttr == nil { + q, err := getQlist(i.Name, nil) + if err != nil { + return err + } + + if err := i.UpdateFromQList(q); err != nil { + return err + } + + procAttr, err = i.managerSysProc() + if err != nil { + return err + } + } + + q, err := getQlist(i.Name, procAttr) if err != nil { return err } @@ -228,6 +251,32 @@ func (i *Instance) DetermineManager() (string, string, error) { return i.getUserAndGroupFromParameters("Manager", managerUserKey, managerGroupKey) } +// managerSysProc is used to run instance management commands as a different user (if the current user isn't the manager) +func (i *Instance) managerSysProc() (*syscall.SysProcAttr, error) { + // can't find manager if we don't have a directory + if i.Directory == "" { + return nil, nil + } + + mgr, _, err := i.DetermineManager() + if err != nil { + return nil, err + } + + uid, gid, err := lookupUser(mgr) + if err != nil { + return nil, err + } + + log.WithFields(log.Fields{"user": mgr, "uid": uid, "gid": gid}).Debug("instance manager sysproc") + return &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + }, nil +} + // DetermineOwner will determine the owner of an instance by reader the parameters file associate with this instance. // The owner is the user which owns the files from the installers and as who most Caché processes will be running. // It returns the owner and owner group as strings and any error encountered. @@ -323,7 +372,14 @@ func (i *Instance) LicenseKeyFilePath() string { func (i *Instance) Start() error { // TODO: Think about a nozstu flag if there's a reason if i.Status.Down() { - if output, err := exec.Command(i.controlPath(), "start", i.Name, "quietly").CombinedOutput(); err != nil { + cmd := exec.Command(i.controlPath(), "start", i.Name, "quietly") + procAttr, err := i.managerSysProc() + if err != nil { + return err + } + + cmd.SysProcAttr = procAttr + if output, err := cmd.CombinedOutput(); err != nil { log.WithError(err).WithFields(log.Fields{"output": string(output), "instance": i.Name}).Debug("Error start quietly") return fmt.Errorf("error starting instance, error: %w", err) } @@ -351,7 +407,13 @@ func (i *Instance) Stop() error { args = append(args, "bypass") } args = append(args, "quietly") - if output, err := exec.Command(i.controlPath(), args...).CombinedOutput(); err != nil { + cmd := exec.Command(i.controlPath(), args...) + procAttr, err := i.managerSysProc() + if err != nil { + return err + } + cmd.SysProcAttr = procAttr + if output, err := cmd.CombinedOutput(); err != nil { ilog.WithError(err).WithFields(log.Fields{"output": string(output), "args": args}).Debug("Error stopping") return fmt.Errorf("error stopping instance, error: %w", err) } @@ -401,22 +463,12 @@ func (i *Instance) ExecuteAsUser(execUser string) error { return err } - u, err := user.Lookup(execUser) - if err != nil { - return err - } - - uid, err := strconv.ParseUint(u.Uid, 10, 32) + uid, gid, err := lookupUser(execUser) if err != nil { return err } - gid, err := strconv.ParseUint(u.Gid, 10, 32) - if err != nil { - return err - } - - log.WithFields(log.Fields{"user": execUser, "uid": u.Uid, "gid": u.Gid}).Debug("Configured to execute as alternate user") + log.WithFields(log.Fields{"user": execUser, "uid": uid, "gid": gid}).Debug("Configured to execute as alternate user") i.executionSysProcAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ Uid: uint32(uid), @@ -426,6 +478,23 @@ func (i *Instance) ExecuteAsUser(execUser string) error { return nil } +func lookupUser(execUser string) (uid, gid uint64, err error) { + var u *user.User + u, err = user.Lookup(execUser) + if err != nil { + return + } + + uid, err = strconv.ParseUint(u.Uid, 10, 32) + if err != nil { + return + } + + gid, err = strconv.ParseUint(u.Gid, 10, 32) + + return +} + // ImportSource will import the source specified using a glob pattern into Caché with the provided qualifiers. // sourcePathGlob only allows a subset of glob patterns. It must be in the format /p/a/t/h/**/*.xml // @@ -562,8 +631,7 @@ func (i *Instance) ExecuteString(namespace string, code string) (string, error) // ReadParametersISC will read the current instances parameters ISC file into a simple data structure. // It returns the ParametersISC data structure and any error encountered. func (i *Instance) ReadParametersISC() (ParametersISC, error) { - pfp := filepath.Join(i.Directory, iscParametersFile) - f, err := os.Open(pfp) + f, err := parameterReader(i.Directory, iscParametersFile) if err != nil { return nil, err } @@ -572,6 +640,15 @@ func (i *Instance) ReadParametersISC() (ParametersISC, error) { return LoadParametersISC(f) } +func fileParameterReader(directory string, file string) (io.ReadCloser, error) { + pfp := filepath.Join(directory, file) + f, err := os.Open(pfp) + if err != nil { + return nil, err + } + return f, nil +} + // WaitForReady waits indefinitely for an instance to be up and ready for use func (i *Instance) WaitForReady(ctx context.Context) error { return i.WaitForReadyWithInterval(ctx, 100*time.Millisecond) diff --git a/instance_test.go b/instance_test.go index 3c15a9b..ab5b780 100644 --- a/instance_test.go +++ b/instance_test.go @@ -17,11 +17,16 @@ limitations under the License. package isclib import ( + "bytes" "context" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + "fmt" + "io" + "os/user" "syscall" "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("Instance", func() { @@ -41,6 +46,24 @@ var _ = Describe("Instance", func() { origCSessionCommand string origIrisSessionCommand string ) + + BeforeEach(func() { + // make just enough of a parameters.isc to be able to find the manager user + parameterReader = func(directory string, file string) (io.ReadCloser, error) { + u, err := user.Current() + if err != nil { + return nil, err + } + g, err := user.LookupGroupId(u.Gid) + if err != nil { + return nil, err + } + + parametersContent := fmt.Sprintf("security_settings.manager_user: %s\nsecurity_settings.manager_group: %s", u.Username, g.Name) + return io.NopCloser(bytes.NewBufferString(parametersContent)), nil + } + }) + Describe("InstanceFromQList", func() { Context("Invalid qlist", func() { BeforeEach(func() { @@ -396,11 +419,11 @@ var _ = Describe("Instance", func() { Context("Does come up", func() { BeforeEach(func() { timeout = 500 * time.Millisecond - getQlist = func(instanceName string) (string, error) { + getQlist = func(instanceName string, _ *syscall.SysProcAttr) (string, error) { return legacyqlist, nil } time.AfterFunc(timeout/2, func() { - getQlist = func(instanceName string) (string, error) { + getQlist = func(instanceName string, _ *syscall.SysProcAttr) (string, error) { return durableqlist, nil } }) diff --git a/isclib.go b/isclib.go index bf3cee4..aba038d 100644 --- a/isclib.go +++ b/isclib.go @@ -170,7 +170,7 @@ func SetExecuteTemporaryDirectory(path string) { // LoadInstances returns a listing of all Caché/Ensemble instances on this system. // It returns the list of instances and any error encountered. func LoadInstances() (Instances, error) { - qs, err := qlist("") + qs, err := qlist("", nil) if err != nil { return nil, err } @@ -194,14 +194,15 @@ func LoadInstances() (Instances, error) { } // LoadInstance retrieves a single instance by name. -// The instance name is case insensitive. +// The instance name is case-insensitive. // It returns the instance and any error encountered. func LoadInstance(name string) (*Instance, error) { - q, err := qlist(name) - if err != nil { + i := &Instance{Name: name} + if err := i.Update(); err != nil { return nil, err } - return InstanceFromQList(q) + + return i, nil } // InstanceFromQList will parse the output of a qlist into an Instance struct. @@ -213,5 +214,13 @@ func InstanceFromQList(qlist string) (*Instance, error) { return nil, err } + // if the status is unknown, we may be running as a different user, + // do the full update (running qlist again as the correct user) + if i.Status == InstanceStatusUnknown { + if err := i.Update(); err != nil { + return nil, err + } + } + return i, nil } diff --git a/parametersisc.go b/parametersisc.go index bffb270..9b4dbad 100644 --- a/parametersisc.go +++ b/parametersisc.go @@ -93,9 +93,11 @@ func LoadParametersISC(r io.Reader) (ParametersISC, error) { // Values will, given a set of identifiers making up a parameter key, return the values at that key // Identifiers can be... -// the full key (group.name) -// the group, name as two separate parameters -// A single parameter representing the name of a parameter in the "" group +// +// - the full key (group.name) +// - the group, name as two separate parameters +// - A single parameter representing the name of a parameter in the "" group +// // It returns the values at that key or an empty slice if it does not exist func (pi ParametersISC) Values(identifiers ...string) []string { var group, name string @@ -136,9 +138,11 @@ func (pi ParametersISC) Values(identifiers ...string) []string { // Value will, given a set of identifiers making up a parameter key, return the single value at that key // Identifiers can be... -// the full key (group.name) -// the group, name as two separate parameters -// A single parameter representing the name of a parameter in the "" group +// +// - the full key (group.name) +// - the group, name as two separate parameters +// - A single parameter representing the name of a parameter in the "" group +// // It returns the value if a single value exists for the key or "" if it does not func (pi ParametersISC) Value(identifiers ...string) string { values := pi.Values(identifiers...) diff --git a/qlist.go b/qlist.go index 7c23f8b..e80bdb7 100644 --- a/qlist.go +++ b/qlist.go @@ -20,6 +20,7 @@ import ( "fmt" "os/exec" "strings" + "syscall" log "github.com/sirupsen/logrus" ) @@ -27,7 +28,8 @@ import ( // qlist returns the results of executing qlist for the specified instance. // If instanceName is "", it will return the results of an argumentless qlist (which contains all instances) // It returns a string containing the combined standard input and output of the qlist command and any error which occurred. -func qlist(instanceName string) (string, error) { +// If procAttr is not nil, it uses it to switch to run qlist as a different user +func qlist(instanceName string, procAttr *syscall.SysProcAttr) (string, error) { // Example qlist output... // DOCKER^/ensemble/instances/docker/^2015.2.2.805.0.16216^down, last used Fri May 13 18:12:33 2016^cache.cpf^56772^57772^62972^^ // DOCKER^/ensemble/instances/docker^2018.1.1.643.0^running, since Mon Jul 23 14:42:09 2018^iris.cpf^1972^57772^62972^ok^IRIS^^^/ensemble/instances/docker @@ -48,6 +50,7 @@ func qlist(instanceName string) (string, error) { return qlist, nil } + cmd.SysProcAttr = procAttr out, err := cmd.CombinedOutput() if err != nil { log.WithError(err).WithFields(log.Fields{"output": string(out), "command": cmd.Path, "args": cmd.Args}).Debug("Error running qlist")