// 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 int, withHA bool) (*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" + " --bind_ip 0.0.0.0" + " --port " + fmt.Sprint(port) + " --noprealloc" + " --syslog" + " --smallfiles" + " --keyFile " + utils.ShQuote(sharedSecretPath(dataDir)) if withHA { mongoCmd += " --replSet " + ReplicaSetName } 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 }
// 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 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 (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 }
// 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 }
// 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.api.dataDir, params), nil }
// 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 }
// 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.api.dataDir, 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) }
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 cmd := ssh.Command(userAddr, []string{"sudo", "-n", "bash", "-c " + utils.ShQuote(script)}, nil) var stderrBuf bytes.Buffer var stdoutBuf bytes.Buffer cmd.Stderr = &stderrBuf cmd.Stdout = &stdoutBuf err := cmd.Run() if err != nil { return fmt.Errorf("ssh command failed: %v (%q)", err, stderrBuf.String()) } progress("ssh command succedded: %q", stdoutBuf.String()) return nil }
func cmdlist(cmds []interface{}) ([]string, error) { result := make([]string, 0, len(cmds)) for _, cmd := range cmds { switch cmd := cmd.(type) { case []string: // Quote args, so shell meta-characters are not interpreted. for i, arg := range cmd[1:] { cmd[i] = utils.ShQuote(arg) } result = append(result, strings.Join(cmd, " ")) case string: result = append(result, cmd) default: return nil, fmt.Errorf("unexpected command type: %T", cmd) } } return result, nil }
// Get implements storage.StorageReader.Get. func (s *SSHStorage) Get(name string) (io.ReadCloser, error) { logger.Debugf("getting %q from storage", name) path, err := s.path(name) if err != nil { return nil, err } out, err := s.runf(flockShared, "base64 < %s", utils.ShQuote(path)) if err != nil { err := err.(SSHStorageError) if strings.Contains(err.Output, "No such file") { return nil, coreerrors.NewNotFound(err, "") } return nil, err } decoded, err := base64.StdEncoding.DecodeString(out) if err != nil { return nil, err } return ioutil.NopCloser(bytes.NewBuffer(decoded)), nil }
// MachineAgentUpstartService returns the upstart config for a machine agent // based on the tag and machineId passed in. func MachineAgentUpstartService(name, toolsDir, dataDir, logDir, tag, machineId string, env map[string]string) *Conf { svc := NewService(name) logFile := path.Join(logDir, tag+".log") // The machine agent always starts with debug turned on. The logger worker // will update this to the system logging environment as soon as it starts. return &Conf{ Service: *svc, Desc: fmt.Sprintf("juju %s agent", tag), Limit: map[string]string{ "nofile": fmt.Sprintf("%d %d", maxAgentFiles, maxAgentFiles), }, Cmd: path.Join(toolsDir, "jujud") + " machine" + " --data-dir " + utils.ShQuote(dataDir) + " --machine-id " + machineId + " --debug", Out: logFile, Env: env, } }
// 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() if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { return "", err } // Explicitly disabling apt_upgrade so as not to trample // the target machine's existing configuration. cloudcfg.SetAptUpgrade(false) configScript, err := sshinit.ConfigureScript(cloudcfg) if err != nil { return "", err } 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 }
// List implements storage.StorageReader.List. func (s *SSHStorage) List(prefix string) ([]string, error) { remotepath, err := s.path(prefix) if err != nil { return nil, err } dir, prefix := path.Split(remotepath) quotedDir := utils.ShQuote(dir) out, err := s.runf(flockShared, "(test -d %s && find %s -type f) || true", quotedDir, quotedDir) if err != nil { return nil, err } if out == "" { return nil, nil } var names []string for _, name := range strings.Split(out, "\n") { if strings.HasPrefix(name[len(dir):], prefix) { names = append(names, name[len(s.remotepath)+1:]) } } sort.Strings(names) return names, 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 osenv.ProxySettings, ) ([]byte, error) { config := coreCloudinit.New() config.AddScripts( "set -xe", // ensure we run all the scripts or abort. ) config.AddSSHAuthorizedKeys(authorizedKeys) cloudinit.MaybeAddCloudArchiveCloudTools(config, series) cloudinit.AddAptCommands(aptProxy, config) config.AddScripts( fmt.Sprintf( "printf '%%s\n' %s > %s", utils.ShQuote(templateShutdownUpstartScript), templateShutdownUpstartFilename, )) data, err := config.Render() if err != nil { return nil, err } return data, nil }
func shquote(p string) string { return utils.ShQuote(p) }
} // FinishBootstrap completes the bootstrap process by connecting // to the instance via SSH and carrying out the cloud-config. // // Note: FinishBootstrap is exposed so it can be replaced for testing. var FinishBootstrap = func(ctx environs.BootstrapContext, client ssh.Client, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error { interrupted := make(chan os.Signal, 1) ctx.InterruptNotify(interrupted) defer ctx.StopInterruptNotify(interrupted) // 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 MachineConfig. 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(machineConfig.DataDir, cloudinit.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(machineConfig.MachineNonce)) addr, err := waitSSH( ctx, interrupted,
// RemoveAll implements storage.StorageWriter.RemoveAll func (s *SSHStorage) RemoveAll() error { _, err := s.runf(flockExclusive, "rm -fr %s/*", utils.ShQuote(s.remotepath)) return err }