github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/wrench/wrench.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package wrench
     5  
     6  import (
     7  	"bufio"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/juju/loggo"
    15  
    16  	"github.com/juju/juju/core/paths"
    17  )
    18  
    19  var (
    20  	enabledMu sync.Mutex
    21  	enabled   = true
    22  
    23  	dataDir   = paths.DataDir(paths.CurrentOS())
    24  	wrenchDir = filepath.Join(dataDir, "wrench")
    25  	jujuUid   = os.Getuid()
    26  )
    27  
    28  var logger = loggo.GetLogger("juju.wrench")
    29  
    30  // IsActive returns true if a "wrench" of a certain category and
    31  // feature should be "dropped in the works".
    32  //
    33  // This function may be called at specific points in the Juju codebase
    34  // to introduce otherwise hard to induce failure modes for the
    35  // purposes of manual or CI testing. The "<juju_datadir>/wrench/"
    36  // directory will be checked for "wrench files" which this function
    37  // looks for.
    38  //
    39  // Wrench files are line-based, with each line indicating some
    40  // (mis-)feature to enable for a given part of the code. The should be
    41  // created on the host where the fault should be triggered.
    42  //
    43  // For example, /var/lib/juju/wrench/machine-agent could contain:
    44  //
    45  //	refuse-upgrade
    46  //	fail-api-server-start
    47  //
    48  // The caller need not worry about errors. Any errors that occur will
    49  // be logged and false will be returned.
    50  func IsActive(category, feature string) bool {
    51  	if !IsEnabled() {
    52  		return false
    53  	}
    54  	if !checkWrenchDir(wrenchDir) {
    55  		return false
    56  	}
    57  	fileName := filepath.Join(wrenchDir, category)
    58  	if !checkWrenchFile(category, feature, fileName) {
    59  		return false
    60  	}
    61  
    62  	wrenchFile, err := os.Open(fileName)
    63  	if err != nil {
    64  		logger.Errorf("unable to read wrench data for %s/%s (ignored): %v",
    65  			category, feature, err)
    66  		return false
    67  	}
    68  	defer wrenchFile.Close()
    69  	lines := bufio.NewScanner(wrenchFile)
    70  	for lines.Scan() {
    71  		line := strings.TrimSpace(lines.Text())
    72  		if line == feature {
    73  			logger.Tracef("wrench for %s/%s is active", category, feature)
    74  			return true
    75  		}
    76  	}
    77  	if err := lines.Err(); err != nil {
    78  		logger.Errorf("error while reading wrench data for %s/%s (ignored): %v",
    79  			category, feature, err)
    80  	}
    81  	return false
    82  }
    83  
    84  // SetEnabled turns the wrench feature on or off globally.
    85  //
    86  // If false is given, all future IsActive calls will unconditionally
    87  // return false. If true is given, all future IsActive calls will
    88  // return true for active wrenches.
    89  //
    90  // The previous value for the global wrench enable flag is returned.
    91  func SetEnabled(next bool) bool {
    92  	enabledMu.Lock()
    93  	defer enabledMu.Unlock()
    94  	previous := enabled
    95  	enabled = next
    96  	return previous
    97  }
    98  
    99  // IsEnabled returns true if the wrench feature is turned on globally.
   100  func IsEnabled() bool {
   101  	enabledMu.Lock()
   102  	defer enabledMu.Unlock()
   103  	return enabled
   104  }
   105  
   106  var stat = os.Stat // To support patching
   107  
   108  func checkWrenchDir(dirName string) bool {
   109  	dirinfo, err := stat(dirName)
   110  	if err != nil {
   111  		logger.Tracef("couldn't read wrench directory: %v", err)
   112  		return false
   113  	}
   114  	if !isOwnedByJujuUser(dirinfo) {
   115  		logger.Errorf("wrench directory has incorrect ownership - wrench "+
   116  			"functionality disabled (%s)", wrenchDir)
   117  		return false
   118  	}
   119  	return true
   120  }
   121  
   122  func checkWrenchFile(category, feature, fileName string) bool {
   123  	fileinfo, err := stat(fileName)
   124  	if err != nil {
   125  		logger.Tracef("no wrench data for %s/%s (ignored): %v",
   126  			category, feature, err)
   127  		return false
   128  	}
   129  	if !isOwnedByJujuUser(fileinfo) {
   130  		logger.Errorf("wrench file for %s/%s has incorrect ownership "+
   131  			"- ignoring %s", category, feature, fileName)
   132  		return false
   133  	}
   134  	// Windows is not fully POSIX compliant
   135  	if runtime.GOOS != "windows" {
   136  		if fileinfo.Mode()&0022 != 0 {
   137  			logger.Errorf("wrench file for %s/%s should only be writable by "+
   138  				"owner - ignoring %s", category, feature, fileName)
   139  			return false
   140  		}
   141  	}
   142  	return true
   143  }