github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/worker/provisioner/lxc-broker.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provisioner 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "net" 11 "os" 12 "strings" 13 "text/template" 14 15 "github.com/juju/errors" 16 "github.com/juju/loggo" 17 "github.com/juju/names" 18 "github.com/juju/utils/exec" 19 20 "github.com/juju/juju/agent" 21 apiprovisioner "github.com/juju/juju/api/provisioner" 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/cloudconfig/instancecfg" 24 "github.com/juju/juju/container" 25 "github.com/juju/juju/container/lxc" 26 "github.com/juju/juju/environs" 27 "github.com/juju/juju/instance" 28 "github.com/juju/juju/network" 29 "github.com/juju/juju/tools" 30 "github.com/juju/juju/version" 31 ) 32 33 var lxcLogger = loggo.GetLogger("juju.provisioner.lxc") 34 35 var _ environs.InstanceBroker = (*lxcBroker)(nil) 36 37 type APICalls interface { 38 ContainerConfig() (params.ContainerConfig, error) 39 PrepareContainerInterfaceInfo(names.MachineTag) ([]network.InterfaceInfo, error) 40 GetContainerInterfaceInfo(names.MachineTag) ([]network.InterfaceInfo, error) 41 } 42 43 var _ APICalls = (*apiprovisioner.State)(nil) 44 45 // Override for testing. 46 var NewLxcBroker = newLxcBroker 47 48 func newLxcBroker( 49 api APICalls, 50 agentConfig agent.Config, 51 managerConfig container.ManagerConfig, 52 imageURLGetter container.ImageURLGetter, 53 enableNAT bool, 54 defaultMTU int, 55 ) (environs.InstanceBroker, error) { 56 manager, err := lxc.NewContainerManager(managerConfig, imageURLGetter) 57 if err != nil { 58 return nil, err 59 } 60 return &lxcBroker{ 61 manager: manager, 62 api: api, 63 agentConfig: agentConfig, 64 enableNAT: enableNAT, 65 defaultMTU: defaultMTU, 66 }, nil 67 } 68 69 type lxcBroker struct { 70 manager container.Manager 71 api APICalls 72 agentConfig agent.Config 73 enableNAT bool 74 defaultMTU int 75 } 76 77 // StartInstance is specified in the Broker interface. 78 func (broker *lxcBroker) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 79 if args.InstanceConfig.HasNetworks() { 80 return nil, errors.New("starting lxc containers with networks is not supported yet") 81 } 82 // TODO: refactor common code out of the container brokers. 83 machineId := args.InstanceConfig.MachineId 84 lxcLogger.Infof("starting lxc container for machineId: %s", machineId) 85 86 // Default to using the host network until we can configure. 87 bridgeDevice := broker.agentConfig.Value(agent.LxcBridge) 88 if bridgeDevice == "" { 89 bridgeDevice = lxc.DefaultLxcBridge 90 } 91 92 if !environs.AddressAllocationEnabled() { 93 logger.Debugf( 94 "address allocation feature flag not enabled; using DHCP for container %q", 95 machineId, 96 ) 97 } else { 98 logger.Debugf("trying to allocate static IP for container %q", machineId) 99 allocatedInfo, err := configureContainerNetwork( 100 machineId, 101 bridgeDevice, 102 broker.api, 103 args.NetworkInfo, 104 true, // allocate a new address. 105 broker.enableNAT, 106 ) 107 if err != nil { 108 // It's fine, just ignore it. The effect will be that the 109 // container won't have a static address configured. 110 logger.Infof("not allocating static IP for container %q: %v", machineId, err) 111 } else { 112 args.NetworkInfo = allocatedInfo 113 } 114 } 115 network := container.BridgeNetworkConfig(bridgeDevice, broker.defaultMTU, args.NetworkInfo) 116 117 // The provisioner worker will provide all tools it knows about 118 // (after applying explicitly specified constraints), which may 119 // include tools for architectures other than the host's. We 120 // must constrain to the host's architecture for LXC. 121 archTools, err := args.Tools.Match(tools.Filter{ 122 Arch: version.Current.Arch, 123 }) 124 if err == tools.ErrNoMatches { 125 return nil, errors.Errorf( 126 "need tools for arch %s, only found %s", 127 version.Current.Arch, 128 args.Tools.Arches(), 129 ) 130 } 131 132 series := archTools.OneSeries() 133 args.InstanceConfig.MachineContainerType = instance.LXC 134 args.InstanceConfig.Tools = archTools[0] 135 136 config, err := broker.api.ContainerConfig() 137 if err != nil { 138 lxcLogger.Errorf("failed to get container config: %v", err) 139 return nil, err 140 } 141 storageConfig := &container.StorageConfig{ 142 AllowMount: config.AllowLXCLoopMounts, 143 } 144 145 if err := instancecfg.PopulateInstanceConfig( 146 args.InstanceConfig, 147 config.ProviderType, 148 config.AuthorizedKeys, 149 config.SSLHostnameVerification, 150 config.Proxy, 151 config.AptProxy, 152 config.AptMirror, 153 config.PreferIPv6, 154 config.EnableOSRefreshUpdate, 155 config.EnableOSUpgrade, 156 ); err != nil { 157 lxcLogger.Errorf("failed to populate machine config: %v", err) 158 return nil, err 159 } 160 161 inst, hardware, err := broker.manager.CreateContainer(args.InstanceConfig, series, network, storageConfig) 162 if err != nil { 163 lxcLogger.Errorf("failed to start container: %v", err) 164 return nil, err 165 } 166 lxcLogger.Infof("started lxc container for machineId: %s, %s, %s", machineId, inst.Id(), hardware.String()) 167 return &environs.StartInstanceResult{ 168 Instance: inst, 169 Hardware: hardware, 170 }, nil 171 } 172 173 // StopInstances shuts down the given instances. 174 func (broker *lxcBroker) StopInstances(ids ...instance.Id) error { 175 // TODO: potentially parallelise. 176 for _, id := range ids { 177 lxcLogger.Infof("stopping lxc container for instance: %s", id) 178 if err := broker.manager.DestroyContainer(id); err != nil { 179 lxcLogger.Errorf("container did not stop: %v", err) 180 return err 181 } 182 } 183 return nil 184 } 185 186 // AllInstances only returns running containers. 187 func (broker *lxcBroker) AllInstances() (result []instance.Instance, err error) { 188 return broker.manager.ListContainers() 189 } 190 191 type hostArchToolsFinder struct { 192 f ToolsFinder 193 } 194 195 // FindTools is defined on the ToolsFinder interface. 196 func (h hostArchToolsFinder) FindTools(v version.Number, series string, arch *string) (tools.List, error) { 197 // Override the arch constraint with the arch of the host. 198 return h.f.FindTools(v, series, &version.Current.Arch) 199 } 200 201 // resolvConf is the full path to the resolv.conf file on the local 202 // system. Defined here so it can be overriden for testing. 203 var resolvConf = "/etc/resolv.conf" 204 205 // localDNSServers parses the /etc/resolv.conf file (if available) and 206 // extracts all nameservers addresses, and the default search domain 207 // and returns them. 208 func localDNSServers() ([]network.Address, string, error) { 209 file, err := os.Open(resolvConf) 210 if os.IsNotExist(err) { 211 return nil, "", nil 212 } else if err != nil { 213 return nil, "", errors.Annotatef(err, "cannot open %q", resolvConf) 214 } 215 defer file.Close() 216 217 var addresses []network.Address 218 var searchDomain string 219 scanner := bufio.NewScanner(file) 220 for scanner.Scan() { 221 line := strings.TrimSpace(scanner.Text()) 222 if strings.HasPrefix(line, "#") { 223 // Skip comments. 224 continue 225 } 226 if strings.HasPrefix(line, "nameserver") { 227 address := strings.TrimPrefix(line, "nameserver") 228 // Drop comments after the address, if any. 229 if strings.Contains(address, "#") { 230 address = address[:strings.Index(address, "#")] 231 } 232 address = strings.TrimSpace(address) 233 addresses = append(addresses, network.NewAddress(address)) 234 } 235 if strings.HasPrefix(line, "search") { 236 searchDomain = strings.TrimPrefix(line, "search") 237 // Drop comments after the domain, if any. 238 if strings.Contains(searchDomain, "#") { 239 searchDomain = searchDomain[:strings.Index(searchDomain, "#")] 240 } 241 searchDomain = strings.TrimSpace(searchDomain) 242 } 243 } 244 245 if err := scanner.Err(); err != nil { 246 return nil, "", errors.Annotatef(err, "cannot read DNS servers from %q", resolvConf) 247 } 248 return addresses, searchDomain, nil 249 } 250 251 // ipRouteAdd is the command template to add a static route for 252 // .ContainerIP using the .HostBridge device (usually lxcbr0 or virbr0). 253 var ipRouteAdd = mustParseTemplate("ipRouteAdd", ` 254 ip route add {{.ContainerIP}} dev {{.HostBridge}}`[1:]) 255 256 type IptablesRule struct { 257 Table string 258 Chain string 259 Rule string 260 } 261 262 var skipSNATRule = IptablesRule{ 263 // For EC2, to get internet access we need traffic to appear with 264 // source address matching the container's host. For internal 265 // traffic we want to keep the container IP because it is used 266 // by some services. This rule sits above the SNAT rule, which 267 // changes the source address of traffic to the container host IP 268 // address, skipping this modification if the traffic destination 269 // is inside the EC2 VPC. 270 "nat", 271 "POSTROUTING", 272 "-d {{.SubnetCIDR}} -o {{.HostIF}} -j RETURN", 273 } 274 275 var iptablesRules = map[string]IptablesRule{ 276 // iptablesCheckSNAT is the command template to verify if a SNAT 277 // rule already exists for the host NIC named .HostIF (usually 278 // eth0) and source address .HostIP (usually eth0's address). We 279 // need to check whether the rule exists because we only want to 280 // add it once. Exit code 0 means the rule exists, 1 means it 281 // doesn't. 282 "iptablesSNAT": { 283 "nat", 284 "POSTROUTING", 285 "-o {{.HostIF}} -j SNAT --to-source {{.HostIP}}", 286 }, "iptablesForwardOut": { 287 // Ensure that we have ACCEPT rules that apply to the containers that 288 // we are creating so any DROP rules added by libvirt while setting 289 // up virbr0 further down the chain don't disrupt wanted traffic. 290 "filter", 291 "FORWARD", 292 "-d {{.ContainerCIDR}} -o {{.HostBridge}} -j ACCEPT", 293 }, "iptablesForwardIn": { 294 "filter", 295 "FORWARD", 296 "-s {{.ContainerCIDR}} -i {{.HostBridge}} -j ACCEPT", 297 }} 298 299 // mustParseTemplate works like template.Parse, but panics on error. 300 func mustParseTemplate(name, source string) *template.Template { 301 templ, err := template.New(name).Parse(source) 302 if err != nil { 303 panic(err.Error()) 304 } 305 return templ 306 } 307 308 // mustExecTemplate works like template.Parse followed by template.Execute, 309 // but panics on error. 310 func mustExecTemplate(name, tmpl string, data interface{}) string { 311 t := mustParseTemplate(name, tmpl) 312 var buf bytes.Buffer 313 if err := t.Execute(&buf, data); err != nil { 314 panic(err.Error()) 315 } 316 return buf.String() 317 } 318 319 // runTemplateCommand executes the given template with the given data, 320 // which generates a command to execute. If exitNonZeroOK is true, no 321 // error is returned if the exit code is not 0, otherwise an error is 322 // returned. 323 func runTemplateCommand(t *template.Template, exitNonZeroOK bool, data interface{}) ( 324 exitCode int, err error, 325 ) { 326 // Clone the template to ensure the original won't be changed. 327 cloned, err := t.Clone() 328 if err != nil { 329 return -1, errors.Annotatef(err, "cannot clone command template %q", t.Name()) 330 } 331 var buf bytes.Buffer 332 if err := cloned.Execute(&buf, data); err != nil { 333 return -1, errors.Annotatef(err, "cannot execute command template %q", t.Name()) 334 } 335 command := buf.String() 336 logger.Debugf("running command %q", command) 337 result, err := exec.RunCommands(exec.RunParams{Commands: command}) 338 if err != nil { 339 return -1, errors.Annotatef(err, "cannot run command %q", command) 340 } 341 exitCode = result.Code 342 stdout := string(result.Stdout) 343 stderr := string(result.Stderr) 344 logger.Debugf( 345 "command %q returned code=%d, stdout=%q, stderr=%q", 346 command, exitCode, stdout, stderr, 347 ) 348 if exitCode != 0 { 349 if exitNonZeroOK { 350 return exitCode, nil 351 } 352 return exitCode, errors.Errorf( 353 "command %q failed with exit code %d", 354 command, exitCode, 355 ) 356 } 357 return 0, nil 358 } 359 360 // setupRoutesAndIPTables sets up on the host machine the needed 361 // iptables rules and static routes for an addressable container. 362 var setupRoutesAndIPTables = func( 363 primaryNIC string, 364 primaryAddr network.Address, 365 bridgeName string, 366 ifaceInfo []network.InterfaceInfo, 367 enableNAT bool, 368 ) error { 369 370 if primaryNIC == "" || primaryAddr.Value == "" || bridgeName == "" || len(ifaceInfo) == 0 { 371 return errors.Errorf("primaryNIC, primaryAddr, bridgeName, and ifaceInfo must be all set") 372 } 373 374 for _, iface := range ifaceInfo { 375 containerIP := iface.Address.Value 376 if containerIP == "" { 377 return errors.Errorf("container IP %q must be set", containerIP) 378 } 379 data := struct { 380 HostIF string 381 HostIP string 382 HostBridge string 383 ContainerIP string 384 ContainerCIDR string 385 SubnetCIDR string 386 }{primaryNIC, primaryAddr.Value, bridgeName, containerIP, iface.CIDR, iface.CIDR} 387 388 var addRuleIfDoesNotExist = func(name string, rule IptablesRule) error { 389 check := mustExecTemplate("rule", "iptables -t {{.Table}} -C {{.Chain}} {{.Rule}}", rule) 390 t := mustParseTemplate(name+"Check", check) 391 392 code, err := runTemplateCommand(t, true, data) 393 if err != nil { 394 return errors.Trace(err) 395 } 396 switch code { 397 case 0: 398 // Rule does exist. Do nothing 399 case 1: 400 // Rule does not exist, add it. We insert the rule at the top of the list so it precedes any 401 // REJECT rules. 402 action := mustExecTemplate("action", "iptables -t {{.Table}} -I {{.Chain}} 1 {{.Rule}}", rule) 403 t = mustParseTemplate(name+"Add", action) 404 _, err = runTemplateCommand(t, false, data) 405 if err != nil { 406 return errors.Trace(err) 407 } 408 default: 409 // Unexpected code - better report it. 410 return errors.Errorf("iptables failed with unexpected exit code %d", code) 411 } 412 return nil 413 } 414 415 for name, rule := range iptablesRules { 416 if !enableNAT && name == "iptablesSNAT" { 417 // Do not add the SNAT rule if we shouldn't enable 418 // NAT. 419 continue 420 } 421 if err := addRuleIfDoesNotExist(name, rule); err != nil { 422 return err 423 } 424 } 425 426 // TODO(dooferlad): subnets should be a list of subnets in the EC2 VPC and 427 // should be empty for MAAS. See bug http://pad.lv/1443942 428 if enableNAT { 429 // Only add the following hack to allow AWS egress traffic 430 // for hosted containers to work. 431 subnets := []string{data.HostIP + "/16"} 432 for _, subnet := range subnets { 433 data.SubnetCIDR = subnet 434 if err := addRuleIfDoesNotExist("skipSNAT", skipSNATRule); err != nil { 435 return err 436 } 437 } 438 } 439 440 code, err := runTemplateCommand(ipRouteAdd, false, data) 441 // Ignore errors if the exit code was 2, which signals that the route was not added 442 // because it already exists. 443 if code != 2 && err != nil { 444 return errors.Trace(err) 445 } 446 if code == 2 { 447 logger.Tracef("route already exists - not added") 448 } else { 449 logger.Tracef("route added: container uses host network interface") 450 } 451 } 452 logger.Infof("successfully configured iptables and routes for container interfaces") 453 454 return nil 455 } 456 457 var ( 458 netInterfaces = net.Interfaces 459 interfaceAddrs = (*net.Interface).Addrs 460 ) 461 462 // discoverPrimaryNIC returns the name of the first network interface 463 // on the machine which is up and has address, along with the first 464 // address it has. 465 func discoverPrimaryNIC() (string, network.Address, error) { 466 interfaces, err := netInterfaces() 467 if err != nil { 468 return "", network.Address{}, errors.Annotatef(err, "cannot get network interfaces") 469 } 470 logger.Tracef("trying to discover primary network interface") 471 for _, iface := range interfaces { 472 if iface.Flags&net.FlagLoopback != 0 { 473 // Skip the loopback. 474 logger.Tracef("not using loopback interface %q", iface.Name) 475 continue 476 } 477 if iface.Flags&net.FlagUp != 0 { 478 // Possibly the primary, but ensure it has an address as 479 // well. 480 logger.Tracef("verifying interface %q has addresses", iface.Name) 481 addrs, err := interfaceAddrs(&iface) 482 if err != nil { 483 return "", network.Address{}, errors.Annotatef(err, "cannot get %q addresses", iface.Name) 484 } 485 if len(addrs) > 0 { 486 // We found it. 487 // Check if it's an IP or a CIDR. 488 addr := addrs[0].String() 489 ip := net.ParseIP(addr) 490 if ip == nil { 491 // Try a CIDR. 492 ip, _, err = net.ParseCIDR(addr) 493 if err != nil { 494 return "", network.Address{}, errors.Annotatef(err, "cannot parse address %q", addr) 495 } 496 } 497 addr = ip.String() 498 499 logger.Tracef("primary network interface is %q, address %q", iface.Name, addr) 500 return iface.Name, network.NewAddress(addr), nil 501 } 502 } 503 } 504 return "", network.Address{}, errors.Errorf("cannot detect the primary network interface") 505 } 506 507 // MACAddressTemplate is used to generate a unique MAC address for a 508 // container. Every 'x' is replaced by a random hexadecimal digit, 509 // while the rest is kept as-is. 510 const MACAddressTemplate = "00:16:3e:xx:xx:xx" 511 512 // configureContainerNetworking tries to allocate a static IP address 513 // for the given containerId using the provisioner API, when 514 // allocateAddress is true. Otherwise it configures the container with 515 // an already allocated address, when allocateAddress is false (e.g. 516 // after a host reboot). If the API call fails, it's not critical - 517 // just a warning, and it won't cause StartInstance to fail. 518 func configureContainerNetwork( 519 containerId, bridgeDevice string, 520 apiFacade APICalls, 521 ifaceInfo []network.InterfaceInfo, 522 allocateAddress bool, 523 enableNAT bool, 524 ) (finalIfaceInfo []network.InterfaceInfo, err error) { 525 defer func() { 526 if err != nil { 527 logger.Warningf( 528 "failed configuring a static IP for container %q: %v", 529 containerId, err, 530 ) 531 } 532 }() 533 534 if len(ifaceInfo) != 0 { 535 // When we already have interface info, don't overwrite it. 536 return nil, nil 537 } 538 539 var primaryNIC string 540 var primaryAddr network.Address 541 primaryNIC, primaryAddr, err = discoverPrimaryNIC() 542 if err != nil { 543 return nil, errors.Trace(err) 544 } 545 546 if allocateAddress { 547 logger.Debugf("trying to allocate a static IP for container %q", containerId) 548 finalIfaceInfo, err = apiFacade.PrepareContainerInterfaceInfo(names.NewMachineTag(containerId)) 549 } else { 550 logger.Debugf("getting allocated static IP for container %q", containerId) 551 finalIfaceInfo, err = apiFacade.GetContainerInterfaceInfo(names.NewMachineTag(containerId)) 552 } 553 if err != nil { 554 return nil, errors.Trace(err) 555 } 556 logger.Debugf("container interface info result %#v", finalIfaceInfo) 557 558 // Populate ConfigType and DNSServers as needed. 559 var dnsServers []network.Address 560 var searchDomain string 561 dnsServers, searchDomain, err = localDNSServers() 562 if err != nil { 563 return nil, errors.Trace(err) 564 } 565 // Generate the final configuration for each container interface. 566 for i, _ := range finalIfaceInfo { 567 // Always start at the first device index and generate the 568 // interface name based on that. We need to do this otherwise 569 // the container will inherit the host's device index and 570 // interface name. 571 finalIfaceInfo[i].DeviceIndex = i 572 finalIfaceInfo[i].InterfaceName = fmt.Sprintf("eth%d", i) 573 finalIfaceInfo[i].MACAddress = MACAddressTemplate 574 finalIfaceInfo[i].ConfigType = network.ConfigStatic 575 finalIfaceInfo[i].DNSServers = dnsServers 576 finalIfaceInfo[i].DNSSearch = searchDomain 577 finalIfaceInfo[i].GatewayAddress = primaryAddr 578 } 579 err = setupRoutesAndIPTables( 580 primaryNIC, 581 primaryAddr, 582 bridgeDevice, 583 finalIfaceInfo, 584 enableNAT, 585 ) 586 if err != nil { 587 return nil, errors.Trace(err) 588 } 589 return finalIfaceInfo, nil 590 } 591 592 // MaintainInstance checks that the container's host has the required iptables and routing 593 // rules to make the container visible to both the host and other machines on the same subnet. 594 func (broker *lxcBroker) MaintainInstance(args environs.StartInstanceParams) error { 595 machineId := args.InstanceConfig.MachineId 596 if !environs.AddressAllocationEnabled() { 597 lxcLogger.Debugf("address allocation disabled: Not running maintenance for lxc container with machineId: %s", 598 machineId) 599 return nil 600 } 601 602 lxcLogger.Debugf("running maintenance for lxc container with machineId: %s", machineId) 603 604 // Default to using the host network until we can configure. 605 bridgeDevice := broker.agentConfig.Value(agent.LxcBridge) 606 if bridgeDevice == "" { 607 bridgeDevice = lxc.DefaultLxcBridge 608 } 609 _, err := configureContainerNetwork( 610 machineId, 611 bridgeDevice, 612 broker.api, 613 args.NetworkInfo, 614 false, // don't allocate a new address. 615 broker.enableNAT, 616 ) 617 return err 618 }