github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/container/kvm/kvm.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package kvm 5 6 import ( 7 "fmt" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 12 "github.com/juju/errors" 13 "github.com/juju/loggo" 14 "github.com/juju/utils/arch" 15 16 "github.com/juju/juju/agent" 17 "github.com/juju/juju/cloudconfig/cloudinit" 18 "github.com/juju/juju/cloudconfig/containerinit" 19 "github.com/juju/juju/cloudconfig/instancecfg" 20 "github.com/juju/juju/container" 21 "github.com/juju/juju/core/constraints" 22 "github.com/juju/juju/core/instance" 23 "github.com/juju/juju/core/status" 24 "github.com/juju/juju/environs" 25 "github.com/juju/juju/environs/config" 26 "github.com/juju/juju/environs/imagemetadata" 27 "github.com/juju/juju/environs/instances" 28 ) 29 30 var ( 31 logger = loggo.GetLogger("juju.container.kvm") 32 33 // KvmObjectFactory implements the container factory interface for kvm 34 // containers. 35 KvmObjectFactory ContainerFactory = &containerFactory{} 36 37 // In order for Juju to be able to create the hardware characteristics of 38 // the kvm machines it creates, we need to be explicit in our definition 39 // of memory, cores and root-disk. The defaults here have been 40 // extracted from the uvt-kvm executable. 41 42 // DefaultMemory is the default RAM to use in a container. 43 DefaultMemory uint64 = 512 // MB 44 // DefaultCpu is the default number of CPUs to use in a container. 45 DefaultCpu uint64 = 1 46 // DefaultDisk is the default root disk size. 47 DefaultDisk uint64 = 8 // GB 48 49 // There are some values where it doesn't make sense to go below. 50 51 // MinMemory is the minimum RAM we will launch with. 52 MinMemory uint64 = 512 // MB 53 // MinCpu is the minimum number of CPUs to launch with. 54 MinCpu uint64 = 1 55 // MinDisk is the minimum root disk size we will launch with. 56 MinDisk uint64 = 2 // GB 57 ) 58 59 // Utilized to provide a hard-coded path to kvm-ok 60 var kvmPath = "/usr/sbin" 61 62 // IsKVMSupported calls into the kvm-ok executable from the cpu-checkers package. 63 // It is a variable to allow us to override behaviour in the tests. 64 var IsKVMSupported = func() (bool, error) { 65 66 // Prefer the user's $PATH first, but check /usr/sbin if we can't 67 // find kvm-ok there 68 var foundPath string 69 const binName = "kvm-ok" 70 if path, err := exec.LookPath(binName); err == nil { 71 foundPath = path 72 } else if path, err := exec.LookPath(filepath.Join(kvmPath, binName)); err == nil { 73 foundPath = path 74 } else { 75 return false, errors.NotFoundf("%s executable", binName) 76 } 77 78 command := exec.Command(foundPath) 79 output, err := command.CombinedOutput() 80 81 if err != nil { 82 return false, errors.Annotate(err, string(output)) 83 } 84 logger.Debugf("%s output:\n%s", binName, output) 85 return command.ProcessState.Success(), nil 86 } 87 88 // NewContainerManager returns a manager object that can start and stop kvm 89 // containers. 90 func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) { 91 modelUUID := conf.PopValue(container.ConfigModelUUID) 92 if modelUUID == "" { 93 return nil, errors.Errorf("model UUID is required") 94 } 95 namespace, err := instance.NewNamespace(modelUUID) 96 if err != nil { 97 return nil, errors.Trace(err) 98 } 99 logDir := conf.PopValue(container.ConfigLogDir) 100 if logDir == "" { 101 logDir = agent.DefaultPaths.LogDir 102 } 103 104 availabilityZone := conf.PopValue(container.ConfigAvailabilityZone) 105 if availabilityZone == "" { 106 logger.Infof("Availability zone will be empty for this container manager") 107 } 108 109 imageMetaDataURL := conf.PopValue(config.ContainerImageMetadataURLKey) 110 imageStream := conf.PopValue(config.ContainerImageStreamKey) 111 112 conf.WarnAboutUnused() 113 return &containerManager{ 114 namespace: namespace, 115 logdir: logDir, 116 availabilityZone: availabilityZone, 117 imageMetadataURL: imageMetaDataURL, 118 imageStream: imageStream, 119 }, nil 120 } 121 122 // containerManager handles all of the business logic at the juju specific 123 // level. It makes sure that the necessary directories are in place, that the 124 // user-data is written out in the right place, and that OS images are sourced 125 // from the correct location. 126 type containerManager struct { 127 namespace instance.Namespace 128 logdir string 129 availabilityZone string 130 imageMetadataURL string 131 imageStream string 132 } 133 134 var _ container.Manager = (*containerManager)(nil) 135 136 // Namespace implements container.Manager. 137 func (manager *containerManager) Namespace() instance.Namespace { 138 return manager.namespace 139 } 140 141 // Exposed so tests can observe our side-effects 142 var startParams StartParams 143 144 func (manager *containerManager) CreateContainer( 145 instanceConfig *instancecfg.InstanceConfig, 146 cons constraints.Value, 147 series string, 148 networkConfig *container.NetworkConfig, 149 storageConfig *container.StorageConfig, 150 callback environs.StatusCallbackFunc, 151 ) (_ instances.Instance, hc *instance.HardwareCharacteristics, err error) { 152 153 name, err := manager.namespace.Hostname(instanceConfig.MachineId) 154 if err != nil { 155 return nil, nil, errors.Trace(err) 156 } 157 158 defer func() { 159 if err != nil { 160 callback(status.ProvisioningError, fmt.Sprintf("Creating container: %v", err), nil) 161 } 162 }() 163 164 // Set the MachineContainerHostname to match the name returned by virsh list 165 instanceConfig.MachineContainerHostname = name 166 167 // Note here that the kvmObjectFactory only returns a valid container 168 // object, and doesn't actually construct the underlying kvm container on 169 // disk. 170 kvmContainer := KvmObjectFactory.New(name) 171 172 hc = &instance.HardwareCharacteristics{AvailabilityZone: &manager.availabilityZone} 173 174 // Create the cloud-init. 175 directory, err := container.NewDirectory(name) 176 if err != nil { 177 return nil, nil, errors.Annotate(err, "failed to create container directory") 178 } 179 logger.Tracef("write cloud-init") 180 userDataFilename, err := containerinit.WriteUserData(instanceConfig, networkConfig, directory) 181 if err != nil { 182 logger.Infof("machine config api %#v", *instanceConfig.APIInfo) 183 err = errors.Annotate(err, "failed to write user data") 184 logger.Infof(err.Error()) 185 return nil, nil, err 186 } 187 // Create the container. 188 startParams = ParseConstraintsToStartParams(cons) 189 startParams.Arch = arch.HostArch() 190 startParams.Series = series 191 startParams.Network = networkConfig 192 startParams.UserDataFile = userDataFilename 193 startParams.NetworkConfigData = cloudinit.CloudInitNetworkConfigDisabled 194 startParams.StatusCallback = callback 195 startParams.Stream = manager.imageStream 196 197 // Check whether a container image metadata URL was configured. 198 // Default to Ubuntu cloud images if configured stream is not "released". 199 imURL := manager.imageMetadataURL 200 if manager.imageMetadataURL == "" && manager.imageStream != imagemetadata.ReleasedStream { 201 imURL = imagemetadata.UbuntuCloudImagesURL 202 imURL, err = imagemetadata.ImageMetadataURL(imURL, manager.imageStream) 203 if err != nil { 204 return nil, nil, errors.Annotate(err, "generating image metadata source") 205 } 206 } 207 startParams.ImageDownloadURL = imURL 208 209 var hardware instance.HardwareCharacteristics 210 hardware, err = instance.ParseHardware( 211 fmt.Sprintf("arch=%s mem=%vM root-disk=%vG cores=%v", 212 startParams.Arch, startParams.Memory, startParams.RootDisk, startParams.CpuCores)) 213 if err != nil { 214 return nil, nil, errors.Annotate(err, "failed to parse hardware") 215 } 216 217 callback(status.Provisioning, "Creating container; it might take some time", nil) 218 logger.Tracef("create the container, constraints: %v", cons) 219 if err := kvmContainer.Start(startParams); err != nil { 220 err = errors.Annotate(err, "kvm container creation failed") 221 return nil, nil, err 222 } 223 logger.Tracef("kvm container created") 224 callback(status.Running, "Container started", nil) 225 return &kvmInstance{kvmContainer, name}, &hardware, nil 226 } 227 228 func (manager *containerManager) IsInitialized() bool { 229 requiredBinaries := []string{ 230 "virsh", 231 "qemu-utils", 232 } 233 for _, bin := range requiredBinaries { 234 if _, err := exec.LookPath(bin); err != nil { 235 return false 236 } 237 } 238 return true 239 } 240 241 func (manager *containerManager) DestroyContainer(id instance.Id) error { 242 name := string(id) 243 kvmContainer := KvmObjectFactory.New(name) 244 if err := kvmContainer.Stop(); err != nil { 245 logger.Errorf("failed to stop kvm container: %v", err) 246 return err 247 } 248 return container.RemoveDirectory(name) 249 } 250 251 func (manager *containerManager) ListContainers() (result []instances.Instance, err error) { 252 containers, err := KvmObjectFactory.List() 253 if err != nil { 254 logger.Errorf("failed getting all instances: %v", err) 255 return 256 } 257 managerPrefix := manager.namespace.Prefix() 258 for _, container := range containers { 259 // Filter out those not starting with our name. 260 name := container.Name() 261 if !strings.HasPrefix(name, managerPrefix) { 262 continue 263 } 264 if container.IsRunning() { 265 result = append(result, &kvmInstance{container, name}) 266 } 267 } 268 return 269 } 270 271 // ParseConstraintsToStartParams takes a constraints object and returns a bare 272 // StartParams object that has Memory, Cpu, and Disk populated. If there are 273 // no defined values in the constraints for those fields, default values are 274 // used. Other constrains cause a warning to be emitted. 275 func ParseConstraintsToStartParams(cons constraints.Value) StartParams { 276 params := StartParams{ 277 Memory: DefaultMemory, 278 CpuCores: DefaultCpu, 279 RootDisk: DefaultDisk, 280 } 281 282 if cons.Mem != nil { 283 mem := *cons.Mem 284 if mem < MinMemory { 285 params.Memory = MinMemory 286 } else { 287 params.Memory = mem 288 } 289 } 290 if cons.CpuCores != nil { 291 cores := *cons.CpuCores 292 if cores < MinCpu { 293 params.CpuCores = MinCpu 294 } else { 295 params.CpuCores = cores 296 } 297 } 298 if cons.RootDisk != nil { 299 size := *cons.RootDisk / 1024 300 if size < MinDisk { 301 params.RootDisk = MinDisk 302 } else { 303 params.RootDisk = size 304 } 305 } 306 if cons.Arch != nil { 307 logger.Infof("arch constraint of %q being ignored as not supported", *cons.Arch) 308 } 309 if cons.Container != nil { 310 logger.Infof("container constraint of %q being ignored as not supported", *cons.Container) 311 } 312 if cons.CpuPower != nil { 313 logger.Infof("cpu-power constraint of %v being ignored as not supported", *cons.CpuPower) 314 } 315 if cons.Tags != nil { 316 logger.Infof("tags constraint of %q being ignored as not supported", strings.Join(*cons.Tags, ",")) 317 } 318 319 return params 320 }