Its been well over a year since Salim Afiune started the effort of making Test-Kitchen (a popular infrastructure testing tool) compatible with Windows. I got involved in late 2014 to improve the performance of copying files to Windows Test instances from Windows or Linux hosts. There was a lot of dust flying in the air as this work was nearing completion in early 2015. Shawn Neal nicely refactored some of the work I had submitted to the winrm gem into a spin off gem called winrm-fs. At the same time, Fletcher Nichol was feverishly working to polish off the Test-Kitchen work by Chef Conf 2015 and was making some optimizations on Shawn's refactorings and those found their way into a new gem winrm-transport.
Today we are releasing a new version of the winrm gem that pulls in alot of this work into the core winrm classes and will make it possible to pull the rest into winrm-fs, centralizing ruby winrm client implementations and soon deprecating the winrm-transport gem. If you use the winrm gem, there are now some changes available that improve the performance of cross platform remote execution calls to windows along with a couple other new features. This post summarizes the changes and explains how to take advantage of them.
Running multiple commands in one shell
The most common pattern for invoking a command in a windows shell has been to use the run_cmd or run_powershell_script methods of the WinRMWebService class.
endpoint = 'https://other_machine:5986/wsman' winrm = WinRM::WinRMWebService.new(endpoint, :ssl, user: 'user', pass: 'pass') winrm.run_cmd('ipconfig /all') do |stdout, stderr| STDOUT.print stdout STDERR.print stderr end winrm.run_powershell_script('Get-Process') do |stdout, stderr| STDOUT.print stdout STDERR.print stderr end
Under the hood both run_cmd and run_powershell_script each make about 5 round trips to execute the command and return its output:
- Open a "shell" (equivalent of launching a cmd.exe instance)
- Create a command
- Request command output (potentially several calls for long running streamed output or long blocking calls)
- Terminate the command
- Close the shell
The first call, opening the shell, can be very expensive. It has to spawn a new process and involves authenticating the credentials with windows. If you need to make several calls using these methods, a new shell is spawned for each one. And things are even worse with powershell since that spawns yet another process (powershell.exe) from the command shell incurring the cost of creating a new runspace on each call. Stay tuned for a future winrm release (likely 1.6) that implements the powershell remoting protocol (psrp) to avoid that extra overhead.
While there is currently a way to make several calls in the same shell, its not very friendly or safe (you could easily end up with orphaned processes running on the remote windows machine). Typically this is not such a big deal because most will batch up several calls into one larger script. But here's the kicker: you are limited to a 8000 character command in a windows command shell (again stay tuned for the psrp implementation to avoid that). So imagine you are copying a 100MB file over the command line. Well, you will have to break that up into 8k chunks. While this may provide an excellent opportunity to review the six previous Star Wars episodes as you wait to transfer your music library, its far from ideal.
Using the CommandExecutor to stream commands
This 1.5 release, exposes a new class, CommandExecutor that you can use to make several commands from the same shell. The CommandExecutor provides run_command and run_powershell_script methods but these simply run a command, collect the output and terminate the command. You get a CommandExecutor by calling create_executor from a WinRMWebService instance. There are two usage patterns:
endpoint = 'https://other_machine:5986/wsman' winrm = WinRM::WinRMWebService.new(endpoint, :ssl, user: 'user', pass: 'pass') winrm.create_executor do |executor| executor.run_cmd('ipconfig /all') do |stdout, stderr| STDOUT.print stdout STDERR.print stderr end executor.run_powershell_script('Get-Process') do |stdout, stderr| STDOUT.print stdout STDERR.print stderr end end
This yields an executor to a block that uses the executor to make calls. When the block completes, the shell opened by the executor will be closed.
The other pattern:
endpoint = 'https://other_machine:5986/wsman' winrm = WinRM::WinRMWebService.new(endpoint, :ssl, user: 'user', pass: 'pass') executor = winrm.create_executor executor.run_cmd('ipconfig /all') do |stdout, stderr| STDOUT.print stdout STDERR.print stderr end executor.run_powershell_script('Get-Process') do |stdout, stderr| STDOUT.print stdout STDERR.print stderr end executor.close
Here we are responsible for the executor and this closing the shell it owns. Its important to close the shell because if it remains open, that process will continue to live on the remote machine. Will it dream?...we may never know.
Self signed SSL certificates and ssl_peer_fingerprint
If you are using a self signed certificate, you can currently use the :no_ssl_peer_verification option to disable verification of the certificate on the client:
WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :basic_auth_only => true, :no_ssl_peer_verification => true)
This is not ideal since it still risks "Man in the Middle" attacks. Still not completely ideal but better than completely ignoring validation is using a known fingerprint and passing that to the :ssl_peer_fingerprint option:
WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :ssl_peer_fingerprint => '6C04B1A997BA19454B0CD31C65D7020A6FC2669D')
This ensures that all messages are encrypted with a certificate bearing the given fingerprint. Thanks here is due to Chris McClimans (HippieHacker) for submitting this feature.
Retry logic added to opening a shell
Especially if provisioning a new machine, it's possible the winrm service is not yet running when first attempting to connect. The WinRMWebService now accepts new options :retry_limit and :retry_delay to specify the maximum number of attempts to make and how long to wait in between. These default to 3 attempts and a 10 second delay.
WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass, :retry_limit => 30, :retry_delay => 10)
Logging
The WinRMWebService now exposes a logger attribute and uses the logging gem to manage logging behavior. By default this appends to STDOUT and has a level of :warn, but one can adjust the level or add additional appenders.
winrm = WinRM::WinRMWebService.new(endpoint, :ssl, :user => myuser, :pass => mypass) # suppress warnings winrm.logger.warn = :error # Log to a file winrm.logger.add_appenders(Logging.appenders.file('error.log'))
If a consuming application uses its own logger that complies to the logging API, you can simply swap it in:
winrm.logger = my_logger
Up next: PSRP
Thats it for WinRM 1.5 but I'm really looking forward to productionizing a spike I recently completed getting the powershell remoting protocol working, which promises to improve performance and opens up other exciting cross platform scenarios.