github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cloudconfig/userdatacfg.go (about)

     1  // Copyright 2012, 2013, 2014, 2015 Canonical Ltd.
     2  // Copyright 2014, 2015 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package cloudconfig
     6  
     7  import (
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/os"
    12  	"github.com/juju/os/series"
    13  	"github.com/juju/utils"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/agent"
    17  	"github.com/juju/juju/cloudconfig/cloudinit"
    18  	"github.com/juju/juju/cloudconfig/instancecfg"
    19  )
    20  
    21  const (
    22  	// fileSchemePrefix is the prefix for file:// URLs.
    23  	fileSchemePrefix  = "file://"
    24  	httpSchemePrefix  = "http://"
    25  	httpsSchemePrefix = "https://"
    26  
    27  	// NonceFile is written by cloud-init as the last thing it does.
    28  	// The file will contain the machine's nonce. The filename is
    29  	// relative to the Juju data-dir.
    30  	NonceFile = "nonce.txt"
    31  )
    32  
    33  // UserdataConfig is the bridge between instancecfg and cloudinit
    34  // It supports different levels of configuration for instances
    35  type UserdataConfig interface {
    36  	// Configure is a convenience function that updates the cloudinit.Config
    37  	// with appropriate configuration. It will run ConfigureBasic() and
    38  	// ConfigureJuju()
    39  	Configure() error
    40  
    41  	// ConfigureBasic updates the provided cloudinit.Config with
    42  	// basic configuration to initialise an OS image.
    43  	ConfigureBasic() error
    44  
    45  	// ConfigureJuju updates the provided cloudinit.Config with configuration
    46  	// to initialise a Juju machine agent.
    47  	ConfigureJuju() error
    48  
    49  	// ConfigureCustomOverrides updates the provided cloudinit.Config with
    50  	// user provided cloudinit data.  Data provided will overwrite current
    51  	// values with three exceptions: preruncmd was handled in ConfigureBasic()
    52  	// and packages and postruncmd were handled in ConfigureJuju().
    53  	ConfigureCustomOverrides() error
    54  }
    55  
    56  // NewUserdataConfig is supposed to take in an instanceConfig as well as a
    57  // cloudinit.cloudConfig and add attributes in the cloudinit structure based on
    58  // the values inside instanceConfig and on the series
    59  func NewUserdataConfig(icfg *instancecfg.InstanceConfig, conf cloudinit.CloudConfig) (UserdataConfig, error) {
    60  	// TODO(ericsnow) bug #1426217
    61  	// Protect icfg and conf better.
    62  	operatingSystem, err := series.GetOSFromSeries(icfg.Series)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	base := baseConfigure{
    68  		tag:  names.NewMachineTag(icfg.MachineId),
    69  		icfg: icfg,
    70  		conf: conf,
    71  		os:   operatingSystem,
    72  	}
    73  
    74  	switch operatingSystem {
    75  	case os.Ubuntu:
    76  		return &unixConfigure{base}, nil
    77  	case os.CentOS:
    78  		return &unixConfigure{base}, nil
    79  	case os.OpenSUSE:
    80  		return &unixConfigure{base}, nil
    81  	case os.Windows:
    82  		return &windowsConfigure{base}, nil
    83  	default:
    84  		return nil, errors.NotSupportedf("OS %s", icfg.Series)
    85  	}
    86  }
    87  
    88  type baseConfigure struct {
    89  	tag  names.Tag
    90  	icfg *instancecfg.InstanceConfig
    91  	conf cloudinit.CloudConfig
    92  	os   os.OSType
    93  }
    94  
    95  // addAgentInfo adds agent-required information to the agent's directory
    96  // and returns the agent directory name.
    97  func (c *baseConfigure) addAgentInfo(tag names.Tag) (agent.Config, error) {
    98  	acfg, err := c.icfg.AgentConfig(tag, c.icfg.AgentVersion().Number)
    99  	if err != nil {
   100  		return nil, errors.Trace(err)
   101  	}
   102  	acfg.SetValue(agent.AgentServiceName, c.icfg.MachineAgentServiceName)
   103  	cmds, err := acfg.WriteCommands(c.conf.ShellRenderer())
   104  	if err != nil {
   105  		return nil, errors.Annotate(err, "failed to write commands")
   106  	}
   107  	c.conf.AddScripts(cmds...)
   108  	return acfg, nil
   109  }
   110  
   111  func (c *baseConfigure) addMachineAgentToBoot() error {
   112  	svc, err := c.icfg.InitService(c.conf.ShellRenderer())
   113  	if err != nil {
   114  		return errors.Trace(err)
   115  	}
   116  
   117  	// Make the agent run via a symbolic link to the actual tools
   118  	// directory, so it can upgrade itself without needing to change
   119  	// the init script.
   120  	toolsDir := c.icfg.ToolsDir(c.conf.ShellRenderer())
   121  	c.conf.AddScripts(c.toolsSymlinkCommand(toolsDir))
   122  
   123  	name := c.tag.String()
   124  	cmds, err := svc.InstallCommands()
   125  	if err != nil {
   126  		return errors.Annotatef(err, "cannot make cloud-init init script for the %s agent", name)
   127  	}
   128  	startCmds, err := svc.StartCommands()
   129  	if err != nil {
   130  		return errors.Annotatef(err, "cannot make cloud-init init script for the %s agent", name)
   131  	}
   132  	cmds = append(cmds, startCmds...)
   133  
   134  	svcName := c.icfg.MachineAgentServiceName
   135  	// TODO (gsamfira): This is temporary until we find a cleaner way to fix
   136  	// cloudinit.LogProgressCmd to not add >&9 on Windows.
   137  	targetOS, err := series.GetOSFromSeries(c.icfg.Series)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	if targetOS != os.Windows {
   142  		c.conf.AddRunCmd(cloudinit.LogProgressCmd("Starting Juju machine agent (service %s)", svcName))
   143  	}
   144  	c.conf.AddScripts(cmds...)
   145  	return nil
   146  }
   147  
   148  // SetUbuntuUser creates an "ubuntu" use for unix systems so the juju client
   149  // can access the machine using ssh with the configuration we expect.
   150  // On precise, the default cloudinit version is too old to support the users
   151  // option, so instead rely on the default user being created and adding keys.
   152  // It may make sense in the future to add a "juju" user instead across
   153  // all distributions.
   154  func SetUbuntuUser(conf cloudinit.CloudConfig, authorizedKeys string) {
   155  	targetSeries := conf.GetSeries()
   156  	if targetSeries == "precise" {
   157  		conf.SetSSHAuthorizedKeys(authorizedKeys)
   158  	} else {
   159  		var groups []string
   160  		targetOS, _ := series.GetOSFromSeries(targetSeries)
   161  		switch targetOS {
   162  		case os.Ubuntu:
   163  			groups = UbuntuGroups
   164  		case os.CentOS:
   165  			groups = CentOSGroups
   166  		case os.OpenSUSE:
   167  			groups = OpenSUSEGroups
   168  		}
   169  		conf.AddUser(&cloudinit.User{
   170  			Name:              "ubuntu",
   171  			Groups:            groups,
   172  			Shell:             "/bin/bash",
   173  			Sudo:              []string{"ALL=(ALL) NOPASSWD:ALL"},
   174  			SSHAuthorizedKeys: authorizedKeys,
   175  		})
   176  	}
   177  }
   178  
   179  // TODO(ericsnow) toolsSymlinkCommand should just be replaced with a
   180  // call to shell.Renderer.Symlink.
   181  
   182  func (c *baseConfigure) toolsSymlinkCommand(toolsDir string) string {
   183  	switch c.os {
   184  	case os.Windows:
   185  		return fmt.Sprintf(
   186  			`cmd.exe /C mklink /D %s %v`,
   187  			c.conf.ShellRenderer().FromSlash(toolsDir),
   188  			c.icfg.AgentVersion(),
   189  		)
   190  	default:
   191  		// TODO(dfc) ln -nfs, so it doesn't fail if for some reason that
   192  		// the target already exists.
   193  		return fmt.Sprintf(
   194  			"ln -s %v %s",
   195  			c.icfg.AgentVersion(),
   196  			shquote(toolsDir),
   197  		)
   198  	}
   199  }
   200  
   201  func shquote(p string) string {
   202  	return utils.ShQuote(p)
   203  }