Merge pull request #47028 from dwoz/pypsexec

Add support for pypsexec and smbprotocol
This commit is contained in:
Daniel Wozniak 2018-05-04 10:54:25 -07:00 committed by GitHub
commit 6147f08df7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 879 additions and 116 deletions

View file

@ -9,6 +9,13 @@ Windows images.
Requirements
============
.. note::
Support ``winexe`` and ``impacket`` has been deprecated and will be removed in
Fluorine. These dependencies are replaced by ``pypsexec`` and ``smbprotocol``
respectivly. These are pure python alternatives that are compatible with all
supported python versions.
Salt Cloud makes use of `impacket` and `winexe` to set up the Windows Salt
Minion installer.
@ -32,6 +39,15 @@ channels:
.. __: http://software.opensuse.org/package/winexe
* `pypsexec project home`__
.. __: https://github.com/jborean93/pypsexec
* `smbprotocol project home`__
.. __: https://github.com/jborean93/smbprotocol
Optionally WinRM can be used instead of `winexe` if the python module `pywinrm`
is available and WinRM is supported on the target Windows version. Information
on pywinrm can be found at the project home:

View file

@ -434,7 +434,7 @@ The ``vault`` utils module had the following changes:
Please see the :mod:`vault execution module <salt.modules.vault>` documentation for
details on the new configuration schema.
=======
=====================
SaltSSH major updates
=====================
@ -471,7 +471,25 @@ a minimal tarball using runners and include that. But this is only possible, whe
Salt version is also available on the Master machine, although does not need to be directly
installed together with the older Python interpreter.
=======
========================
Salt-Cloud major updates
========================
Dependency Deprecations
=======================
Salt-Cloud has been updated to use the ``pypsexec`` Python library instead of the
``winexe`` executable. Both ``winexe`` and ``pypsexec`` run remote commands
against Windows OSes. Since ``winexe`` is not packaged for every system, it has
been deprecated in favor of ``pypsexec``.
Salt-Cloud has deprecated the use ``impacket`` in favor of ``smbprotocol``.
This changes was made because ``impacket`` is not compatible with Python 3.
====================
State Module Changes
====================

View file

@ -3,7 +3,7 @@
Create ssh executor system
'''
# Import python libs
from __future__ import absolute_import, print_function
from __future__ import absolute_import, print_function, unicode_literals
import base64
import copy
import getpass
@ -1243,7 +1243,7 @@ ARGS = {arguments}\n'''.format(config=self.minion_config,
shim_tmp_file.write(salt.utils.stringutils.to_bytes(cmd_str))
# Copy shim to target system, under $HOME/.<randomized name>
target_shim_file = '.{0}.{1}'.format(binascii.hexlify(os.urandom(6)), extension)
target_shim_file = '.{0}.{1}'.format(binascii.hexlify(os.urandom(6)).decode('ascii'), extension)
if self.winrm:
target_shim_file = saltwinshell.get_target_shim_file(self, target_shim_file)
self.shell.send(shim_tmp_file.name, target_shim_file, makedirs=True)
@ -1367,7 +1367,9 @@ ARGS = {arguments}\n'''.format(config=self.minion_config,
return stdout, stderr, retcode
def categorize_shim_errors(self, stdout, stderr, retcode):
def categorize_shim_errors(self, stdout_bytes, stderr_bytes, retcode):
stdout = salt.utils.stringutils.to_unicode(stdout_bytes)
stderr = salt.utils.stringutils.to_unicode(stderr_bytes)
if re.search(RSTR_RE, stdout) and stdout != RSTR+'\n':
# RSTR was found in stdout which means that the shim
# functioned without *errors* . . . but there may be shim

View file

@ -2,7 +2,7 @@
'''
Manage transport commands via ssh
'''
from __future__ import absolute_import, print_function
from __future__ import absolute_import, print_function, unicode_literals
# Import python libs
import re

View file

@ -2372,7 +2372,7 @@ def wait_for_instance(
vm_['win_password'] = win_passwd
break
# SMB used whether winexe or winrm
# SMB used whether psexec or winrm
if not salt.utils.cloud.wait_for_port(ip_address,
port=445,
timeout=ssh_connect_timeout):
@ -2380,10 +2380,10 @@ def wait_for_instance(
'Failed to connect to remote windows host'
)
# If not using winrm keep same winexe behavior
# If not using winrm keep same psexec behavior
if not use_winrm:
log.debug('Trying to authenticate via SMB using winexe')
log.debug('Trying to authenticate via SMB using psexec')
if not salt.utils.cloud.validate_windows_cred(ip_address,
username,

View file

@ -554,3 +554,9 @@ class VMwareVmCreationError(VMwareSaltError):
'''
Used when a configuration parameter is incorrect
'''
class MissingSmb(SaltException):
'''
Raised when no smb library is found.
'''

View file

@ -28,7 +28,7 @@ provided `kubeconfig` entry is preferred.
.. warning::
Configuration options changed in Flourine. The following configuration options have been removed:
Configuration options changed in Fluorine. The following configuration options have been removed:
- kubernetes.user
- kubernetes.password

View file

@ -47,7 +47,7 @@ Installation Prerequisites
:requires: purestorage
:platform: all
.. versionadded:: Flourine
.. versionadded:: Fluorine
'''
@ -170,7 +170,7 @@ def snap_create(name, suffix=None):
Will return False if filesystem selected to snap does not exist.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem to snapshot
@ -211,7 +211,7 @@ def snap_delete(name, suffix=None, eradicate=False):
Will return False if selected snapshot does not exist.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem
@ -255,7 +255,7 @@ def snap_eradicate(name, suffix=None):
Will return False if snapshot is not in a deleted state.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem
@ -288,7 +288,7 @@ def fs_create(name, size=None, proto='NFS', nfs_rules='*(rw,no_root_squash)', sn
Will return False if filesystem already exists.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem (truncated to 63 characters)
@ -359,7 +359,7 @@ def fs_delete(name, eradicate=False):
Will return False if filesystem doesn't exist or is already in a deleted state.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem
@ -403,7 +403,7 @@ def fs_eradicate(name):
Will return False is filesystem is not in a deleted state.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem
@ -433,7 +433,7 @@ def fs_extend(name, size):
Will return False if new size is less than or equal to existing size.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem
@ -474,7 +474,7 @@ def fs_update(name, rules, snapshot=False):
Allows for change of NFS export rules and enabling/disabled
of snapshotting capability.
.. versionadded:: Flourine
.. versionadded:: Fluorine
name : string
name of filesystem

View file

@ -8,7 +8,7 @@ salt.modules.kubernetes for more information.
.. warning::
Configuration options will change in Flourine.
Configuration options will change in Fluorine.
The kubernetes module is used to manage different kubernetes resources.

View file

@ -115,10 +115,10 @@ def installed(name,
'''
if 'force' in kwargs:
salt.utils.versions.warn_until(
'Flourine',
'Fluorine',
'Parameter \'force\' has been detected in the argument list. This'
'parameter is no longer used and has been replaced by \'recurse\''
'as of Salt 2018.3.0. This warning will be removed in Salt Flourine.'
'as of Salt 2018.3.0. This warning will be removed in Salt Fluorine.'
)
kwargs.pop('force')

View file

@ -10,6 +10,7 @@ import os
import stat
import codecs
import shutil
import uuid
import hashlib
import socket
import tempfile
@ -27,11 +28,22 @@ import uuid
try:
import salt.utils.smb
HAS_SMB = True
except ImportError:
HAS_SMB = False
try:
from pypsexec.client import Client as PsExecClient
from pypsexec.scmr import Service as ScmrService
from pypsexec.exceptions import SCMRException
from smbprotocol.tree import TreeConnect
from smbprotocol.exceptions import SMBResponseException
logging.getLogger('smbprotocol').setLevel(logging.WARNING)
logging.getLogger('pypsexec').setLevel(logging.WARNING)
HAS_PSEXEC = True
except ImportError:
HAS_PSEXEC = False
try:
import winrm
from winrm.exceptions import WinRMTransportError
@ -51,6 +63,7 @@ import salt.utils.crypt
import salt.utils.data
import salt.utils.event
import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.versions
@ -124,6 +137,13 @@ def __render_script(path, vm_=None, opts=None, minion=''):
return six.text_type(fp_.read())
def has_winexe():
'''
True when winexe is found on the system
'''
return salt.utils.path.which('winexe')
def os_script(os_, vm_=None, opts=None, minion=''):
'''
Return the script as a string for the specific os
@ -644,7 +664,7 @@ def wait_for_port(host, port=22, timeout=900, gateway=None):
'''
Wait until a connection to the specified port can be made on a specified
host. This is usually port 22 (for SSH), but in the case of Windows
installations, it might be port 445 (for winexe). It may also be an
installations, it might be port 445 (for psexec). It may also be an
alternate port for SSH, depending on the base image.
'''
start = time.time()
@ -779,15 +799,85 @@ def wait_for_port(host, port=22, timeout=900, gateway=None):
)
def wait_for_winexesvc(host, port, username, password, timeout=900):
class Client(object):
'''
Wait until winexe connection can be established.
Wrap pypsexec.client.Client to fix some stability issues:
- Set the service name from a keyword arg, this allows multiple service
instances to be created in a single process.
- Keep trying service and file deletes since they may not succeed on the
first try. Raises an exception if they do not succeed after a timeout
period.
'''
def __init__(self, server, username=None, password=None, port=445,
encrypt=True, service_name=None):
self.service_name = service_name
self._exe_file = "{0}.exe".format(self.service_name)
self._client = PsExecClient(server, username, password, port, encrypt)
self._service = ScmrService(self.service_name, self._client.session)
def connect(self):
return self._client.connect()
def disconnect(self):
return self._client.disconnect()
def create_service(self):
return self._client.create_service()
def run_executabe(self, *args, **kwargs):
return self._client.run_executable(*args, **kwargs)
def remove_service(self, wait_timeout=10, sleep_wait=1):
'''
Removes the PAExec service and executable that was created as part of
the create_service function. This does not remove any older executables
or services from previous runs, use cleanup() instead for that purpose.
'''
# Stops/remove the PAExec service and removes the executable
log.debug("Deleting PAExec service at the end of the process")
wait_start = time.time()
while True:
try:
self._service.delete()
except SCMRException as exc:
log.debug("Exception encountered while deleting service %s", repr(exc))
if time.time() - wait_start > wait_timeout:
raise exc
time.sleep(sleep_wait)
continue
break
# delete the PAExec executable
smb_tree = TreeConnect(
self._client.session,
r"\\{0}\ADMIN$".format(self._client.connection.server_name)
)
log.info("Connecting to SMB Tree %s", smb_tree.share_name)
smb_tree.connect()
wait_start = time.time()
while True:
try:
log.info("Creating open to PAExec file with delete on close flags")
self._client._delete_file(smb_tree, self._exe_file)
except SMBResponseException as exc:
log.debug("Exception deleting file %s %s", self._exe_file, repr(exc))
if time.time() - wait_start > wait_timeout:
raise exc
time.sleep(sleep_wait)
continue
break
log.info("Disconnecting from SMB Tree %s", smb_tree.share_name)
smb_tree.disconnect()
def run_winexe_command(cmd, args, host, username, password, port=445):
'''
Run a command remotly via the winexe executable
'''
start = time.time()
log.debug(
'Attempting winexe connection to host %s on port %s',
host, port
)
creds = "-U '{0}%{1}' //{2}".format(
username,
password,
@ -797,15 +887,47 @@ def wait_for_winexesvc(host, port, username, password, timeout=900):
username,
host
)
cmd = 'winexe {0} {1} {2}'.format(creds, cmd, args)
logging_cmd = 'winexe {0} {1} {2}'.format(logging_creds, cmd, args)
return win_cmd(cmd, logging_command=logging_cmd)
def run_psexec_command(cmd, args, host, username, password, port=445):
'''
Run a command remotly using the psexec protocol
'''
if has_winexe() and not HAS_PSEXEC:
ret_code = run_winexe_command(cmd, args, host, username, password, port)
return None, None, ret_code
service_name = 'PS-Exec-{0}'.format(uuid.uuid4())
stdout, stderr, ret_code = '', '', None
client = Client(host, username, password, port=port, encrypt=False, service_name=service_name)
client.connect()
try:
client.create_service()
stdout, stderr, ret_code = client.run_executable(cmd, args)
finally:
client.remove_service()
client.disconnect()
return stdout, stderr, ret_code
def wait_for_winexe(host, port, username, password, timeout=900):
'''
Wait until winexe connection can be established.
'''
start = time.time()
log.debug(
'Attempting winexe connection to host %s on port %s',
host, port
)
try_count = 0
while True:
try_count += 1
try:
# Shell out to winexe to check %TEMP%
ret_code = win_cmd(
'winexe {0} "sc query winexesvc"'.format(creds),
logging_command=logging_creds
ret_code = run_winexe_command(
"sc", "query winexesvc", host, username, password, port
)
if ret_code == 0:
log.debug('winexe connected...')
@ -815,11 +937,39 @@ def wait_for_winexesvc(host, port, username, password, timeout=900):
log.debug('Caught exception in wait_for_winexesvc: %s', exc)
if time.time() - start > timeout:
log.error('winexe connection timed out: %s', timeout)
return False
time.sleep(1)
def wait_for_psexecsvc(host, port, username, password, timeout=900):
'''
Wait until psexec connection can be established.
'''
if has_winexe() and not HAS_PSEXEC:
return wait_for_winexe(host, port, username, password, timeout)
start = time.time()
try_count = 0
while True:
try_count += 1
ret_code = 1
try:
stdout, stderr, ret_code = run_psexec_command(
'cmd.exe', '/c hostname', host, username, password, port=port
)
except Exception as exc:
log.exception("Unable to execute command")
if ret_code == 0:
log.debug('psexec connected...')
return True
if time.time() - start > timeout:
return False
log.debug(
'Retrying winexe connection to host %s on port %s (try %s)',
host, port, try_count
'Retrying psexec connection to host {0} on port {1} '
'(try {2})'.format(
host,
port,
try_count
)
)
time.sleep(1)
@ -868,7 +1018,7 @@ def wait_for_winrm(host, port, username, password, timeout=900, use_ssl=True, ve
time.sleep(1)
def validate_windows_cred(host,
def validate_windows_cred_winexe(host,
username='Administrator',
password=None,
retries=10,
@ -885,12 +1035,30 @@ def validate_windows_cred(host,
username,
host
)
for i in range(retries):
ret_code = win_cmd(
cmd,
logging_command=logging_cmd
)
return ret_code == 0
def validate_windows_cred(host,
username='Administrator',
password=None,
retries=10,
retry_delay=1):
'''
Check if the windows credentials are valid
'''
for i in xrange(retries):
ret_code = 1
try:
stdout, stderr, ret_code = run_psexec_command(
'cmd.exe', '/c hostname', host, username, password, port=445
)
except Exception as exc:
log.exception("Exceoption while executing psexec")
if ret_code == 0:
break
time.sleep(retry_delay)
@ -999,6 +1167,13 @@ def deploy_windows(host,
log.error('WinRM requested but module winrm could not be imported')
return False
if not use_winrm and has_winexe() and not HAS_PSEXEC:
salt.utils.versions.warn_until(
'Fluorine',
'Support for winexe has been depricated and will be remove in '
'Sodium, please install pypsexec instead.'
)
starttime = time.mktime(time.localtime())
log.debug('Deploying %s at %s (Windows)', host, starttime)
log.trace('HAS_WINRM: %s, use_winrm: %s', HAS_WINRM, use_winrm)
@ -1019,7 +1194,7 @@ def deploy_windows(host,
if winrm_session is not None:
service_available = True
else:
service_available = wait_for_winexesvc(host=host, port=port,
service_available = wait_for_psexecsvc(host=host, port=port,
username=username, password=password,
timeout=port_timeout * 60)
@ -1027,27 +1202,13 @@ def deploy_windows(host,
log.debug('SMB port %s on %s is available', port, host)
log.debug('Logging into %s:%s as %s', host, port, username)
newtimeout = timeout - (time.mktime(time.localtime()) - starttime)
smb_conn = salt.utils.smb.get_conn(host, username, password)
if smb_conn is False:
log.error('Please install impacket to enable SMB functionality')
log.error('Please install smbprotocol to enable SMB functionality')
return False
creds = "-U '{0}%{1}' //{2}".format(
username,
password,
host
)
logging_creds = "-U '{0}%XXX-REDACTED-XXX' //{1}".format(
username,
host
)
salt.utils.smb.mkdirs('salttemp', conn=smb_conn)
salt.utils.smb.mkdirs('salt/conf/pki/minion', conn=smb_conn)
# minion_pub, minion_pem
kwargs = {'hostname': host,
'creds': creds}
if minion_pub:
salt.utils.smb.put_str(minion_pub, 'salt\\conf\\pki\\minion\\minion.pub', conn=smb_conn)
@ -1059,8 +1220,12 @@ def deploy_windows(host,
# Read master-sign.pub file
log.debug("Copying master_sign.pub file from %s to minion", master_sign_pub_file)
try:
with salt.utils.files.fopen(master_sign_pub_file, 'rb') as master_sign_fh:
smb_conn.putFile('C$', 'salt\\conf\\pki\\minion\\master_sign.pub', master_sign_fh.read)
salt.utils.smb.put_file(
master_sign_pub_file,
'salt\\conf\\pki\\minion\\master_sign.pub',
'C$',
conn=smb_conn,
)
except Exception as e:
log.debug("Exception copying master_sign.pub file %s to minion", master_sign_pub_file)
@ -1071,30 +1236,26 @@ def deploy_windows(host,
comps = win_installer.split('/')
local_path = '/'.join(comps[:-1])
installer = comps[-1]
with salt.utils.files.fopen(win_installer, 'rb') as inst_fh:
smb_conn.putFile('C$', 'salttemp/{0}'.format(installer), inst_fh.read)
salt.utils.smb.put_file(
win_installer,
'salttemp\\{0}'.format(installer),
'C$',
conn=smb_conn,
)
if use_winrm:
winrm_cmd(winrm_session, 'c:\\salttemp\\{0}'.format(installer), ['/S', '/master={0}'.format(master),
'/minion-name={0}'.format(name)]
)
else:
# Shell out to winexe to execute win_installer
# We don't actually need to set the master and the minion here since
# the minion config file will be set next via impacket
cmd = 'winexe {0} "c:\\salttemp\\{1} /S /master={2} /minion-name={3}"'.format(
creds,
installer,
master,
name
cmd = 'c:\\salttemp\\{0}'.format(installer)
args = "/S /master={0} /minion-name={1}".format(master, name)
stdout, stderr, ret_code = run_psexec_command(
cmd, args, host, username, password
)
logging_cmd = 'winexe {0} "c:\\salttemp\\{1} /S /master={2} /minion-name={3}"'.format(
logging_creds,
installer,
master,
name
)
win_cmd(cmd, logging_command=logging_cmd)
if ret_code != 0:
raise Exception("Fail installer %d", ret_code)
# Copy over minion_conf
if minion_conf:
@ -1133,29 +1294,28 @@ def deploy_windows(host,
if use_winrm:
winrm_cmd(winrm_session, 'rmdir', ['/Q', '/S', 'C:\\salttemp\\'])
else:
smb_conn.deleteFile('C$', 'salttemp/{0}'.format(installer))
smb_conn.deleteDirectory('C$', 'salttemp')
# Shell out to winexe to ensure salt-minion service started
salt.utils.smb.delete_file('salttemp\\{0}'.format(installer), 'C$', conn=smb_conn)
salt.utils.smb.delete_directory('salttemp', 'C$', conn=smb_conn)
# Shell out to psexec to ensure salt-minion service started
if use_winrm:
winrm_cmd(winrm_session, 'sc', ['stop', 'salt-minion'])
time.sleep(5)
winrm_cmd(winrm_session, 'sc', ['start', 'salt-minion'])
else:
stop_cmd = 'winexe {0} "sc stop salt-minion"'.format(
creds
stdout, stderr, ret_code = run_psexec_command(
'cmd.exe', '/c sc stop salt-minion', host, username, password
)
logging_stop_cmd = 'winexe {0} "sc stop salt-minion"'.format(
logging_creds
)
win_cmd(stop_cmd, logging_command=logging_stop_cmd)
if ret_code != 0:
return False
time.sleep(5)
start_cmd = 'winexe {0} "sc start salt-minion"'.format(creds)
logging_start_cmd = 'winexe {0} "sc start salt-minion"'.format(
logging_creds
log.debug('Run psexec: sc start salt-minion')
stdout, stderr, ret_code = run_psexec_command(
'cmd.exe', '/c sc start salt-minion', host, username, password
)
win_cmd(start_cmd, logging_command=logging_start_cmd)
if ret_code != 0:
return False
# Fire deploy action
fire_event(
@ -2733,9 +2893,9 @@ def cache_nodes_ip(opts, base=None):
addresses. Returns a dict.
'''
salt.utils.versions.warn_until(
'Flourine',
'Fluorine',
'This function is incomplete and non-functional '
'and will be removed in Salt Flourine.'
'and will be removed in Salt Fluorine.'
)
if base is None:
base = opts['cachedir']

View file

@ -6,13 +6,18 @@ Utility functions for SMB connections
'''
from __future__ import absolute_import, print_function, unicode_literals
import socket
import uuid
# Import python libs
import salt.utils.files
import salt.utils.stringutils
import salt.utils.versions
import logging
log = logging.getLogger(__name__)
from salt.exceptions import MissingSmb
try:
import impacket.smbconnection
from impacket.smbconnection import SessionError as smbSessionError
@ -21,6 +26,114 @@ try:
except ImportError:
HAS_IMPACKET = False
try:
from smbprotocol.connection import Connection
from smbprotocol.session import Session
from smbprotocol.tree import TreeConnect
from smbprotocol.open import (
Open, ImpersonationLevel, FilePipePrinterAccessMask, FileAttributes,
CreateDisposition, CreateOptions, ShareAccess, DirectoryAccessMask,
FileInformationClass
)
from smbprotocol.create_contexts import (
CreateContextName,
SMB2CreateContextRequest, SMB2CreateQueryMaximalAccessRequest
)
from smbprotocol.security_descriptor import (
AccessAllowedAce, AccessMask, AclPacket, SDControl, SIDPacket,
SMB2CreateSDBuffer
)
logging.getLogger('smbprotocol').setLevel(logging.WARNING)
HAS_SMBPROTOCOL = True
except ImportError:
HAS_SMBPROTOCOL = False
class SMBProto(object):
def __init__(self, server, username, password, port=445):
connection_id = uuid.uuid4()
addr = socket.gethostbyname(server)
self.server = server
connection = Connection(connection_id, addr, port, require_signing=True)
self.session = Session(connection, username, password, require_encryption=False)
def connect(self):
self.connection.connect()
self.session.connect()
def close(self):
self.session.connection.disconnect(True)
@property
def connection(self):
return self.session.connection
def tree_connect(self, share):
if share.endswith('$'):
share = r'\\{}\{}'.format(self.server, share)
tree = TreeConnect(self.session, share)
tree.connect()
return tree
@staticmethod
def normalize_filename(file):
return file.lstrip('\\')
@classmethod
def open_file(cls, tree, file):
file = cls.normalize_filename(file)
# ensure file is created, get maximal access, and set everybody read access
max_req = SMB2CreateContextRequest()
max_req['buffer_name'] = CreateContextName.SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST
max_req['buffer_data'] = SMB2CreateQueryMaximalAccessRequest()
# create security buffer that sets the ACL for everyone to have read access
everyone_sid = SIDPacket()
everyone_sid.from_string("S-1-1-0")
ace = AccessAllowedAce()
ace['mask'] = AccessMask.GENERIC_ALL
ace['sid'] = everyone_sid
acl = AclPacket()
acl['aces'] = [ace]
sec_desc = SMB2CreateSDBuffer()
sec_desc['control'].set_flag(SDControl.SELF_RELATIVE)
sec_desc.set_dacl(acl)
sd_buffer = SMB2CreateContextRequest()
sd_buffer['buffer_name'] = CreateContextName.SMB2_CREATE_SD_BUFFER
sd_buffer['buffer_data'] = sec_desc
create_contexts = [
max_req,
sd_buffer
]
file_open = Open(tree, file)
open_info = file_open.create(
ImpersonationLevel.Impersonation,
FilePipePrinterAccessMask.GENERIC_READ |
FilePipePrinterAccessMask.GENERIC_WRITE,
FileAttributes.FILE_ATTRIBUTE_NORMAL,
ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE,
CreateDisposition.FILE_OVERWRITE_IF,
CreateOptions.FILE_NON_DIRECTORY_FILE,
)
return file_open
@staticmethod
def open_directory(tree, name, create=False):
# ensure directory is created
dir_open = Open(tree, name)
if create:
dir_open.create(
ImpersonationLevel.Impersonation,
DirectoryAccessMask.GENERIC_READ | DirectoryAccessMask.GENERIC_WRITE,
FileAttributes.FILE_ATTRIBUTE_DIRECTORY,
ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE,
CreateDisposition.FILE_OPEN_IF,
CreateOptions.FILE_DIRECTORY_FILE
)
return dir_open
class StrHandle(object):
'''
@ -44,22 +157,42 @@ class StrHandle(object):
return ''
def get_conn(host=None, username=None, password=None):
'''
Get an SMB connection
'''
if not HAS_IMPACKET:
return False
def _get_conn_impacket(host=None, username=None, password=None, client_name=None, port=445):
conn = impacket.smbconnection.SMBConnection(
remoteName='*SMBSERVER',
remoteName=host,
remoteHost=host,
myName=client_name,
)
conn.login(user=username, password=password)
return conn
def mkdirs(path, share='C$', conn=None, host=None, username=None, password=None):
def _get_conn_smbprotocol(host='', username='', password='', client_name='', port=445):
conn = SMBProto(host, username, password, port)
conn.connect()
return conn
def get_conn(host='', username=None, password=None, port=445):
'''
Get an SMB connection
'''
if HAS_IMPACKET and not HAS_SMBPROTOCOL:
salt.utils.versions.warn_until(
'Fluorine',
'Support of impacket has been depricated and will be '
'removed in Sodium. Please install smbprotocol instead.'
)
if HAS_SMBPROTOCOL:
log.info('Get connection smbprotocol')
return _get_conn_smbprotocol(host, username, password, port=port)
elif HAS_IMPACKET:
log.info('Get connection impacket')
return _get_conn_impacket(host, username, password, port=port)
return False
def _mkdirs_impacket(path, share='C$', conn=None, host=None, username=None, password=None):
'''
Recursively create a directory structure on an SMB share
@ -84,11 +217,42 @@ def mkdirs(path, share='C$', conn=None, host=None, username=None, password=None)
pos += 1
def put_str(content, path, share='C$', conn=None, host=None, username=None, password=None):
'''
Wrapper around impacket.smbconnection.putFile() that allows a string to be
uploaded, without first writing it as a local file
'''
def _mkdirs_smbprotocol(path, share='C$', conn=None, host=None, username=None, password=None):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
tree = conn.tree_connect(share)
comps = path.split('/')
pos = 1
for comp in comps:
cwd = '\\'.join(comps[0:pos])
dir_open = conn.open_directory(tree, cwd, create=True)
compound_messages = [
dir_open.query_directory("*",
FileInformationClass.FILE_NAMES_INFORMATION,
send=False),
dir_open.close(False, send=False)
]
requests = conn.session.connection.send_compound([x[0] for x in compound_messages],
conn.session.session_id,
tree.tree_connect_id)
for i, request in enumerate(requests):
response = compound_messages[i][1](request)
pos += 1
def mkdirs(path, share='C$', conn=None, host=None, username=None, password=None):
if HAS_SMBPROTOCOL:
return _mkdirs_smbprotocol(path, share, conn=conn, host=host, username=username, password=password)
elif HAS_IMPACKET:
return _mkdirs_impacket(path, share, conn=conn, host=host, username=username, password=password)
raise MissingSmb("SMB library required (impacket or smbprotocol)")
def _put_str_impacket(content, path, share='C$', conn=None, host=None, username=None, password=None):
if conn is None:
conn = get_conn(host, username, password)
@ -99,7 +263,39 @@ def put_str(content, path, share='C$', conn=None, host=None, username=None, pass
conn.putFile(share, path, fh_.string)
def put_file(local_path, path, share='C$', conn=None, host=None, username=None, password=None):
def _put_str_smbprotocol(
content, path, share='C$', conn=None, host=None, username=None,
password=None):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
tree = conn.tree_connect(share)
try:
file_open = conn.open_file(tree, path)
file_open.write(salt.utils.stringutils.to_bytes(content), 0)
finally:
file_open.close()
def put_str(content, path, share='C$', conn=None, host=None, username=None, password=None):
'''
Wrapper around impacket.smbconnection.putFile() that allows a string to be
uploaded, without first writing it as a local file
'''
if HAS_SMBPROTOCOL:
return _put_str_smbprotocol(
content, path, share, conn=conn, host=host,
username=username, password=password
)
elif HAS_IMPACKET:
return _put_str_impacket(
content, path, share, conn=conn, host=host, username=username, password=password
)
raise MissingSmb("SMB library required (impacket or smbprotocol)")
def _put_file_impacket(local_path, path, share='C$', conn=None, host=None, username=None, password=None):
'''
Wrapper around impacket.smbconnection.putFile() that allows a file to be
uploaded
@ -116,5 +312,153 @@ def put_file(local_path, path, share='C$', conn=None, host=None, username=None,
if conn is False:
return False
with salt.utils.files.fopen(local_path, 'rb') as fh_:
if hasattr(local_path, 'read'):
conn.putFile(share, path, local_path)
return
with salt.utils.fopen(local_path, 'rb') as fh_:
conn.putFile(share, path, fh_.read)
def _put_file_smbprotocol(
local_path, path, share='C$', conn=None, host=None, username=None,
password=None, chunk_size=1024 * 1024):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
tree = conn.tree_connect(share)
file_open = conn.open_file(tree, path)
with salt.utils.files.fopen(local_path, 'rb') as fh_:
try:
position = 0
while True:
chunk = fh_.read(chunk_size)
if not chunk:
break
file_open.write(chunk, position)
position += len(chunk)
finally:
file_open.close(False)
def put_file(local_path, path, share='C$', conn=None, host=None, username=None, password=None):
'''
Wrapper around impacket.smbconnection.putFile() that allows a file to be
uploaded
Example usage:
import salt.utils.smb
smb_conn = salt.utils.smb.get_conn('10.0.0.45', 'vagrant', 'vagrant')
salt.utils.smb.put_file('/root/test.pdf', 'temp\\myfiles\\test1.pdf', conn=smb_conn)
'''
if HAS_SMBPROTOCOL:
return _put_file_smbprotocol(
local_path, path, share, conn=conn, host=host, username=username,
password=password
)
elif HAS_IMPACKET:
return _put_file_impacket(
local_path, path, share, conn=conn, host=host, username=username,
password=password
)
raise MissingSmb("SMB library required (impacket or smbprotocol)")
def _delete_file_impacket(path, share='C$', conn=None, host=None, username=None, password=None):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
conn.deleteFile(share, path)
def _delete_file_smbprotocol(path, share='C$', conn=None, host=None, username=None, password=None):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
tree = conn.tree_connect(share)
file_open = Open(tree, path)
delete_msgs = [
file_open.create(
ImpersonationLevel.Impersonation,
FilePipePrinterAccessMask.GENERIC_READ |
FilePipePrinterAccessMask.DELETE,
FileAttributes.FILE_ATTRIBUTE_NORMAL,
ShareAccess.FILE_SHARE_READ | ShareAccess.FILE_SHARE_WRITE,
CreateDisposition.FILE_OPEN,
CreateOptions.FILE_NON_DIRECTORY_FILE |
CreateOptions.FILE_DELETE_ON_CLOSE,
send=False
),
file_open.close(False, send=False)
]
requests = conn.connection.send_compound([x[0] for x in delete_msgs],
conn.session.session_id,
tree.tree_connect_id, related=True)
responses = []
for i, request in enumerate(requests):
# A SMBResponseException will be raised if something went wrong
response = delete_msgs[i][1](request)
responses.append(response)
def delete_file(path, share='C$', conn=None, host=None, username=None, password=None):
if HAS_SMBPROTOCOL:
return _delete_file_smbprotocol(path, share, conn=conn, host=host, username=username, password=password)
elif HAS_IMPACKET:
return _delete_file_impacket(path, share, conn=conn, host=host, username=username, password=password)
raise MissingSmb("SMB library required (impacket or smbprotocol)")
def _delete_directory_impacket(path, share='C$', conn=None, host=None, username=None, password=None):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
conn.deleteDirectory(share, path)
def _delete_directory_smbprotocol(path, share='C$', conn=None, host=None, username=None, password=None):
if conn is None:
conn = get_conn(host, username, password)
if conn is False:
return False
log.warn("PATH: %s %s", share, path)
tree = conn.tree_connect(share)
dir_open = Open(tree, path)
delete_msgs = [
dir_open.create(
ImpersonationLevel.Impersonation,
DirectoryAccessMask.DELETE,
FileAttributes.FILE_ATTRIBUTE_DIRECTORY,
0,
CreateDisposition.FILE_OPEN,
CreateOptions.FILE_DIRECTORY_FILE |
CreateOptions.FILE_DELETE_ON_CLOSE,
send=False
),
dir_open.close(False, send=False)
]
delete_reqs = conn.connection.send_compound([x[0] for x in delete_msgs],
sid=conn.session.session_id,
tid=tree.tree_connect_id,
related=True)
for i, request in enumerate(delete_reqs):
# A SMBResponseException will be raised if something went wrong
response = delete_msgs[i][1](request)
def delete_directory(path, share='C$', conn=None, host=None, username=None, password=None):
if HAS_SMBPROTOCOL:
return _delete_directory_smbprotocol(
path, share, conn=conn, host=host, username=username, password=password
)
elif HAS_IMPACKET:
return _delete_directory_impacket(
path, share, conn=conn, host=host, username=username, password=password
)
raise MissingSmb("SMB library required (impacket or smbprotocol)")

View file

@ -16,7 +16,6 @@ import salt.utils.files
from tests.support.case import ShellCase
from tests.support.paths import FILES
from tests.support.helpers import expensiveTest, generate_random_name
from tests.support.unit import expectedFailure
from tests.support import win_installer
# Create the cloud instance name to be used throughout the tests
@ -95,11 +94,13 @@ class EC2Test(ShellCase):
id_ = config[profile_str][PROVIDER_NAME]['id']
key = config[profile_str][PROVIDER_NAME]['key']
key_name = config[profile_str][PROVIDER_NAME]['keyname']
sec_group = config[profile_str][PROVIDER_NAME]['securitygroup']
private_key = config[profile_str][PROVIDER_NAME]['private_key']
location = config[profile_str][PROVIDER_NAME]['location']
group_or_subnet = config[profile_str][PROVIDER_NAME].get('securitygroup', '')
if not group_or_subnet:
group_or_subnet = config[profile_str][PROVIDER_NAME].get('subnetid', '')
conf_items = [id_, key, key_name, sec_group, private_key, location]
conf_items = [id_, key, key_name, private_key, location, group_or_subnet]
missing_conf_item = []
for item in conf_items:
@ -211,13 +212,12 @@ class EC2Test(ShellCase):
'''
self._test_instance('ec2-test')
@expectedFailure
def test_win2012r2_winexe(self):
def test_win2012r2_psexec(self):
'''
Tests creating and deleting a Windows 2012r2instance on EC2 using
winexe (classic)
psexec (classic)
'''
# TODO: winexe calls hang and the test fails by timing out. The same
# TODO: psexec calls hang and the test fails by timing out. The same
# same calls succeed when run outside of the test environment.
self.override_profile_config(
'ec2-win2012r2-test',
@ -227,7 +227,7 @@ class EC2Test(ShellCase):
'win_installer': self.copy_file(self.INSTALLER),
},
)
self._test_instance('ec2-win2012r2-test', debug=True, timeout=500)
self._test_instance('ec2-win2012r2-test', debug=True, timeout=800)
def test_win2012r2_winrm(self):
'''
@ -245,8 +245,7 @@ class EC2Test(ShellCase):
)
self._test_instance('ec2-win2012r2-test', debug=True, timeout=500)
@expectedFailure
def test_win2016_winexe(self):
def test_win2016_psexec(self):
'''
Tests creating and deleting a Windows 2016 instance on EC2 using winrm
(classic)
@ -261,7 +260,7 @@ class EC2Test(ShellCase):
'win_installer': self.copy_file(self.INSTALLER),
},
)
self._test_instance('ec2-win2016-test', debug=True, timeout=500)
self._test_instance('ec2-win2016-test', debug=True, timeout=800)
def test_win2016_winrm(self):
'''

View file

@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
'''
Test utility methods that communicate with SMB shares.
'''
from __future__ import absolute_import
import getpass
import logging
import os
import signal
import subprocess
import tempfile
import time
import salt.utils.files
import salt.utils.path
import salt.utils.smb
from tests.support.unit import skipIf
from tests.support.case import TestCase
log = logging.getLogger(__name__)
CONFIG = (
'[global]\n'
'realm = saltstack.com\n'
'interfaces = lo 127.0.0.0/8\n'
'smb ports = 1445\n'
'log level = 2\n'
'map to guest = Bad User\n'
'enable core files = no\n'
'passdb backend = smbpasswd\n'
'smb passwd file = {passwdb}\n'
'lock directory = {samba_dir}\n'
'state directory = {samba_dir}\n'
'cache directory = {samba_dir}\n'
'pid directory = {samba_dir}\n'
'private dir = {samba_dir}\n'
'ncalrpc dir = {samba_dir}\n'
'socket options = IPTOS_LOWDELAY TCP_NODELAY\n'
'min receivefile size = 0\n'
'write cache size = 0\n'
'client ntlmv2 auth = no\n'
'client min protocol = SMB3_11\n'
'client plaintext auth = no\n'
'\n'
'[public]\n'
'path = {public_dir}\n'
'read only = no\n'
'guest ok = no\n'
'writeable = yes\n'
'force user = {user}\n'
)
TBE = (
'{}:0:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:AC8E657F8'
'3DF82BEEA5D43BDAF7800CC:[U ]:LCT-507C14C7:'
)
def which_smbd():
'''
Find the smbd executable and cache the result if it exits.
'''
if hasattr(which_smbd, 'cached_result'):
return which_smbd.cached_result
smbd = salt.utils.path.which('smbd')
if smbd:
which_smbd.cached_result = smbd
return smbd
@skipIf(not which_smbd(), 'Skip when no smbd binary found')
class TestSmb(TestCase):
_smbd = None
@staticmethod
def check_pid(pid):
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
@classmethod
def setUpClass(cls):
tmpdir = tempfile.mkdtemp()
cls.samba_dir = os.path.join(tmpdir, 'samba')
cls.public_dir = os.path.join(tmpdir, 'public')
os.makedirs(cls.samba_dir)
os.makedirs(cls.public_dir)
os.chmod(cls.samba_dir, 0o775)
os.chmod(cls.public_dir, 0o775)
passwdb = os.path.join(tmpdir, 'passwdb')
cls.username = getpass.getuser()
with salt.utils.files.fopen(passwdb, 'w') as fp:
fp.write(TBE.format(cls.username))
samba_conf = os.path.join(tmpdir, 'smb.conf')
with salt.utils.files.fopen(samba_conf, 'w') as fp:
fp.write(
CONFIG.format(
samba_dir=cls.samba_dir,
public_dir=cls.public_dir,
passwdb=passwdb,
user=cls.username,
)
)
cls._smbd = subprocess.Popen(
'{0} -FS -P0 -s {1}'.format(which_smbd(), samba_conf),
shell=True
)
time.sleep(1)
pidfile = os.path.join(cls.samba_dir, 'smbd.pid')
with salt.utils.files.fopen(pidfile, 'r') as fp:
cls._pid = int(fp.read().strip())
if not cls.check_pid(cls._pid):
raise Exception('Unable to locate smbd\'s pid file')
@classmethod
def tearDownClass(cls):
log.warn('teardown')
os.kill(cls._pid, signal.SIGTERM)
def test_write_file(self):
'''
Transfer a file over SMB
'''
name = 'test_write_file.txt'
content = 'write test file content'
share_path = os.path.join(self.public_dir, name)
assert not os.path.exists(share_path)
local_path = tempfile.mktemp()
with salt.utils.files.fopen(local_path, 'w') as fp:
fp.write(content)
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
salt.utils.smb.put_file(local_path, name, 'public', conn=conn)
conn.close()
assert os.path.exists(share_path)
with salt.utils.files.fopen(share_path, 'r') as fp:
result = fp.read()
assert result == content
def test_write_str(self):
'''
Write a string to a file over SMB
'''
name = 'test_write_str.txt'
content = 'write test file content'
share_path = os.path.join(self.public_dir, name)
assert not os.path.exists(share_path)
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
salt.utils.smb.put_str(content, name, 'public', conn=conn)
conn.close()
assert os.path.exists(share_path)
with salt.utils.files.fopen(share_path, 'r') as fp:
result = fp.read()
assert result == content
def test_delete_file(self):
'''
Validate deletion of files over SMB
'''
name = 'test_delete_file.txt'
content = 'read test file content'
share_path = os.path.join(self.public_dir, name)
with salt.utils.files.fopen(share_path, 'w') as fp:
fp.write(content)
assert os.path.exists(share_path)
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
salt.utils.smb.delete_file(name, 'public', conn=conn)
conn.close()
assert not os.path.exists(share_path)
def test_mkdirs(self):
'''
Create directories over SMB
'''
dir_name = 'mkdirs/test'
share_path = os.path.join(self.public_dir, dir_name)
assert not os.path.exists(share_path)
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
salt.utils.smb.mkdirs(dir_name, 'public', conn=conn)
conn.close()
assert os.path.exists(share_path)
def test_delete_dirs(self):
'''
Validate deletion of directoreies over SMB
'''
dir_name = 'deldirs'
subdir_name = 'deldirs/test'
local_path = os.path.join(self.public_dir, subdir_name)
os.makedirs(local_path)
assert os.path.exists(local_path)
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
salt.utils.smb.delete_directory(subdir_name, 'public', conn=conn)
conn.close()
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
salt.utils.smb.delete_directory(dir_name, 'public', conn=conn)
conn.close()
assert not os.path.exists(local_path)
assert not os.path.exists(os.path.join(self.public_dir, dir_name))
def test_connection(self):
'''
Validate creation of an SMB connection
'''
conn = salt.utils.smb.get_conn('127.0.0.1', self.username, 'foo', port=1445)
conn.close()