github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/prechecker_test.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state_test 5 6 import ( 7 "fmt" 8 9 "github.com/juju/errors" 10 "github.com/juju/names/v5" 11 jc "github.com/juju/testing/checkers" 12 gc "gopkg.in/check.v1" 13 14 "github.com/juju/juju/agent" 15 corebase "github.com/juju/juju/core/base" 16 "github.com/juju/juju/core/constraints" 17 "github.com/juju/juju/core/instance" 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/environs/context" 20 "github.com/juju/juju/state" 21 "github.com/juju/juju/storage" 22 ) 23 24 type PrecheckerSuite struct { 25 ConnSuite 26 prechecker mockPrechecker 27 } 28 29 var _ = gc.Suite(&PrecheckerSuite{}) 30 31 type mockPrechecker struct { 32 precheckInstanceError error 33 precheckInstanceArgs environs.PrecheckInstanceParams 34 } 35 36 func (p *mockPrechecker) PrecheckInstance(ctx context.ProviderCallContext, args environs.PrecheckInstanceParams) error { 37 p.precheckInstanceArgs = args 38 return p.precheckInstanceError 39 } 40 41 func (s *PrecheckerSuite) SetUpTest(c *gc.C) { 42 s.ConnSuite.SetUpTest(c) 43 s.prechecker = mockPrechecker{} 44 s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) { 45 return &s.prechecker, nil 46 } 47 } 48 49 func (s *PrecheckerSuite) TestPrecheckInstance(c *gc.C) { 50 // PrecheckInstance should be called with the specified 51 // series and no placement, and the specified constraints 52 // merged with the model constraints, when attempting 53 // to create an instance. 54 modelCons := constraints.MustParse("mem=4G") 55 placement := "" 56 template, err := s.addOneMachine(c, modelCons, placement) 57 c.Assert(err, jc.ErrorIsNil) 58 c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, template.Base.String()) 59 c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, placement) 60 validator := constraints.NewValidator() 61 cons, err := validator.Merge(modelCons, template.Constraints) 62 c.Assert(err, jc.ErrorIsNil) 63 c.Assert(s.prechecker.precheckInstanceArgs.Constraints, gc.DeepEquals, cons) 64 } 65 66 func (s *PrecheckerSuite) TestPrecheckInstanceWithPlacement(c *gc.C) { 67 // PrecheckInstance should be called with the specified 68 // series and placement. If placement is provided all 69 // model constraints should be ignored, otherwise they 70 // should be merged with provided constraints, when 71 // attempting to create an instance 72 modelCons := constraints.MustParse("mem=4G") 73 placement := "abc123" 74 template, err := s.addOneMachine(c, modelCons, placement) 75 c.Assert(err, jc.ErrorIsNil) 76 c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, template.Base.String()) 77 c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, placement) 78 c.Assert(s.prechecker.precheckInstanceArgs.Constraints, gc.DeepEquals, template.Constraints) 79 } 80 81 func (s *PrecheckerSuite) TestPrecheckErrors(c *gc.C) { 82 // Ensure that AddOneMachine fails when PrecheckInstance returns an error. 83 s.prechecker.precheckInstanceError = fmt.Errorf("no instance for you") 84 _, err := s.addOneMachine(c, constraints.Value{}, "placement") 85 c.Assert(err, gc.ErrorMatches, ".*no instance for you") 86 87 // If the policy's Prechecker method fails, that will be returned first. 88 s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) { 89 return nil, fmt.Errorf("no prechecker for you") 90 } 91 _, err = s.addOneMachine(c, constraints.Value{}, "placement") 92 c.Assert(err, gc.ErrorMatches, ".*no prechecker for you") 93 } 94 95 func (s *PrecheckerSuite) TestPrecheckPrecheckerUnimplemented(c *gc.C) { 96 var precheckerErr error 97 s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) { 98 return nil, precheckerErr 99 } 100 _, err := s.addOneMachine(c, constraints.Value{}, "placement") 101 c.Assert(err, gc.ErrorMatches, "cannot add a new machine: policy returned nil prechecker without an error") 102 precheckerErr = errors.NotImplementedf("Prechecker") 103 _, err = s.addOneMachine(c, constraints.Value{}, "placement") 104 c.Assert(err, jc.ErrorIsNil) 105 } 106 107 func (s *PrecheckerSuite) TestPrecheckNoPolicy(c *gc.C) { 108 s.policy.GetPrechecker = func() (environs.InstancePrechecker, error) { 109 c.Errorf("should not have been invoked") 110 return nil, nil 111 } 112 state.SetPolicy(s.State, nil) 113 _, err := s.addOneMachine(c, constraints.Value{}, "placement") 114 c.Assert(err, jc.ErrorIsNil) 115 } 116 117 func (s *PrecheckerSuite) addOneMachine(c *gc.C, modelCons constraints.Value, placement string) (state.MachineTemplate, error) { 118 _, template, err := s.addMachine(c, modelCons, placement) 119 return template, err 120 } 121 122 func (s *PrecheckerSuite) addMachine(c *gc.C, modelCons constraints.Value, placement string) (*state.Machine, state.MachineTemplate, error) { 123 err := s.State.SetModelConstraints(modelCons) 124 c.Assert(err, jc.ErrorIsNil) 125 oneJob := []state.MachineJob{state.JobHostUnits} 126 extraCons := constraints.MustParse("cores=4") 127 template := state.MachineTemplate{ 128 Base: state.UbuntuBase("20.04"), 129 Constraints: extraCons, 130 Jobs: oneJob, 131 Placement: placement, 132 } 133 machine, err := s.State.AddOneMachine(template) 134 return machine, template, err 135 } 136 137 func (s *PrecheckerSuite) TestPrecheckInstanceInjectMachine(c *gc.C) { 138 template := state.MachineTemplate{ 139 InstanceId: instance.Id("bootstrap"), 140 Base: state.UbuntuBase("22.04"), 141 Nonce: agent.BootstrapNonce, 142 Jobs: []state.MachineJob{state.JobManageModel}, 143 Placement: "anyoldthing", 144 } 145 _, err := s.State.AddOneMachine(template) 146 c.Assert(err, jc.ErrorIsNil) 147 // PrecheckInstance should not have been called, as we've 148 // injected a machine with an existing instance. 149 c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, "") 150 c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, "") 151 } 152 153 func (s *PrecheckerSuite) TestPrecheckContainerNewMachine(c *gc.C) { 154 // Attempting to add a container to a new machine should cause 155 // PrecheckInstance to be called. 156 template := state.MachineTemplate{ 157 Base: state.UbuntuBase("22.04"), 158 Jobs: []state.MachineJob{state.JobHostUnits}, 159 Placement: "intertubes", 160 } 161 _, err := s.State.AddMachineInsideNewMachine(template, template, instance.LXD) 162 c.Assert(err, jc.ErrorIsNil) 163 c.Assert(s.prechecker.precheckInstanceArgs.Base.String(), gc.Equals, template.Base.String()) 164 c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, template.Placement) 165 } 166 167 func (s *PrecheckerSuite) TestPrecheckAddApplication(c *gc.C) { 168 // Deploy an application for the purpose of creating a 169 // storage instance. We'll then destroy the unit and detach 170 // the storage, so that it can be attached to a new 171 // application unit. 172 ch := s.AddTestingCharm(c, "storage-block") 173 app, err := s.State.AddApplication(state.AddApplicationArgs{ 174 Name: "storage-block", 175 Charm: ch, 176 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 177 OS: "ubuntu", 178 Channel: "20.04/stable", 179 }}, 180 NumUnits: 1, 181 Storage: map[string]state.StorageConstraints{ 182 "data": {Count: 1, Pool: "modelscoped"}, 183 "allecto": {Count: 1, Pool: "modelscoped"}, 184 }, 185 }) 186 c.Assert(err, jc.ErrorIsNil) 187 188 unit, err := app.AddUnit(state.AddUnitParams{}) 189 c.Assert(err, jc.ErrorIsNil) 190 err = unit.AssignToNewMachine() 191 c.Assert(err, jc.ErrorIsNil) 192 machineId, err := unit.AssignedMachineId() 193 c.Assert(err, jc.ErrorIsNil) 194 machineTag := names.NewMachineTag(machineId) 195 196 sb, err := state.NewStorageBackend(s.State) 197 c.Assert(err, jc.ErrorIsNil) 198 storageAttachments, err := sb.UnitStorageAttachments(unit.UnitTag()) 199 c.Assert(err, jc.ErrorIsNil) 200 c.Assert(storageAttachments, gc.HasLen, 2) 201 storageTags := []names.StorageTag{ 202 storageAttachments[0].StorageInstance(), 203 storageAttachments[1].StorageInstance(), 204 } 205 206 volumeTags := make([]names.VolumeTag, len(storageTags)) 207 for i, storageTag := range storageTags { 208 volume, err := sb.StorageInstanceVolume(storageTag) 209 c.Assert(err, jc.ErrorIsNil) 210 volumeTags[i] = volume.VolumeTag() 211 } 212 // Provision only the first volume. 213 err = sb.SetVolumeInfo(volumeTags[0], state.VolumeInfo{ 214 VolumeId: "foo", 215 Pool: "modelscoped", 216 }) 217 c.Assert(err, jc.ErrorIsNil) 218 219 err = unit.Destroy() 220 c.Assert(err, jc.ErrorIsNil) 221 for _, storageTag := range storageTags { 222 err = sb.DetachStorage(storageTag, unit.UnitTag(), false, dontWait) 223 c.Assert(err, jc.ErrorIsNil) 224 } 225 for _, volumeTag := range volumeTags { 226 err = sb.DetachVolume(machineTag, volumeTag, false) 227 c.Assert(err, jc.ErrorIsNil) 228 err = sb.RemoveVolumeAttachment(machineTag, volumeTag, false) 229 c.Assert(err, jc.ErrorIsNil) 230 } 231 232 _, err = s.State.AddApplication(state.AddApplicationArgs{ 233 Name: "storage-block-the-second", 234 Charm: ch, 235 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 236 OS: "ubuntu", 237 Channel: "20.04/stable", 238 }}, 239 NumUnits: 1, 240 Placement: []*instance.Placement{{ 241 Scope: s.State.ModelUUID(), 242 Directive: "whatever", 243 }}, 244 AttachStorage: storageTags, 245 }) 246 c.Assert(err, jc.ErrorIsNil) 247 248 // The volume corresponding to the provisioned storage volume (only) 249 // should be presented to PrecheckInstance. The unprovisioned volume 250 // will be provisioned later by the storage provisioner. 251 c.Assert(s.prechecker.precheckInstanceArgs.Placement, gc.Equals, "whatever") 252 c.Assert(s.prechecker.precheckInstanceArgs.VolumeAttachments, jc.DeepEquals, []storage.VolumeAttachmentParams{{ 253 AttachmentParams: storage.AttachmentParams{ 254 Provider: "modelscoped", 255 }, 256 Volume: volumeTags[0], 257 VolumeId: "foo", 258 }}) 259 } 260 261 func (s *PrecheckerSuite) TestPrecheckAddApplicationNoPlacement(c *gc.C) { 262 s.prechecker.precheckInstanceError = errors.Errorf("failed for some reason") 263 ch := s.AddTestingCharm(c, "wordpress") 264 _, err := s.State.AddApplication(state.AddApplicationArgs{ 265 Name: "wordpress", 266 Charm: ch, 267 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 268 OS: "ubuntu", 269 Channel: "12.10/stable", 270 }}, 271 NumUnits: 1, 272 Constraints: constraints.MustParse("root-disk=20G"), 273 }) 274 c.Assert(err, gc.ErrorMatches, `cannot add application "wordpress": failed for some reason`) 275 c.Assert(s.prechecker.precheckInstanceArgs, jc.DeepEquals, environs.PrecheckInstanceParams{ 276 Base: corebase.MakeDefaultBase("ubuntu", "12.10"), 277 Constraints: constraints.MustParse("arch=amd64 root-disk=20G"), 278 }) 279 } 280 281 func (s *PrecheckerSuite) TestPrecheckAddApplicationAllMachinePlacement(c *gc.C) { 282 m1, _, err := s.addMachine(c, constraints.MustParse(""), "") 283 c.Assert(err, jc.ErrorIsNil) 284 m2, _, err := s.addMachine(c, constraints.MustParse(""), "") 285 c.Assert(err, jc.ErrorIsNil) 286 287 // Make sure the prechecker isn't called. 288 s.prechecker.precheckInstanceError = errors.Errorf("boom!") 289 290 ch := s.AddTestingCharm(c, "wordpress") 291 _, err = s.State.AddApplication(state.AddApplicationArgs{ 292 Name: "wordpress", 293 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 294 OS: "ubuntu", 295 Channel: "20.04/stable", 296 }}, 297 Charm: ch, 298 NumUnits: 2, 299 Placement: []*instance.Placement{ 300 instance.MustParsePlacement(m1.Id()), 301 instance.MustParsePlacement(m2.Id()), 302 }, 303 }) 304 c.Assert(err, jc.ErrorIsNil) 305 } 306 307 func (s *PrecheckerSuite) TestPrecheckAddApplicationMixedPlacement(c *gc.C) { 308 m1, _, err := s.addMachine(c, constraints.MustParse(""), "") 309 c.Assert(err, jc.ErrorIsNil) 310 311 // Make sure the prechecker still gets called if there's a machine 312 // placement and a directive that needs to be passed to the 313 // provider. 314 315 s.prechecker.precheckInstanceError = errors.Errorf("hey now") 316 ch := s.AddTestingCharm(c, "wordpress") 317 _, err = s.State.AddApplication(state.AddApplicationArgs{ 318 Name: "wordpress", 319 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 320 OS: "ubuntu", 321 Channel: "20.04/stable", 322 }}, 323 Charm: ch, 324 NumUnits: 2, 325 Placement: []*instance.Placement{ 326 {Scope: instance.MachineScope, Directive: m1.Id()}, 327 {Scope: s.State.ModelUUID(), Directive: "somewhere"}, 328 }, 329 }) 330 c.Assert(err, gc.ErrorMatches, `cannot add application "wordpress": hey now`) 331 c.Assert(s.prechecker.precheckInstanceArgs, jc.DeepEquals, environs.PrecheckInstanceParams{ 332 Base: corebase.MakeDefaultBase("ubuntu", "20.04"), 333 Placement: "somewhere", 334 Constraints: constraints.MustParse("arch=amd64"), 335 }) 336 }