mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge pull request #47028 from dwoz/pypsexec
Add support for pypsexec and smbprotocol
This commit is contained in:
commit
6147f08df7
14 changed files with 879 additions and 116 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
====================
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -554,3 +554,9 @@ class VMwareVmCreationError(VMwareSaltError):
|
|||
'''
|
||||
Used when a configuration parameter is incorrect
|
||||
'''
|
||||
|
||||
|
||||
class MissingSmb(SaltException):
|
||||
'''
|
||||
Raised when no smb library is found.
|
||||
'''
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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):
|
||||
'''
|
||||
|
|
218
tests/integration/utils/test_smb.py
Normal file
218
tests/integration/utils/test_smb.py
Normal 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()
|
Loading…
Add table
Reference in a new issue