github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/cloudconfig/cloudinit/cloudinit.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Copyright 2015 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package cloudinit
     6  
     7  import (
     8  	"strings"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/packaging/v3/commands"
    12  	"github.com/juju/packaging/v3/config"
    13  	"github.com/juju/utils/v3/shell"
    14  	"github.com/juju/utils/v3/ssh"
    15  
    16  	jujupackaging "github.com/juju/juju/packaging"
    17  )
    18  
    19  // cloudConfig represents a set of cloud-init configuration options.
    20  type cloudConfig struct {
    21  	// osName is the os for which this cloudConfig is made for.
    22  	osName string
    23  
    24  	// paccmder is a map containing PackageCommander instances for all
    25  	// package managers supported by a particular target.
    26  	paccmder map[jujupackaging.PackageManagerName]commands.PackageCommander
    27  
    28  	// pacconfer is a map containing PackagingConfigurer instances for all
    29  	// package managers supported by a particular target.
    30  	pacconfer map[jujupackaging.PackageManagerName]config.PackagingConfigurer
    31  
    32  	// renderer is the shell Renderer for this cloudConfig.
    33  	renderer shell.Renderer
    34  
    35  	// fileTransporter allows the cloud config to optionally emit files that it
    36  	// wants to be planted on the target machine.
    37  	fileTransporter FileTransporter
    38  
    39  	// attrs is the map of options set on this cloudConfig.
    40  	// main attributes used in the options map and their corresponding types:
    41  	//
    42  	// user					string
    43  	// package_update		bool
    44  	// package_upgrade		bool
    45  	// packages				[]string
    46  	// runcmd				[]string
    47  	// bootcmd				[]string
    48  	// disable_ec2_metadata	bool
    49  	// final_message		string
    50  	// locale				string
    51  	// mounts				[][]string
    52  	// output				map[OutputKind]string
    53  	// shh_keys				map[SSHKeyType]string
    54  	// ssh_authorized_keys	[]string
    55  	// disable_root			bool
    56  	//
    57  	// used only for Ubuntu but implemented as runcmds on CentOS:
    58  	// apt_proxy			string
    59  	// apt_mirror			string/bool
    60  	// apt_sources			[]*packaging.Source
    61  	//
    62  	// instead, the following corresponding options are used temporarily,
    63  	// but are translated to runcmds and removed right before rendering:
    64  	// package_proxy
    65  	// package_mirror
    66  	// package_sources
    67  	// package_preferences
    68  	//
    69  	// old TODO's:
    70  	// byobu
    71  	// grub_dpkg
    72  	// mcollective
    73  	// phone_home
    74  	// puppet
    75  	// resizefs
    76  	// rightscale_userdata
    77  	// scripts_per_boot
    78  	// scripts_per_instance
    79  	// scripts_per_once
    80  	// scripts_user
    81  	// set_hostname
    82  	// set_passwords
    83  	// ssh_import_id
    84  	// timezone
    85  	// update_etc_hosts
    86  	// update_hostname
    87  	attrs map[string]interface{}
    88  
    89  	// omitNetplanHWAddrMatch if true, causes Netplan to be rendered without
    90  	// a stanza that matches by MAC address in order to apply configuration to
    91  	// a device.
    92  	// This will be recruited for LXD, where we have observed 22.04 containers
    93  	// being assigned a different MAC to the one configured.
    94  	// For these cases we fall back to the default match by ID (name).
    95  	// MAC address matching is still required by KVM where devices are assigned
    96  	// different names by the kernel to those we configured.
    97  	omitNetplanHWAddrMatch bool
    98  }
    99  
   100  // getPackagingConfigurer is defined on the AdvancedPackagingConfig interface.
   101  func (cfg *cloudConfig) getPackagingConfigurer(mgrName jujupackaging.PackageManagerName) config.PackagingConfigurer {
   102  	if cfg.pacconfer == nil {
   103  		return nil
   104  	}
   105  	return cfg.pacconfer[mgrName]
   106  }
   107  
   108  // GetOS is defined on the CloudConfig interface.
   109  func (cfg *cloudConfig) GetOS() string {
   110  	return cfg.osName
   111  }
   112  
   113  // SetAttr is defined on the CloudConfig interface.
   114  func (cfg *cloudConfig) SetAttr(name string, value interface{}) {
   115  	cfg.attrs[name] = value
   116  }
   117  
   118  // UnsetAttr is defined on the CloudConfig interface.
   119  func (cfg *cloudConfig) UnsetAttr(name string) {
   120  	delete(cfg.attrs, name)
   121  }
   122  
   123  func (cfg *cloudConfig) SetFileTransporter(ft FileTransporter) {
   124  	cfg.fileTransporter = ft
   125  }
   126  
   127  func annotateKeys(rawKeys string) []string {
   128  	cfgKeys := []string{}
   129  	keys := ssh.SplitAuthorisedKeys(rawKeys)
   130  	for _, key := range keys {
   131  		// ensure our keys have "Juju:" prepended to differentiate
   132  		// Juju-managed keys and externally added ones
   133  		jujuKey := ssh.EnsureJujuComment(key)
   134  		cfgKeys = append(cfgKeys, jujuKey)
   135  	}
   136  	return cfgKeys
   137  }
   138  
   139  // AddUser is defined on the UsersConfig interface.
   140  func (cfg *cloudConfig) AddUser(user *User) {
   141  	users, _ := cfg.attrs["users"].([]map[string]interface{})
   142  	newUser := map[string]interface{}{
   143  		"name":        user.Name,
   144  		"lock_passwd": true,
   145  	}
   146  	if user.Groups != nil {
   147  		newUser["groups"] = user.Groups
   148  	}
   149  	if user.Shell != "" {
   150  		newUser["shell"] = user.Shell
   151  	}
   152  	if user.SSHAuthorizedKeys != "" {
   153  		newUser["ssh_authorized_keys"] = annotateKeys(user.SSHAuthorizedKeys)
   154  	}
   155  	if user.Sudo != "" {
   156  		newUser["sudo"] = user.Sudo
   157  	}
   158  	cfg.SetAttr("users", append(users, newUser))
   159  }
   160  
   161  // UnsetUsers is defined on the UsersConfig interface.
   162  func (cfg *cloudConfig) UnsetUsers() {
   163  	cfg.UnsetAttr("users")
   164  }
   165  
   166  // SetSystemUpdate is defined on the SystemUpdateConfig interface.
   167  func (cfg *cloudConfig) SetSystemUpdate(yes bool) {
   168  	cfg.SetAttr("package_update", yes)
   169  }
   170  
   171  // UnsetSystemUpdate is defined on the SystemUpdateConfig interface.
   172  func (cfg *cloudConfig) UnsetSystemUpdate() {
   173  	cfg.UnsetAttr("package_update")
   174  }
   175  
   176  // SystemUpdate is defined on the SystemUpdateConfig interface.
   177  func (cfg *cloudConfig) SystemUpdate() bool {
   178  	update, _ := cfg.attrs["package_update"].(bool)
   179  	return update
   180  }
   181  
   182  // SetSystemUpgrade is defined on the SystemUpgradeConfig interface.
   183  func (cfg *cloudConfig) SetSystemUpgrade(yes bool) {
   184  	cfg.SetAttr("package_upgrade", yes)
   185  }
   186  
   187  // UnsetSystemUpgrade is defined on the SystemUpgradeConfig interface.
   188  func (cfg *cloudConfig) UnsetSystemUpgrade() {
   189  	cfg.UnsetAttr("package_upgrade")
   190  }
   191  
   192  // SystemUpgrade is defined on the SystemUpgradeConfig interface.
   193  func (cfg *cloudConfig) SystemUpgrade() bool {
   194  	upgrade, _ := cfg.attrs["package_upgrade"].(bool)
   195  	return upgrade
   196  }
   197  
   198  // AddPackage is defined on the PackagingConfig interface.
   199  func (cfg *cloudConfig) AddPackage(pack string) {
   200  	cfg.attrs["packages"] = append(cfg.Packages(), pack)
   201  }
   202  
   203  // RemovePackage is defined on the PackagingConfig interface.
   204  func (cfg *cloudConfig) RemovePackage(pack string) {
   205  	cfg.attrs["packages"] = removeStringFromSlice(cfg.Packages(), pack)
   206  }
   207  
   208  // Packages is defined on the PackagingConfig interface.
   209  func (cfg *cloudConfig) Packages() []string {
   210  	packs, _ := cfg.attrs["packages"].([]string)
   211  	return packs
   212  }
   213  
   214  // AddRunCmd is defined on the RunCmdsConfig interface.
   215  func (cfg *cloudConfig) AddRunCmd(args ...string) {
   216  	cfg.attrs["runcmd"] = append(cfg.RunCmds(), strings.Join(args, " "))
   217  }
   218  
   219  // AddScripts is defined on the RunCmdsConfig interface.
   220  func (cfg *cloudConfig) AddScripts(script ...string) {
   221  	for _, line := range script {
   222  		cfg.AddRunCmd(line)
   223  	}
   224  }
   225  
   226  // PrependRunCmd is defined on the RunCmdsConfig interface.
   227  func (cfg *cloudConfig) PrependRunCmd(args ...string) {
   228  	cfg.attrs["runcmd"] = append([]string{strings.Join(args, " ")}, cfg.RunCmds()...)
   229  }
   230  
   231  // RemoveRunCmd is defined on the RunCmdsConfig interface.
   232  func (cfg *cloudConfig) RemoveRunCmd(cmd string) {
   233  	cfg.attrs["runcmd"] = removeStringFromSlice(cfg.RunCmds(), cmd)
   234  }
   235  
   236  // RunCmds is defined on the RunCmdsConfig interface.
   237  func (cfg *cloudConfig) RunCmds() []string {
   238  	cmds, _ := cfg.attrs["runcmd"].([]string)
   239  	return cmds
   240  }
   241  
   242  // AddBootCmd is defined on the BootCmdsConfig interface.
   243  func (cfg *cloudConfig) AddBootCmd(args ...string) {
   244  	cfg.attrs["bootcmd"] = append(cfg.BootCmds(), strings.Join(args, " "))
   245  }
   246  
   247  // RemoveBootCmd is defined on the BootCmdsConfig interface.
   248  func (cfg *cloudConfig) RemoveBootCmd(cmd string) {
   249  	cfg.attrs["bootcmd"] = removeStringFromSlice(cfg.BootCmds(), cmd)
   250  }
   251  
   252  // BootCmds is defined on the BootCmdsConfig interface.
   253  func (cfg *cloudConfig) BootCmds() []string {
   254  	cmds, _ := cfg.attrs["bootcmd"].([]string)
   255  	return cmds
   256  }
   257  
   258  // SetDisableEC2Metadata is defined on the EC2MetadataConfig interface.
   259  func (cfg *cloudConfig) SetDisableEC2Metadata(set bool) {
   260  	cfg.SetAttr("disable_ec2_metadata", set)
   261  }
   262  
   263  // UnsetDisableEC2Metadata is defined on the EC2MetadataConfig interface.
   264  func (cfg *cloudConfig) UnsetDisableEC2Metadata() {
   265  	cfg.UnsetAttr("disable_ec2_metadata")
   266  }
   267  
   268  // DisableEC2Metadata is defined on the EC2MetadataConfig interface.
   269  func (cfg *cloudConfig) DisableEC2Metadata() bool {
   270  	disEC2, _ := cfg.attrs["disable_ec2_metadata"].(bool)
   271  	return disEC2
   272  }
   273  
   274  // SetFinalMessage is defined on the FinalMessageConfig interface.
   275  func (cfg *cloudConfig) SetFinalMessage(message string) {
   276  	cfg.SetAttr("final_message", message)
   277  }
   278  
   279  // UnsetFinalMessage is defined on the FinalMessageConfig interface.
   280  func (cfg *cloudConfig) UnsetFinalMessage() {
   281  	cfg.UnsetAttr("final_message")
   282  }
   283  
   284  // FinalMessage is defined on the FinalMessageConfig interface.
   285  func (cfg *cloudConfig) FinalMessage() string {
   286  	message, _ := cfg.attrs["final_message"].(string)
   287  	return message
   288  }
   289  
   290  // SetLocale is defined on the LocaleConfig interface.
   291  func (cfg *cloudConfig) SetLocale(locale string) {
   292  	cfg.SetAttr("locale", locale)
   293  }
   294  
   295  // UnsetLocale is defined on the LocaleConfig interface.
   296  func (cfg *cloudConfig) UnsetLocale() {
   297  	cfg.UnsetAttr("locale")
   298  }
   299  
   300  // Locale is defined on the LocaleConfig interface.
   301  func (cfg *cloudConfig) Locale() string {
   302  	locale, _ := cfg.attrs["locale"].(string)
   303  	return locale
   304  }
   305  
   306  // AddMount adds takes arguments for installing a mount point in /etc/fstab
   307  // The options are of the order and format specific to fstab entries:
   308  // <device> <mountpoint> <filesystem> <options> <backup setting> <fsck priority>
   309  func (cfg *cloudConfig) AddMount(mount ...string) {
   310  	mounts, _ := cfg.attrs["mounts"].([][]string)
   311  	cfg.SetAttr("mounts", append(mounts, mount))
   312  }
   313  
   314  // SetOutput is defined on the OutputConfig interface.
   315  func (cfg *cloudConfig) SetOutput(kind OutputKind, stdout, stderr string) {
   316  	out, _ := cfg.attrs["output"].(map[string]interface{})
   317  	if out == nil {
   318  		out = make(map[string]interface{})
   319  	}
   320  
   321  	if stderr == "" {
   322  		out[string(kind)] = stdout
   323  	} else {
   324  		out[string(kind)] = []string{stdout, stderr}
   325  	}
   326  
   327  	cfg.SetAttr("output", out)
   328  }
   329  
   330  // Output is defined on the OutputConfig interface.
   331  func (cfg *cloudConfig) Output(kind OutputKind) (stdout, stderr string) {
   332  	if out, ok := cfg.attrs["output"].(map[string]interface{}); ok {
   333  		switch out := out[string(kind)].(type) {
   334  		case string:
   335  			stdout = out
   336  		case []string:
   337  			stdout, stderr = out[0], out[1]
   338  		}
   339  	}
   340  
   341  	return stdout, stderr
   342  }
   343  
   344  // SetSSHAuthorizedKeys is defined on the SSHAuthorizedKeysConfig interface.
   345  func (cfg *cloudConfig) SetSSHAuthorizedKeys(rawKeys string) {
   346  	keys := annotateKeys(rawKeys)
   347  	if len(keys) != 0 {
   348  		cfg.SetAttr("ssh_authorized_keys", keys)
   349  	} else {
   350  		cfg.UnsetAttr("ssh_authorized_keys")
   351  	}
   352  }
   353  
   354  // SetSSHKeys is defined on the SSHKeysConfig interface.
   355  func (cfg *cloudConfig) SetSSHKeys(keys SSHKeys) error {
   356  	if len(keys) == 0 {
   357  		cfg.UnsetAttr("ssh_keys")
   358  		return nil
   359  	}
   360  	attr := make(map[string]interface{})
   361  	for _, key := range keys {
   362  		privateKeyName, publicKeyName, err := NamesForSSHKeyAlgorithm(key.PublicKeyAlgorithm)
   363  		if err != nil {
   364  			return errors.Trace(err)
   365  		}
   366  		attr[string(privateKeyName)] = key.Private
   367  		attr[string(publicKeyName)] = key.Public
   368  	}
   369  	cfg.SetAttr("ssh_keys", attr)
   370  	return nil
   371  }
   372  
   373  // SetDisableRoot is defined on the RootUserConfig interface.
   374  func (cfg *cloudConfig) SetDisableRoot(disable bool) {
   375  	cfg.SetAttr("disable_root", disable)
   376  }
   377  
   378  // UnsetDisableRoot is defined on the RootUserConfig interface.
   379  func (cfg *cloudConfig) UnsetDisableRoot() {
   380  	cfg.UnsetAttr("disable_root")
   381  }
   382  
   383  // DisableRoot is defined on the RootUserConfig interface.
   384  func (cfg *cloudConfig) DisableRoot() bool {
   385  	disable, _ := cfg.attrs["disable_root"].(bool)
   386  	return disable
   387  }
   388  
   389  // ManageEtcHosts enables or disables management of /etc/hosts.
   390  func (cfg *cloudConfig) ManageEtcHosts(manage bool) {
   391  	if manage {
   392  		cfg.SetAttr("manage_etc_hosts", true)
   393  	} else {
   394  		cfg.UnsetAttr("manage_etc_hosts")
   395  	}
   396  }
   397  
   398  // AddRunTextFile is defined on the WrittenFilesConfig interface.
   399  func (cfg *cloudConfig) AddRunTextFile(filename, contents string, perm uint) {
   400  	cfg.AddScripts(addFileCmds(filename, []byte(contents), perm, false)...)
   401  }
   402  
   403  // AddBootTextFile is defined on the WrittenFilesConfig interface.
   404  func (cfg *cloudConfig) AddBootTextFile(filename, contents string, perm uint) {
   405  	for _, cmd := range addFileCmds(filename, []byte(contents), perm, false) {
   406  		cfg.AddBootCmd(cmd)
   407  	}
   408  }
   409  
   410  // AddRunBinaryFile is defined on the WrittenFilesConfig interface.
   411  func (cfg *cloudConfig) AddRunBinaryFile(filename string, data []byte, mode uint) {
   412  	if cfg.fileTransporter != nil {
   413  		source := cfg.fileTransporter.SendBytes(filename, data)
   414  		cfg.AddScripts(addFileCopyCmds(source, filename, mode)...)
   415  		return
   416  	}
   417  	cfg.AddScripts(addFileCmds(filename, data, mode, true)...)
   418  }
   419  
   420  // ShellRenderer is defined on the RenderConfig interface.
   421  func (cfg *cloudConfig) ShellRenderer() shell.Renderer {
   422  	return cfg.renderer
   423  }