From afcc8516fc45b0d11d384bdcf730dcbb21ee30d7 Mon Sep 17 00:00:00 2001 From: ScriptAutomate Date: Tue, 25 May 2021 15:49:00 -0500 Subject: [PATCH] open source init --- .gitignore | 85 ++++++++++ CODE_OF_CONDUCT.md | 3 + LICENSE | 197 ++++++++++++++++++++++ MANIFEST.in | 4 + NOTICE | 9 + README.md | 125 ++++++++++++++ Synchronous-ZipAndUnzip.psm1 | 317 +++++++++++++++++++++++++++++++++++ enable_winrm.ps1 | 33 ++++ saltwinshell/__init__.py | 4 + saltwinshell/core.pyx | 181 ++++++++++++++++++++ saltwinshell/version.py | 3 + setup.py | 48 ++++++ 12 files changed, 1009 insertions(+) create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 NOTICE create mode 100644 README.md create mode 100644 Synchronous-ZipAndUnzip.psm1 create mode 100644 enable_winrm.ps1 create mode 100644 saltwinshell/__init__.py create mode 100644 saltwinshell/core.pyx create mode 100644 saltwinshell/version.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43f73cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# Generated MANIFEST file +MANIFEST + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# C code (from cython) +*.c + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# kdevelop +*.kdev4 + +# pycharm +.idea* + +# Local VIM RC +.lvimrc + +# Swap files +.*.swp + +# nvim +*.un~ +*~ + +# setup compile left-overs +*.py_orig + +# CI root directory +.ci/.rootdir diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a922970 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +Agentless Salt for Windows is available through Salt Project. +Reference the [`CODE_OF_CONDUCT.md`](https://github.com/saltstack/salt/blob/master/CODE_OF_CONDUCT.md) +provided by `salt` for more information. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5093fc7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,197 @@ +Agentless Salt for Windows +Copyright 2021 VMware, Inc. + +The Apache 2.0 license (the "License") set forth below applies to all +parts of the Agentless Salt for Windows project. You may not use this file +except in compliance with the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 VMware, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..87dbd4b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include Synchronous-ZipAndUnzip.psm1 +include enable_winrm.ps1 +include Salt-Env-2017.7-AMD64-Py2.zip +include Salt-Env-2017.7-x86-Py2.zip diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..999ef15 --- /dev/null +++ b/NOTICE @@ -0,0 +1,9 @@ +Agentless Salt for Windows +Copyright 2021 VMware, Inc. + +This product is licensed to you under the Apache 2.0 license (the "License"). +You may not use this product except in compliance with the Apache 2.0 License. + +This product may include a number of subcomponents with separate copyright +notices and license terms. Your use of these subcomponents is subject to the +terms and conditions of the subcomponent's license, as noted in the LICENSE file. diff --git a/README.md b/README.md new file mode 100644 index 0000000..28986a9 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# Agentless Salt for Windows + +> **NOTE:** This is an _archived_ repository! Agentless Salt for Windows was originally created for SaltStack Enterprise customers and has not been updated since 2017. This repository is being open sourced, but the project is not supported at this time. Feel free to fork and make use of the code in accordance with the `LICENSE` file. + +## About + +The Agentless Windows Module can run any Salt SSH command on a target Windows server or desktop, such as the following: + +``` +salt-ssh testwin disk.usage +``` + +## Requirements + +### Target Windows systems + +- English version of Windows +- Windows versions: + - Windows 7 + - Windows 8.1 + - Windows 10 + - Windows Server 2008 R2 + - Windows Server 2012 R2 + - Windows Server 2016 +- Powershell 3.0 or later +- WinRM must be configured and running + +Winrm must be set up and configured on the Windows machine to run https on port +5986. The Windows firewall also must be set up to allow inbound port 5986. + +A convenience script is available in `enable_winrm.ps1`. + +### Salt master + +- The `/etc/salt/roster` file must have a configuration section for every Windows machine you want to connect to. The configuration must have a local admin user and password for each machine, as in the following example. + +```yaml +win2012dev: # Minion ID + host: + user: + passwd: + winrm: True +``` + +> **NOTE:** Domain credentials are not supported. + +- Python 2 must be installed on the Salt master. The `salt-ssh` module for Windows is supported only on Python 2, not later versions. +- `pip` for Python 2 must be installed. + +#### CentOS + +To install `pip` for Python 2 on CentOS: + +```bash +yum install epel-release -y +yum install python-pip +pip install -U setuptools +``` + +#### Ubuntu + +To install `pip` for Python 2 on Ubuntu: + +```bash +apt-get install python-pip +``` + +## Installation instructions + +Download the latest release onto your Salt Master: + +https://github.com/saltstack/saltwinshell/releases/download/v2017.7/saltwinshell-2017.7-cp27-cp27mu-linux_x86_64.whl + +Install on your working Salt Master: + +``` +pip install -U ./saltwinshell-2017.7-cp27-cp27mu-linux_x86_64.whl +``` + +Once winrm is configured and running on your Windows system(s), you should be able to start using `salt-ssh` +against them. + +``` +salt-ssh test.ping +``` + +The first time you run `salt-ssh` against a Windows minion it will take a bit +longer than usual since it has to deploy a working Salt python environment. +Subsequent runs should be much faster. + +The shell libraries for agentless Windows to work via `salt-ssh`, installing +this library activates the ability to hit windows targets. + +## Release process + +- Create a new branch: + +``` +git checkout -b +``` + +- Make updates. +- Create a tag: + +``` +git tag -a v2018.3 -m "Version v2018.3 release" -s +git push origin 2018.3 +git push --tags +``` + +- Copy in your python environment zip files to the root. Make sure they match +what the setup.py is expecting. +- Run the following: + + ``` + python setup.py bdist_wheel + ``` + + The file you want will be found in the `dist` directory and will look something like: + + ``` + saltwinshell-2017.7-cp27-cp27mu-linux_x86_64.whl + ``` + +- Upload `.whl` file as a new release on GitHub diff --git a/Synchronous-ZipAndUnzip.psm1 b/Synchronous-ZipAndUnzip.psm1 new file mode 100644 index 0000000..634368c --- /dev/null +++ b/Synchronous-ZipAndUnzip.psm1 @@ -0,0 +1,317 @@ +#Requires -Version 2.0 + +# Copyright 2021 VMware, Inc. +# SPDX-License-Identifier: Apache-2.0 + +# Recursive function to calculate the total number of files and directories in the Zip file. +function GetNumberOfItemsInZipFileItems($shellItems) +{ + [int]$totalItems = $shellItems.Count + foreach ($shellItem in $shellItems) + { + if ($shellItem.IsFolder) + { $totalItems += GetNumberOfItemsInZipFileItems -shellItems $shellItem.GetFolder.Items() } + } + $totalItems +} + +# Recursive function to move a directory into a Zip file, since we can move files out of a Zip file, but not directories, and copying a directory into a Zip file when it already exists is not allowed. +function MoveDirectoryIntoZipFile($parentInZipFileShell, $pathOfItemToCopy) +{ + # Get the name of the file/directory to copy, and the item itself. + $nameOfItemToCopy = Split-Path -Path $pathOfItemToCopy -Leaf + if ($parentInZipFileShell.IsFolder) + { $parentInZipFileShell = $parentInZipFileShell.GetFolder } + $itemToCopyShell = $parentInZipFileShell.ParseName($nameOfItemToCopy) + + # If this item does not exist in the Zip file yet, or it is a file, move it over. + if ($itemToCopyShell -eq $null -or !$itemToCopyShell.IsFolder) + { + $parentInZipFileShell.MoveHere($pathOfItemToCopy) + + # Wait for the file to be moved before continuing, to avoid erros about the zip file being locked or a file not being found. + while (Test-Path -Path $pathOfItemToCopy) + { Start-Sleep -Milliseconds 10 } + } + # Else this is a directory that already exists in the Zip file, so we need to traverse it and copy each file/directory within it. + else + { + # Copy each file/directory in the directory to the Zip file. + foreach ($item in (Get-ChildItem -Path $pathOfItemToCopy -Force)) + { + MoveDirectoryIntoZipFile -parentInZipFileShell $itemToCopyShell -pathOfItemToCopy $item.FullName + } + } +} + +# Recursive function to move all of the files that start with the File Name Prefix to the Directory To Move Files To. +function MoveFilesOutOfZipFileItems($shellItems, $directoryToMoveFilesToShell, $fileNamePrefix) +{ + # Loop through every item in the file/directory. + foreach ($shellItem in $shellItems) + { + # If this is a directory, recursively call this function to iterate over all files/directories within it. + if ($shellItem.IsFolder) + { + $totalItems += MoveFilesOutOfZipFileItems -shellItems $shellItem.GetFolder.Items() -directoryToMoveFilesTo $directoryToMoveFilesToShell -fileNameToMatch $fileNameToMatch + } + # Else this is a file. + else + { + # If this file name starts with the File Name Prefix, move it to the specified directory. + if ($shellItem.Name.StartsWith($fileNamePrefix)) + { + $directoryToMoveFilesToShell.MoveHere($shellItem) + } + } + } +} + +function Expand-ZipFile +{ + [CmdletBinding()] + param + ( + [parameter(Position=1,Mandatory=$true)] + [ValidateScript({(Test-Path -Path $_ -PathType Leaf) -and $_.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase)})] + [string]$ZipFilePath, + + [parameter(Position=2,Mandatory=$false)] + [string]$DestinationDirectoryPath, + + [Alias("Force")] + [switch]$OverwriteWithoutPrompting + ) + + BEGIN { } + END { } + PROCESS + { + # If a Destination Directory was not given, create one in the same directory as the Zip file, with the same name as the Zip file. + if ($DestinationDirectoryPath -eq $null -or $DestinationDirectoryPath.Trim() -eq [string]::Empty) + { + $zipFileDirectoryPath = Split-Path -Path $ZipFilePath -Parent + $zipFileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($ZipFilePath) + $DestinationDirectoryPath = Join-Path -Path $zipFileDirectoryPath -ChildPath $zipFileNameWithoutExtension + } + + # If the directory to unzip the files to does not exist yet, create it. + if (!(Test-Path -Path $DestinationDirectoryPath -PathType Container)) + { New-Item -Path $DestinationDirectoryPath -ItemType Container > $null } + + # Flags and values found at: https://msdn.microsoft.com/en-us/library/windows/desktop/bb759795%28v=vs.85%29.aspx + $FOF_SILENT = 0x0004 + $FOF_NOCONFIRMATION = 0x0010 + $FOF_NOERRORUI = 0x0400 + + # Set the flag values based on the parameters provided. + $copyFlags = 0 + if ($OverwriteWithoutPrompting) + { $copyFlags = $FOF_NOCONFIRMATION } + # { $copyFlags = $FOF_SILENT + $FOF_NOCONFIRMATION + $FOF_NOERRORUI } + + # Get the Shell object, Destination Directory, and Zip file. + $shell = New-Object -ComObject Shell.Application + $destinationDirectoryShell = $shell.NameSpace($DestinationDirectoryPath) + $zipShell = $shell.NameSpace($ZipFilePath) + + # Start copying the Zip files into the destination directory, using the flags specified by the user. This is an asynchronous operation. + $destinationDirectoryShell.CopyHere($zipShell.Items(), $copyFlags) + + # Get the number of files and directories in the Zip file. + $numberOfItemsInZipFile = GetNumberOfItemsInZipFileItems -shellItems $zipShell.Items() + + # The Copy (i.e. unzip) operation is asynchronous, so wait until it is complete before continuing. That is, sleep until the Destination Directory has the same number of files as the Zip file. + while ((Get-ChildItem -Path $DestinationDirectoryPath -Recurse -Force).Count -lt $numberOfItemsInZipFile) + { Start-Sleep -Milliseconds 100 } + } +} + +function Compress-ZipFile +{ + [CmdletBinding()] + param + ( + [parameter(Position=1,Mandatory=$true)] + [ValidateScript({Test-Path -Path $_})] + [string]$FileOrDirectoryPathToAddToZipFile, + + [parameter(Position=2,Mandatory=$false)] + [string]$ZipFilePath, + + [Alias("Force")] + [switch]$OverwriteWithoutPrompting + ) + + BEGIN { } + END { } + PROCESS + { + # If a Zip File Path was not given, create one in the same directory as the file/directory being added to the zip file, with the same name as the file/directory. + if ($ZipFilePath -eq $null -or $ZipFilePath.Trim() -eq [string]::Empty) + { $ZipFilePath = Join-Path -Path $FileOrDirectoryPathToAddToZipFile -ChildPath '.zip' } + + # If the Zip file to create does not have an extension of .zip (which is required by the shell.application), add it. + if (!$ZipFilePath.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase)) + { $ZipFilePath += '.zip' } + + # If the Zip file to add the file to does not exist yet, create it. + if (!(Test-Path -Path $ZipFilePath -PathType Leaf)) + { New-Item -Path $ZipFilePath -ItemType File > $null } + + # Get the Name of the file or directory to add to the Zip file. + $fileOrDirectoryNameToAddToZipFile = Split-Path -Path $FileOrDirectoryPathToAddToZipFile -Leaf + + # Get the number of files and directories to add to the Zip file. + $numberOfFilesAndDirectoriesToAddToZipFile = (Get-ChildItem -Path $FileOrDirectoryPathToAddToZipFile -Recurse -Force).Count + + # Get if we are adding a file or directory to the Zip file. + $itemToAddToZipIsAFile = Test-Path -Path $FileOrDirectoryPathToAddToZipFile -PathType Leaf + + # Get Shell object and the Zip File. + $shell = New-Object -ComObject Shell.Application + $zipShell = $shell.NameSpace($ZipFilePath) + + # We will want to check if we can do a simple copy operation into the Zip file or not. Assume that we can't to start with. + # We can if the file/directory does not exist in the Zip file already, or it is a file and the user wants to be prompted on conflicts. + $canPerformSimpleCopyIntoZipFile = $false + + # If the file/directory does not already exist in the Zip file, or it does exist, but it is a file and the user wants to be prompted on conflicts, then we can perform a simple copy into the Zip file. + $fileOrDirectoryInZipFileShell = $zipShell.ParseName($fileOrDirectoryNameToAddToZipFile) + $itemToAddToZipIsAFileAndUserWantsToBePromptedOnConflicts = ($itemToAddToZipIsAFile -and !$OverwriteWithoutPrompting) + if ($fileOrDirectoryInZipFileShell -eq $null -or $itemToAddToZipIsAFileAndUserWantsToBePromptedOnConflicts) + { + $canPerformSimpleCopyIntoZipFile = $true + } + + # If we can perform a simple copy operation to get the file/directory into the Zip file. + if ($canPerformSimpleCopyIntoZipFile) + { + # Start copying the file/directory into the Zip file since there won't be any conflicts. This is an asynchronous operation. + $zipShell.CopyHere($FileOrDirectoryPathToAddToZipFile) # Copy Flags are ignored when copying files into a zip file, so can't use them like we did with the Expand-ZipFile function. + + # The Copy operation is asynchronous, so wait until it is complete before continuing. + # Wait until we can see that the file/directory has been created. + while ($zipShell.ParseName($fileOrDirectoryNameToAddToZipFile) -eq $null) + { Start-Sleep -Milliseconds 100 } + + # If we are copying a directory into the Zip file, we want to wait until all of the files/directories have been copied. + if (!$itemToAddToZipIsAFile) + { + # Get the number of files and directories that should be copied into the Zip file. + $numberOfItemsToCopyIntoZipFile = (Get-ChildItem -Path $FileOrDirectoryPathToAddToZipFile -Recurse -Force).Count + + # Get a handle to the new directory we created in the Zip file. + $newDirectoryInZipFileShell = $zipShell.ParseName($fileOrDirectoryNameToAddToZipFile) + + # Wait until the new directory in the Zip file has the expected number of files and directories in it. + while ((GetNumberOfItemsInZipFileItems -shellItems $newDirectoryInZipFileShell.GetFolder.Items()) -lt $numberOfItemsToCopyIntoZipFile) + { Start-Sleep -Milliseconds 100 } + } + } + # Else we cannot do a simple copy operation. We instead need to move the files out of the Zip file so that we can merge the directory, or overwrite the file without the user being prompted. + # We cannot move a directory into the Zip file if a directory with the same name already exists, as a MessageBox warning is thrown, not a conflict resolution prompt like with files. + # We cannot silently overwrite an existing file in the Zip file, as the flags passed to the CopyHere/MoveHere functions seem to be ignored when copying into a Zip file. + else + { + # Create a temp directory to hold our file/directory. + $tempDirectoryPath = $null + $tempDirectoryPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName()) + New-Item -Path $tempDirectoryPath -ItemType Container > $null + + # If we will be moving a directory into the temp directory. + $numberOfItemsInZipFilesDirectory = 0 + if ($fileOrDirectoryInZipFileShell.IsFolder) + { + # Get the number of files and directories in the Zip file's directory. + $numberOfItemsInZipFilesDirectory = GetNumberOfItemsInZipFileItems -shellItems $fileOrDirectoryInZipFileShell.GetFolder.Items() + } + + # Start moving the file/directory out of the Zip file and into a temp directory. This is an asynchronous operation. + $tempDirectoryShell = $shell.NameSpace($tempDirectoryPath) + $tempDirectoryShell.MoveHere($fileOrDirectoryInZipFileShell) + + # If we are moving a directory, we need to wait until all of the files and directories in that Zip file's directory have been moved. + $fileOrDirectoryPathInTempDirectory = Join-Path -Path $tempDirectoryPath -ChildPath $fileOrDirectoryNameToAddToZipFile + if ($fileOrDirectoryInZipFileShell.IsFolder) + { + # The Move operation is asynchronous, so wait until it is complete before continuing. That is, sleep until the Destination Directory has the same number of files as the directory in the Zip file. + while ((Get-ChildItem -Path $fileOrDirectoryPathInTempDirectory -Recurse -Force).Count -lt $numberOfItemsInZipFilesDirectory) + { Start-Sleep -Milliseconds 100 } + } + # Else we are just moving a file, so we just need to check for when that one file has been moved. + else + { + # The Move operation is asynchronous, so wait until it is complete before continuing. + while (!(Test-Path -Path $fileOrDirectoryPathInTempDirectory)) + { Start-Sleep -Milliseconds 100 } + } + + # We want to copy the file/directory to add to the Zip file to the same location in the temp directory, so that files/directories are merged. + # If we should automatically overwrite files, do it. + if ($OverwriteWithoutPrompting) + { Copy-Item -Path $FileOrDirectoryPathToAddToZipFile -Destination $tempDirectoryPath -Recurse -Force } + # Else the user should be prompted on each conflict. + else + { Copy-Item -Path $FileOrDirectoryPathToAddToZipFile -Destination $tempDirectoryPath -Recurse -Confirm -ErrorAction SilentlyContinue } # SilentlyContinue errors to avoid an error for every directory copied. + + # For whatever reason the zip.MoveHere() function is not able to move empty directories into the Zip file, so we have to put dummy files into these directories + # and then remove the dummy files from the Zip file after. + # If we are copying a directory into the Zip file. + $dummyFileNamePrefix = 'Dummy.File' + [int]$numberOfDummyFilesCreated = 0 + if ($fileOrDirectoryInZipFileShell.IsFolder) + { + # Place a dummy file in each of the empty directories so that it gets copied into the Zip file without an error. + $emptyDirectories = Get-ChildItem -Path $fileOrDirectoryPathInTempDirectory -Recurse -Force -Directory | Where-Object { (Get-ChildItem -Path $_ -Force) -eq $null } + foreach ($emptyDirectory in $emptyDirectories) + { + $numberOfDummyFilesCreated++ + New-Item -Path (Join-Path -Path $emptyDirectory.FullName -ChildPath "$dummyFileNamePrefix$numberOfDummyFilesCreated") -ItemType File -Force > $null + } + } + + # If we need to copy a directory back into the Zip file. + if ($fileOrDirectoryInZipFileShell.IsFolder) + { + MoveDirectoryIntoZipFile -parentInZipFileShell $zipShell -pathOfItemToCopy $fileOrDirectoryPathInTempDirectory + } + # Else we need to copy a file back into the Zip file. + else + { + # Start moving the merged file back into the Zip file. This is an asynchronous operation. + $zipShell.MoveHere($fileOrDirectoryPathInTempDirectory) + } + + # The Move operation is asynchronous, so wait until it is complete before continuing. + # Sleep until all of the files have been moved into the zip file. The MoveHere() function leaves empty directories behind, so we only need to watch for files. + do + { + Start-Sleep -Milliseconds 100 + $files = Get-ChildItem -Path $fileOrDirectoryPathInTempDirectory -Force -Recurse | Where-Object { !$_.PSIsContainer } + } while ($files -ne $null) + + # If there are dummy files that need to be moved out of the Zip file. + if ($numberOfDummyFilesCreated -gt 0) + { + # Move all of the dummy files out of the supposed-to-be empty directories in the Zip file. + MoveFilesOutOfZipFileItems -shellItems $zipShell.items() -directoryToMoveFilesToShell $tempDirectoryShell -fileNamePrefix $dummyFileNamePrefix + + # The Move operation is asynchronous, so wait until it is complete before continuing. + # Sleep until all of the dummy files have been moved out of the zip file. + do + { + Start-Sleep -Milliseconds 100 + [Object[]]$files = Get-ChildItem -Path $tempDirectoryPath -Force -Recurse | Where-Object { !$_.PSIsContainer -and $_.Name.StartsWith($dummyFileNamePrefix) } + } while ($files -eq $null -or $files.Count -lt $numberOfDummyFilesCreated) + } + + # Delete the temp directory that we created. + Remove-Item -Path $tempDirectoryPath -Force -Recurse > $null + } + } +} + +# Specify which functions should be publicly accessible. +Export-ModuleMember -Function Expand-ZipFile +Export-ModuleMember -Function Compress-ZipFile \ No newline at end of file diff --git a/enable_winrm.ps1 b/enable_winrm.ps1 new file mode 100644 index 0000000..457584d --- /dev/null +++ b/enable_winrm.ps1 @@ -0,0 +1,33 @@ +# Copyright 2021 VMware, Inc. +# SPDX-License-Identifier: Apache-2.0 +New-NetFirewallRule -Name "SMB445" -DisplayName "SMB445" -Protocol TCP -LocalPort 445 +New-NetFirewallRule -Name "WINRM5986" -DisplayName "WINRM5986" -Protocol TCP -LocalPort 5986 + +winrm quickconfig -q +winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}' +winrm set winrm/config '@{MaxTimeoutms="1800000"}' +winrm set winrm/config/service/auth '@{Basic="true"}' + +$SourceStoreScope = 'LocalMachine' +$SourceStorename = 'Remote Desktop' + +$SourceStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $SourceStorename, $SourceStoreScope +$SourceStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) + +$cert = $SourceStore.Certificates | Where-Object -FilterScript { + $_.subject -like '*' + } + + $DestStoreScope = 'LocalMachine' + $DestStoreName = 'My' + + $DestStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $DestStoreName, $DestStoreScope + $DestStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $DestStore.Add($cert) + + $SourceStore.Close() + $DestStore.Close() + + winrm create winrm/config/listener?Address=*+Transport=HTTPS `@`{Hostname=`"($certId)`"`;CertificateThumbprint=`"($cert.Thumbprint)`"`} + + Restart-Service winrm diff --git a/saltwinshell/__init__.py b/saltwinshell/__init__.py new file mode 100644 index 0000000..c72bd6d --- /dev/null +++ b/saltwinshell/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2021 VMware, Inc. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import absolute_import +from .core import * diff --git a/saltwinshell/core.pyx b/saltwinshell/core.pyx new file mode 100644 index 0000000..0ac34ab --- /dev/null +++ b/saltwinshell/core.pyx @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +''' +Core functions to impliment the salt windows shell +''' +# Copyright 2021 VMware, Inc. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import absolute_import + +import os +import logging +import salt.exceptions +import salt.ext.six +import salt.utils.smb +try: + import salt.utils.stringutils as stringutils +except ImportError: + # This exception handling can be removed once 2017.7 is no longer + # supported. + import salt.utils as stringutils +from salt.client.ssh.shell import Shell as LinuxShell +import ntpath +from saltwinshell.version import version as version + +# Import 3rd party libs +try: + from winrm.protocol import Protocol + HAS_WINRM = True +except Exception: + HAS_WINRM = False + +log = logging.getLogger(__name__) + +# Windows Powershell Shim - actually, this is python +SSH_PS_SHIM = \ + '\n'.join( + [s.strip() for s in r''' +import base64 + +exec(base64.b64decode("""{SSH_PY_CODE}""")) +'''.split('\n')]) + + +def set_winvars(self, ssh_python_env=None): + ''' + Set the Win Vars + ''' + self.thin_dir = 'c:\\saltremote/thin' + self.python_dir = 'c:\\saltremote/bin' + pyver = 'Py3' + if salt.ext.six.PY2: + pyver = 'Py2' + self.python_env_map = { + 'AMD64': 'Salt-Env-{0}-AMD64-{1}.zip'.format(version, pyver), + 'x86' : 'Salt-Env-{0}-x86-{1}.zip'.format(version, pyver), + } + self.python_saltwinshell = '/usr/local/saltwinshell' + if os.path.exists('/usr/saltwinshell'): + self.python_saltwinshell = '/usr/saltwinshell' + + +def gen_shim(py_code_enc): + ''' + Generate a PowerShell shim + ''' + cmd = SSH_PS_SHIM.format(SSH_PY_CODE=py_code_enc) + return cmd + + +def get_target_shim_file(self, target_shim_file): + ''' + Get the target shim file + ''' + return ntpath.normpath(ntpath.sep.join((self.python_dir, target_shim_file))) + + +def call_python(self, target_shim_file): + ''' + Call python stuff + ''' + return self.shell.exec_cmd('{0} {1}'.format(ntpath.normpath(ntpath.sep.join((self.python_dir, 'python.exe'))), ntpath.normpath(target_shim_file))) + + +def deploy_python(self): + ''' + Deploy the Windows python environment + ''' + if not self.python_env: + log.debug('No Python Environment found. Determining which env to use') + self.python_env = os.path.join(self.python_saltwinshell, self.python_env_map[self.arch]) + if not os.path.isfile(self.python_env): + if os.path.isfile(os.path.join(self.python_saltwinshell, self.python_env_map[self.arch])): + self.python_env = os.path.join(self.python_saltwinshell, self.python_env_map[self.arch]) + if not os.path.isfile(self.python_env): + raise salt.exceptions.SaltConfigurationError( 'Python env: {0} doesn\'t exist.'.format(self.python_env)) + self.shell.send( + self.python_env, + os.path.join(self.python_dir, 'bin.zip'), + makedirs=True, + ) + self.shell.send( + os.path.join(self.python_saltwinshell, 'Synchronous-ZipAndUnzip.psm1'), + os.path.join(self.python_dir, 'Synchronous-ZipAndUnzip.psm1'), + makedirs=True, + ) + UNZIP_SHIM = ''' +$THIS_SCRIPTS_DIRECTORY_PATH = '{0}' +$SynchronousZipAndUnzipModulePath = Join-Path $THIS_SCRIPTS_DIRECTORY_PATH 'Synchronous-ZipAndUnzip.psm1' +Import-Module -Name $SynchronousZipAndUnzipModulePath + +# Variables used to test the functions. +$zipFilePath = "{1}"; +$destinationDirectoryPath = "{2}"; + +# Unzip the Zip file to a new UnzippedContents directory. +Expand-ZipFile -ZipFilePath $zipFilePath -DestinationDirectoryPath $destinationDirectoryPath -OverwriteWithoutPrompting +'''.format(ntpath.normpath(self.python_dir), ntpath.normpath(ntpath.join(self.python_dir, 'bin.zip')), ntpath.normpath(self.python_dir)) + stdout, stderr, retcode = self.shim_cmd(UNZIP_SHIM, extension='ps1') + return True + + +class Shell(LinuxShell): + def send(self, local, remote, makedirs=False): + ''' + send a file to a remote system using smb + ''' + ret_stdout = ret_stderr = retcode = None + if makedirs: + self.exec_cmd('mkdir {0}'.format(ntpath.dirname(ntpath.normpath(remote)))) + + smb_conn = salt.utils.smb.get_conn(self.host, self.user, self.passwd) + if smb_conn is False: + ret_stderr = 'Please install impacket to enable SMB functionality' + log.error(ret_stderr) + return ret_stdout, ret_stderr, 1 + + log.debug('Copying {0} to {1} on minion'.format(local, ntpath.normpath(remote))) + if remote.startswith('c:'): + drive, remote = remote.split(':') + salt.utils.smb.put_file(local, ntpath.normpath(remote), conn=smb_conn) + retcode = 0 + + return ret_stdout, ret_stderr, retcode + + def exec_cmd(self, cmd): + ''' + Execute a remote command + ''' + logmsg = 'Executing command: {0}'.format(cmd) + if self.passwd: + logmsg = logmsg.replace(self.passwd, ('*' * 6)) + if 'decode("base64")' in logmsg or 'base64.b64decode(' in logmsg: + log.debug('Executed SHIM command. Command logged to TRACE') + log.trace(logmsg) + else: + log.debug(logmsg) + + ret = self._run_cmd(cmd) + return ret + + def _run_cmd(self, cmd, key_accept=False, passwd_retries=3): + ''' + Execute a shell command via PowerShell. + ''' + if not HAS_WINRM: + return None, 'pywinrm is required to be installed on the Salt Master', 1 + + p = Protocol( + endpoint='https://{0}:5986/wsman'.format(self.host), + transport='ntlm', + username=self.user, + password=self.passwd, + server_cert_validation='ignore') + shell_id = p.open_shell() + command_id = p.run_command(shell_id, cmd) + std_out_bytes, std_err_bytes, status_code = p.get_command_output(shell_id, command_id) + p.cleanup_command(shell_id, command_id) + p.close_shell(shell_id) + std_out = stringutils.to_unicode(std_out_bytes) + std_err = stringutils.to_unicode(std_err_bytes) + return std_out, std_err, status_code + diff --git a/saltwinshell/version.py b/saltwinshell/version.py new file mode 100644 index 0000000..745ea1b --- /dev/null +++ b/saltwinshell/version.py @@ -0,0 +1,3 @@ +# Copyright 2021 VMware, Inc. +# SPDX-License-Identifier: Apache-2.0 +version = '2017.7' diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6f35aa7 --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2021 VMware, Inc. +# SPDX-License-Identifier: Apache-2.0 + +import sys +from setuptools import setup +from Cython.Build import cythonize + +NAME = 'saltwinshell' +DESC = 'Agentless Salt for Windows, compatible with salt-ssh' +PYVER = 'Py3' +if sys.version_info.major == 2: + PYVER = 'Py2' + +# Version info -- read without importing +_locals = {} +with open('saltwinshell/version.py') as fp: + exec(fp.read(), None, _locals) +VERSION = _locals['version'] + +setup(name=NAME, + version=VERSION, + description=DESC, + author='VMware, Inc.', + author_email='saltproject@vmware.com', + url='https://saltproject.io/', + classifiers=[ + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Development Status :: 5 - Production/Stable', + ], + packages=['saltwinshell'], + install_requires=[ + 'smbprotocol', + 'pywinrm', + 'pyopenssl', + ], + data_files=[ + ('saltwinshell',['enable_winrm.ps1']), + ('saltwinshell',['Synchronous-ZipAndUnzip.psm1']), + ('saltwinshell',['Salt-Env-{0}-AMD64-{1}.zip'.format(VERSION, PYVER)]), + ('saltwinshell',['Salt-Env-{0}-x86-{1}.zip'.format(VERSION, PYVER)]), + ], + ext_modules=cythonize('saltwinshell/core.pyx'), + )