launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/manual/init.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package manual
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"launchpad.net/juju-core/instance"
    15  	"launchpad.net/juju-core/utils"
    16  	"launchpad.net/juju-core/utils/ssh"
    17  )
    18  
    19  // checkProvisionedScript is the script to run on the remote machine
    20  // to check if a machine has already been provisioned.
    21  //
    22  // This is a little convoluted to avoid returning an error in the
    23  // common case of no matching files.
    24  const checkProvisionedScript = "ls /etc/init/ | grep juju.*\\.conf || exit 0"
    25  
    26  // checkProvisioned checks if any juju upstart jobs already
    27  // exist on the host machine.
    28  func checkProvisioned(host string) (bool, error) {
    29  	logger.Infof("Checking if %s is already provisioned", host)
    30  	cmd := ssh.Command("ubuntu@"+host, []string{"/bin/bash"}, nil)
    31  	var stdout, stderr bytes.Buffer
    32  	cmd.Stdout = &stdout
    33  	cmd.Stderr = &stderr
    34  	cmd.Stdin = strings.NewReader(checkProvisionedScript)
    35  	if err := cmd.Run(); err != nil {
    36  		if stderr.Len() != 0 {
    37  			err = fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String()))
    38  		}
    39  		return false, err
    40  	}
    41  	output := strings.TrimSpace(stdout.String())
    42  	provisioned := len(output) > 0
    43  	if provisioned {
    44  		logger.Infof("%s is already provisioned [%q]", host, output)
    45  	} else {
    46  		logger.Infof("%s is not provisioned", host)
    47  	}
    48  	return provisioned, nil
    49  }
    50  
    51  // DetectSeriesAndHardwareCharacteristics detects the OS
    52  // series and hardware characteristics of the remote machine
    53  // by connecting to the machine and executing a bash script.
    54  func DetectSeriesAndHardwareCharacteristics(host string) (hc instance.HardwareCharacteristics, series string, err error) {
    55  	logger.Infof("Detecting series and characteristics on %s", host)
    56  	cmd := ssh.Command("ubuntu@"+host, []string{"/bin/bash"}, nil)
    57  	var stdout, stderr bytes.Buffer
    58  	cmd.Stdout = &stdout
    59  	cmd.Stderr = &stderr
    60  	cmd.Stdin = bytes.NewBufferString(detectionScript)
    61  	if err := cmd.Run(); err != nil {
    62  		if stderr.Len() != 0 {
    63  			err = fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String()))
    64  		}
    65  		return hc, "", err
    66  	}
    67  	lines := strings.Split(stdout.String(), "\n")
    68  	series = strings.TrimSpace(lines[0])
    69  
    70  	// Normalise arch.
    71  	arch := strings.TrimSpace(lines[1])
    72  	for _, re := range archREs {
    73  		if re.Match([]byte(arch)) {
    74  			hc.Arch = &re.arch
    75  			break
    76  		}
    77  	}
    78  	if hc.Arch == nil {
    79  		err = fmt.Errorf("unrecognised architecture: %s", arch)
    80  		return hc, "", err
    81  	}
    82  
    83  	// HardwareCharacteristics wants memory in megabytes,
    84  	// meminfo reports it in kilobytes.
    85  	memkB := strings.Fields(lines[2])[1] // "MemTotal: NNN kB"
    86  	hc.Mem = new(uint64)
    87  	*hc.Mem, err = strconv.ParseUint(memkB, 10, 0)
    88  	*hc.Mem /= 1024
    89  
    90  	// For each "physical id", count the number of cores.
    91  	// This way we only count physical cores, not additional
    92  	// logical cores due to hyperthreading.
    93  	recorded := make(map[string]bool)
    94  	var physicalId string
    95  	hc.CpuCores = new(uint64)
    96  	for _, line := range lines[3:] {
    97  		if strings.HasPrefix(line, "physical id") {
    98  			physicalId = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
    99  		} else if strings.HasPrefix(line, "cpu cores") {
   100  			var cores uint64
   101  			value := strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
   102  			if cores, err = strconv.ParseUint(value, 10, 0); err != nil {
   103  				return hc, "", err
   104  			}
   105  			if !recorded[physicalId] {
   106  				*hc.CpuCores += cores
   107  				recorded[physicalId] = true
   108  			}
   109  		}
   110  	}
   111  	if *hc.CpuCores == 0 {
   112  		// In the case of a single-core, non-HT CPU, we'll see no
   113  		// "physical id" or "cpu cores" lines.
   114  		*hc.CpuCores = 1
   115  	}
   116  
   117  	// TODO(axw) calculate CpuPower. What algorithm do we use?
   118  	logger.Infof("series: %s, characteristics: %s", series, hc)
   119  	return hc, series, nil
   120  }
   121  
   122  // archREs maps regular expressions for matching
   123  // `uname -m` to architectures recognised by Juju.
   124  var archREs = []struct {
   125  	*regexp.Regexp
   126  	arch string
   127  }{
   128  	{regexp.MustCompile("amd64|x86_64"), "amd64"},
   129  	{regexp.MustCompile("i[3-9]86"), "i386"},
   130  	{regexp.MustCompile("armv.*"), "arm"},
   131  }
   132  
   133  const detectionScript = `#!/bin/bash
   134  set -e
   135  lsb_release -cs
   136  uname -m
   137  grep MemTotal /proc/meminfo
   138  cat /proc/cpuinfo`
   139  
   140  // InitUbuntuUser adds the ubuntu user if it doesn't
   141  // already exist, updates its ~/.ssh/authorized_keys,
   142  // and enables passwordless sudo for it.
   143  //
   144  // InitUbuntuUser will initially attempt to login as
   145  // the ubuntu user, and verify that passwordless sudo
   146  // is enabled; only if this is false will there be an
   147  // attempt with the specified login.
   148  //
   149  // authorizedKeys may be empty, in which case the file
   150  // will be created and left empty.
   151  //
   152  // stdin and stdout will be used for remote sudo prompts,
   153  // if the ubuntu user must be created/updated.
   154  func InitUbuntuUser(host, login, authorizedKeys string, stdin io.Reader, stdout io.Writer) error {
   155  	logger.Infof("initialising %q, user %q", host, login)
   156  
   157  	// To avoid unnecessary prompting for the specified login,
   158  	// initUbuntuUser will first attempt to ssh to the machine
   159  	// as "ubuntu" with password authentication disabled, and
   160  	// ensure that it can use sudo without a password.
   161  	//
   162  	// Note that we explicitly do not allocate a PTY, so we
   163  	// get a failure if sudo prompts.
   164  	cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, nil)
   165  	if cmd.Run() == nil {
   166  		logger.Infof("ubuntu user is already initialised")
   167  		return nil
   168  	}
   169  
   170  	// Failed to login as ubuntu (or passwordless sudo is not enabled).
   171  	// Use specified login, and execute the initUbuntuScript below.
   172  	if login != "" {
   173  		host = login + "@" + host
   174  	}
   175  	script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(authorizedKeys))
   176  	var options ssh.Options
   177  	options.AllowPasswordAuthentication()
   178  	options.EnablePTY()
   179  	cmd = ssh.Command(host, []string{"sudo", "/bin/bash -c " + utils.ShQuote(script)}, &options)
   180  	var stderr bytes.Buffer
   181  	cmd.Stdin = stdin
   182  	cmd.Stdout = stdout // for sudo prompt
   183  	cmd.Stderr = &stderr
   184  	if err := cmd.Run(); err != nil {
   185  		if stderr.Len() != 0 {
   186  			err = fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String()))
   187  		}
   188  		return err
   189  	}
   190  	return nil
   191  }
   192  
   193  const initUbuntuScript = `
   194  set -e
   195  (id ubuntu &> /dev/null) || useradd -m ubuntu
   196  umask 0077
   197  temp=$(mktemp)
   198  echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > $temp
   199  install -m 0440 $temp /etc/sudoers.d/90-juju-ubuntu
   200  rm $temp
   201  su ubuntu -c 'install -D -m 0600 /dev/null ~/.ssh/authorized_keys'
   202  export authorized_keys=%s
   203  if [ ! -z "$authorized_keys" ]; then
   204      su ubuntu -c 'printf "%%s\n" "$authorized_keys" >> ~/.ssh/authorized_keys'
   205  fi`