Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rudimentary windows support for bcfg2 #391

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b5ed496
Add basic windows support for bcfg2
chschenk Jul 12, 2017
354df63
Fix PEP-8 Style issues
chschenk Jul 18, 2017
cb7c965
Return extra services in WinService
chschenk Jul 18, 2017
63a45c2
Add import for binary Files
chschenk Jul 18, 2017
7c7990c
Implement a locking mechanism which works on Windows and Linux
chschenk Jul 21, 2017
6c5832d
Readd locked function for bcfg2-server
chschenk Jul 21, 2017
0dc7c3f
Fix Style Issues
chschenk Jul 21, 2017
19f05d5
More Style Fixes
chschenk Jul 21, 2017
1a28369
Fix style issues
chschenk Jul 21, 2017
4c907fe
Fix Error with lambda functions
chschenk Jul 24, 2017
54c0c1e
Use compat to import modules which doesnt exists on windows
chschenk Jul 24, 2017
ef4aea5
Fix more Style Issues
chschenk Jul 24, 2017
a19f5c3
Fix Style Issues
chschenk Jul 24, 2017
ad69ab5
More Style Fixes
chschenk Jul 24, 2017
cdd5a7d
Fix more Style Issues
chschenk Jul 24, 2017
4059e22
Better error handling in Flock class
chschenk Jul 24, 2017
190d785
Fix style and error handling issues
chschenk Jul 24, 2017
2bd891f
In Actions Shell should be False by default
chschenk Aug 8, 2017
c7e9e5a
Add WinFS support for nonexistent path types
chschenk Apr 23, 2018
c40da03
Fixing pylint errors
chschenk Jun 8, 2018
03ad988
Fixing more pylint errors
chschenk Jun 13, 2018
82dd867
Don't use "except as" because it only works in python 2.6 and newer
chschenk Aug 1, 2018
17d8888
Don't use "except as" because it only works in python 2.6 and newer
chschenk Aug 1, 2018
80efd67
Added the "real" logger to locking mechanism
chschenk Sep 20, 2018
febfafd
Added missing import for python3 compatibility
chschenk Sep 20, 2018
460979c
Fixing exception Handling for Python 2.4
chschenk Sep 25, 2018
73adee8
Fixing exception Handling for Python 2.4
chschenk Sep 25, 2018
ebc546d
Disable pylint W0622 in WinFS.py like in POSIX/File.py
chschenk Sep 25, 2018
8815e8f
Added missing space to comment
chschenk Sep 25, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/lib/Bcfg2/Client/Tools/WinAction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""WinAction driver"""
import subprocess

import Bcfg2.Client.Tools
from Bcfg2.Utils import safe_input


class WinAction(Bcfg2.Client.Tools.Tool):
"""Implement Actions"""
name = 'WinAction'
__handles__ = [('Action', None)]
__req__ = {'Action': ['name', 'timing', 'when', 'command', 'status']}

def RunAction(self, entry):
"""This method handles command execution and status return."""
shell = True
Copy link
Member

@solj solj Aug 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this default to False? It seems to me like this block of code will always result in shell = True

shell_string = ''
if entry.get('shell', 'false') == 'true':
shell = True
shell_string = '(in shell) '

if not Bcfg2.Options.setup.dry_run:
if Bcfg2.Options.setup.interactive:
prompt = ('Run Action %s%s, %s: (y/N): ' %
(shell_string, entry.get('name'),
entry.get('command')))
ans = safe_input(prompt)
if ans not in ['y', 'Y']:
return False
if False:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if False: seems wrong here.

if entry.get('build', 'true') == 'false':
self.logger.debug("Action: Deferring execution of %s due "
"to build mode" % entry.get('command'))
return False
self.logger.debug("Running Action %s %s" %
(shell_string, entry.get('name')))
rv = self.cmd.run(entry.get('command'), shell=shell,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=False)
self.logger.debug("Action: %s got return code %s" %
(entry.get('command'), rv.retval))
entry.set('rc', str(rv.retval))
return entry.get('status', 'check') == 'ignore' or rv.success
else:
self.logger.debug("In dryrun mode: not running action: %s" %
(entry.get('name')))
return False

def VerifyAction(self, dummy, _):
"""Actions always verify true."""
return True

def InstallAction(self, entry):
"""Run actions as pre-checks for bundle installation."""
if entry.get('timing') != 'post':
return self.RunAction(entry)
return True

def BundleUpdated(self, bundle):
"""Run postinstalls when bundles have been updated."""
states = dict()
for action in bundle.findall("Action"):
if action.get('timing') in ['post', 'both']:
if not self._install_allowed(action):
continue
states[action] = self.RunAction(action)
return states

def BundleNotUpdated(self, bundle):
"""Run Actions when bundles have not been updated."""
states = dict()
for action in bundle.findall("Action"):
if (action.get('timing') in ['post', 'both'] and
action.get('when') != 'modified'):
if not self._install_allowed(action):
continue
states[action] = self.RunAction(action)
return states
178 changes: 178 additions & 0 deletions src/lib/Bcfg2/Client/Tools/WinFS.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""All Windows Type client support for Bcfg2."""
import sys
import os
import stat
import tempfile
import Bcfg2.Options
import Bcfg2.Client.Tools


class WinFS(Bcfg2.Client.Tools.Tool):
"""Windows File support code."""
name = 'WinFS'
__handles__ = [('Path', 'file')]

def __init__(self, config):
Bcfg2.Client.Tools.Tool.__init__(self, config)
self.__req__ = dict(Path=dict())
self.__req__['Path']['file'] = ['name', 'mode', 'owner', 'group']

def _getFilePath(self, entry):
filePath = os.path.expandvars(os.path.normpath(entry.get('name')[1:]))
if(not filePath[1] == ':'):
self.logger.info("Skipping \"%s\" because it doesnt look like a Windows Path" % filePath)
return False
return filePath

def VerifyPath(self, entry, _):
"""Path always verify true."""
filePath = self._getFilePath(entry)
if(not filePath):
return False
ondisk = self._exists(filePath)
tempdata, is_binary = self._get_data(entry)
if isinstance(tempdata, str) and str != unicode:
tempdatasize = len(tempdata)
else:
tempdatasize = len(tempdata.encode(Bcfg2.Options.setup.encoding))

different = False
content = None
if not ondisk:
# first, see if the target file exists at all; if not,
# they're clearly different
different = True
content = ""
elif tempdatasize != ondisk[stat.ST_SIZE]:
# next, see if the size of the target file is different
# from the size of the desired content
different = True
else:
# finally, read in the target file and compare them
# directly. comparison could be done with a checksum,
# which might be faster for big binary files, but slower
# for everything else
try:
content = open(filePath).read()
except UnicodeDecodeError:
content = open(filePath,
encoding=Bcfg2.Options.setup.encoding).read()
except IOError:
self.logger.error("Windows: Failed to read %s: %s" %
(filePath, sys.exc_info()[1]))
return False
different = str(content) != str(tempdata)
return not different

def InstallPath(self, entry):
"""Install device entries."""
filePath = self._getFilePath(entry)

if not filePath:
return False

self.logger.debug("Installing: " + filePath)
if not os.path.exists(os.path.dirname(filePath)):
if not self._makedirs(path=filePath):
return False
newfile = self._write_tmpfile(entry, filePath)
if not newfile:
return False
rv = True
if not self._rename_tmpfile(newfile, filePath):
return False

return rv

def _makedirs(self, path):
""" os.makedirs helpfully creates all parent directories for us."""
created = []
cur = path
#while cur and cur != '/':
# if not os.path.exists(cur):
# created.append(cur)
# cur = os.path.dirname(cur)
rv = True
try:
os.makedirs(os.path.dirname(path))
except OSError:
err = sys.exc_info()[1]
self.logger.error('Windows: Failed to create directory %s: %s' %
(path, err))
rv = False
return rv

def _write_tmpfile(self, entry, filePath):
""" Write the file data to a temp file """
filedata = self._get_data(entry)[0]
# get a temp file to write to that is in the same directory as
# the existing file in order to preserve any permissions
# protections on that directory, and also to avoid issues with
# /tmp set nosetuid while creating files that are supposed to
# be setuid
try:
(newfd, newfile) = \
tempfile.mkstemp(prefix=os.path.basename(filePath),
dir=os.path.dirname(filePath))
except OSError:
err = sys.exc_info()[1]
self.logger.error("Windows: Failed to create temp file in %s: %s" % (filePath, err))
return False
try:
if isinstance(filedata, str) and str != unicode:
os.fdopen(newfd, 'w').write(filedata)
else:
os.fdopen(newfd, 'wb').write(
filedata.encode(Bcfg2.Options.setup.encoding))
except (OSError, IOError):
err = sys.exc_info()[1]
self.logger.error("Windows: Failed to open temp file %s for writing "
"%s: %s" %
(newfile, filePath, err))
return False
return newfile

def _get_data(self, entry):
""" Get a tuple of (<file data>, <is binary>) for the given entry """
is_binary = entry.get('encoding', 'ascii') == 'base64'
if entry.get('empty', 'false') == 'true' or not entry.text:
tempdata = ''
elif is_binary:
tempdata = b64decode(entry.text)
else:
tempdata = entry.text
if isinstance(tempdata, unicode) and unicode != str:
try:
tempdata = tempdata.encode(Bcfg2.Options.setup.encoding)
except UnicodeEncodeError:
err = sys.exc_info()[1]
self.logger.error("Windows: Error encoding file %s: %s" %
(entry.get('name'), err))
return (tempdata, is_binary)

def _rename_tmpfile(self, newfile, filePath):
""" Rename the given file to the appropriate filename for entry """
try:
if(os.path.isfile(filePath)):
os.unlink(filePath)
os.rename(newfile, filePath)
return True
except OSError:
err = sys.exc_info()[1]
self.logger.error("Windows: Failed to rename temp file %s to %s: %s"
% (newfile, filePath, err))
try:
os.unlink(newfile)
except OSError:
err = sys.exc_info()[1]
self.logger.error("Windows: Could not remove temp file %s: %s" %
(newfile, err))
return False

def _exists(self, filePath):
""" check for existing paths and optionally remove them. if
the path exists, return the lstat of it """
try:
return os.lstat(filePath)
except OSError:
return None
87 changes: 87 additions & 0 deletions src/lib/Bcfg2/Client/Tools/WinService.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""WinService support for Bcfg2."""

import glob
import re
import subprocess

import Bcfg2.Client.Tools
import Bcfg2.Client.XML


class WinService(Bcfg2.Client.Tools.SvcTool):
"""WinService service support for Bcfg2."""
name = 'WinService'
__handles__ = [('Service', 'windows')]
__req__ = {'Service': ['name', 'status']}

def get_svc_command(self, service, action):
return "powershell.exe %s-Service %s" % (action, service.get('name'))

def VerifyService(self, entry, _):
"""Verify Service status for entry
"""

if entry.get('status') == 'ignore':
return True

if entry.get('parameters'):
params = entry.get('parameters')
else:
params = ''

try:
output = self.cmd.run('powershell.exe (Get-Service %s).Status' %
(entry.get('name')),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=False).stdout.splitlines()[0]
except IndexError:
self.logger.error("Service %s not an Windows service" %
entry.get('name'))
return False


if output is None:
# service does not exist
entry.set('current_status', 'off')
status = False
elif output.lower() == 'running':
# service is running
entry.set('current_status', 'on')
if entry.get('status') == 'off':
status = False
else:
status = True
else:
# service is not running
entry.set('current_status', 'off')
if entry.get('status') == 'on':
status = False
else:
status = True

return status

def InstallService(self, entry):
"""Install Service for entry."""
if entry.get('status') == 'on':
cmd = "start"
elif entry.get('status') == 'off':
cmd = "stop"
return self.cmd.run(self.get_svc_command(entry, cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False).success

def restart_service(self, service):
"""Restart a service.

:param service: The service entry to modify
:type service: lxml.etree._Element
:returns: Bcfg2.Utils.ExecutorResult - The return value from
:class:`Bcfg2.Utils.Executor.run`
"""
self.logger.debug('Restarting service %s' % service.get('name'))
restart_target = service.get('target', 'restart')
return self.cmd.run(self.get_svc_command(service, restart_target), stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)

def FindExtra(self):
"""Locate extra Windows services."""
return []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it possible to get the list of services, that are scheduled to start automatically? Maybe something like this would work:

Get-WMIObject win32_service -Filter "StartMode = 'auto'" | Format-Table Name -HideTableHeaders

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just found the python wmi module. Maybe we just want to rely on this, instead "parsing" the output of powershell scripts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thought about this too, but decided against it because the WMI module is not included in the standard version of python for windows and, at least in my environment, it is extremely difficult to deploy extra pip packages onto windows servers (especially when I consider updating these packages).

Loading