// 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 (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 }
// 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 }
// 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 }
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, errors.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, } }
// 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 proxy.Settings, ) ([]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 }
// RemoveAll implements storage.StorageWriter.RemoveAll func (s *SSHStorage) RemoveAll() error { _, err := s.runf(flockExclusive, "rm -fr %s/*", utils.ShQuote(s.remotepath)) return err }
} // 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,
func shquote(p string) string { return utils.ShQuote(p) }