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