github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cloudconfig/containerinit/container_userdata.go (about) 1 // Copyright 2013, 2015 Canonical Ltd. 2 // Copyright 2015 Cloudbase Solutions SRL 3 // Licensed under the AGPLv3, see LICENCE file for details. 4 5 package containerinit 6 7 import ( 8 "bytes" 9 "fmt" 10 "io/ioutil" 11 "net" 12 "path/filepath" 13 "strings" 14 15 "github.com/juju/errors" 16 "github.com/juju/loggo" 17 "github.com/juju/utils/proxy" 18 "github.com/juju/utils/set" 19 20 "github.com/juju/juju/cloudconfig" 21 "github.com/juju/juju/cloudconfig/cloudinit" 22 "github.com/juju/juju/cloudconfig/instancecfg" 23 "github.com/juju/juju/container" 24 "github.com/juju/juju/network" 25 "github.com/juju/juju/service" 26 "github.com/juju/juju/service/common" 27 ) 28 29 var ( 30 logger = loggo.GetLogger("juju.cloudconfig.containerinit") 31 ) 32 33 // WriteUserData generates the cloud-init user-data using the 34 // specified machine and network config for a container, and writes 35 // the serialized form out to a cloud-init file in the directory 36 // specified. 37 func WriteUserData( 38 instanceConfig *instancecfg.InstanceConfig, 39 networkConfig *container.NetworkConfig, 40 directory string, 41 ) (string, error) { 42 userData, err := CloudInitUserData(instanceConfig, networkConfig) 43 if err != nil { 44 logger.Errorf("failed to create user data: %v", err) 45 return "", err 46 } 47 return WriteCloudInitFile(directory, userData) 48 } 49 50 // WriteCloudInitFile writes the data out to a cloud-init file in the 51 // directory specified, and returns the filename. 52 func WriteCloudInitFile(directory string, userData []byte) (string, error) { 53 userDataFilename := filepath.Join(directory, "cloud-init") 54 if err := ioutil.WriteFile(userDataFilename, userData, 0644); err != nil { 55 logger.Errorf("failed to write user data: %v", err) 56 return "", err 57 } 58 return userDataFilename, nil 59 } 60 61 var ( 62 systemNetworkInterfacesFile = "/etc/network/interfaces" 63 networkInterfacesFile = systemNetworkInterfacesFile + "-juju" 64 ) 65 66 // GenerateNetworkConfig renders a network config for one or more network 67 // interfaces, using the given non-nil networkConfig containing a non-empty 68 // Interfaces field. 69 func GenerateNetworkConfig(networkConfig *container.NetworkConfig) (string, error) { 70 if networkConfig == nil || len(networkConfig.Interfaces) == 0 { 71 return "", errors.Errorf("missing container network config") 72 } 73 logger.Debugf("generating network config from %#v", *networkConfig) 74 75 prepared := PrepareNetworkConfigFromInterfaces(networkConfig.Interfaces) 76 77 var output bytes.Buffer 78 gatewayHandled := false 79 for _, name := range prepared.InterfaceNames { 80 output.WriteString("\n") 81 if name == "lo" { 82 output.WriteString("auto ") 83 autoStarted := strings.Join(prepared.AutoStarted, " ") 84 output.WriteString(autoStarted + "\n\n") 85 output.WriteString("iface lo inet loopback\n") 86 87 dnsServers := strings.Join(prepared.DNSServers, " ") 88 if dnsServers != "" { 89 output.WriteString(" dns-nameservers ") 90 output.WriteString(dnsServers + "\n") 91 } 92 93 dnsSearchDomains := strings.Join(prepared.DNSSearchDomains, " ") 94 if dnsSearchDomains != "" { 95 output.WriteString(" dns-search ") 96 output.WriteString(dnsSearchDomains + "\n") 97 } 98 continue 99 } 100 101 address, hasAddress := prepared.NameToAddress[name] 102 if !hasAddress { 103 output.WriteString("iface " + name + " inet manual\n") 104 continue 105 } else if address == string(network.ConfigDHCP) { 106 output.WriteString("iface " + name + " inet dhcp\n") 107 // We're expecting to get a default gateway 108 // from the DHCP lease. 109 gatewayHandled = true 110 continue 111 } 112 113 output.WriteString("iface " + name + " inet static\n") 114 output.WriteString(" address " + address + "\n") 115 if !gatewayHandled && prepared.GatewayAddress != "" { 116 _, network, err := net.ParseCIDR(address) 117 if err != nil { 118 return "", errors.Annotatef(err, "invalid gateway for interface %q with address %q", name, address) 119 } 120 121 gatewayIP := net.ParseIP(prepared.GatewayAddress) 122 if network.Contains(gatewayIP) { 123 output.WriteString(" gateway " + prepared.GatewayAddress + "\n") 124 gatewayHandled = true // write it only once 125 } 126 } 127 } 128 129 generatedConfig := output.String() 130 logger.Debugf("generated network config:\n%s", generatedConfig) 131 132 if !gatewayHandled { 133 logger.Infof("generated network config has no gateway") 134 } 135 136 return generatedConfig, nil 137 } 138 139 // PreparedConfig holds all the necessary information to render a persistent 140 // network config to a file. 141 type PreparedConfig struct { 142 InterfaceNames []string 143 AutoStarted []string 144 DNSServers []string 145 DNSSearchDomains []string 146 NameToAddress map[string]string 147 GatewayAddress string 148 } 149 150 // PrepareNetworkConfigFromInterfaces collects the necessary information to 151 // render a persistent network config from the given slice of 152 // network.InterfaceInfo. The result always includes the loopback interface. 153 func PrepareNetworkConfigFromInterfaces(interfaces []network.InterfaceInfo) *PreparedConfig { 154 dnsServers := set.NewStrings() 155 dnsSearchDomains := set.NewStrings() 156 gatewayAddress := "" 157 namesInOrder := make([]string, 1, len(interfaces)+1) 158 nameToAddress := make(map[string]string) 159 160 // Always include the loopback. 161 namesInOrder[0] = "lo" 162 autoStarted := set.NewStrings("lo") 163 164 for _, info := range interfaces { 165 if !info.NoAutoStart { 166 autoStarted.Add(info.InterfaceName) 167 } 168 169 if cidr := info.CIDRAddress(); cidr != "" { 170 nameToAddress[info.InterfaceName] = cidr 171 } else if info.ConfigType == network.ConfigDHCP { 172 nameToAddress[info.InterfaceName] = string(network.ConfigDHCP) 173 } 174 175 for _, dns := range info.DNSServers { 176 dnsServers.Add(dns.Value) 177 } 178 179 dnsSearchDomains = dnsSearchDomains.Union(set.NewStrings(info.DNSSearchDomains...)) 180 181 if gatewayAddress == "" && info.GatewayAddress.Value != "" { 182 gatewayAddress = info.GatewayAddress.Value 183 } 184 185 namesInOrder = append(namesInOrder, info.InterfaceName) 186 } 187 188 prepared := &PreparedConfig{ 189 InterfaceNames: namesInOrder, 190 NameToAddress: nameToAddress, 191 AutoStarted: autoStarted.SortedValues(), 192 DNSServers: dnsServers.SortedValues(), 193 DNSSearchDomains: dnsSearchDomains.SortedValues(), 194 GatewayAddress: gatewayAddress, 195 } 196 197 logger.Debugf("prepared network config for rendering: %+v", prepared) 198 return prepared 199 } 200 201 // newCloudInitConfigWithNetworks creates a cloud-init config which 202 // might include per-interface networking config if both networkConfig 203 // is not nil and its Interfaces field is not empty. 204 func newCloudInitConfigWithNetworks(series string, networkConfig *container.NetworkConfig) (cloudinit.CloudConfig, error) { 205 cloudConfig, err := cloudinit.New(series) 206 if err != nil { 207 return nil, errors.Trace(err) 208 } 209 210 if networkConfig != nil { 211 config, err := GenerateNetworkConfig(networkConfig) 212 if err != nil { 213 return nil, errors.Trace(err) 214 } 215 cloudConfig.AddBootTextFile(networkInterfacesFile, config, 0644) 216 cloudConfig.AddRunCmd(raiseJujuNetworkInterfacesScript(systemNetworkInterfacesFile, networkInterfacesFile)) 217 } 218 219 return cloudConfig, nil 220 } 221 222 func CloudInitUserData( 223 instanceConfig *instancecfg.InstanceConfig, 224 networkConfig *container.NetworkConfig, 225 ) ([]byte, error) { 226 cloudConfig, err := newCloudInitConfigWithNetworks(instanceConfig.Series, networkConfig) 227 if err != nil { 228 return nil, errors.Trace(err) 229 } 230 udata, err := cloudconfig.NewUserdataConfig(instanceConfig, cloudConfig) 231 if err != nil { 232 return nil, errors.Trace(err) 233 } 234 if err = udata.Configure(); err != nil { 235 return nil, errors.Trace(err) 236 } 237 // Run ifconfig to get the addresses of the internal container at least 238 // logged in the host. 239 cloudConfig.AddRunCmd("ifconfig") 240 241 if instanceConfig.MachineContainerHostname != "" { 242 logger.Debugf("Cloud-init configured to set hostname") 243 cloudConfig.SetAttr("hostname", instanceConfig.MachineContainerHostname) 244 } 245 246 cloudConfig.SetAttr("manage_etc_hosts", true) 247 248 data, err := cloudConfig.RenderYAML() 249 if err != nil { 250 return nil, errors.Trace(err) 251 } 252 return data, nil 253 } 254 255 // TemplateUserData returns a minimal user data necessary for the template. 256 // This should have the authorized keys, base packages, the cloud archive if 257 // necessary, initial apt proxy config, and it should do the apt-get 258 // update/upgrade initially. 259 func TemplateUserData( 260 series string, 261 authorizedKeys string, 262 aptProxy proxy.Settings, 263 aptMirror string, 264 enablePackageUpdates bool, 265 enableOSUpgrades bool, 266 networkConfig *container.NetworkConfig, 267 ) ([]byte, error) { 268 var config cloudinit.CloudConfig 269 var err error 270 if networkConfig != nil { 271 config, err = newCloudInitConfigWithNetworks(series, networkConfig) 272 if err != nil { 273 return nil, errors.Trace(err) 274 } 275 } else { 276 config, err = cloudinit.New(series) 277 if err != nil { 278 return nil, errors.Trace(err) 279 } 280 } 281 cloudconfig.SetUbuntuUser(config, authorizedKeys) 282 config.AddScripts( 283 "set -xe", // ensure we run all the scripts or abort. 284 ) 285 // For LTS series which need support for the cloud-tools archive, 286 // we need to enable apt-get update regardless of the environ 287 // setting, otherwise provisioning will fail. 288 if series == "precise" && !enablePackageUpdates { 289 logger.Infof("series %q requires cloud-tools archive: enabling updates", series) 290 enablePackageUpdates = true 291 } 292 293 if enablePackageUpdates && config.RequiresCloudArchiveCloudTools() { 294 config.AddCloudArchiveCloudTools() 295 } 296 config.AddPackageCommands(aptProxy, aptMirror, enablePackageUpdates, enableOSUpgrades) 297 298 initSystem, err := service.VersionInitSystem(series) 299 if err != nil { 300 return nil, errors.Trace(err) 301 } 302 cmds, err := shutdownInitCommands(initSystem, series) 303 if err != nil { 304 return nil, errors.Trace(err) 305 } 306 config.AddScripts(strings.Join(cmds, "\n")) 307 308 data, err := config.RenderYAML() 309 if err != nil { 310 return nil, err 311 } 312 return data, nil 313 } 314 315 func shutdownInitCommands(initSystem, series string) ([]string, error) { 316 shutdownCmd := "/sbin/shutdown -h now" 317 name := "juju-template-restart" 318 desc := "juju shutdown job" 319 320 execStart := shutdownCmd 321 322 conf := common.Conf{ 323 Desc: desc, 324 Transient: true, 325 AfterStopped: "cloud-final", 326 ExecStart: execStart, 327 } 328 // systemd uses targets for synchronization of services 329 if initSystem == service.InitSystemSystemd { 330 conf.AfterStopped = "cloud-config.target" 331 } 332 333 svc, err := service.NewService(name, conf, series) 334 if err != nil { 335 return nil, errors.Trace(err) 336 } 337 338 cmds, err := svc.InstallCommands() 339 if err != nil { 340 return nil, errors.Trace(err) 341 } 342 343 startCommands, err := svc.StartCommands() 344 if err != nil { 345 return nil, errors.Trace(err) 346 } 347 cmds = append(cmds, startCommands...) 348 349 return cmds, nil 350 } 351 352 // raiseJujuNetworkInterfacesScript returns a cloud-init script to 353 // raise Juju's network interfaces supplied via cloud-init. 354 // 355 // Note: we sleep to mitigate against LP #1337873 and LP #1269921. 356 func raiseJujuNetworkInterfacesScript(oldInterfacesFile, newInterfacesFile string) string { 357 return fmt.Sprintf(` 358 if [ -f %[2]s ]; then 359 ifdown -a 360 sleep 1.5 361 if ifup -a --interfaces=%[2]s; then 362 cp %[1]s %[1]s-orig 363 cp %[2]s %[1]s 364 else 365 ifup -a 366 fi 367 fi`[1:], 368 oldInterfacesFile, newInterfacesFile) 369 }