Now, my favourite topic! I'm a .NET person however my last coding project was around 2004 :).
What is the topic today? I'm now a devops engineer so I have to work on some infrastructure assignments to prepare the infrastructure environment for the development team. Working with machine preparation, setup and massage it for the build process from Jenkins. It's quite an interesting job that work with low layer on machine provisioning (with vCenter - we will have another thread on this topic) to setup the VM then scripting language for the build process.
- Option 1: is using Puppet as a configuration management tool to control the configuration on the testing VM. We define a yaml file for the request msi files then we will have a gradle project to download those files and run a puppet agent to install those downloaded files. This gradle will be run remotely on the testing VM. We use psexec tool to run the remote execution.
- Option 2: in an effort of moving to a new tool, we can apply Ansible as an "agentless" configuration management tool that is growing quite fast and it supports Windows platform by native powershell. So, similar with Puppet, we will define a list of msi files and a script into a playbook. This playbook will download those msi then run the msi installation on the target VM. Ansible already supported all the remote call via WinRM so the pre-requisite of running Ansible on the target VM, we have to run a powershell script to verify and enable the WinRM and the certification (in order to run the ps1 script we have to change the execution polity on remote VM too)
- Option 3: so as a lightweight solution, I will use WinRM to run the remote command (Invoke-Command) on the target VM. This is actually the native implementation of Ansible in its powershell libraries for Window modules. I will detail out on this option as this is the topic for today.
First item, in order to call Invoke-Command on remote machine in my environment, we have to use remote call base on IP address because we're using ambiguous host name (dns name) for the testing VM so we have to identify VM by IP, we have to add this remote IP into trusted host file of the execution machine (the Jenkins machine) via this recommendation. So I amend the remote IP into trusted host list like below
[Boolean] addTrustedIP ([String] $remoteIP) {
if ($error) {$error.clear()}
$trustedList = (Get-ChildItem WSMan:\localhost\Client\TrustedHosts).Value
if ($trustedList) {
if ($trustedList.indexOf($remoteIP) -ge 0) {
Write-Host "[CommonLib]: IP $remoteIP already in TrustedHosts"
return $true
} else { $trustedList += "," + $remoteIP }
} else { $trustedList = $remoteIP }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $trustedList -Force
if ($error) { return $false }
Write-Host "[CommonLib]: Added $remoteIP into TrustedHosts"
return $true
}
And we should remove it out after remote call completion
[Boolean] removeTrustedIP ([String] $remoteIP){
if ($error) {$error.clear()}
$trustedList = (Get-ChildItem WSMan:\localhost\Client\TrustedHosts).Value
if ($trustedList -or $trustedList.indexOf($remoteIP) -ge 0) {
if ($trustedList -eq $remoteIP) {
Clear-Item WSMan:\localhost\Client\TrustedHosts -Force
} else {
$trustedList = $trustedList.Replace(($remoteIP + ","), "")
if ($trustedList.indexOf($remoteIP) -ge 0) { $trustedList = $trustedList.Replace(("," + $remoteIP), "") }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $trustedList -Force
}
}
if ($error) { return $false }
Write-Host "[CommonLib]: Remove $remoteIP out of TrustedHosts"
return $true
}
Then we will create an access credential to the remote machine
[PSCredential] createRemoteCredential ([String] $remoteIP, [String] $user, [String] $password){
if ($error) {$error.clear()}
$encryptedPwd = ConvertTo-SecureString -String $password -AsPlainText -Force
$psCredential = New-Object System.Management.Automation.PSCredential -ArgumentList $remoteIP\$user, $encryptedPwd
if ($error) { return $null }
return $psCredential
}
Ok, now you are ready, the next task is to create a script block where you will implement your logic that will run on the remote VM. Please note that the session you run on remote VM is different with the context session where you are running the current code, so any of your data in the current context, you have to pass them into the scriptblock in order to use it in the remote VM script.
So now, I want to download the msi file from artifactory then execute the msiexec to install this downloaded package, here it come the full code (as I create the support functions for remote access into a library - Powershell 5, so I will use this lib for a quick access to those functions)
$psLibs = New-Object CommonLibs
$psCredential = $psLibs.createRemoteCredential($VmIp, $VmUser, $VmPwd)
if (!($psLibs.addTrustedIP($env:VmIp))) {Write-Host "Can't add trusted IP"; exit 1}
Get-Item WSMan:\localhost\Client\TrustedHosts
$sbContent = {
param($msiString)
$sourceFolder = "C:\temp"
$logFolder = $sourceFolder + '\Logs'
New-Item -Path $logFolder -ItemType Directory -Force
$targetUrl = "$msiString"
$msiFile = [System.IO.Path]::GetFileName($targetUrl)
$msiPath = $sourceFolder + '\' + $msiFile
(New-Object System.Net.WebClient).DownloadFile((New-Object System.Uri($targetUrl)), "$msiPath")
$msiFile = $msiFile.Replace('.msi', '.log')
$logPath = $logFolder + '\' + $msiFile
$msiParams = "/i $msiPath /qn /l*v $logPath"
$proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "$msiParams" -Verb runAs -PassThru
$handle = $proc.Handle
$proc.WaitForExit()
Write-Host "Installation exit code:" + $proc.ExitCode
$targetService = Get-Service -Name "ServiceName";
Restart-Service $targetService
$targetService.WaitForStatus("Running", (New-TimeSpan -Minutes 1))
}
Invoke-Command -Computer $VmIp -ScriptBlock $sbContent -ArgumentList "$msiList" -Credential $psCredential
if (!($psLibs.removeTrustedIP($VmIp))) {Clear-Item WSMan:\localhost\Client\TrustedHosts -Force}
I added a snippet of code to restart the service after the installation (base on my need) and there is a tricky part here when you want to wait for the installation process to complete. Usually, you can use Start-Process with option -Wait to wait for the process completion, however, I got a bug or something, this will go through this waiting and stop the installation. So I have to use -PassThru to return a process object then wait for the exit event by WaitForExit method. Another tricky part is the $proc.Handle in order to get the proper exitcode. Thanks to this thread for these tricks or some other solutions to wait for process here!
If you want to trigger Invoke-Command then disconnect the PSSession right away for some tasks such as reboot, release IP ... add an option -InDisconnectedSession. This will disconnect the PSSession right after the scriptblock execution.
A note before we call the remote connect, by some reasons, the WinRM service is not ready for the PSSession establishing, you can add this function for the verification
[Boolean] verifyWinRMService ([String] $remoteIP){
if ($error) {$error.clear()}
$timeout = 9 #timeout 90s
if (!(Test-WSMan -ComputerName $remoteIP -ErrorAction SilentlyContinue)) {
for ($i = 0; $i -lt $timeout; $i++) {
Write-Host "[CommonLib]: Wait for WinRM in 10s ..."
Start-Sleep -s 10
if (Test-WSMan -ComputerName $remoteIP -ErrorAction SilentlyContinue) { $i = $timeout; $error.clear() }
}
}
Write-Host "[CommonLib]: WinRM is ready on $remoteIP!"
if ($error) { return $false }
return $true
}
I think this should be enough for today, actually there is another note for install msu file via WinRM. I will be back on that item in another small topic.
What is the topic today? I'm now a devops engineer so I have to work on some infrastructure assignments to prepare the infrastructure environment for the development team. Working with machine preparation, setup and massage it for the build process from Jenkins. It's quite an interesting job that work with low layer on machine provisioning (with vCenter - we will have another thread on this topic) to setup the VM then scripting language for the build process.
Use Case:
I want to have a Jenkins job to install some msi files into the testing machine, those msi artifacts are located in artifactory. So from the Jenkins machine we will need to access into the testing machine, let call remote VM (as I'm using VM from vCenter), to download and install those msi files from artifactory.Solution:
There are couple solutions on this story as this is quite a standard devops assignment.- Option 1: is using Puppet as a configuration management tool to control the configuration on the testing VM. We define a yaml file for the request msi files then we will have a gradle project to download those files and run a puppet agent to install those downloaded files. This gradle will be run remotely on the testing VM. We use psexec tool to run the remote execution.
- Option 2: in an effort of moving to a new tool, we can apply Ansible as an "agentless" configuration management tool that is growing quite fast and it supports Windows platform by native powershell. So, similar with Puppet, we will define a list of msi files and a script into a playbook. This playbook will download those msi then run the msi installation on the target VM. Ansible already supported all the remote call via WinRM so the pre-requisite of running Ansible on the target VM, we have to run a powershell script to verify and enable the WinRM and the certification (in order to run the ps1 script we have to change the execution polity on remote VM too)
- Option 3: so as a lightweight solution, I will use WinRM to run the remote command (Invoke-Command) on the target VM. This is actually the native implementation of Ansible in its powershell libraries for Window modules. I will detail out on this option as this is the topic for today.
First item, in order to call Invoke-Command on remote machine in my environment, we have to use remote call base on IP address because we're using ambiguous host name (dns name) for the testing VM so we have to identify VM by IP, we have to add this remote IP into trusted host file of the execution machine (the Jenkins machine) via this recommendation. So I amend the remote IP into trusted host list like below
[Boolean] addTrustedIP ([String] $remoteIP) {
if ($error) {$error.clear()}
$trustedList = (Get-ChildItem WSMan:\localhost\Client\TrustedHosts).Value
if ($trustedList) {
if ($trustedList.indexOf($remoteIP) -ge 0) {
Write-Host "[CommonLib]: IP $remoteIP already in TrustedHosts"
return $true
} else { $trustedList += "," + $remoteIP }
} else { $trustedList = $remoteIP }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $trustedList -Force
if ($error) { return $false }
Write-Host "[CommonLib]: Added $remoteIP into TrustedHosts"
return $true
}
And we should remove it out after remote call completion
[Boolean] removeTrustedIP ([String] $remoteIP){
if ($error) {$error.clear()}
$trustedList = (Get-ChildItem WSMan:\localhost\Client\TrustedHosts).Value
if ($trustedList -or $trustedList.indexOf($remoteIP) -ge 0) {
if ($trustedList -eq $remoteIP) {
Clear-Item WSMan:\localhost\Client\TrustedHosts -Force
} else {
$trustedList = $trustedList.Replace(($remoteIP + ","), "")
if ($trustedList.indexOf($remoteIP) -ge 0) { $trustedList = $trustedList.Replace(("," + $remoteIP), "") }
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $trustedList -Force
}
}
if ($error) { return $false }
Write-Host "[CommonLib]: Remove $remoteIP out of TrustedHosts"
return $true
}
Then we will create an access credential to the remote machine
[PSCredential] createRemoteCredential ([String] $remoteIP, [String] $user, [String] $password){
if ($error) {$error.clear()}
$encryptedPwd = ConvertTo-SecureString -String $password -AsPlainText -Force
$psCredential = New-Object System.Management.Automation.PSCredential -ArgumentList $remoteIP\$user, $encryptedPwd
if ($error) { return $null }
return $psCredential
}
Ok, now you are ready, the next task is to create a script block where you will implement your logic that will run on the remote VM. Please note that the session you run on remote VM is different with the context session where you are running the current code, so any of your data in the current context, you have to pass them into the scriptblock in order to use it in the remote VM script.
So now, I want to download the msi file from artifactory then execute the msiexec to install this downloaded package, here it come the full code (as I create the support functions for remote access into a library - Powershell 5, so I will use this lib for a quick access to those functions)
$psLibs = New-Object CommonLibs
$psCredential = $psLibs.createRemoteCredential($VmIp, $VmUser, $VmPwd)
if (!($psLibs.addTrustedIP($env:VmIp))) {Write-Host "Can't add trusted IP"; exit 1}
Get-Item WSMan:\localhost\Client\TrustedHosts
$sbContent = {
param($msiString)
$sourceFolder = "C:\temp"
$logFolder = $sourceFolder + '\Logs'
New-Item -Path $logFolder -ItemType Directory -Force
$targetUrl = "$msiString"
$msiFile = [System.IO.Path]::GetFileName($targetUrl)
$msiPath = $sourceFolder + '\' + $msiFile
(New-Object System.Net.WebClient).DownloadFile((New-Object System.Uri($targetUrl)), "$msiPath")
$msiFile = $msiFile.Replace('.msi', '.log')
$logPath = $logFolder + '\' + $msiFile
$msiParams = "/i $msiPath /qn /l*v $logPath"
$proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "$msiParams" -Verb runAs -PassThru
$handle = $proc.Handle
$proc.WaitForExit()
Write-Host "Installation exit code:" + $proc.ExitCode
$targetService = Get-Service -Name "ServiceName";
Restart-Service $targetService
$targetService.WaitForStatus("Running", (New-TimeSpan -Minutes 1))
}
Invoke-Command -Computer $VmIp -ScriptBlock $sbContent -ArgumentList "$msiList" -Credential $psCredential
if (!($psLibs.removeTrustedIP($VmIp))) {Clear-Item WSMan:\localhost\Client\TrustedHosts -Force}
I added a snippet of code to restart the service after the installation (base on my need) and there is a tricky part here when you want to wait for the installation process to complete. Usually, you can use Start-Process with option -Wait to wait for the process completion, however, I got a bug or something, this will go through this waiting and stop the installation. So I have to use -PassThru to return a process object then wait for the exit event by WaitForExit method. Another tricky part is the $proc.Handle in order to get the proper exitcode. Thanks to this thread for these tricks or some other solutions to wait for process here!
If you want to trigger Invoke-Command then disconnect the PSSession right away for some tasks such as reboot, release IP ... add an option -InDisconnectedSession. This will disconnect the PSSession right after the scriptblock execution.
A note before we call the remote connect, by some reasons, the WinRM service is not ready for the PSSession establishing, you can add this function for the verification
[Boolean] verifyWinRMService ([String] $remoteIP){
if ($error) {$error.clear()}
$timeout = 9 #timeout 90s
if (!(Test-WSMan -ComputerName $remoteIP -ErrorAction SilentlyContinue)) {
for ($i = 0; $i -lt $timeout; $i++) {
Write-Host "[CommonLib]: Wait for WinRM in 10s ..."
Start-Sleep -s 10
if (Test-WSMan -ComputerName $remoteIP -ErrorAction SilentlyContinue) { $i = $timeout; $error.clear() }
}
}
Write-Host "[CommonLib]: WinRM is ready on $remoteIP!"
if ($error) { return $false }
return $true
}
I think this should be enough for today, actually there is another note for install msu file via WinRM. I will be back on that item in another small topic.
Comments
Post a Comment