github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/apiserver/common"
    17  	"github.com/juju/juju/apiserver/facade"
    18  	"github.com/juju/juju/apiserver/params"
    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/mongo"
    23  	"github.com/juju/juju/network"
    24  	"github.com/juju/juju/permission"
    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  // NewHighAvailabilityAPI creates a new server-side highavailability API end point.
    46  func NewHighAvailabilityAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*HighAvailabilityAPI, error) {
    47  	// Only clients can access the high availability facade.
    48  	if !authorizer.AuthClient() {
    49  		return nil, common.ErrPerm
    50  	}
    51  	return &HighAvailabilityAPI{
    52  		state:      st,
    53  		resources:  resources,
    54  		authorizer: authorizer,
    55  	}, nil
    56  }
    57  
    58  // EnableHA adds controller machines as necessary to ensure the
    59  // controller has the number of machines specified.
    60  func (api *HighAvailabilityAPI) EnableHA(args params.ControllersSpecs) (params.ControllersChangeResults, error) {
    61  	results := params.ControllersChangeResults{}
    62  
    63  	admin, err := api.authorizer.HasPermission(permission.SuperuserAccess, api.state.ControllerTag())
    64  	if err != nil && !errors.IsNotFound(err) {
    65  		return results, errors.Trace(err)
    66  	}
    67  	if !admin {
    68  		return results, common.ServerError(common.ErrPerm)
    69  	}
    70  
    71  	if len(args.Specs) == 0 {
    72  		return results, nil
    73  	}
    74  	if len(args.Specs) > 1 {
    75  		return results, errors.New("only one controller spec is supported")
    76  	}
    77  
    78  	result, err := api.enableHASingle(api.state, args.Specs[0])
    79  	results.Results = make([]params.ControllersChangeResult, 1)
    80  	results.Results[0].Result = result
    81  	results.Results[0].Error = common.ServerError(err)
    82  	return results, nil
    83  }
    84  
    85  func (api *HighAvailabilityAPI) enableHASingle(st *state.State, spec params.ControllersSpec) (
    86  	params.ControllersChanges, error,
    87  ) {
    88  	if !st.IsController() {
    89  		return params.ControllersChanges{}, errors.New("unsupported with hosted models")
    90  	}
    91  	// Check if changes are allowed and the command may proceed.
    92  	blockChecker := common.NewBlockChecker(st)
    93  	if err := blockChecker.ChangeAllowed(); err != nil {
    94  		return params.ControllersChanges{}, errors.Trace(err)
    95  	}
    96  
    97  	cInfo, err := st.ControllerInfo()
    98  	if err != nil {
    99  		return params.ControllersChanges{}, err
   100  	}
   101  
   102  	// If there were no supplied constraints, use the original bootstrap
   103  	// constraints.
   104  	if constraints.IsEmpty(&spec.Constraints) || spec.Series == "" {
   105  		referenceMachine, err := getReferenceController(st, cInfo.MachineIds)
   106  		if err != nil {
   107  			return params.ControllersChanges{}, errors.Trace(err)
   108  		}
   109  		if constraints.IsEmpty(&spec.Constraints) {
   110  			cons, err := referenceMachine.Constraints()
   111  			if err != nil {
   112  				return params.ControllersChanges{}, errors.Trace(err)
   113  			}
   114  			spec.Constraints = cons
   115  		}
   116  		if spec.Series == "" {
   117  			spec.Series = referenceMachine.Series()
   118  		}
   119  	}
   120  
   121  	// Retrieve the controller configuration and merge any implied space
   122  	// constraints into the spec constraints.
   123  	cfg, err := st.ControllerConfig()
   124  	if err != nil {
   125  		return params.ControllersChanges{}, errors.Annotate(err, "retrieving controller config")
   126  	}
   127  	if err = validateCurrentControllers(st, cfg, cInfo.MachineIds); err != nil {
   128  		return params.ControllersChanges{}, errors.Trace(err)
   129  	}
   130  	spec.Constraints.Spaces = cfg.AsSpaceConstraints(spec.Constraints.Spaces)
   131  
   132  	if err = validatePlacementForSpaces(st, spec.Constraints.Spaces, spec.Placement); err != nil {
   133  		return params.ControllersChanges{}, errors.Trace(err)
   134  	}
   135  
   136  	// Might be nicer to pass the spec itself to this method.
   137  	changes, err := st.EnableHA(spec.NumControllers, spec.Constraints, spec.Series, spec.Placement)
   138  	if err != nil {
   139  		return params.ControllersChanges{}, err
   140  	}
   141  	return controllersChanges(changes), nil
   142  }
   143  
   144  // getReferenceController looks up the ideal controller to use as a reference for Constraints and Series
   145  func getReferenceController(st *state.State, machineIds []string) (*state.Machine, error) {
   146  	// Sort the controller IDs from low to high and take the first.
   147  	// This will typically give the initial bootstrap machine.
   148  	var controllerIds []int
   149  	for _, id := range machineIds {
   150  		idNum, err := strconv.Atoi(id)
   151  		if err != nil {
   152  			logger.Warningf("ignoring non numeric controller id %v", id)
   153  			continue
   154  		}
   155  		controllerIds = append(controllerIds, idNum)
   156  	}
   157  	if len(controllerIds) == 0 {
   158  		return nil, errors.Errorf("internal error; failed to find any controllers")
   159  	}
   160  	sort.Ints(controllerIds)
   161  	controllerId := controllerIds[0]
   162  
   163  	// Load the controller machine and get its constraints.
   164  	controller, err := st.Machine(strconv.Itoa(controllerId))
   165  	if err != nil {
   166  		return nil, errors.Annotatef(err, "reading controller id %v", controllerId)
   167  	}
   168  	return controller, nil
   169  }
   170  
   171  // validateCurrentControllers checks for a scenario where there is no HA space
   172  // in controller configuration and more than one machine-local address on any
   173  // of the controller machines. An error is returned if it is detected.
   174  // When HA space is set, there are other code paths that ensure controllers
   175  // have at least one address in the space.
   176  func validateCurrentControllers(st *state.State, cfg controller.Config, machineIds []string) error {
   177  	if cfg.JujuHASpace() != "" {
   178  		return nil
   179  	}
   180  
   181  	var badIds []string
   182  	for _, id := range machineIds {
   183  		controller, err := st.Machine(id)
   184  		if err != nil {
   185  			return errors.Annotatef(err, "reading controller id %v", id)
   186  		}
   187  		addresses := controller.Addresses()
   188  		if len(addresses) == 0 {
   189  			// machines without any address are essentially not started yet
   190  			continue
   191  		}
   192  		internal := network.SelectInternalAddresses(addresses, false)
   193  		if len(internal) != 1 {
   194  			badIds = append(badIds, id)
   195  		}
   196  	}
   197  	if len(badIds) > 0 {
   198  		return errors.Errorf(
   199  			"juju-ha-space is not set and a unique usable address was not found for machines: %s"+
   200  				"\nrun \"juju config juju-ha-space=<name>\" to set a space for Mongo peer communication",
   201  			strings.Join(badIds, ", "),
   202  		)
   203  	}
   204  	return nil
   205  }
   206  
   207  // validatePlacementForSpaces checks whether there are both space constraints
   208  // and machine placement directives.
   209  // If there are, checks are made to ensure that the machines specified have at
   210  // least one address in all of the spaces.
   211  func validatePlacementForSpaces(st *state.State, spaces *[]string, placement []string) error {
   212  	if spaces == nil || len(*spaces) == 0 || len(placement) == 0 {
   213  		return nil
   214  	}
   215  
   216  	for _, v := range placement {
   217  		p, err := instance.ParsePlacement(v)
   218  		if err != nil {
   219  			if err == instance.ErrPlacementScopeMissing {
   220  				// Where an unscoped placement is not parsed as a machine ID,
   221  				// such as for a MaaS node name, just allow it through.
   222  				// TODO (manadart 2018-03-27): Possible work at the provider
   223  				// level to accommodate placement and space constraints during
   224  				// instance pre-check may be entertained in the future.
   225  				continue
   226  			}
   227  			return errors.Annotate(err, "parsing placement")
   228  		}
   229  		if p.Directive == "" {
   230  			continue
   231  		}
   232  
   233  		m, err := st.Machine(p.Directive)
   234  		if err != nil {
   235  			if errors.IsNotFound(err) {
   236  				// Don't throw out of here when the machine does not exist.
   237  				// Validate others if required and leave it handled downstream.
   238  				continue
   239  			}
   240  			return errors.Annotate(err, "retrieving machine")
   241  		}
   242  
   243  		for _, space := range *spaces {
   244  			spaceName := network.SpaceName(space)
   245  			inSpace := false
   246  			for _, addr := range m.Addresses() {
   247  				if addr.SpaceName == spaceName {
   248  					inSpace = true
   249  					break
   250  				}
   251  			}
   252  			if !inSpace {
   253  				return fmt.Errorf("machine %q has no addresses in space %q", p.Directive, space)
   254  			}
   255  		}
   256  	}
   257  	return nil
   258  }
   259  
   260  // controllersChanges generates a new params instance from the state instance.
   261  func controllersChanges(change state.ControllersChanges) params.ControllersChanges {
   262  	return params.ControllersChanges{
   263  		Added:      machineIdsToTags(change.Added...),
   264  		Maintained: machineIdsToTags(change.Maintained...),
   265  		Removed:    machineIdsToTags(change.Removed...),
   266  		Promoted:   machineIdsToTags(change.Promoted...),
   267  		Demoted:    machineIdsToTags(change.Demoted...),
   268  		Converted:  machineIdsToTags(change.Converted...),
   269  	}
   270  }
   271  
   272  // machineIdsToTags returns a slice of machine tag strings created from the
   273  // input machine IDs.
   274  func machineIdsToTags(ids ...string) []string {
   275  	var result []string
   276  	for _, id := range ids {
   277  		result = append(result, names.NewMachineTag(id).String())
   278  	}
   279  	return result
   280  }
   281  
   282  // StopHAReplicationForUpgrade will prompt the HA cluster to enter upgrade
   283  // mongo mode.
   284  func (api *HighAvailabilityAPI) StopHAReplicationForUpgrade(args params.UpgradeMongoParams) (
   285  	params.MongoUpgradeResults, error,
   286  ) {
   287  	ha, err := api.state.SetUpgradeMongoMode(mongo.Version{
   288  		Major:         args.Target.Major,
   289  		Minor:         args.Target.Minor,
   290  		Patch:         args.Target.Patch,
   291  		StorageEngine: mongo.StorageEngine(args.Target.StorageEngine),
   292  	})
   293  	if err != nil {
   294  		return params.MongoUpgradeResults{}, errors.Annotate(err, "cannot stop HA for upgrade")
   295  	}
   296  	members := make([]params.HAMember, len(ha.Members))
   297  	for i, m := range ha.Members {
   298  		members[i] = params.HAMember{
   299  			Tag:           m.Tag,
   300  			PublicAddress: m.PublicAddress,
   301  			Series:        m.Series,
   302  		}
   303  	}
   304  	return params.MongoUpgradeResults{
   305  		Master: params.HAMember{
   306  			Tag:           ha.Master.Tag,
   307  			PublicAddress: ha.Master.PublicAddress,
   308  			Series:        ha.Master.Series,
   309  		},
   310  		Members:   members,
   311  		RsMembers: ha.RsMembers,
   312  	}, nil
   313  }
   314  
   315  // ResumeHAReplicationAfterUpgrade will add the upgraded members of HA
   316  // cluster to the upgraded master.
   317  func (api *HighAvailabilityAPI) ResumeHAReplicationAfterUpgrade(args params.ResumeReplicationParams) error {
   318  	return api.state.ResumeReplication(args.Members)
   319  }