github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cloudconfig/userdatacfg_win.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  	"encoding/base64"
     9  	"encoding/json"
    10  	"fmt"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/os/series"
    16  	"github.com/juju/utils/cert"
    17  	"github.com/juju/utils/featureflag"
    18  	"gopkg.in/juju/names.v2"
    19  
    20  	"github.com/juju/juju/juju/osenv"
    21  	"github.com/juju/juju/juju/paths"
    22  	"github.com/juju/juju/tools"
    23  )
    24  
    25  //go:generate go run ../generate/filetoconst/filetoconst.go UserDataScript windowsuserdatafiles/userdata.ps1 winuserdatawrapper.go 2016 cloudconfig
    26  //go:generate go run ../generate/winuserdata/winuserdata.go 2016 winuserdata.go cloudconfig
    27  
    28  type aclType string
    29  
    30  const (
    31  	fileSystem    aclType = "FileSystem"
    32  	registryEntry aclType = "Registry"
    33  )
    34  
    35  type windowsConfigure struct {
    36  	baseConfigure
    37  }
    38  
    39  // Configure updates the provided cloudinit.Config with
    40  // configuration to initialize a Juju machine agent.
    41  func (w *windowsConfigure) Configure() error {
    42  	if err := w.ConfigureBasic(); err != nil {
    43  		return err
    44  	}
    45  	if err := w.ConfigureJuju(); err != nil {
    46  		return err
    47  	}
    48  	return w.ConfigureCustomOverrides()
    49  }
    50  
    51  // ConfigureBasic implements UserdataConfig.ConfigureBasic
    52  func (w *windowsConfigure) ConfigureBasic() error {
    53  
    54  	tmpDir, err := paths.TempDir(w.icfg.Series)
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	renderer := w.conf.ShellRenderer()
    60  	dataDir := renderer.FromSlash(w.icfg.DataDir)
    61  	baseDir := renderer.FromSlash(filepath.Dir(tmpDir))
    62  	binDir := renderer.Join(baseDir, "bin")
    63  
    64  	w.conf.AddScripts(windowsPowershellHelpers)
    65  
    66  	// The jujud user only gets created on non-nano versions for now.
    67  	if !series.IsWindowsNano(w.icfg.Series) {
    68  		w.conf.AddScripts(addJujuUser)
    69  	}
    70  
    71  	w.conf.AddScripts(
    72  		// Some providers create a baseDir before this step, but we need to
    73  		// make sure it exists before applying icacls
    74  		fmt.Sprintf(`mkdir -Force "%s"`, renderer.FromSlash(baseDir)),
    75  		fmt.Sprintf(`mkdir %s`, renderer.FromSlash(tmpDir)),
    76  		fmt.Sprintf(`mkdir "%s"`, binDir),
    77  		fmt.Sprintf(`mkdir "%s\locks"`, renderer.FromSlash(dataDir)),
    78  		`setx /m PATH "$env:PATH;C:\Juju\bin\"`,
    79  		// This is necessary for setACLs to work
    80  		`$adminsGroup = (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")).Translate([System.Security.Principal.NTAccount])`,
    81  		fmt.Sprintf(`icacls "%s" /inheritance:r /grant "${adminsGroup}:(OI)(CI)(F)" /t`, renderer.FromSlash(baseDir)),
    82  	)
    83  
    84  	// TODO(bogdanteleaga): This, together with the call above, should be using setACLs, once it starts working across all windows versions properly.
    85  	// Until then, if we change permissions, both this and setACLs should be changed to do the same thing.
    86  	if !series.IsWindowsNano(w.icfg.Series) {
    87  		w.conf.AddScripts(fmt.Sprintf(`icacls "%s" /inheritance:r /grant "jujud:(OI)(CI)(F)" /t`, renderer.FromSlash(baseDir)))
    88  	}
    89  
    90  	noncefile := renderer.Join(dataDir, NonceFile)
    91  	w.conf.AddScripts(
    92  		fmt.Sprintf(`Set-Content "%s" "%s"`, noncefile, shquote(w.icfg.MachineNonce)),
    93  	)
    94  	return nil
    95  }
    96  
    97  // ConfigureJuju implements UserdataConfig.ConfigureJuju
    98  func (w *windowsConfigure) ConfigureJuju() error {
    99  	if err := w.icfg.VerifyConfig(); err != nil {
   100  		return errors.Trace(err)
   101  	}
   102  	if w.icfg.Controller != nil {
   103  		return errors.Errorf("controllers not supported on windows")
   104  	}
   105  
   106  	tools := w.icfg.ToolsList()[0]
   107  	toolsJson, err := json.Marshal(tools)
   108  	if err != nil {
   109  		return errors.Annotate(err, "while serializing the agent binaries")
   110  	}
   111  
   112  	renderer := w.conf.ShellRenderer()
   113  	w.conf.AddScripts(
   114  		fmt.Sprintf(`$binDir="%s"`, renderer.FromSlash(w.icfg.JujuTools())),
   115  		fmt.Sprintf(`mkdir '%s'`, renderer.FromSlash(w.icfg.LogDir)),
   116  		`mkdir $binDir`,
   117  	)
   118  
   119  	toolsDownloadCmds, err := addDownloadToolsCmds(
   120  		w.icfg.Series, w.icfg.APIInfo.CACert, w.icfg.ToolsList(),
   121  	)
   122  	if err != nil {
   123  		return errors.Trace(err)
   124  	}
   125  	w.conf.AddScripts(toolsDownloadCmds...)
   126  
   127  	w.conf.AddScripts(
   128  		`$dToolsHash = Get-FileSHA256 -FilePath "$binDir\tools.tar.gz"`,
   129  		fmt.Sprintf(`$dToolsHash > "$binDir\juju%s.sha256"`, tools.Version),
   130  		fmt.Sprintf(`if ($dToolsHash.ToLower() -ne "%s"){ Throw "Tools checksum mismatch"}`,
   131  			tools.SHA256),
   132  		fmt.Sprintf(`GUnZip-File -infile $binDir\tools.tar.gz -outdir $binDir`),
   133  		`rm "$binDir\tools.tar*"`,
   134  		fmt.Sprintf(`Set-Content $binDir\downloaded-tools.txt '%s'`, string(toolsJson)),
   135  	)
   136  
   137  	for _, cmd := range createJujuRegistryKeyCmds(w.icfg.Series) {
   138  		w.conf.AddRunCmd(cmd)
   139  	}
   140  
   141  	machineTag := names.NewMachineTag(w.icfg.MachineId)
   142  	_, err = w.addAgentInfo(machineTag)
   143  	if err != nil {
   144  		return errors.Trace(err)
   145  	}
   146  	return w.addMachineAgentToBoot()
   147  }
   148  
   149  // ConfigureCustomOverrides implements UserdataConfig.ConfigureCustomOverrides
   150  func (w *windowsConfigure) ConfigureCustomOverrides() error {
   151  	// TODO HML 2017-12-08
   152  	// Implement for Windows support of model-config cloudinit-userdata.
   153  	return nil
   154  }
   155  
   156  // createJujuRegistryKeyCmds is going to create a juju registry key and set
   157  // permissions on it such that it's only accessible to administrators
   158  func createJujuRegistryKeyCmds(series string) []string {
   159  	aclCmds := setACLs(osenv.JujuRegistryKey, registryEntry, series)
   160  	regCmds := []string{
   161  
   162  		// Create a registry key for storing juju related information
   163  		fmt.Sprintf(`New-Item -Path '%s'`, osenv.JujuRegistryKey),
   164  
   165  		// Create a JUJU_DEV_FEATURE_FLAGS entry which may or may not be empty.
   166  		fmt.Sprintf(`New-ItemProperty -Path '%s' -Name '%s'`,
   167  			osenv.JujuRegistryKey,
   168  			osenv.JujuFeatureFlagEnvKey),
   169  		fmt.Sprintf(`Set-ItemProperty -Path '%s' -Name '%s' -Value '%s'`,
   170  			osenv.JujuRegistryKey,
   171  			osenv.JujuFeatureFlagEnvKey,
   172  			featureflag.AsEnvironmentValue()),
   173  	}
   174  	return append(regCmds[:1], append(aclCmds, regCmds[1:]...)...)
   175  }
   176  
   177  func setACLs(path string, permType aclType, ser string) []string {
   178  	ruleModel := `$rule = New-Object System.Security.AccessControl.%sAccessRule %s`
   179  	permModel := `%s = "%s", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"`
   180  	adminPermVar := `$adminPerm`
   181  	jujudPermVar := `$jujudPerm`
   182  
   183  	rulesToAdd := []string{
   184  		// $adminsGroup must be defined before calling setACLs
   185  		fmt.Sprintf(permModel, adminPermVar, `$adminsGroup`),
   186  		fmt.Sprintf(ruleModel, permType, adminPermVar),
   187  		`$acl.AddAccessRule($rule)`,
   188  	}
   189  
   190  	if !series.IsWindowsNano(ser) {
   191  		jujudUserACLRules := []string{
   192  			fmt.Sprintf(permModel, jujudPermVar, `jujud`),
   193  			fmt.Sprintf(ruleModel, permType, jujudPermVar),
   194  			`$acl.AddAccessRule($rule)`,
   195  		}
   196  
   197  		rulesToAdd = append(rulesToAdd, jujudUserACLRules...)
   198  	}
   199  
   200  	aclCmds := []string{
   201  		fmt.Sprintf(`$acl = Get-Acl -Path '%s'`, path),
   202  
   203  		// Reset the ACL's on it and add administrator access only.
   204  		`$acl.SetAccessRuleProtection($true, $false)`,
   205  
   206  		fmt.Sprintf(`Set-Acl -Path '%s' -AclObject $acl`, path),
   207  	}
   208  
   209  	return append(aclCmds[:2], append(rulesToAdd, aclCmds[2:]...)...)
   210  }
   211  
   212  func addDownloadToolsCmds(ser string, certificate string, toolsList tools.List) ([]string, error) {
   213  	var cmds []string
   214  	var getDownloadFileCmd func(url string) string
   215  	if series.IsWindowsNano(ser) {
   216  		parsedCert, err := cert.ParseCert(certificate)
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		caCert := base64.StdEncoding.EncodeToString(parsedCert.Raw)
   221  		cmds = []string{fmt.Sprintf(`$cacert = "%s"`, caCert),
   222  			`$cert_bytes = $cacert | %{ ,[System.Text.Encoding]::UTF8.GetBytes($_) }`,
   223  			`$cert = new-object System.Security.Cryptography.X509Certificates.X509Certificate2(,$cert_bytes)`,
   224  			`$store = Get-Item Cert:\LocalMachine\AuthRoot`,
   225  			`$store.Open("ReadWrite")`,
   226  			`$store.Add($cert)`,
   227  		}
   228  		getDownloadFileCmd = func(url string) string {
   229  			return fmt.Sprintf(`Invoke-FastWebRequest -URI '%s' -OutFile "$binDir\tools.tar.gz"`, url)
   230  		}
   231  	} else {
   232  		cmds = []string{
   233  			`$WebClient = New-Object System.Net.WebClient`,
   234  			`[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}`,
   235  			`[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12`,
   236  		}
   237  		getDownloadFileCmd = func(url string) string {
   238  			return fmt.Sprintf(`$WebClient.DownloadFile('%s', "$binDir\tools.tar.gz");`, url)
   239  		}
   240  	}
   241  
   242  	// Attempt all of the URLs, one after the other, until one succeeds.
   243  	// If all of the URLs fail, we retry the whole lot. We retry in this
   244  	// way, rather than retrying individually, to avoid one permanently
   245  	// bad URL from holding up the download.
   246  	downloadCmds := make([]string, len(toolsList))
   247  	for i, tools := range toolsList {
   248  		downloadCmds[i] = fmt.Sprintf("{ %s }", getDownloadFileCmd(tools.URL))
   249  	}
   250  	downloadCmd := fmt.Sprintf("ExecRetry { TryExecAll @(%s) }", strings.Join(downloadCmds, ", "))
   251  	cmds = append(cmds, downloadCmd)
   252  
   253  	return cmds, nil
   254  }