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

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package debinterfaces
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  
    17  	"github.com/juju/juju/utils/scriptrunner"
    18  )
    19  
    20  var logger = loggo.GetLogger("juju.network.debinterfaces")
    21  
    22  // ActivationParams contains options to use when bridging interfaces
    23  type ActivationParams struct {
    24  	Clock clock.Clock
    25  	// map deviceName -> bridgeName
    26  	Devices          map[string]string
    27  	DryRun           bool
    28  	Filename         string
    29  	ReconfigureDelay int
    30  	Timeout          time.Duration
    31  }
    32  
    33  // ActivationResult captures the result of actively bridging the
    34  // interfaces using ifup/ifdown.
    35  type ActivationResult struct {
    36  	Stdout []byte
    37  	Stderr []byte
    38  	Code   int
    39  }
    40  
    41  func activationCmd(oldContent, newContent string, params *ActivationParams) string {
    42  	if params.ReconfigureDelay < 0 {
    43  		params.ReconfigureDelay = 0
    44  	}
    45  	deviceNames := make([]string, len(params.Devices))
    46  	i := 0
    47  	for k := range params.Devices {
    48  		deviceNames[i] = k
    49  		i++
    50  	}
    51  	sort.Strings(deviceNames)
    52  	backupFilename := fmt.Sprintf("%s.backup-%d", params.Filename, params.Clock.Now().Unix())
    53  	// The magic value of 25694 here causes the script to sleep for 30 seconds, simulating timeout
    54  	// The value of 25695 causes the script to fail.
    55  	return fmt.Sprintf(`
    56  #!/bin/bash
    57  
    58  set -eu
    59  
    60  : ${DRYRUN:=}
    61  
    62  if [ $DRYRUN ]; then
    63    if [ %[4]d == 25694 ]; then sleep 30; fi
    64    if [ %[4]d == 25695 ]; then echo "artificial failure" >&2; exit 1; fi
    65    if [ %[4]d == 25696 ]; then echo "a very very VERY long artificial failure that should cause the code to shorten it and direct user to logs" >&2; exit 1; fi
    66  fi
    67  
    68  write_backup() {
    69      cat << 'EOF' > "$1"
    70  %[5]s
    71  EOF
    72  }
    73  
    74  write_content() {
    75      cat << 'EOF' > "$1"
    76  %[6]s
    77  EOF
    78  }
    79  
    80  if [ -n %[2]q ]; then
    81      ${DRYRUN} write_backup %[2]q
    82  fi
    83  ${DRYRUN} write_content %[3]q
    84  ${DRYRUN} ifdown --interfaces=%[1]q %[7]s || ${DRYRUN} ifdown --interfaces=%[1]q %[7]s || (${DRYRUN} ifup --interfaces=%[1]q -a; exit 1)
    85  ${DRYRUN} sleep %[4]d
    86  ${DRYRUN} cp %[3]q %[1]q
    87  # we want to have full control over what happens next
    88  set +e
    89  ${DRYRUN} ifup --interfaces=%[1]q -a || ${DRYRUN} ifup --interfaces=%[1]q -a
    90  RESULT=$?
    91  if [ ${RESULT} != 0 ]; then
    92      echo "Bringing up bridged interfaces failed, see system logs and %[3]q" >&2
    93      ${DRYRUN} ifdown --interfaces=%[1]q %[7]s
    94      ${DRYRUN} cp %[2]q %[1]q
    95      ${DRYRUN} ifup --interfaces=%[1]q -a
    96      exit ${RESULT}
    97  fi
    98  `,
    99  		params.Filename,
   100  		backupFilename,
   101  		params.Filename+".new",
   102  		params.ReconfigureDelay,
   103  		oldContent,
   104  		newContent,
   105  		strings.Join(deviceNames, " "))[1:]
   106  }
   107  
   108  // BridgeAndActivate will parse a debian-styled interfaces(5) file,
   109  // change the stanza definitions of the requested devices to be
   110  // bridged, then reconfigure the network using the ifupdown package
   111  // for the new bridges.
   112  func BridgeAndActivate(params ActivationParams) (*ActivationResult, error) {
   113  	if len(params.Devices) == 0 {
   114  		return nil, errors.New("no devices specified")
   115  	}
   116  
   117  	stanzas, err := Parse(params.Filename)
   118  
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	origContent := FormatStanzas(FlattenStanzas(stanzas), 4)
   124  	bridgedStanzas := Bridge(stanzas, params.Devices)
   125  	bridgedContent := FormatStanzas(FlattenStanzas(bridgedStanzas), 4)
   126  
   127  	if origContent == bridgedContent {
   128  		return nil, nil // nothing to do; old == new.
   129  	}
   130  
   131  	cmd := activationCmd(origContent, bridgedContent, &params)
   132  
   133  	environ := os.Environ()
   134  	if params.DryRun {
   135  		environ = append(environ, "DRYRUN=echo")
   136  	}
   137  	result, err := scriptrunner.RunCommand(cmd, environ, params.Clock, params.Timeout)
   138  
   139  	activationResult := ActivationResult{
   140  		Stderr: result.Stderr,
   141  		Stdout: result.Stdout,
   142  		Code:   result.Code,
   143  	}
   144  
   145  	if err != nil {
   146  		return &activationResult, fmt.Errorf("bridge activation error: %w", err)
   147  	}
   148  
   149  	logger.Infof("bridge activation result=%v", result.Code)
   150  
   151  	if result.Code != 0 {
   152  		logger.Errorf("bridge activation stdout\n%s\n", result.Stdout)
   153  		logger.Errorf("bridge activation stderr\n%s\n", result.Stderr)
   154  		// We want to suppress long output from ifup, ifdown - it will be shown in status message!
   155  		if len(result.Stderr) < 40 {
   156  			return &activationResult, fmt.Errorf("bridge activation failed: %s", string(result.Stderr))
   157  		} else {
   158  			return &activationResult, errors.New("bridge activation failed, see logs for details")
   159  		}
   160  	}
   161  
   162  	logger.Tracef("bridge activation stdout\n%s\n", result.Stdout)
   163  	logger.Tracef("bridge activation stderr\n%s\n", result.Stderr)
   164  
   165  	return &activationResult, nil
   166  }