-
Notifications
You must be signed in to change notification settings - Fork 58
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
b5ed496
354df63
cb7c965
63a45c2
7c7990c
6c5832d
0dc7c3f
19f05d5
1a28369
4c907fe
54c0c1e
ef4aea5
a19f5c3
ad69ab5
cdd5a7d
4059e22
190d785
2bd891f
c7e9e5a
c40da03
03ad988
82dd867
17d8888
80efd67
febfafd
460979c
73adee8
ebc546d
8815e8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 |
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 |
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 [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). |
There was a problem hiding this comment.
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