// newConf returns the init system config for the mongo state service. func newConf(dataDir, dbDir, mongoPath string, port, oplogSizeMB int, wantNumaCtl bool) common.Conf { mongoCmd := mongoPath + " --auth" + " --dbpath " + utils.ShQuote(dbDir) + " --sslOnNormalPorts" + " --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(dataDir)) + " --sslPEMKeyPassword ignored" + " --port " + fmt.Sprint(port) + " --noprealloc" + " --syslog" + " --smallfiles" + " --journal" + " --keyFile " + utils.ShQuote(sharedSecretPath(dataDir)) + " --replSet " + ReplicaSetName + " --ipv6" + " --oplogSize " + strconv.Itoa(oplogSizeMB) extraScript := "" if wantNumaCtl { extraScript = fmt.Sprintf(detectMultiNodeScript, multinodeVarName, multinodeVarName) mongoCmd = fmt.Sprintf(numaCtlWrap, multinodeVarName) + mongoCmd } conf := common.Conf{ Desc: "juju state database", Limit: map[string]int{ "nofile": maxFiles, "nproc": maxProcs, }, Timeout: serviceTimeout, ExtraScript: extraScript, ExecStart: mongoCmd, } return conf }
// NewSSHStorage creates a new SSHStorage, connected to the // specified host, managing state under the specified remote path. func NewSSHStorage(params NewSSHStorageParams) (*SSHStorage, error) { if params.StorageDir == "" { return nil, errors.New("storagedir must be specified and non-empty") } if params.TmpDir == "" { return nil, errors.New("tmpdir must be specified and non-empty") } script := fmt.Sprintf( "install -d -g $SUDO_GID -o $SUDO_UID %s %s", utils.ShQuote(params.StorageDir), utils.ShQuote(params.TmpDir), ) cmd := sshCommand(params.Host, "sudo", "-n", "/bin/bash") var stderr bytes.Buffer cmd.Stderr = &stderr cmd.Stdin = strings.NewReader(script) if err := cmd.Run(); err != nil { err = fmt.Errorf("failed to create storage dir: %v (%v)", err, strings.TrimSpace(stderr.String())) return nil, err } // We could use sftp, but then we'd be at the mercy of // sftp's output messages for checking errors. Instead, // we execute an interactive bash shell. cmd = sshCommand(params.Host, "bash") stdin, err := cmd.StdinPipe() if err != nil { return nil, err } stdout, err := cmd.StdoutPipe() if err != nil { stdin.Close() return nil, err } // Combine stdout and stderr, so we can easily // get at catastrophic failure messages. cmd.Stderr = cmd.Stdout stor := &SSHStorage{ host: params.Host, remotepath: params.StorageDir, tmpdir: params.TmpDir, cmd: cmd, stdin: stdin, stdout: stdout, scanner: bufio.NewScanner(stdout), } cmd.Start() // Verify we have write permissions. _, err = stor.runf(flockExclusive, "touch %s", utils.ShQuote(params.StorageDir)) if err != nil { stdin.Close() stdout.Close() cmd.Wait() return nil, err } return stor, nil }
func (e *manualEnviron) Destroy() error { script := ` set -x touch %s pkill -%d jujud && exit stop %s rm -f /etc/init/juju* rm -fr %s %s exit 0 ` script = fmt.Sprintf( script, // WARNING: this is linked with the use of uninstallFile in // the agent package. Don't change it without extreme care, // and handling for mismatches with already-deployed agents. utils.ShQuote(path.Join( agent.DefaultPaths.DataDir, agent.UninstallFile, )), terminationworker.TerminationSignal, mongo.ServiceName, utils.ShQuote(agent.DefaultPaths.DataDir), utils.ShQuote(agent.DefaultPaths.LogDir), ) _, err := runSSHCommand( "ubuntu@"+e.envConfig().bootstrapHost(), []string{"sudo", "/bin/bash"}, script, ) return err }
// upstartService returns the upstart config for the mongo state service. // It also returns the path to the mongod executable that the upstart config // will be using. func upstartService(namespace, dataDir, dbDir string, port, oplogSizeMB int) (*upstart.Conf, string, error) { svc := upstart.NewService(ServiceName(namespace)) mongoPath, err := Path() if err != nil { return nil, "", err } mongoCmd := mongoPath + " --auth" + " --dbpath=" + utils.ShQuote(dbDir) + " --sslOnNormalPorts" + " --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(dataDir)) + " --sslPEMKeyPassword ignored" + " --port " + fmt.Sprint(port) + " --noprealloc" + " --syslog" + " --smallfiles" + " --journal" + " --keyFile " + utils.ShQuote(sharedSecretPath(dataDir)) + " --replSet " + ReplicaSetName + " --ipv6 " + " --oplogSize " + strconv.Itoa(oplogSizeMB) conf := &upstart.Conf{ Service: *svc, Desc: "juju state database", Limit: map[string]string{ "nofile": fmt.Sprintf("%d %d", maxFiles, maxFiles), "nproc": fmt.Sprintf("%d %d", maxProcs, maxProcs), }, Cmd: mongoCmd, } return conf, mongoPath, nil }
func (e *manualEnviron) Destroy() error { script := ` set -x touch %s pkill -%d jujud && exit stop %s rm -f /etc/init/juju* rm -f /etc/rsyslog.d/*juju* rm -fr %s %s exit 0 ` script = fmt.Sprintf( script, utils.ShQuote(path.Join( agent.DefaultPaths.DataDir, agent.UninstallAgentFile, )), terminationworker.TerminationSignal, mongo.ServiceName(""), utils.ShQuote(agent.DefaultPaths.DataDir), utils.ShQuote(agent.DefaultPaths.LogDir), ) _, err := runSSHCommand( "ubuntu@"+e.envConfig().bootstrapHost(), []string{"sudo", "/bin/bash"}, script, ) return err }
// DestroyController implements the Environ interface. func (e *manualEnviron) DestroyController(controllerUUID string) error { script := ` set -x touch %s # If jujud is running, we then wait for a while for it to stop. stopped=0 if pkill -%d jujud; then for i in ` + "`seq 1 30`" + `; do if pgrep jujud > /dev/null ; then sleep 1 else echo "jujud stopped" stopped=1 break fi done fi if [ $stopped -ne 1 ]; then # If jujud didn't stop nicely, we kill it hard here. %spkill -9 jujud service %s stop fi rm -f /etc/init/juju* rm -f /etc/systemd/system/juju* rm -fr %s %s exit 0 ` var diagnostics string if featureflag.Enabled(feature.DeveloperMode) { diagnostics = ` echo "Dump engine report and goroutines for stuck jujud" source /etc/profile.d/juju-introspection.sh juju-engine-report juju-goroutines ` } script = fmt.Sprintf( script, // WARNING: this is linked with the use of uninstallFile in // the agent package. Don't change it without extreme care, // and handling for mismatches with already-deployed agents. utils.ShQuote(path.Join( agent.DefaultPaths.DataDir, agent.UninstallFile, )), terminationworker.TerminationSignal, diagnostics, mongo.ServiceName, utils.ShQuote(agent.DefaultPaths.DataDir), utils.ShQuote(agent.DefaultPaths.LogDir), ) logger.Tracef("destroy controller script: %s", script) stdout, stderr, err := runSSHCommand( "ubuntu@"+e.host, []string{"sudo", "/bin/bash"}, script, ) logger.Debugf("script stdout: \n%s", stdout) logger.Debugf("script stderr: \n%s", stderr) return err }
func writeFileCommands(filename string, contents []byte, permission int) []string { quotedFilename := utils.ShQuote(filename) quotedContents := utils.ShQuote(string(contents)) return []string{ fmt.Sprintf("install -m %o /dev/null %s", permission, quotedFilename), fmt.Sprintf(`printf '%%s\n' %s > %s`, quotedContents, quotedFilename), } }
func (w *UbuntuRenderer) WriteFile(filename string, contents string, permission int) []string { quotedFilename := utils.ShQuote(filename) quotedContents := utils.ShQuote(contents) return []string{ fmt.Sprintf("install -m %o /dev/null %s", permission, quotedFilename), fmt.Sprintf(`printf '%%s\n' %s > %s`, quotedContents, quotedFilename), } }
// newConf returns the init system config for the mongo state service. func newConf(args ConfigArgs) common.Conf { mongoCmd := args.MongoPath + " --dbpath " + utils.ShQuote(args.DBDir) + " --sslOnNormalPorts" + " --sslPEMKeyFile " + utils.ShQuote(sslKeyPath(args.DataDir)) + // --sslPEMKeyPassword has to have its argument passed with = thanks to // https://bugs.launchpad.net/juju-core/+bug/1581284. " --sslPEMKeyPassword=ignored" + " --port " + fmt.Sprint(args.Port) + " --syslog" + " --journal" + " --replSet " + ReplicaSetName + " --quiet" + " --oplogSize " + strconv.Itoa(args.OplogSizeMB) if args.IPv6 { mongoCmd = mongoCmd + " --ipv6" } if args.Auth { mongoCmd = mongoCmd + " --auth" + " --keyFile " + utils.ShQuote(sharedSecretPath(args.DataDir)) } else { mongoCmd = mongoCmd + " --noauth" } if args.Version.StorageEngine != WiredTiger { mongoCmd = mongoCmd + " --noprealloc" + " --smallfiles" } else { mongoCmd = mongoCmd + " --storageEngine wiredTiger" } extraScript := "" if args.WantNumaCtl { extraScript = fmt.Sprintf(detectMultiNodeScript, multinodeVarName, multinodeVarName) mongoCmd = fmt.Sprintf(numaCtlWrap, multinodeVarName) + mongoCmd } conf := common.Conf{ Desc: "juju state database", Limit: map[string]int{ "nofile": maxFiles, "nproc": maxProcs, }, Timeout: serviceTimeout, ExtraScript: extraScript, ExecStart: mongoCmd, } return conf }
func (s *SSHStorage) run(flockmode flockmode, command string, input io.Reader, inputlen int64) (string, error) { const rcPrefix = "JUJU-RC: " command = fmt.Sprintf( "SHELL=/bin/bash flock %s %s -c %s", flockmode, utils.ShQuote(s.remotepath), utils.ShQuote(command), ) stdin := bufio.NewWriter(s.stdin) if input != nil { command = fmt.Sprintf("base64 -d << '@EOF' | (%s)", command) } command = fmt.Sprintf("(%s) 2>&1; echo %s$?", command, rcPrefix) if _, err := stdin.WriteString(command + "\n"); err != nil { return "", fmt.Errorf("failed to write command: %v", err) } if input != nil { if err := copyAsBase64(stdin, input); err != nil { return "", s.terminate(fmt.Errorf("failed to write input: %v", err)) } } if err := stdin.Flush(); err != nil { return "", s.terminate(fmt.Errorf("failed to write input: %v", err)) } var output []string for s.scanner.Scan() { line := s.scanner.Text() if strings.HasPrefix(line, rcPrefix) { line := line[len(rcPrefix):] rc, err := strconv.Atoi(line) if err != nil { return "", fmt.Errorf("failed to parse exit code %q: %v", line, err) } outputJoined := strings.Join(output, "\n") if rc == 0 { return outputJoined, nil } return "", SSHStorageError{outputJoined, rc} } else { output = append(output, line) } } err := fmt.Errorf("failed to locate %q", rcPrefix) if len(output) > 0 { err = fmt.Errorf("%v (output: %q)", err, strings.Join(output, "\n")) } if scannerErr := s.scanner.Err(); scannerErr != nil { err = fmt.Errorf("%v (scanner error: %v)", err, scannerErr) } return "", err }
// addPackageCommands returns a slice of commands that, when run, // will add the required apt repositories and packages. func addPackageCommands(cfg *cloudinit.Config) ([]string, error) { var cmds []string if len(cfg.AptSources()) > 0 { // Ensure add-apt-repository is available. cmds = append(cmds, cloudinit.LogProgressCmd("Installing add-apt-repository")) cmds = append(cmds, aptget+"install python-software-properties") } for _, src := range cfg.AptSources() { // PPA keys are obtained by add-apt-repository, from launchpad. if !strings.HasPrefix(src.Source, "ppa:") { if src.Key != "" { key := utils.ShQuote(src.Key) cmd := fmt.Sprintf("printf '%%s\\n' %s | apt-key add -", key) cmds = append(cmds, cmd) } } cmds = append(cmds, cloudinit.LogProgressCmd("Adding apt repository: %s", src.Source)) cmds = append(cmds, "add-apt-repository -y "+utils.ShQuote(src.Source)) if src.Prefs != nil { path := utils.ShQuote(src.Prefs.Path) contents := utils.ShQuote(src.Prefs.FileContents()) cmds = append(cmds, "install -D -m 644 /dev/null "+path) cmds = append(cmds, `printf '%s\n' `+contents+` > `+path) } } if len(cfg.AptSources()) > 0 || cfg.AptUpdate() { cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get update")) cmds = append(cmds, aptget+"update") } if cfg.AptUpgrade() { cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get upgrade")) cmds = append(cmds, aptget+"upgrade") } for _, pkg := range cfg.Packages() { cmds = append(cmds, cloudinit.LogProgressCmd("Installing package: %s", pkg)) if !strings.Contains(pkg, "--target-release") { // We only need to shquote the package name if it does not // contain additional arguments. pkg = utils.ShQuote(pkg) } cmd := fmt.Sprintf(aptget+"install %s", pkg) cmds = append(cmds, cmd) } if len(cmds) > 0 { // setting DEBIAN_FRONTEND=noninteractive prevents debconf // from prompting, always taking default values instead. cmds = append([]string{"export DEBIAN_FRONTEND=noninteractive"}, cmds...) } return cmds, nil }
func (w *proxyWorker) writeEnvironmentFile() error { // Writing the environment file is handled by executing the script: // // On cloud-instance ubuntu images, the ubuntu user is uid 1000, but in // the situation where the ubuntu user has been created as a part of the // manual provisioning process, the user will exist, and will not have the // same uid/gid as the default cloud image. // // It is easier to shell out to check, and is also the same way that the file // is written in the cloud-init process, so consistency FTW. filePath := path.Join(w.config.Directory, w.config.Filename) result, err := exec.RunCommands(exec.RunParams{ Commands: fmt.Sprintf( `[ -e %s ] && (printf '%%s\n' %s > %s && chown ubuntu:ubuntu %s)`, w.config.Directory, utils.ShQuote(w.proxy.AsScriptEnvironment()), filePath, filePath), WorkingDir: w.config.Directory, }) if err != nil { return err } if result.Code != 0 { logger.Errorf("failed writing new proxy values: \n%s\n%s", result.Stdout, result.Stderr) } return nil }
// Run the commands specified on the machines identified through the // list of machines, units and services. func (c *Client) Run(run params.RunParams) (results params.RunResults, err error) { units, err := getAllUnitNames(c.api.state, run.Units, run.Services) if err != nil { return results, err } // We want to create a RemoteExec for each unit and each machine. // If we have both a unit and a machine request, we run it twice, // once for the unit inside the exec context using juju-run, and // the other outside the context just using bash. var params []*RemoteExec var quotedCommands = utils.ShQuote(run.Commands) for _, unit := range units { // We know that the unit is both a principal unit, and that it has an // assigned machine. machineId, _ := unit.AssignedMachineId() machine, err := c.api.state.Machine(machineId) if err != nil { return results, err } command := fmt.Sprintf("juju-run %s %s", unit.Name(), quotedCommands) execParam := remoteParamsForMachine(machine, command, run.Timeout) execParam.UnitId = unit.Name() params = append(params, execParam) } for _, machineId := range run.Machines { machine, err := c.api.state.Machine(machineId) if err != nil { return results, err } command := fmt.Sprintf("juju-run --no-context %s", quotedCommands) execParam := remoteParamsForMachine(machine, command, run.Timeout) params = append(params, execParam) } return ParallelExecute(c.getDataDir(), params), nil }
// ProvisioningScript generates a bash script that can be // executed on a remote host to carry out the cloud-init // configuration. func ProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) { cloudcfg := coreCloudinit.New() cloudcfg.SetAptUpdate(mcfg.EnableOSRefreshUpdate) cloudcfg.SetAptUpgrade(mcfg.EnableOSUpgrade) udata, err := cloudinit.NewUserdataConfig(mcfg, cloudcfg) if err != nil { return "", errors.Annotate(err, "error generating cloud-config") } if err := udata.ConfigureJuju(); err != nil { return "", errors.Annotate(err, "error generating cloud-config") } configScript, err := sshinit.ConfigureScript(cloudcfg) if err != nil { return "", errors.Annotate(err, "error converting cloud-config to script") } var buf bytes.Buffer // Always remove the cloud-init-output.log file first, if it exists. fmt.Fprintf(&buf, "rm -f %s\n", utils.ShQuote(mcfg.CloudInitOutputLog)) // If something goes wrong, dump cloud-init-output.log to stderr. buf.WriteString(shell.DumpFileOnErrorScript(mcfg.CloudInitOutputLog)) buf.WriteString(configScript) return buf.String(), nil }
// mockShellCommand creates a new command with the given // name and contents, and patches $PATH so that it will be // executed by preference. It returns the name of a file // that is written by each call to the command - mockShellCalls // can be used to retrieve the calls. func mockShellCommand(c *gc.C, s *testing.CleanupSuite, name string) string { dir := c.MkDir() s.PatchEnvPathPrepend(dir) // Note the shell script produces output of the form: // +arg1+\n // +arg2+\n // ... // +argn+\n // - // // It would be nice if there was a simple way of unambiguously // quoting shell arguments, but this will do as long // as no argument contains a newline character. outputFile := filepath.Join(dir, name+".out") contents := `#!/bin/sh { for i in "$@"; do echo +"$i"+ done echo - } >> ` + utils.ShQuote(outputFile) + ` ` err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0755) c.Assert(err, gc.IsNil) return outputFile }
func (w *MachineEnvironmentWorker) writeEnvironmentFile() error { // Writing the environment file is handled by executing the script for two // primary reasons: // // 1: In order to have the local provider specify the environment settings // for the machine agent running on the host, this worker needs to run, // but it shouldn't be touching any files on the disk. If however there is // an ubuntu user, it will. This shouldn't be a problem. // // 2: On cloud-instance ubuntu images, the ubuntu user is uid 1000, but in // the situation where the ubuntu user has been created as a part of the // manual provisioning process, the user will exist, and will not have the // same uid/gid as the default cloud image. // // It is easier to shell out to check both these things, and is also the // same way that the file is written in the cloud-init process, so // consistency FTW. filePath := path.Join(ProxyDirectory, ProxyFile) result, err := exec.RunCommands(exec.RunParams{ Commands: fmt.Sprintf( `[ -e %s ] && (printf '%%s\n' %s > %s && chown ubuntu:ubuntu %s)`, ProxyDirectory, utils.ShQuote(w.proxy.AsScriptEnvironment()), filePath, filePath), WorkingDir: ProxyDirectory, }) if err != nil { return err } if result.Code != 0 { logger.Errorf("failed writing new proxy values: \n%s\n%s", result.Stdout, result.Stderr) } return nil }
func (e *manualEnviron) verifyBootstrapHost() error { // First verify that the environment is bootstrapped by checking // if the agents directory exists. Note that we cannot test the // root data directory, as that is created in the process of // initialising sshstorage. agentsDir := path.Join(agent.DefaultPaths.DataDir, "agents") const noAgentDir = "no-agent-dir" stdin := fmt.Sprintf( "test -d %s || echo %s", utils.ShQuote(agentsDir), noAgentDir, ) out, _, err := runSSHCommand( "ubuntu@"+e.host, []string{"/bin/bash"}, stdin, ) if err != nil { return err } if out = strings.TrimSpace(out); len(out) > 0 { if out == noAgentDir { return environs.ErrNotBootstrapped } err := errors.Errorf("unexpected output: %q", out) logger.Infof(err.Error()) return err } return nil }
// templateUserData returns a minimal user data necessary for the template. // This should have the authorized keys, base packages, the cloud archive if // necessary, initial apt proxy config, and it should do the apt-get // update/upgrade initially. func templateUserData( series string, authorizedKeys string, aptProxy proxy.Settings, enablePackageUpdates bool, enableOSUpgrades bool, ) ([]byte, error) { config := corecloudinit.New() config.AddScripts( "set -xe", // ensure we run all the scripts or abort. ) config.AddSSHAuthorizedKeys(authorizedKeys) if enablePackageUpdates { cloudinit.MaybeAddCloudArchiveCloudTools(config, series) } cloudinit.AddAptCommands(aptProxy, config, enablePackageUpdates, enableOSUpgrades) config.AddScripts( fmt.Sprintf( "printf '%%s\n' %s > %s", utils.ShQuote(templateShutdownUpstartScript), templateShutdownUpstartFilename, )) renderer, err := corecloudinit.NewRenderer(series) if err != nil { return nil, err } data, err := renderer.Render(config) if err != nil { return nil, err } return data, nil }
// RunConfigureScript connects to the specified host over // SSH, and executes the provided script which is expected // to have been returned by cloudinit ConfigureScript. func RunConfigureScript(script string, params ConfigureParams) error { logger.Tracef("Running script on %s: %s", params.Host, script) encoded := base64.StdEncoding.EncodeToString([]byte(` set -e tmpfile=$(mktemp) trap "rm -f $tmpfile" EXIT cat > $tmpfile /bin/bash $tmpfile `)) // bash will read a byte at a time when consuming commands // from stdin. We avoid sending the entire script -- which // will be very large when uploading tools -- directly to // bash for this reason. Instead, run cat which will write // the script to disk, and then execute it from there. cmd := ssh.Command(params.Host, []string{ "sudo", "/bin/bash", "-c", // The outer bash interprets the $(...), and executes // the decoded script in the nested bash. This avoids // linebreaks in the commandline, which the go.crypto- // based client has trouble with. fmt.Sprintf( `/bin/bash -c "$(echo %s | base64 -d)"`, utils.ShQuote(encoded), ), }, nil) cmd.Stdin = strings.NewReader(script) cmd.Stderr = params.ProgressWriter return cmd.Run() }
// cloudinitRunCmd returns the shell command that, when run, will create the // "machine info" file containing the hostname of a machine. // That command is destined to be used by cloudinit. func (info *machineInfo) cloudinitRunCmd() (string, error) { yaml, err := goyaml.Marshal(info) if err != nil { return "", err } script := fmt.Sprintf(`mkdir -p %s; echo -n %s > %s`, utils.ShQuote(environs.DataDir), utils.ShQuote(string(yaml)), utils.ShQuote(_MAASInstanceFilename)) return script, nil }
// Remove implements storage.StorageWriter.Remove func (s *SSHStorage) Remove(name string) error { path, err := s.path(name) if err != nil { return err } path = utils.ShQuote(path) _, err = s.runf(flockExclusive, "rm -f %s", path) return err }
func (c *configInternal) WriteCommands() ([]string, error) { data, err := c.fileContents() if err != nil { return nil, err } commands := []string{"mkdir -p " + utils.ShQuote(c.Dir())} commands = append(commands, writeFileCommands(c.File(agentConfigFilename), data, 0600)...) return commands, nil }
// Put implements storage.StorageWriter.Put func (s *SSHStorage) Put(name string, r io.Reader, length int64) error { logger.Debugf("putting %q (len %d) to storage", name, length) path, err := s.path(name) if err != nil { return err } path = utils.ShQuote(path) tmpdir := utils.ShQuote(s.tmpdir) // Write to a temporary file ($TMPFILE), then mv atomically. command := fmt.Sprintf("mkdir -p `dirname %s` && cat >| $TMPFILE", path) command = fmt.Sprintf( "TMPFILE=`mktemp --tmpdir=%s` && ((%s && mv $TMPFILE %s) || rm -f $TMPFILE)", tmpdir, command, path, ) _, err = s.run(flockExclusive, command+"\n", r, length) return err }
// Updates proxy settings used when rendering the conf as a script func (cfg *ubuntuCloudConfig) updateProxySettings(proxySettings proxy.Settings) { // Write out the apt proxy settings if (proxySettings != proxy.Settings{}) { filename := config.AptProxyConfigFile cfg.AddBootCmd(fmt.Sprintf( `printf '%%s\n' %s > %s`, utils.ShQuote(cfg.paccmder.ProxyConfigContents(proxySettings)), filename)) } }
func GetCheckNonceCommand(instanceConfig *instancecfg.InstanceConfig) string { // Each attempt to connect to an address must verify the machine is the // bootstrap machine by checking its nonce file exists and contains the // nonce in the InstanceConfig. This also blocks sshinit from proceeding // until cloud-init has completed, which is necessary to ensure apt // invocations don't trample each other. nonceFile := utils.ShQuote(path.Join(instanceConfig.DataDir, cloudconfig.NonceFile)) checkNonceCommand := fmt.Sprintf(` noncefile=%s if [ ! -e "$noncefile" ]; then echo "$noncefile does not exist" >&2 exit 1 fi content=$(cat $noncefile) if [ "$content" != %s ]; then echo "$noncefile contents do not match machine nonce" >&2 exit 1 fi `, nonceFile, utils.ShQuote(instanceConfig.MachineNonce)) return checkNonceCommand }
// addFile is a helper function returns all the required shell commands to write // a file (be it text or binary) with regards to the given parameters // NOTE: if the file already exists, it will be overwritten. func addFileCmds(filename string, data []byte, mode uint, binary bool) []string { // Note: recent versions of cloud-init have the "write_files" // module, which can write arbitrary files. We currently support // 12.04 LTS, which uses an older version of cloud-init without // this module. // TODO (aznashwan): eagerly await 2017 and to do the right thing here p := utils.ShQuote(filename) cmds := []string{fmt.Sprintf("install -D -m %o /dev/null %s", mode, p)} // Don't use the shell's echo builtin here; the interpretation // of escape sequences differs between shells, namely bash and // dash. Instead, we use printf (or we could use /bin/echo). if binary { encoded := base64.StdEncoding.EncodeToString(data) cmds = append(cmds, fmt.Sprintf(`printf %%s %s | base64 -d > %s`, encoded, p)) } else { cmds = append(cmds, fmt.Sprintf(`printf '%%s\n' %s > %s`, utils.ShQuote(string(data)), p)) } return cmds }
// runViaSSH runs script in the remote machine with address addr. func runViaSSH(addr string, script string) error { // This is taken from cmd/juju/ssh.go there is no other clear way to set user userAddr := "ubuntu@" + addr sshOptions := ssh.Options{} sshOptions.SetIdentities("/var/lib/juju/system-identity") userCmd := sshCommand(userAddr, []string{"sudo", "-n", "bash", "-c " + utils.ShQuote(script)}, &sshOptions) var stderrBuf bytes.Buffer userCmd.Stderr = &stderrBuf if err := userCmd.Run(); err != nil { return errors.Annotatef(err, "ssh command failed: %q", stderrBuf.String()) } return nil }
// RunOnAllMachines attempts to run the specified command on all the machines. func (c *Client) RunOnAllMachines(run params.RunParams) (params.RunResults, error) { machines, err := c.api.state.AllMachines() if err != nil { return params.RunResults{}, err } var params []*RemoteExec quotedCommands := utils.ShQuote(run.Commands) command := fmt.Sprintf("juju-run --no-context %s", quotedCommands) for _, machine := range machines { params = append(params, remoteParamsForMachine(machine, command, run.Timeout)) } return ParallelExecute(c.getDataDir(), params), nil }
// InitUbuntuUser adds the ubuntu user if it doesn't // already exist, updates its ~/.ssh/authorized_keys, // and enables passwordless sudo for it. // // InitUbuntuUser will initially attempt to login as // the ubuntu user, and verify that passwordless sudo // is enabled; only if this is false will there be an // attempt with the specified login. // // authorizedKeys may be empty, in which case the file // will be created and left empty. // // stdin and stdout will be used for remote sudo prompts, // if the ubuntu user must be created/updated. func InitUbuntuUser(host, login, authorizedKeys string, stdin io.Reader, stdout io.Writer) error { logger.Infof("initialising %q, user %q", host, login) // To avoid unnecessary prompting for the specified login, // initUbuntuUser will first attempt to ssh to the machine // as "ubuntu" with password authentication disabled, and // ensure that it can use sudo without a password. // // Note that we explicitly do not allocate a PTY, so we // get a failure if sudo prompts. cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, nil) if cmd.Run() == nil { logger.Infof("ubuntu user is already initialised") return nil } // Failed to login as ubuntu (or passwordless sudo is not enabled). // Use specified login, and execute the initUbuntuScript below. if login != "" { host = login + "@" + host } script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(authorizedKeys)) var options ssh.Options options.AllowPasswordAuthentication() options.EnablePTY() cmd = ssh.Command(host, []string{"sudo", "/bin/bash -c " + utils.ShQuote(script)}, &options) var stderr bytes.Buffer cmd.Stdin = stdin cmd.Stdout = stdout // for sudo prompt cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if stderr.Len() != 0 { err = fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String())) } return err } return nil }
// DumpFileOnErrorScript returns a bash script that // may be used to dump the contents of the specified // file to stderr when the shell exits with an error. func DumpFileOnErrorScript(filename string) string { script := ` dump_file() { code=$? if [ $code -ne 0 -a -e %s ]; then cat %s >&2 fi exit $code } trap dump_file EXIT `[1:] filename = utils.ShQuote(filename) return fmt.Sprintf(script, filename, filename) }