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 }