github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/lxd/container.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "fmt" 8 "math" 9 "reflect" 10 "strings" 11 "time" 12 13 "github.com/canonical/lxd/shared/api" 14 "github.com/canonical/lxd/shared/units" 15 "github.com/juju/clock" 16 "github.com/juju/errors" 17 "github.com/juju/retry" 18 19 "github.com/juju/juju/core/arch" 20 "github.com/juju/juju/core/constraints" 21 "github.com/juju/juju/core/instance" 22 corenetwork "github.com/juju/juju/core/network" 23 "github.com/juju/juju/network" 24 ) 25 26 const ( 27 UserNamespacePrefix = "user." 28 UserDataKey = UserNamespacePrefix + "user-data" 29 NetworkConfigKey = UserNamespacePrefix + "network-config" 30 JujuModelKey = UserNamespacePrefix + "juju-model" 31 AutoStartKey = "boot.autostart" 32 ) 33 34 // ContainerSpec represents the data required to create a new container. 35 type ContainerSpec struct { 36 Architecture string 37 Name string 38 Image SourcedImage 39 Devices map[string]device 40 Config map[string]string 41 Profiles []string 42 InstanceType string 43 VirtType instance.VirtType 44 } 45 46 // ApplyConstraints applies the input constraints as valid LXD container 47 // configuration to the container spec. 48 // Note that we pass these through as supplied. If an instance type constraint 49 // has been specified along with specific cores/mem constraints, 50 // LXD behaviour is to override with the specific ones even when lower. 51 func (c *ContainerSpec) ApplyConstraints(serverVersion string, cons constraints.Value) { 52 if cons.HasInstanceType() { 53 c.InstanceType = *cons.InstanceType 54 } 55 if cons.HasCpuCores() { 56 c.Config["limits.cpu"] = fmt.Sprintf("%d", *cons.CpuCores) 57 } 58 if cons.HasMem() { 59 c.Config["limits.memory"] = fmt.Sprintf("%dMiB", *cons.Mem) 60 } 61 if cons.HasArch() { 62 c.Architecture = *cons.Arch 63 } 64 65 if cons.HasRootDisk() || cons.HasRootDiskSource() { 66 // If we have a root disk and no source, 67 // assume that it must come from the default pool. 68 rootDiskSource := "default" 69 if cons.HasRootDiskSource() { 70 rootDiskSource = *cons.RootDiskSource 71 } 72 73 if c.Devices == nil { 74 c.Devices = map[string]map[string]string{} 75 } 76 77 c.Devices["root"] = map[string]string{ 78 "type": "disk", 79 "pool": rootDiskSource, 80 "path": "/", 81 } 82 83 if cons.HasRootDisk() { 84 c.Devices["root"]["size"] = fmt.Sprintf("%dMiB", *cons.RootDisk) 85 } 86 } 87 88 if cons.HasVirtType() { 89 90 virtType, err := instance.ParseVirtType(*cons.VirtType) 91 if err != nil { 92 logger.Errorf("failed to parse virt-type constraint %q, ignoring err: %v", *cons.VirtType, err) 93 } else { 94 c.VirtType = virtType 95 } 96 } 97 } 98 99 // Container extends the upstream LXD container type. 100 type Container struct { 101 api.Instance 102 } 103 104 // Metadata returns the value from container config for the input key. 105 // Such values are stored with the "user" namespace prefix. 106 func (c *Container) Metadata(key string) string { 107 return c.Config[UserNamespacePrefix+key] 108 } 109 110 // Arch returns the architecture of the container. 111 func (c *Container) Arch() string { 112 return arch.NormaliseArch(c.Architecture) 113 } 114 115 // VirtType returns the virtualisation type of the container. 116 func (c *Container) VirtType() instance.VirtType { 117 return instance.VirtType(c.Type) 118 } 119 120 // CPUs returns the configured limit for number of container CPU cores. 121 // If unset, zero is returned. 122 func (c *Container) CPUs() uint { 123 var cores uint 124 if v := c.Config["limits.cpu"]; v != "" { 125 _, err := fmt.Sscanf(v, "%d", &cores) 126 if err != nil { 127 logger.Errorf("failed to parse %q into uint, ignoring err: %s", v, err) 128 } 129 } 130 return cores 131 } 132 133 // Mem returns the configured limit for container memory in MiB. 134 func (c *Container) Mem() uint { 135 v := c.Config["limits.memory"] 136 if v == "" { 137 return 0 138 } 139 140 bytes, err := units.ParseByteSizeString(v) 141 if err != nil { 142 logger.Errorf("failed to parse %q into bytes, ignoring err: %s", v, err) 143 return 0 144 } 145 146 const oneMiB = 1024 * 1024 147 mib := bytes / oneMiB 148 if mib > math.MaxUint32 { 149 logger.Errorf("byte string %q overflowed uint32, using max value", v) 150 return math.MaxUint32 151 } 152 153 return uint(mib) 154 } 155 156 // AddDisk modifies updates the container's devices map to represent a disk 157 // device described by the input arguments. 158 // If the device already exists, an error is returned. 159 func (c *Container) AddDisk(name, path, source, pool string, readOnly bool) error { 160 if _, ok := c.Devices[name]; ok { 161 return errors.Errorf("container %q already has a device %q", c.Name, name) 162 } 163 164 if c.Devices == nil { 165 c.Devices = map[string]device{} 166 } 167 c.Devices[name] = map[string]string{ 168 "path": path, 169 "source": source, 170 "type": "disk", 171 } 172 if pool != "" { 173 c.Devices[name]["pool"] = pool 174 } 175 if readOnly { 176 c.Devices[name]["readonly"] = "true" 177 } 178 return nil 179 } 180 181 // aliveStatuses is the list of status strings that indicate 182 // a container is "alive". 183 var aliveStatuses = []string{ 184 api.Ready.String(), 185 api.Starting.String(), 186 api.Started.String(), 187 api.Running.String(), 188 api.Stopping.String(), 189 api.Stopped.String(), 190 } 191 192 // AliveContainers returns the list of containers based on the input namespace 193 // prefixed that are in a status indicating they are "alive". 194 func (s *Server) AliveContainers(prefix string) ([]Container, error) { 195 c, err := s.FilterContainers(prefix, aliveStatuses...) 196 return c, errors.Trace(err) 197 } 198 199 // FilterContainers retrieves the list of containers from the server and filters 200 // them based on the input namespace prefix and any supplied statuses. 201 func (s *Server) FilterContainers(prefix string, statuses ...string) ([]Container, error) { 202 instances, err := s.GetInstances(api.InstanceTypeAny) 203 if err != nil { 204 return nil, errors.Trace(err) 205 } 206 207 var results []Container 208 for _, c := range instances { 209 if prefix != "" && !strings.HasPrefix(c.Name, prefix) { 210 continue 211 } 212 if len(statuses) > 0 && !containerHasStatus(c, statuses) { 213 continue 214 } 215 results = append(results, Container{c}) 216 } 217 return results, nil 218 } 219 220 // ContainerAddresses gets usable network addresses for the container 221 // identified by the input name. 222 func (s *Server) ContainerAddresses(name string) ([]corenetwork.ProviderAddress, error) { 223 state, _, err := s.GetInstanceState(name) 224 if err != nil { 225 return nil, errors.Trace(err) 226 } 227 228 networks := state.Network 229 if networks == nil { 230 return []corenetwork.ProviderAddress{}, nil 231 } 232 233 var results []corenetwork.ProviderAddress 234 for netName, net := range networks { 235 if netName == network.DefaultLXDBridge { 236 continue 237 } 238 for _, addr := range net.Addresses { 239 netAddr := corenetwork.NewMachineAddress(addr.Address).AsProviderAddress() 240 if netAddr.Scope == corenetwork.ScopeLinkLocal || netAddr.Scope == corenetwork.ScopeMachineLocal { 241 logger.Tracef("ignoring address %q for container %q", addr, name) 242 continue 243 } 244 results = append(results, netAddr) 245 } 246 } 247 return results, nil 248 } 249 250 // CreateContainerFromSpec creates a new container based on the input spec, 251 // and starts it immediately. 252 // If the container fails to be started, it is removed. 253 // Upon successful creation and start, the container is returned. 254 func (s *Server) CreateContainerFromSpec(spec ContainerSpec) (*Container, error) { 255 logger.Infof("starting new container %q (image %q)", spec.Name, spec.Image.Image.Filename) 256 logger.Debugf("new container has profiles %v", spec.Profiles) 257 258 ephemeral := false 259 req := api.InstancesPost{ 260 Name: spec.Name, 261 InstanceType: spec.InstanceType, 262 Type: instance.NormaliseVirtType(spec.VirtType), 263 InstancePut: api.InstancePut{ 264 Architecture: spec.Architecture, 265 Profiles: spec.Profiles, 266 Devices: spec.Devices, 267 Config: spec.Config, 268 Ephemeral: ephemeral, 269 }, 270 } 271 op, err := s.CreateInstanceFromImage(spec.Image.LXDServer, *spec.Image.Image, req) 272 if err != nil { 273 return s.handleAlreadyExistsError(err, spec, ephemeral) 274 } 275 276 if err := op.Wait(); err != nil { 277 return s.handleAlreadyExistsError(err, spec, ephemeral) 278 } 279 opInfo, err := op.GetTarget() 280 if err != nil { 281 return s.handleAlreadyExistsError(err, spec, ephemeral) 282 } 283 if opInfo.StatusCode != api.Success { 284 return nil, fmt.Errorf("container creation failed: %s", opInfo.Err) 285 } 286 287 logger.Debugf("created container %q, waiting for start...", spec.Name) 288 289 if err := s.StartContainer(spec.Name); err != nil { 290 if remErr := s.RemoveContainer(spec.Name); remErr != nil { 291 logger.Errorf("failed to remove container after unsuccessful start: %s", remErr.Error()) 292 } 293 return nil, errors.Trace(err) 294 } 295 296 container, _, err := s.GetInstance(spec.Name) 297 if err != nil { 298 return nil, errors.Trace(err) 299 } 300 return &Container{ 301 Instance: *container, 302 }, nil 303 } 304 305 func (s *Server) handleAlreadyExistsError(err error, spec ContainerSpec, ephemeral bool) (*Container, error) { 306 if IsLXDAlreadyExists(err) { 307 container, runningErr := s.waitForRunningContainer(spec, ephemeral) 308 if runningErr != nil { 309 // It's actually more helpful to display the original error 310 // message, but we'll also log out what the new error message 311 // was, when attempting to wait for it. 312 logger.Debugf("waiting for container to be running: %v", runningErr) 313 return nil, errors.Trace(err) 314 } 315 c := Container{*container} 316 return &c, nil 317 } 318 return nil, errors.Trace(err) 319 } 320 321 func (s *Server) waitForRunningContainer(spec ContainerSpec, ephemeral bool) (*api.Instance, error) { 322 var container *api.Instance 323 err := retry.Call(retry.CallArgs{ 324 Func: func() error { 325 var err error 326 container, _, err = s.GetInstance(spec.Name) 327 if err != nil { 328 return errors.Trace(err) 329 } 330 331 switch container.StatusCode { 332 case api.Running: 333 return nil 334 case api.Started, api.Starting, api.Success: 335 return errors.Errorf("waiting for container to be running") 336 default: 337 return errors.Errorf("waiting for container") 338 } 339 }, 340 Attempts: 60, 341 MaxDuration: time.Minute * 5, 342 Delay: time.Second * 10, 343 Clock: s.clock, 344 }) 345 if err != nil { 346 return nil, errors.Trace(err) 347 } 348 // Ensure that the container matches the spec we launched it with. 349 if matchesContainerSpec(container, spec, ephemeral) { 350 return container, nil 351 } 352 return nil, errors.Errorf("container %q does not match container spec", spec.Name) 353 } 354 355 func matchesContainerSpec(container *api.Instance, spec ContainerSpec, ephemeral bool) bool { 356 // If we don't match the spec from the container, then we're not 357 // sure what we've got here. Return the original error message. 358 return container.Architecture == spec.Architecture && 359 container.Ephemeral == ephemeral && 360 reflect.DeepEqual(container.Profiles, spec.Profiles) && 361 reflect.DeepEqual(container.Devices, spec.Devices) && 362 reflect.DeepEqual(container.Config, spec.Config) 363 } 364 365 // StartContainer starts the extant container identified by the input name. 366 func (s *Server) StartContainer(name string) error { 367 req := api.InstanceStatePut{ 368 Action: "start", 369 Timeout: -1, 370 Force: false, 371 Stateful: false, 372 } 373 op, err := s.UpdateInstanceState(name, req, "") 374 if err != nil { 375 return errors.Trace(err) 376 } 377 378 return errors.Trace(op.Wait()) 379 } 380 381 // Remove containers stops and deletes containers matching the input list of 382 // names. Any failed removals are indicated in the returned error. 383 func (s *Server) RemoveContainers(names []string) error { 384 if len(names) == 0 { 385 return nil 386 } 387 388 var failed []string 389 for _, name := range names { 390 if err := s.RemoveContainer(name); err != nil { 391 failed = append(failed, name) 392 logger.Errorf("removing container %q: %v", name, err) 393 } 394 } 395 if len(failed) != 0 { 396 return errors.Errorf("failed to remove containers: %s", strings.Join(failed, ", ")) 397 } 398 return nil 399 } 400 401 // Remove container first ensures that the container is stopped, 402 // then deletes it. 403 func (s *Server) RemoveContainer(name string) error { 404 state, eTag, err := s.GetInstanceState(name) 405 if err != nil { 406 return errors.Trace(err) 407 } 408 409 if state.StatusCode != api.Stopped { 410 req := api.InstanceStatePut{ 411 Action: "stop", 412 Timeout: -1, 413 Force: true, 414 Stateful: false, 415 } 416 op, err := s.UpdateInstanceState(name, req, eTag) 417 if err != nil { 418 return errors.Trace(err) 419 } 420 if err := op.Wait(); err != nil { 421 return errors.Trace(err) 422 } 423 } 424 425 // NOTE(achilleasa): the (apt) lxd version that ships with bionic 426 // does not automatically remove veth devices if attached to an OVS 427 // bridge. The operator must manually remove these devices from the 428 // bridge by running "ovs-vsctl --if-exists del-port X". This issue 429 // has been fixed in newer lxd versions. 430 431 // LXD has issues deleting containers, even if they've been stopped. The 432 // general advice passed back from the LXD team is to retry it again, to 433 // see if this helps clean up the containers. 434 // ZFS exacerbates this more for the LXD setup, but there is no way to 435 // know as the LXD client doesn't return typed errors. 436 retryArgs := retry.CallArgs{ 437 Clock: s.Clock(), 438 IsFatalError: func(err error) bool { 439 return errors.IsBadRequest(err) 440 }, 441 Func: func() error { 442 op, err := s.DeleteInstance(name) 443 if err != nil { 444 // sigh, LXD not found container - it's been deleted so, we 445 // just need to return nil. 446 if IsLXDNotFound(errors.Cause(err)) { 447 return nil 448 } 449 return errors.BadRequestf(err.Error()) 450 } 451 return errors.Trace(op.Wait()) 452 }, 453 Delay: 2 * time.Second, 454 Attempts: 3, 455 } 456 if err := retry.Call(retryArgs); err != nil { 457 return errors.Trace(errors.Cause(err)) 458 } 459 return nil 460 } 461 462 // WriteContainer writes the current representation of the input container to 463 // the server. 464 func (s *Server) WriteContainer(c *Container) error { 465 resp, err := s.UpdateInstance(c.Name, c.Writable(), "") 466 if err != nil { 467 return errors.Trace(err) 468 } 469 if err := resp.Wait(); err != nil { 470 return errors.Trace(err) 471 } 472 return nil 473 } 474 475 func (s *Server) Clock() clock.Clock { 476 if s.clock == nil { 477 return clock.WallClock 478 } 479 return s.clock 480 } 481 482 // containerHasStatus returns true if the input container has a status 483 // matching one from the input list. 484 func containerHasStatus(container api.Instance, statuses []string) bool { 485 for _, status := range statuses { 486 if container.StatusCode.String() == status { 487 return true 488 } 489 } 490 return false 491 }