Skip to content

Commit

Permalink
DAOS-1669 control: Create privileged binary lib (daos-stack#2786)
Browse files Browse the repository at this point in the history
Extracts the core logic from daos_admin into a library
that can be re-used to create other privileged DAOS helper
applications.

Signed-off-by: Kris Jacque <[email protected]>
  • Loading branch information
kjacque authored May 29, 2020
1 parent 89ff066 commit 3614881
Show file tree
Hide file tree
Showing 10 changed files with 1,525 additions and 429 deletions.
367 changes: 223 additions & 144 deletions src/control/cmd/daos_admin/handler.go

Large diffs are not rendered by default.

594 changes: 390 additions & 204 deletions src/control/cmd/daos_admin/handler_test.go

Large diffs are not rendered by default.

102 changes: 22 additions & 80 deletions src/control/cmd/daos_admin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,98 +24,40 @@
package main

import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/pkg/errors"

"github.com/daos-stack/daos/src/control/build"
"github.com/daos-stack/daos/src/control/common"
"github.com/daos-stack/daos/src/control/logging"
"github.com/daos-stack/daos/src/control/pbin"
"github.com/daos-stack/daos/src/control/server/storage/bdev"
"github.com/daos-stack/daos/src/control/server/storage/scm"
)

// exitWithError logs the error to stderr and exits.
func exitWithError(log logging.Logger, err error) {
if err == nil {
err = errors.New("Unknown error")
}
log.Error(err.Error())
os.Exit(1)
}

// sendFailureAndExit attempts to send the failure back
// to the parent and then exits.
func sendFailureAndExit(log logging.Logger, err error, dest io.Writer) {
res := &pbin.Response{}
sendErr := sendFailure(err, res, dest)
if sendErr != nil {
exitWithError(log, errors.Wrap(sendErr, fmt.Sprintf("failed to send %s", err)))
}
exitWithError(log, err)
}
func main() {
app := pbin.NewApp().
WithAllowedCallers("daos_server")

func configureLogging(binName string) logging.Logger {
logLevel := logging.LogLevelError
combinedOut := ioutil.Discard
if logPath, set := os.LookupEnv(pbin.DaosAdminLogFileEnvVar); set {
lf, err := common.AppendFile(logPath)
if err == nil {
combinedOut = lf
logLevel = logging.LogLevelDebug
}
app = app.WithLogFile(logPath)
}

// By default, we only want to log errors to stderr.
return logging.NewCombinedLogger(binName, combinedOut).
WithErrorLogger(logging.NewCommandLineErrorLogger(os.Stderr)).
WithLogLevel(logLevel)
}
addMethodHandlers(app)

func checkParentName(log logging.Logger) {
pPath, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", os.Getppid()))
err := app.Run()
if err != nil {
exitWithError(log, errors.Wrap(err, "failed to check parent process binary"))
}
daosServer := "daos_server"
if !strings.HasSuffix(pPath, daosServer) {
exitWithError(log, errors.Errorf("%s (version %s) may only be invoked by %s",
os.Args[0], build.DaosVersion, daosServer))
os.Exit(1)
}
}

func main() {
binName := filepath.Base(os.Args[0])
log := configureLogging(binName)

checkParentName(log)

// set up the r/w pipe from the parent process
conn := pbin.NewStdioConn(binName, "daos_server", os.Stdin, os.Stdout)

if os.Geteuid() != 0 {
sendFailureAndExit(log, pbin.PrivilegedHelperNotPrivileged(os.Args[0]), conn)
}

// hack for stuff that doesn't use geteuid() (e.g. ipmctl)
if err := setuid(0); err != nil {
sendFailureAndExit(log, errors.Wrap(err, "unable to setuid(0)"), conn)
}

req, err := readRequest(conn)
if err != nil {
exitWithError(log, err)
}

scmProvider := scm.DefaultProvider(log).WithForwardingDisabled()
bdevProvider := bdev.DefaultProvider(log).WithForwardingDisabled()
if err := handleRequest(log, scmProvider, bdevProvider, req, conn); err != nil {
exitWithError(log, err)
}
// addMethodHandlers adds all of daos_admin's supported handler functions.
func addMethodHandlers(app *pbin.App) {
app.AddHandler("Ping", &pingHandler{})

app.AddHandler("ScmMount", &scmMountUnmountHandler{})
app.AddHandler("ScmUnmount", &scmMountUnmountHandler{})
app.AddHandler("ScmFormat", &scmFormatCheckHandler{})
app.AddHandler("ScmCheckFormat", &scmFormatCheckHandler{})
app.AddHandler("ScmScan", &scmScanHandler{})
app.AddHandler("ScmPrepare", &scmPrepHandler{})

app.AddHandler("BdevInit", &bdevInitHandler{})
app.AddHandler("BdevScan", &bdevScanHandler{})
app.AddHandler("BdevPrepare", &bdevPrepHandler{})
app.AddHandler("BdevFormat", &bdevFormatHandler{})
}
245 changes: 245 additions & 0 deletions src/control/pbin/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
//
// (C) Copyright 2020 Intel Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// GOVERNMENT LICENSE RIGHTS-OPEN SOURCE SOFTWARE
// The Government's rights to use, modify, reproduce, release, perform, display,
// or disclose this software are subject to the terms of the Apache License as
// provided in Contract No. 8F-30005.
// Any reproduction of computer software, computer software documentation, or
// portions thereof marked with this legend must also reproduce the markings.
//

package pbin

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strings"

"github.com/pkg/errors"

"github.com/daos-stack/daos/src/control/build"
"github.com/daos-stack/daos/src/control/common"
"github.com/daos-stack/daos/src/control/logging"
)

// processProvider is an interface for interacting with the current process.
type processProvider interface {
CurrentProcessName() string
ParentProcessName() (string, error)
IsPrivileged() bool
ElevatePrivileges() error
}

// NewApp sets up a new privileged application.
func NewApp() *App {
app := &App{
process: &Process{},
input: os.Stdin,
output: os.Stdout,
handlers: make(map[string]RequestHandler),
}

app.configureLogging("")
return app
}

// App is a framework for a privileged external helper application to be invoked by one or
// more DAOS processes.
type App struct {
log logging.Logger
allowedCallers []string
process processProvider
input io.ReadCloser
output io.WriteCloser
handlers map[string]RequestHandler
}

// Name returns the name of this application.
func (a *App) Name() string {
return a.process.CurrentProcessName()
}

// WithLogFile sets up App logging to a file at a given path.
func (a *App) WithLogFile(path string) *App {
a.configureLogging(path)
return a
}

// configureLogging configures the application's logger. If logPath
// is empty, non-error messages are not logged.
func (a *App) configureLogging(logPath string) {
logLevel := logging.LogLevelError
combinedOut := ioutil.Discard
if logPath != "" {
lf, err := common.AppendFile(logPath)
if err == nil {
combinedOut = lf
logLevel = logging.LogLevelDebug
}
}

// By default, we only want to log errors to stderr.
a.log = logging.NewCombinedLogger(a.Name(), combinedOut).
WithErrorLogger(logging.NewCommandLineErrorLogger(os.Stderr)).
WithLogLevel(logLevel)
}

// WithAllowedCallers adds a list of process names allowed to call this
// application.
func (a *App) WithAllowedCallers(callers ...string) *App {
a.allowedCallers = callers
return a
}

// WithInput adds a custom input source to the App.
func (a *App) WithInput(reader io.ReadCloser) *App {
a.input = reader
return a
}

// WithOutput adds a custom output sink to the App.
func (a *App) WithOutput(writer io.WriteCloser) *App {
a.output = writer
return a
}

// AddHandler adds a new handler to the App for a given method.
// There is at most one handler per method.
func (a *App) AddHandler(method string, handler RequestHandler) {
a.handlers[method] = handler
}

// logError is a convenience method that logs an error and returns the same error.
func (a *App) logError(err error) error {
if a.log != nil && err != nil {
a.log.Error(err.Error())
}
return err
}

// Run executes the helper application process.
func (a *App) Run() error {
parentName, err := a.process.ParentProcessName()
if err != nil {
return a.logError(err)
}

if !a.isCallerPermitted(parentName) {
return a.logError(errors.Errorf("%s (version %s) may only be invoked by: %s",
a.Name(), build.DaosVersion, strings.Join(a.allowedCallers, ", ")))
}

// set up the r/w pipe from the parent process
conn := NewStdioConn(a.Name(), parentName, a.input, a.output)

if err = a.checkPrivileges(); err != nil {
resp := NewResponseWithError(a.logError(err))
sendErr := a.writeResponse(resp, conn)
if sendErr != nil {
return a.logError(sendErr)
}

return err
}

req, err := a.readRequest(conn)
if err != nil {
return a.logError(err)
}

resp := a.handleRequest(req)

err = a.writeResponse(resp, conn)
if err != nil {
return a.logError(err)
}

if resp.Error != nil {
return resp.Error
}
return nil
}

func (a *App) isCallerPermitted(callerName string) bool {
if len(a.allowedCallers) == 0 {
return true
}
for _, name := range a.allowedCallers {
if callerName == name {
return true
}
}
return false
}

func (a *App) checkPrivileges() error {
if !a.process.IsPrivileged() {
return PrivilegedHelperNotPrivileged(a.Name())
}

// hack for stuff that doesn't use geteuid() (e.g. ipmctl)
if err := a.process.ElevatePrivileges(); err != nil {
return err
}

return nil
}

func (a *App) readRequest(rdr io.Reader) (*Request, error) {
buf, err := ReadMessage(rdr)
if err != nil {
return nil, errors.Wrap(err, "failed to read request")
}

var req Request
if err := json.Unmarshal(buf, &req); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal request")
}

return &req, nil
}

func (a *App) handleRequest(req *Request) *Response {
reqHandler, ok := a.handlers[req.Method]
if !ok {
err := a.logError(errors.Errorf("unhandled method %q", req.Method))
return NewResponseWithError(err)
}

resp := reqHandler.Handle(a.log, req)
if resp == nil {
err := a.logError(errors.Errorf("handler for method %q returned nil", req.Method))
return NewResponseWithError(err)
}

if resp.Error != nil {
a.logError(resp.Error)
}
return resp
}

func (a *App) writeResponse(res *Response, dest io.Writer) error {
data, err := json.Marshal(res)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to marshal response %+v", res))
}

_, err = dest.Write(data)
return errors.Wrap(err, fmt.Sprintf("failed to send response %+v", res))
}
Loading

0 comments on commit 3614881

Please sign in to comment.