github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/highavailability/highavailability.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package highavailability
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"github.com/juju/names/v5"
    15  
    16  	"github.com/juju/juju/apiserver/common"
    17  	apiservererrors "github.com/juju/juju/apiserver/errors"
    18  	"github.com/juju/juju/apiserver/facade"
    19  	"github.com/juju/juju/controller"
    20  	"github.com/juju/juju/core/constraints"
    21  	"github.com/juju/juju/core/instance"
    22  	"github.com/juju/juju/core/network"
    23  	"github.com/juju/juju/core/permission"
    24  	"github.com/juju/juju/rpc/params"
    25  	"github.com/juju/juju/state"
    26  )
    27  
    28  var logger = loggo.GetLogger("juju.apiserver.highavailability")
    29  
    30  // HighAvailability defines the methods on the highavailability API end point.
    31  type HighAvailability interface {
    32  	EnableHA(args params.ControllersSpecs) (params.ControllersChangeResults, error)
    33  }
    34  
    35  // HighAvailabilityAPI implements the HighAvailability interface and is the concrete
    36  // implementation of the api end point.
    37  type HighAvailabilityAPI struct {
    38  	state      *state.State
    39  	resources  facade.Resources
    40  	authorizer facade.Authorizer
    41  }
    42  
    43  var _ HighAvailability = (*HighAvailabilityAPI)(nil)
    44  
    45  // EnableHA adds controller machines as necessary to ensure the
    46  // controller has the number of machines specified.
    47  func (api *HighAvailabilityAPI) EnableHA(args params.ControllersSpecs) (params.ControllersChangeResults, error) {
    48  	results := params.ControllersChangeResults{}
    49  
    50  	err := api.authorizer.HasPermission(permission.SuperuserAccess, api.state.ControllerTag())
    51  	if err != nil {
    52  		return results, apiservererrors.ServerError(apiservererrors.ErrPerm)
    53  	}
    54  
    55  	if len(args.Specs) == 0 {
    56  		return results, nil
    57  	}
    58  	if len(args.Specs) > 1 {
    59  		return results, errors.New("only one controller spec is supported")
    60  	}
    61  
    62  	result, err := api.enableHASingle(api.state, args.Specs[0])
    63  	results.Results = make([]params.ControllersChangeResult, 1)
    64  	results.Results[0].Result = result
    65  	results.Results[0].Error = apiservererrors.ServerError(err)
    66  	return results, nil
    67  }
    68  
    69  func (api *HighAvailabilityAPI) enableHASingle(st *state.State, spec params.ControllersSpec) (
    70  	params.ControllersChanges, error,
    71  ) {
    72  	if !st.IsController() {
    73  		return params.ControllersChanges{}, errors.New("unsupported with workload models")
    74  	}
    75  	// Check if changes are allowed and the command may proceed.
    76  	blockChecker := common.NewBlockChecker(st)
    77  	if err := blockChecker.ChangeAllowed(); err != nil {
    78  		return params.ControllersChanges{}, errors.Trace(err)
    79  	}
    80  
    81  	controllerIds, err := st.ControllerIds()
    82  	if err != nil {
    83  		return params.ControllersChanges{}, err
    84  	}
    85  
    86  	referenceMachine, err := getReferenceController(st, controllerIds)
    87  	if err != nil {
    88  		return params.ControllersChanges{}, errors.Trace(err)
    89  	}
    90  	// If there were no supplied constraints, use the original bootstrap
    91  	// constraints.
    92  	if constraints.IsEmpty(&spec.Constraints) {
    93  		if constraints.IsEmpty(&spec.Constraints) {
    94  			cons, err := referenceMachine.Constraints()
    95  			if err != nil {
    96  				return params.ControllersChanges{}, errors.Trace(err)
    97  			}
    98  			spec.Constraints = cons
    99  		}
   100  	}
   101  
   102  	// Retrieve the controller configuration and merge any implied space
   103  	// constraints into the spec constraints.
   104  	cfg, err := st.ControllerConfig()
   105  	if err != nil {
   106  		return params.ControllersChanges{}, errors.Annotate(err, "retrieving controller config")
   107  	}
   108  	if err = validateCurrentControllers(st, cfg, controllerIds); err != nil {
   109  		return params.ControllersChanges{}, errors.Trace(err)
   110  	}
   111  	spec.Constraints.Spaces = cfg.AsSpaceConstraints(spec.Constraints.Spaces)
   112  
   113  	if err = validatePlacementForSpaces(st, spec.Constraints.Spaces, spec.Placement); err != nil {
   114  		return params.ControllersChanges{}, errors.Trace(err)
   115  	}
   116  
   117  	// Might be nicer to pass the spec itself to this method.
   118  	changes, err := st.EnableHA(spec.NumControllers, spec.Constraints, referenceMachine.Base(), spec.Placement)
   119  	if err != nil {
   120  		return params.ControllersChanges{}, err
   121  	}
   122  	return controllersChanges(changes), nil
   123  }
   124  
   125  // getReferenceController looks up the ideal controller to use as a reference for Constraints and Release
   126  func getReferenceController(st *state.State, controllerIds []string) (*state.Machine, error) {
   127  	// Sort the controller IDs from low to high and take the first.
   128  	// This will typically give the initial bootstrap machine.
   129  	var controllerNumbers []int
   130  	for _, id := range controllerIds {
   131  		idNum, err := strconv.Atoi(id)
   132  		if err != nil {
   133  			logger.Warningf("ignoring non numeric controller id %v", id)
   134  			continue
   135  		}
   136  		controllerNumbers = append(controllerNumbers, idNum)
   137  	}
   138  	if len(controllerNumbers) == 0 {
   139  		return nil, errors.Errorf("internal error; failed to find any controllers")
   140  	}
   141  	sort.Ints(controllerNumbers)
   142  	controllerId := controllerNumbers[0]
   143  
   144  	// Load the controller machine and get its constraints.
   145  	cm, err := st.Machine(strconv.Itoa(controllerId))
   146  	if err != nil {
   147  		return nil, errors.Annotatef(err, "reading controller id %v", controllerId)
   148  	}
   149  	return cm, nil
   150  }
   151  
   152  // validateCurrentControllers checks for a scenario where there is no HA space
   153  // in controller configuration and more than one machine-local address on any
   154  // of the controller machines. An error is returned if it is detected.
   155  // When HA space is set, there are other code paths that ensure controllers
   156  // have at least one address in the space.
   157  func validateCurrentControllers(st *state.State, cfg controller.Config, machineIds []string) error {
   158  	if cfg.JujuHASpace() != "" {
   159  		return nil
   160  	}
   161  
   162  	var badIds []string
   163  	for _, id := range machineIds {
   164  		cm, err := st.Machine(id)
   165  		if err != nil {
   166  			return errors.Annotatef(err, "reading controller id %v", id)
   167  		}
   168  		addresses := cm.Addresses()
   169  		if len(addresses) == 0 {
   170  			// machines without any address are essentially not started yet
   171  			continue
   172  		}
   173  		internal := addresses.AllMatchingScope(network.ScopeMatchCloudLocal)
   174  		if len(internal) != 1 {
   175  			badIds = append(badIds, id)
   176  		}
   177  	}
   178  	if len(badIds) > 0 {
   179  		return errors.Errorf(
   180  			"juju-ha-space is not set and a unique usable address was not found for machines: %s"+
   181  				"\nrun \"juju controller-config juju-ha-space=<name>\" to set a space for Mongo peer communication",
   182  			strings.Join(badIds, ", "),
   183  		)
   184  	}
   185  	return nil
   186  }
   187  
   188  // validatePlacementForSpaces checks whether there are both space constraints
   189  // and machine placement directives.
   190  // If there are, checks are made to ensure that the machines specified have at
   191  // least one address in all of the spaces.
   192  func validatePlacementForSpaces(st *state.State, spaceNames *[]string, placement []string) error {
   193  	if spaceNames == nil || len(*spaceNames) == 0 || len(placement) == 0 {
   194  		return nil
   195  	}
   196  
   197  	for _, v := range placement {
   198  		p, err := instance.ParsePlacement(v)
   199  		if err != nil {
   200  			if err == instance.ErrPlacementScopeMissing {
   201  				// Where an unscoped placement is not parsed as a machine ID,
   202  				// such as for a MaaS node name, just allow it through.
   203  				// TODO (manadart 2018-03-27): Possible work at the provider
   204  				// level to accommodate placement and space constraints during
   205  				// instance pre-check may be entertained in the future.
   206  				continue
   207  			}
   208  			return errors.Annotate(err, "parsing placement")
   209  		}
   210  		if p.Directive == "" {
   211  			continue
   212  		}
   213  
   214  		m, err := st.Machine(p.Directive)
   215  		if err != nil {
   216  			if errors.IsNotFound(err) {
   217  				// Don't throw out of here when the machine does not exist.
   218  				// Validate others if required and leave it handled downstream.
   219  				continue
   220  			}
   221  			return errors.Annotate(err, "retrieving machine")
   222  		}
   223  
   224  		spaceInfos, err := st.AllSpaceInfos()
   225  		if err != nil {
   226  			return errors.Trace(err)
   227  		}
   228  
   229  		for _, name := range *spaceNames {
   230  			spaceInfo := spaceInfos.GetByName(name)
   231  			if spaceInfo == nil {
   232  				return errors.NotFoundf("space with name %q", name)
   233  			}
   234  
   235  			inSpace := false
   236  			for _, addr := range m.Addresses() {
   237  				if addr.SpaceID == spaceInfo.ID {
   238  					inSpace = true
   239  					break
   240  				}
   241  			}
   242  			if !inSpace {
   243  				return fmt.Errorf("machine %q has no addresses in space %q", p.Directive, name)
   244  			}
   245  		}
   246  	}
   247  	return nil
   248  }
   249  
   250  // controllersChanges generates a new params instance from the state instance.
   251  func controllersChanges(change state.ControllersChanges) params.ControllersChanges {
   252  	return params.ControllersChanges{
   253  		Added:      machineIdsToTags(change.Added...),
   254  		Maintained: machineIdsToTags(change.Maintained...),
   255  		Removed:    machineIdsToTags(change.Removed...),
   256  		Converted:  machineIdsToTags(change.Converted...),
   257  	}
   258  }
   259  
   260  // machineIdsToTags returns a slice of machine tag strings created from the
   261  // input machine IDs.
   262  func machineIdsToTags(ids ...string) []string {
   263  	var result []string
   264  	for _, id := range ids {
   265  		result = append(result, names.NewMachineTag(id).String())
   266  	}
   267  	return result
   268  }