github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/cloudconfig/userdatacfg_unix.go (about) 1 // Copyright 2012, 2013, 2014, 2015 Canonical Ltd. 2 // Copyright 2014, 2015 Cloudbase Solutions 3 // Licensed under the AGPLv3, see LICENCE file for details. 4 5 package cloudconfig 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io/ioutil" 13 "path" 14 "strings" 15 "text/template" 16 "time" 17 18 "github.com/juju/errors" 19 "github.com/juju/loggo" 20 "github.com/juju/names" 21 "github.com/juju/utils" 22 "github.com/juju/utils/os" 23 "github.com/juju/utils/proxy" 24 "github.com/juju/utils/series" 25 goyaml "gopkg.in/yaml.v1" 26 27 "github.com/juju/juju/cloudconfig/cloudinit" 28 "github.com/juju/juju/environs/config" 29 "github.com/juju/juju/environs/imagemetadata" 30 "github.com/juju/juju/service" 31 "github.com/juju/juju/service/systemd" 32 "github.com/juju/juju/service/upstart" 33 ) 34 35 const ( 36 // curlCommand is the base curl command used to download tools. 37 curlCommand = "curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s '" 38 39 // toolsDownloadAttempts is the number of attempts to make for 40 // each tools URL when downloading tools. 41 toolsDownloadAttempts = 5 42 43 // toolsDownloadWaitTime is the number of seconds to wait between 44 // each iterations of download attempts. 45 toolsDownloadWaitTime = 15 46 47 // toolsDownloadTemplate is a bash template that generates a 48 // bash command to cycle through a list of URLs to download tools. 49 toolsDownloadTemplate = `{{$curl := .ToolsDownloadCommand}} 50 for n in $(seq {{.ToolsDownloadAttempts}}); do 51 {{range .URLs}} 52 printf "Attempt $n to download tools from %s...\n" {{shquote .}} 53 {{$curl}} {{shquote .}} && echo "Tools downloaded successfully." && break 54 {{end}} 55 if [ $n -lt {{.ToolsDownloadAttempts}} ]; then 56 echo "Download failed..... wait {{.ToolsDownloadWaitTime}}s" 57 fi 58 sleep {{.ToolsDownloadWaitTime}} 59 done` 60 ) 61 62 type unixConfigure struct { 63 baseConfigure 64 } 65 66 // TODO(ericsnow) Move Configure to the baseConfigure type? 67 68 // Configure updates the provided cloudinit.Config with 69 // configuration to initialize a Juju machine agent. 70 func (w *unixConfigure) Configure() error { 71 if err := w.ConfigureBasic(); err != nil { 72 return err 73 } 74 return w.ConfigureJuju() 75 } 76 77 // ConfigureBasic updates the provided cloudinit.Config with 78 // basic configuration to initialise an OS image, such that it can 79 // be connected to via SSH, and log to a standard location. 80 // 81 // Any potentially failing operation should not be added to the 82 // configuration, but should instead be done in ConfigureJuju. 83 // 84 // Note: we don't do apt update/upgrade here so as not to have to wait on 85 // apt to finish when performing the second half of image initialisation. 86 // Doing it later brings the benefit of feedback in the face of errors, 87 // but adds to the running time of initialisation due to lack of activity 88 // between image bringup and start of agent installation. 89 func (w *unixConfigure) ConfigureBasic() error { 90 w.conf.AddScripts( 91 "set -xe", // ensure we run all the scripts or abort. 92 ) 93 switch w.os { 94 case os.Ubuntu: 95 w.conf.AddSSHAuthorizedKeys(w.icfg.AuthorizedKeys) 96 if w.icfg.Tools != nil { 97 initSystem, err := service.VersionInitSystem(w.icfg.Series) 98 if err != nil { 99 return errors.Trace(err) 100 } 101 w.addCleanShutdownJob(initSystem) 102 } 103 // On unix systems that are not ubuntu we create an ubuntu user so that we 104 // are able to ssh in the machine and have all the functionality dependant 105 // on having an ubuntu user there. 106 // Hopefully in the future we are going to move all the distirbutions to 107 // having a "juju" user 108 case os.CentOS: 109 w.conf.AddScripts( 110 fmt.Sprintf(initUbuntuScript, utils.ShQuote(w.icfg.AuthorizedKeys)), 111 112 // Mask and stop firewalld, if enabled, so it cannot start. See 113 // http://pad.lv/1492066. firewalld might be missing, in which case 114 // is-enabled and is-active prints an error, which is why the output 115 // is surpressed. 116 "systemctl is-enabled firewalld &> /dev/null && systemctl mask firewalld || true", 117 "systemctl is-active firewalld &> /dev/null && systemctl stop firewalld || true", 118 119 `sed -i "s/^.*requiretty/#Defaults requiretty/" /etc/sudoers`, 120 ) 121 w.addCleanShutdownJob(service.InitSystemSystemd) 122 } 123 w.conf.SetOutput(cloudinit.OutAll, "| tee -a "+w.icfg.CloudInitOutputLog, "") 124 // Create a file in a well-defined location containing the machine's 125 // nonce. The presence and contents of this file will be verified 126 // during bootstrap. 127 // 128 // Note: this must be the last runcmd we do in ConfigureBasic, as 129 // the presence of the nonce file is used to gate the remainder 130 // of synchronous bootstrap. 131 noncefile := path.Join(w.icfg.DataDir, NonceFile) 132 w.conf.AddRunTextFile(noncefile, w.icfg.MachineNonce, 0644) 133 return nil 134 } 135 136 func (w *unixConfigure) addCleanShutdownJob(initSystem string) { 137 switch initSystem { 138 case service.InitSystemUpstart: 139 path, contents := upstart.CleanShutdownJobPath, upstart.CleanShutdownJob 140 w.conf.AddRunTextFile(path, contents, 0644) 141 case service.InitSystemSystemd: 142 path, contents := systemd.CleanShutdownServicePath, systemd.CleanShutdownService 143 w.conf.AddRunTextFile(path, contents, 0644) 144 w.conf.AddScripts(fmt.Sprintf("/bin/systemctl enable '%s'", path)) 145 } 146 } 147 148 func (w *unixConfigure) setDataDirPermissions() string { 149 seriesos, _ := series.GetOSFromSeries(w.icfg.Series) 150 var user string 151 switch seriesos { 152 case os.CentOS: 153 user = "root" 154 default: 155 user = "syslog" 156 } 157 return fmt.Sprintf("chown %s:adm %s", user, w.icfg.LogDir) 158 } 159 160 // ConfigureJuju updates the provided cloudinit.Config with configuration 161 // to initialise a Juju machine agent. 162 func (w *unixConfigure) ConfigureJuju() error { 163 if err := w.icfg.VerifyConfig(); err != nil { 164 return err 165 } 166 167 // Initialise progress reporting. We need to do separately for runcmd 168 // and (possibly, below) for bootcmd, as they may be run in different 169 // shell sessions. 170 initProgressCmd := cloudinit.InitProgressCmd() 171 w.conf.AddRunCmd(initProgressCmd) 172 173 // If we're doing synchronous bootstrap or manual provisioning, then 174 // ConfigureBasic won't have been invoked; thus, the output log won't 175 // have been set. We don't want to show the log to the user, so simply 176 // append to the log file rather than teeing. 177 if stdout, _ := w.conf.Output(cloudinit.OutAll); stdout == "" { 178 w.conf.SetOutput(cloudinit.OutAll, ">> "+w.icfg.CloudInitOutputLog, "") 179 w.conf.AddBootCmd(initProgressCmd) 180 w.conf.AddBootCmd(cloudinit.LogProgressCmd("Logging to %s on remote host", w.icfg.CloudInitOutputLog)) 181 } 182 183 w.conf.AddPackageCommands( 184 w.icfg.AptProxySettings, 185 w.icfg.AptMirror, 186 w.icfg.EnableOSRefreshUpdate, 187 w.icfg.EnableOSUpgrade, 188 ) 189 190 // Write out the normal proxy settings so that the settings are 191 // sourced by bash, and ssh through that. 192 w.conf.AddScripts( 193 // We look to see if the proxy line is there already as 194 // the manual provider may have had it already. The ubuntu 195 // user may not exist (local provider only). 196 `([ ! -e /home/ubuntu/.profile ] || grep -q '.juju-proxy' /home/ubuntu/.profile) || ` + 197 `printf '\n# Added by juju\n[ -f "$HOME/.juju-proxy" ] && . "$HOME/.juju-proxy"\n' >> /home/ubuntu/.profile`) 198 if (w.icfg.ProxySettings != proxy.Settings{}) { 199 exportedProxyEnv := w.icfg.ProxySettings.AsScriptEnvironment() 200 w.conf.AddScripts(strings.Split(exportedProxyEnv, "\n")...) 201 w.conf.AddScripts( 202 fmt.Sprintf( 203 `(id ubuntu &> /dev/null) && (printf '%%s\n' %s > /home/ubuntu/.juju-proxy && chown ubuntu:ubuntu /home/ubuntu/.juju-proxy)`, 204 shquote(w.icfg.ProxySettings.AsScriptEnvironment()))) 205 } 206 207 // Make the lock dir and change the ownership of the lock dir itself to 208 // ubuntu:ubuntu from root:root so the juju-run command run as the ubuntu 209 // user is able to get access to the hook execution lock (like the uniter 210 // itself does.) 211 lockDir := path.Join(w.icfg.DataDir, "locks") 212 w.conf.AddScripts( 213 fmt.Sprintf("mkdir -p %s", lockDir), 214 // We only try to change ownership if there is an ubuntu user defined. 215 fmt.Sprintf("(id ubuntu &> /dev/null) && chown ubuntu:ubuntu %s", lockDir), 216 fmt.Sprintf("mkdir -p %s", w.icfg.LogDir), 217 w.setDataDirPermissions(), 218 ) 219 220 w.conf.AddScripts( 221 "bin="+shquote(w.icfg.JujuTools()), 222 "mkdir -p $bin", 223 ) 224 225 // Make a directory for the tools to live in, then fetch the 226 // tools and unarchive them into it. 227 if strings.HasPrefix(w.icfg.Tools.URL, fileSchemePrefix) { 228 toolsData, err := ioutil.ReadFile(w.icfg.Tools.URL[len(fileSchemePrefix):]) 229 if err != nil { 230 return err 231 } 232 w.conf.AddRunBinaryFile(path.Join(w.icfg.JujuTools(), "tools.tar.gz"), []byte(toolsData), 0644) 233 } else { 234 curlCommand := curlCommand 235 var urls []string 236 if w.icfg.Bootstrap { 237 curlCommand += " --retry 10" 238 if w.icfg.DisableSSLHostnameVerification { 239 curlCommand += " --insecure" 240 } 241 urls = append(urls, w.icfg.Tools.URL) 242 } else { 243 for _, addr := range w.icfg.ApiHostAddrs() { 244 // TODO(axw) encode env UUID in URL when EnvironTag 245 // is guaranteed to be available in APIInfo. 246 url := fmt.Sprintf("https://%s/tools/%s", addr, w.icfg.Tools.Version) 247 urls = append(urls, url) 248 } 249 250 // Don't go through the proxy when downloading tools from the state servers 251 curlCommand += ` --noproxy "*"` 252 253 // Our API server certificates are unusable by curl (invalid subject name), 254 // so we must disable certificate validation. It doesn't actually 255 // matter, because there is no sensitive information being transmitted 256 // and we verify the tools' hash after. 257 curlCommand += " --insecure" 258 } 259 curlCommand += " -o $bin/tools.tar.gz" 260 w.conf.AddRunCmd(cloudinit.LogProgressCmd("Fetching tools: %s <%s>", curlCommand, urls)) 261 w.conf.AddRunCmd(toolsDownloadCommand(curlCommand, urls)) 262 } 263 toolsJson, err := json.Marshal(w.icfg.Tools) 264 if err != nil { 265 return err 266 } 267 268 w.conf.AddScripts( 269 fmt.Sprintf("sha256sum $bin/tools.tar.gz > $bin/juju%s.sha256", w.icfg.Tools.Version), 270 fmt.Sprintf(`grep '%s' $bin/juju%s.sha256 || (echo "Tools checksum mismatch"; exit 1)`, 271 w.icfg.Tools.SHA256, w.icfg.Tools.Version), 272 fmt.Sprintf("tar zxf $bin/tools.tar.gz -C $bin"), 273 fmt.Sprintf("printf %%s %s > $bin/downloaded-tools.txt", shquote(string(toolsJson))), 274 ) 275 276 // Don't remove tools tarball until after bootstrap agent 277 // runs, so it has a chance to add it to its catalogue. 278 defer w.conf.AddRunCmd( 279 fmt.Sprintf("rm $bin/tools.tar.gz && rm $bin/juju%s.sha256", w.icfg.Tools.Version), 280 ) 281 282 // We add the machine agent's configuration info 283 // before running bootstrap-state so that bootstrap-state 284 // has a chance to rerwrite it to change the password. 285 // It would be cleaner to change bootstrap-state to 286 // be responsible for starting the machine agent itself, 287 // but this would not be backwardly compatible. 288 machineTag := names.NewMachineTag(w.icfg.MachineId) 289 _, err = w.addAgentInfo(machineTag) 290 if err != nil { 291 return errors.Trace(err) 292 } 293 294 // Add the cloud archive cloud-tools pocket to apt sources 295 // for series that need it. This gives us up-to-date LXC, 296 // MongoDB, and other infrastructure. 297 // This is only done on ubuntu. 298 if w.conf.SystemUpdate() && w.conf.RequiresCloudArchiveCloudTools() { 299 w.conf.AddCloudArchiveCloudTools() 300 } 301 302 if w.icfg.Bootstrap { 303 var metadataDir string 304 if len(w.icfg.CustomImageMetadata) > 0 { 305 metadataDir = path.Join(w.icfg.DataDir, "simplestreams") 306 index, products, err := imagemetadata.MarshalImageMetadataJSON(w.icfg.CustomImageMetadata, nil, time.Now()) 307 if err != nil { 308 return err 309 } 310 indexFile := path.Join(metadataDir, imagemetadata.IndexStoragePath()) 311 productFile := path.Join(metadataDir, imagemetadata.ProductMetadataStoragePath()) 312 w.conf.AddRunTextFile(indexFile, string(index), 0644) 313 w.conf.AddRunTextFile(productFile, string(products), 0644) 314 metadataDir = " --image-metadata " + shquote(metadataDir) 315 } 316 317 cons := w.icfg.Constraints.String() 318 if cons != "" { 319 cons = " --constraints " + shquote(cons) 320 } 321 var hardware string 322 if w.icfg.HardwareCharacteristics != nil { 323 if hardware = w.icfg.HardwareCharacteristics.String(); hardware != "" { 324 hardware = " --hardware " + shquote(hardware) 325 } 326 } 327 w.conf.AddRunCmd(cloudinit.LogProgressCmd("Bootstrapping Juju machine agent")) 328 loggingOption := " --show-log" 329 // If the bootstrap command was requsted with --debug, then the root 330 // logger will be set to DEBUG. If it is, then we use --debug here too. 331 if loggo.GetLogger("").LogLevel() == loggo.DEBUG { 332 loggingOption = " --debug" 333 } 334 w.conf.AddScripts( 335 // The bootstrapping is always run with debug on. 336 w.icfg.JujuTools() + "/jujud bootstrap-state" + 337 " --data-dir " + shquote(w.icfg.DataDir) + 338 " --env-config " + shquote(base64yaml(w.icfg.Config)) + 339 " --instance-id " + shquote(string(w.icfg.InstanceId)) + 340 hardware + 341 cons + 342 metadataDir + 343 loggingOption, 344 ) 345 } 346 347 return w.addMachineAgentToBoot() 348 } 349 350 // toolsDownloadCommand takes a curl command minus the source URL, 351 // and generates a command that will cycle through the URLs until 352 // one succeeds. 353 func toolsDownloadCommand(curlCommand string, urls []string) string { 354 parsedTemplate := template.Must( 355 template.New("ToolsDownload").Funcs( 356 template.FuncMap{"shquote": shquote}, 357 ).Parse(toolsDownloadTemplate), 358 ) 359 var buf bytes.Buffer 360 err := parsedTemplate.Execute(&buf, map[string]interface{}{ 361 "ToolsDownloadCommand": curlCommand, 362 "ToolsDownloadAttempts": toolsDownloadAttempts, 363 "ToolsDownloadWaitTime": toolsDownloadWaitTime, 364 "URLs": urls, 365 }) 366 if err != nil { 367 panic(errors.Annotate(err, "tools download template error")) 368 } 369 return buf.String() 370 } 371 372 func base64yaml(m *config.Config) string { 373 data, err := goyaml.Marshal(m.AllAttrs()) 374 if err != nil { 375 // can't happen, these values have been validated a number of times 376 panic(err) 377 } 378 return base64.StdEncoding.EncodeToString(data) 379 } 380 381 const initUbuntuScript = ` 382 set -e 383 (id ubuntu &> /dev/null) || useradd -m ubuntu -s /bin/bash 384 umask 0077 385 temp=$(mktemp) 386 echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > $temp 387 install -m 0440 $temp /etc/sudoers.d/90-juju-ubuntu 388 rm $temp 389 su ubuntu -c 'install -D -m 0600 /dev/null ~/.ssh/authorized_keys' 390 export authorized_keys=%s 391 if [ ! -z "$authorized_keys" ]; then 392 su ubuntu -c 'printf "%%s\n" "$authorized_keys" >> ~/.ssh/authorized_keys' 393 fi`