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 }