github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/environs/manual/winrmprovisioner/winrmprovisioner.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Copyright 2016 Cloudbase Solutions SRL 3 // Licensed under the AGPLv3, see LICENCE file for details. 4 5 package winrmprovisioner 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "fmt" 11 "html/template" 12 "io" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/juju/errors" 18 "github.com/juju/os/series" 19 "github.com/juju/utils" 20 "github.com/juju/utils/arch" 21 "github.com/juju/utils/shell" 22 "github.com/juju/utils/winrm" 23 24 "github.com/juju/juju/apiserver/params" 25 "github.com/juju/juju/cloudconfig" 26 "github.com/juju/juju/cloudconfig/cloudinit" 27 "github.com/juju/juju/cloudconfig/instancecfg" 28 "github.com/juju/juju/core/instance" 29 "github.com/juju/juju/environs/manual" 30 "github.com/juju/juju/state/multiwatcher" 31 ) 32 33 // detectJujudProcess powershell script to determine 34 // if the jujud service is up and running in the machine 35 // if it's up the script will output "Yes" if it's down 36 // it will output "No" 37 const detectJujudProcess = ` 38 $jujuSvcs = Get-Service jujud-machine-* 39 if($jujuSvcs -and $jujuSvcs[0].Status -eq "running"){ 40 return "Yes" 41 } 42 return "No" 43 ` 44 45 // detectHardware is a powershell script that determines the following: 46 // - the processor architecture 47 // will try to determine the size of a int ptr, we know that any ptr on a x64 is 48 // always 8 bytes and on x32 4 bytes always 49 // - get the amount of ram the machine has 50 // Use a WMI call to fetch the amount of RAM on the system. See: 51 // https://msdn.microsoft.com/en-us/library/aa394347%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 52 // for more details 53 // - get the operating system name 54 // compare the values we find in the registry with the version information 55 // juju knows about. Once we find a match, we return the series 56 // - get number of cores that the machine has 57 // the process is using the Wmi windows Api to interrogate the os for 58 // the physical number of cores that the machine has. 59 // 60 const detectHardware = ` 61 function Get-Arch { 62 $arch = (Get-ItemProperty "HKLM:\system\CurrentControlSet\Control\Session Manager\Environment").PROCESSOR_ARCHITECTURE 63 return $arch.toString().ToLower() 64 } 65 66 function Get-Ram { 67 return (gcim win32_physicalmemory).Capacity 68 } 69 70 function Get-OSName { 71 $version = @{} 72 {{ range $key, $value := . }} 73 $version.Add("{{$key}}", "{{$value}}") 74 {{ end }} 75 76 $v = $(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName 77 $name = $v.Trim() 78 79 # detection for nano server 80 $k = $(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server\ServerLevels") 81 $nano = $k.NanoServer 82 if($nano -eq 1) { 83 return $version["Windows Server 2016"].Trim() 84 } 85 86 foreach ($h in $version.keys) { 87 $flag = $name.StartsWith($h) 88 89 if ($flag) { 90 return $version[$h].Trim() 91 } 92 } 93 } 94 function Get-NCores { 95 # NumberOfProcessors will return the physical processor, but not the core count of each CPU. So if you have 2 quad core CPUs, NumberOfProcessors will return 2, not 8 96 # Use NumberOfLogicalProcessors here (also takes into account hyperthreading). 97 # see https://msdn.microsoft.com/en-us/library/aa394102%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 for more information 98 Write-Host -NoNewline (gcim win32_computersystem).NumberOfLogicalProcessors 99 } 100 Get-Arch 101 Get-Ram 102 Get-OSName 103 Get-NCores 104 ` 105 106 // newDetectHardwareScript will parse the detectHardware script and add 107 // into the powershell hastable the key,val of the map returned from the 108 // WindowsVersions func from the series pkg. 109 // it will return the script wrapped into a safe powershell base64 110 func newDetectHardwareScript() (string, error) { 111 tmpl := template.Must(template.New("hc").Parse(detectHardware)) 112 var in bytes.Buffer 113 seriesMap := series.WindowsVersions() 114 if err := tmpl.Execute(&in, seriesMap); err != nil { 115 return "", err 116 } 117 return shell.NewPSEncodedCommand(in.String()) 118 } 119 120 // InitAdministratorUser will initially attempt to login as 121 // the Administrator user using the secure client 122 // only if this is false then this will make a new attempt with the unsecure http client. 123 func InitAdministratorUser(args *manual.ProvisionMachineArgs) error { 124 logger.Infof("Trying https client as user %s on %s", args.Host, args.User) 125 err := args.WinRM.Client.Ping() 126 if err == nil { 127 logger.Infof("Https connection is enabled on the host %s with user %s", args.Host, args.User) 128 return nil 129 } 130 131 logger.Debugf("Https client authentication is not enabled on the host %s with user %s", args.Host, args.User) 132 if args.WinRM.Client, err = winrm.NewClient(winrm.ClientConfig{ 133 User: args.User, 134 Host: args.Host, 135 Timeout: 25 * time.Second, 136 Password: winrm.TTYGetPasswd, 137 Secure: false, 138 }); err != nil { 139 return errors.Annotatef(err, "cannot create a new http winrm client ") 140 } 141 142 logger.Infof("Trying http client as user %s on %s", args.Host, args.User) 143 if err = args.WinRM.Client.Ping(); err != nil { 144 logger.Debugf("WinRM unsecure listener is not enabled on %s", args.Host) 145 return errors.Annotatef(err, "cannot provision, because all winrm default connections failed") 146 } 147 148 defClient := args.WinRM.Client 149 logger.Infof("Trying to enable https client certificate authentication") 150 if args.WinRM.Client, err = enableCertAuth(args); err != nil { 151 logger.Infof("Cannot enable client auth cert authentication for winrm") 152 logger.Infof("Reverting back to usecure client interaction") 153 args.WinRM.Client = defClient 154 return nil 155 } 156 157 logger.Infof("Client certs are installed and setup on the %s with user %s", args.Host, args.User) 158 err = args.WinRM.Client.Ping() 159 if err == nil { 160 return nil 161 } 162 163 logger.Infof("Winrm https connection is broken, can't retrive a response") 164 logger.Infof("Reverting back to usecure client interactions") 165 args.WinRM.Client = defClient 166 167 return nil 168 169 } 170 171 // enableCertAuth enables https cert auth interactions 172 // with the winrm listener and returns the client 173 func enableCertAuth(args *manual.ProvisionMachineArgs) (manual.WinrmClientAPI, error) { 174 var stderr bytes.Buffer 175 pass := args.WinRM.Client.Password() 176 177 scripts, err := bindInitScripts(pass, args.WinRM.Keys) 178 if err != nil { 179 return nil, errors.Trace(err) 180 } 181 182 for _, script := range scripts { 183 err = args.WinRM.Client.Run(script, args.Stdout, &stderr) 184 if err != nil { 185 return nil, errors.Trace(err) 186 } 187 if stderr.Len() > 0 { 188 return nil, errors.Errorf( 189 "encountered error executing cert auth script: %s", 190 stderr.String(), 191 ) 192 } 193 } 194 195 cfg := winrm.ClientConfig{ 196 User: args.User, 197 Host: args.Host, 198 Key: args.WinRM.Keys.ClientKey(), 199 Cert: args.WinRM.Keys.ClientCert(), 200 Timeout: 25 * time.Second, 201 Secure: true, 202 } 203 204 caCert := args.WinRM.Keys.CACert() 205 if caCert == nil { 206 logger.Infof("Skipping winrm CA validation") 207 cfg.Insecure = true 208 } else { 209 cfg.CACert = caCert 210 } 211 212 return winrm.NewClient(cfg) 213 } 214 215 // bindInitScripts creates a series of scripts in a standard 216 // (utf-16-le, base64)format for passing to the winrm conn to be executed remotely 217 // we are doing this instead of one big script because winrm supports 218 // just 8192 length commands. We know we have an amount of prefixed scripts 219 // that we want to bind for the init process so create an array of scripts 220 func bindInitScripts(pass string, keys *winrm.X509) ([]string, error) { 221 var ( 222 err error 223 ) 224 225 scripts := make([]string, 3, 3) 226 227 if len(pass) == 0 { 228 return scripts, fmt.Errorf("The password is empty, provide a valid password to enable https interactions") 229 } 230 231 scripts[0], err = shell.NewPSEncodedCommand(setFiles) 232 if err != nil { 233 return nil, err 234 } 235 236 scripts[1] = fmt.Sprintf(setFilesContent, string(keys.ClientCert())) 237 238 scripts[1], err = shell.NewPSEncodedCommand(scripts[1]) 239 if err != nil { 240 return nil, err 241 } 242 243 scripts[2] = fmt.Sprintf(setConnWinrm, pass) 244 scripts[2], err = shell.NewPSEncodedCommand(scripts[2]) 245 if err != nil { 246 return nil, err 247 } 248 249 return nil, nil 250 } 251 252 // setFiles powershell script that will manage and create the conf folder and files 253 const setFiles = ` 254 $jujuHome = [io.path]::Combine($ENV:APPDATA, 'Juju') 255 $x509Path = [io.path]::Combine($jujuHome, 'x509') 256 $certPath = [io.path]::Combine($x509Path,'winrmcert.crt') 257 if (-Not (Test-Path $jujuHome)) { 258 New-Item $jujuHome -Type directory 259 } 260 if (-Not (Test-Path $x509Path)) { 261 New-Item $x509Path -Type directory 262 } 263 if (-Not (Test-Path $certPath)) { 264 New-Item $certPath -Type file 265 } 266 ` 267 268 // setFilesContent powershell script that will write 269 // x509 cert and key into the juju conf. 270 const setFilesContent = ` 271 $cert=@" 272 %s 273 "@ 274 [io.file]::WriteAllText("$ENV:APPDATA\Juju\x509\winrmcert.crt", $cert) 275 ` 276 277 // setConnWinrm powershell script that will create and write the client cert from the juju conf file into the windows 278 // target enabling winrm secure client interactions with the machine 279 const setConnWinrm = ` 280 winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="1024"}' 281 winrm set winrm/config/client/auth '@{Digest="false"}' 282 winrm set winrm/config/service/auth '@{Certificate="true"}' 283 Remove-Item -Path WSMan:\localhost\ClientCertificate\ClientCertificate_* -Recurse -force | Out-null 284 $username = "Administrator" 285 $password = "%s" 286 $client_cert_path = [io.path]::Combine($env:APPDATA, 'Juju', 'x509', 'winrmcert.crt') 287 $clientcert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($client_cert_path) 288 $castore = New-Object System.Security.Cryptography.X509Certificates.X509Store( 289 [System.Security.Cryptography.X509Certificates.StoreName]::Root, 290 [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) 291 $castore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) 292 $castore.Add($clientcert) 293 $subject = [string]::Join([CHAR][BYTE]32, "juju", "winrm", "client", "cert") 294 $secure_password = ConvertTo-SecureString $password -AsPlainText -Force 295 $cred = New-Object System.Management.Automation.PSCredential "$ENV:COMPUTERNAME\$username", $secure_password 296 New-Item -Path WSMan:\localhost\ClientCertificate -Issuer $clientcert.Thumbprint -Subject $subject -Uri * -Credential $cred -Force 297 ` 298 299 // gatherMachineParams collects all the information we know about the machine 300 // we are about to provision. It will winrm into that machine as the provision user 301 // The hostname supplied should not include a username. 302 // If we can, we will reverse lookup the hostname by its IP address, and use 303 // the DNS resolved name, rather than the name that was supplied 304 func gatherMachineParams(hostname string, cli manual.WinrmClientAPI) (*params.AddMachineParams, error) { 305 // Generate a unique nonce for the machine. 306 uuid, err := utils.NewUUID() 307 if err != nil { 308 return nil, err 309 } 310 311 addr, err := manual.HostAddress(hostname) 312 if err != nil { 313 return nil, errors.Annotatef(err, "failed to compute public address for %q", hostname) 314 } 315 316 provisioned, err := checkProvisioned(hostname, cli) 317 if err != nil { 318 return nil, errors.Annotatef(err, "cannot decide if the machine is provisioned") 319 } 320 if provisioned { 321 return nil, manual.ErrProvisioned 322 } 323 324 hc, series, err := DetectSeriesAndHardwareCharacteristics(hostname, cli) 325 if err != nil { 326 err = fmt.Errorf("error detecting windows characteristics: %v", err) 327 return nil, err 328 } 329 // There will never be a corresponding "instance" that any provider 330 // knows about. This is fine, and works well with the provisioner 331 // task. The provisioner task will happily remove any and all dead 332 // machines from state, but will ignore the associated instance ID 333 // if it isn't one that the environment provider knows about. 334 335 instanceId := instance.Id(manual.ManualInstancePrefix + hostname) 336 nonce := fmt.Sprintf("%s:%s", instanceId, uuid) 337 machineParams := ¶ms.AddMachineParams{ 338 Series: series, 339 HardwareCharacteristics: hc, 340 InstanceId: instanceId, 341 Nonce: nonce, 342 Addrs: params.FromNetworkAddresses(addr), 343 Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, 344 } 345 return machineParams, nil 346 } 347 348 // checkProvisioned checks if the machine is already provisioned or not. 349 // if it's already provisioned it will return true 350 func checkProvisioned(host string, cli manual.WinrmClientAPI) (bool, error) { 351 logger.Tracef("Checking if %s windows machine is already provisioned", host) 352 var stdout, stderr bytes.Buffer 353 354 // run the command trough winrm 355 // this script detects if the jujud process, service is up and running 356 script, err := shell.NewPSEncodedCommand(detectJujudProcess) 357 if err != nil { 358 return false, errors.Trace(err) 359 } 360 361 // send the script to the windows machine 362 if err = cli.Run(script, &stdout, &stderr); err != nil { 363 return false, errors.Trace(err) 364 } 365 366 provisioned := strings.Contains(stdout.String(), "Yes") 367 if stderr.Len() != 0 { 368 err = errors.Annotate(err, strings.TrimSpace(stderr.String())) 369 } 370 371 // if the script said yes 372 if provisioned { 373 logger.Infof("%s is already provisioned", host) 374 } else { 375 logger.Infof("%s is not provisioned", host) 376 } 377 378 return provisioned, err 379 } 380 381 // DetectSeriesAndHardwareCharacteristics detects the windows OS 382 // series and hardware characteristics of the remote machine 383 // by connecting to the machine and executing a bash script. 384 func DetectSeriesAndHardwareCharacteristics(host string, cli manual.WinrmClientAPI) (hc instance.HardwareCharacteristics, series string, err error) { 385 logger.Infof("Detecting series and characteristics on %s windows machine", host) 386 var stdout, stderr bytes.Buffer 387 388 script, err := newDetectHardwareScript() 389 if err != nil { 390 return hc, "", err 391 } 392 393 // send the script to the windows machine 394 if err = cli.Run(script, &stdout, &stderr); err != nil { 395 return hc, "", errors.Trace(err) 396 } 397 398 if stderr.Len() != 0 { 399 return hc, "", fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String())) 400 } 401 402 info, err := splitHardWareScript(stdout.String()) 403 if err != nil { 404 return hc, "", errors.Trace(err) 405 } 406 407 series = strings.Replace(info[2], "\r", "", -1) 408 409 if err = initHC(&hc, info); err != nil { 410 return hc, "", errors.Trace(err) 411 } 412 413 return hc, series, nil 414 } 415 416 // initHC it will initialize the hardware characterisrics struct with the 417 // parsed and checked info slice string 418 // info description : 419 // - info[0] the arch of the machine 420 // - info[1] the amount of memory that the machine has 421 // - info[2] the series of the machine 422 // - info[3] the number of cores that the machine has 423 // It returns nil if it parsed successfully. 424 func initHC(hc *instance.HardwareCharacteristics, info []string) error { 425 // add arch 426 arch := arch.NormaliseArch(info[0]) 427 hc.Arch = &arch 428 429 // parse the mem number 430 mem, err := strconv.ParseUint(info[1], 10, 64) 431 if err != nil { 432 return errors.Annotatef(err, "Can't parse mem number of the windows machine") 433 } 434 hc.Mem = new(uint64) 435 *hc.Mem = mem 436 437 // parse the core number 438 cores, err := strconv.ParseUint(info[3], 10, 64) 439 if err != nil { 440 return errors.Annotatef(err, "Can't parse cores number of the windows machine") 441 } 442 443 hc.CpuCores = new(uint64) 444 *hc.CpuCores = cores 445 return nil 446 } 447 448 // splitHardwareScript will split the result from the detectHardware powershell script 449 // to extract the information in a specific order. 450 // this will return a slice of string that will be used in conjunctions with the above function 451 func splitHardWareScript(script string) ([]string, error) { 452 scr := strings.Split(script, "\n") 453 n := len(scr) 454 if n < 3 { 455 return nil, fmt.Errorf("No hardware fields on running the powershell deteciton script, %s", script) 456 } 457 for i := 0; i < n; i++ { 458 scr[i] = strings.TrimSpace(scr[i]) 459 } 460 return scr, nil 461 } 462 463 // RunProvisionScript exported for testing purposes 464 var RunProvisionScript = runProvisionScript 465 466 // runProvisionScript runs the script generated by the provisioner 467 // the script can be big and the underlying protocol dosen't support long messages 468 // we need to send it into little chunks saving first into a file and then execute it. 469 func runProvisionScript(script string, cli manual.WinrmClientAPI, stdin, stderr io.Writer) (err error) { 470 script64 := base64.StdEncoding.EncodeToString([]byte(script)) 471 input := bytes.NewBufferString(script64) // make new buffer out of script 472 // we must make sure to buffer the entire script 473 // in a sequential write fashion first into a script 474 // decouple the provisioning script into little 1024 byte chunks 475 // we are doing this in order to append into a .ps1 file. 476 var buf [1024]byte 477 478 // if the file dosen't exist ,create it 479 // if the file exists just clear/reset it 480 script, err = shell.NewPSEncodedCommand(initChunk) 481 if err != nil { 482 return err 483 } 484 if err = cli.Run(script, stdin, stderr); err != nil { 485 return errors.Trace(err) 486 } 487 488 // sequential read. 489 for input.Len() != 0 { 490 n, err := input.Read(buf[:]) 491 if err != nil && err != io.EOF { 492 return errors.Trace(err) 493 } 494 script, err = shell.NewPSEncodedCommand( 495 fmt.Sprintf(saveChunk, string(buf[:n])), 496 ) 497 if err != nil { 498 return errors.Trace(err) 499 } 500 if err = cli.Run(script, stdin, stderr); err != nil { 501 return errors.Trace(err) 502 } 503 } 504 505 // after the sendAndSave script is successfully done 506 // we must execute the newly writed script 507 script, err = shell.NewPSEncodedCommand(runCmdProv) 508 if err != nil { 509 return err 510 } 511 logger.Debugf("Running the provisioningScript") 512 var outerr bytes.Buffer 513 if err = cli.Run(script, stdin, &outerr); err != nil { 514 return errors.Trace(err) 515 } 516 517 if outerr.Len() != 0 { 518 return fmt.Errorf("%v ", strings.TrimSpace(outerr.String())) 519 } 520 521 return err 522 } 523 524 // initChunk creates or clears the file that the userdata will be appendend. 525 const initChunk = ` 526 $provisionPath= [io.path]::Combine($ENV:APPDATA, 'Juju', 'provision.ps1') 527 if (-Not (Test-Path $provisionPath)) { 528 New-Item $provisionPath -Type file 529 } else { 530 Clear-Content $provisionPath 531 } 532 533 ` 534 535 // saveChunk powershell script that will append into the userdata file created after the 536 // initChunk executed. 537 // this will be called multiple times in a sequential order. 538 const saveChunk = ` 539 $chunk= @" 540 %s 541 "@ 542 $provisionPath= [io.path]::Combine($ENV:APPDATA, 'Juju', 'provision.ps1') 543 $stream = New-Object System.IO.StreamWriter -ArgumentList ([IO.File]::Open($provisionPath, "Append")) 544 $stream.Write($chunk) 545 $stream.close() 546 ` 547 548 // runCmdProv powrshell script that decodes and executes the newly created userdata file 549 // after the process of writing the sequantial script is done above 550 const runCmdProv = ` 551 $provisionPath= [io.path]::Combine($ENV:APPDATA, 'Juju', 'provision.ps1') 552 $script = [IO.File]::ReadAllText($provisionPath) 553 $x = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($script)) 554 Set-Content C:\udata.ps1 $x 555 powershell.exe -ExecutionPolicy RemoteSigned -NonInteractive -File C:\udata.ps1 556 ` 557 558 // ProvisioningScript generates a powershell script that can be 559 // executed on a remote host to carry out the cloud-init 560 // configuration. 561 func ProvisioningScript(icfg *instancecfg.InstanceConfig) (string, error) { 562 cloudcfg, err := cloudinit.New(icfg.Series) 563 if err != nil { 564 return "", errors.Annotate(err, "error creating new cloud config") 565 } 566 567 udata, err := cloudconfig.NewUserdataConfig(icfg, cloudcfg) 568 if err != nil { 569 return "", errors.Annotate(err, "error creating new userdata based on the cloud config") 570 } 571 572 if err := udata.Configure(); err != nil { 573 return "", errors.Annotate(err, "error adding extra configurations in the userdata") 574 } 575 576 return cloudcfg.RenderScript() 577 }