mirror of
https://github.com/munki/munki.git
synced 2026-01-03 13:09:56 -06:00
242 lines
8.1 KiB
Python
242 lines
8.1 KiB
Python
# encoding: utf-8
|
|
#
|
|
# Copyright 2009-2020 Greg Neagle.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the 'License');
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# https://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an 'AS IS' BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""
|
|
osutils.py
|
|
|
|
Created by Greg Neagle on 2016-12-13.
|
|
|
|
Common functions and classes used by the munki tools.
|
|
"""
|
|
from __future__ import absolute_import, print_function
|
|
|
|
import platform
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
|
|
# PyLint cannot properly find names inside Cocoa libraries, so issues bogus
|
|
# No name 'Foo' in module 'Bar' warnings. Disable them.
|
|
# pylint: disable=E0611
|
|
from SystemConfiguration import SCDynamicStoreCopyConsoleUser
|
|
# pylint: enable=E0611
|
|
|
|
from . import display
|
|
from . import munkilog
|
|
|
|
# we use lots of camelCase-style names. Deal with it.
|
|
# pylint: disable=C0103
|
|
|
|
|
|
def getOsVersion(only_major_minor=True, as_tuple=False):
|
|
"""Returns an OS version.
|
|
|
|
Args:
|
|
only_major_minor: Boolean. If True, only include major/minor versions.
|
|
as_tuple: Boolean. If True, return a tuple of ints, otherwise a string.
|
|
"""
|
|
# platform.mac_ver() returns 10.16-style version info on Big Sur
|
|
# and is likely to do so until Python is compiled with the macOS 11 SDK
|
|
# which may not happen for a while. And Apple's odd tricks mean that even
|
|
# reading /System/Library/CoreServices/SystemVersion.plist is unreliable.
|
|
# So let's use a different method.
|
|
try:
|
|
os_version_tuple = subprocess.check_output(
|
|
('/usr/bin/sw_vers', '-productVersion'),
|
|
env={'SYSTEM_VERSION_COMPAT': '0'}
|
|
).decode('UTF-8').rstrip().split('.')
|
|
except subprocess.CalledProcessError:
|
|
os_version_tuple = platform.mac_ver()[0].split(".")
|
|
if only_major_minor:
|
|
os_version_tuple = os_version_tuple[0:2]
|
|
if as_tuple:
|
|
return tuple(map(int, os_version_tuple))
|
|
# default
|
|
return '.'.join(os_version_tuple)
|
|
|
|
|
|
def tmpdir():
|
|
'''Returns a temporary directory for this session'''
|
|
if not hasattr(tmpdir, 'cache'):
|
|
tmpdir.cache = tempfile.mkdtemp(prefix='munki-', dir='/tmp')
|
|
return tmpdir.cache
|
|
|
|
|
|
def cleanUpTmpDir():
|
|
"""Cleans up our temporary directory."""
|
|
if hasattr(tmpdir, 'cache'):
|
|
try:
|
|
shutil.rmtree(tmpdir.cache)
|
|
except (OSError, IOError) as err:
|
|
display.display_warning(
|
|
'Unable to clean up temporary dir %s: %s',
|
|
tmpdir.cache, str(err))
|
|
del tmpdir.cache
|
|
|
|
|
|
def listdir(path):
|
|
"""OS X HFS+ string encoding safe listdir().
|
|
|
|
Args:
|
|
path: path to list contents of
|
|
Returns:
|
|
list of contents, items as str or unicode types
|
|
"""
|
|
# if os.listdir() is supplied a unicode object for the path,
|
|
# it will return unicode filenames instead of their raw fs-dependent
|
|
# version, which is decomposed utf-8 on OS X.
|
|
#
|
|
# we use this to our advantage here and have Python do the decoding
|
|
# work for us, instead of decoding each item in the output list.
|
|
#
|
|
# references:
|
|
# https://docs.python.org/howto/unicode.html#unicode-filenames
|
|
# https://developer.apple.com/library/mac/#qa/qa2001/qa1235.html
|
|
# http://lists.zerezo.com/git/msg643117.html
|
|
# http://unicode.org/reports/tr15/ section 1.2
|
|
# pylint: disable=unicode-builtin
|
|
if isinstance(path, str):
|
|
try:
|
|
path = unicode(path, 'utf-8')
|
|
except NameError:
|
|
# Python 3
|
|
pass
|
|
elif not isinstance(path, unicode):
|
|
path = unicode(path)
|
|
return os.listdir(path)
|
|
|
|
|
|
def getconsoleuser():
|
|
"""Return console user"""
|
|
cfuser = SCDynamicStoreCopyConsoleUser(None, None, None)
|
|
return cfuser[0]
|
|
|
|
|
|
def currentGUIusers():
|
|
"""Gets a list of GUI users by parsing the output of /usr/bin/who"""
|
|
gui_users = []
|
|
proc = subprocess.Popen('/usr/bin/who', shell=False,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
output = proc.communicate()[0].decode("UTF-8")
|
|
lines = output.splitlines()
|
|
for line in lines:
|
|
if 'console' in line:
|
|
parts = line.split()
|
|
gui_users.append(parts[0])
|
|
|
|
# 10.11 sometimes has a phantom '_mbsetupuser' user. Filter it out.
|
|
users_to_ignore = ['_mbsetupuser']
|
|
gui_users = [user for user in gui_users if user not in users_to_ignore]
|
|
|
|
return gui_users
|
|
|
|
|
|
def pythonScriptRunning(scriptname):
|
|
"""Returns Process ID for a running python script"""
|
|
cmd = ['/bin/ps', '-eo', 'pid=,command=']
|
|
proc = subprocess.Popen(cmd, shell=False,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
out = proc.communicate()[0].decode("UTF-8")
|
|
mypid = os.getpid()
|
|
lines = str(out).splitlines()
|
|
for line in lines:
|
|
try:
|
|
(pid, process) = line.split(None, 1)
|
|
except ValueError:
|
|
# funky process line, so we'll skip it
|
|
pass
|
|
else:
|
|
args = process.split()
|
|
try:
|
|
# first look for Python processes
|
|
if (args[0].find('MacOS/Python') != -1 or
|
|
args[0].find('python') != -1):
|
|
# look for first argument being scriptname
|
|
if args[1].find(scriptname) != -1:
|
|
try:
|
|
if int(pid) != int(mypid):
|
|
return pid
|
|
except ValueError:
|
|
# pid must have some funky characters
|
|
pass
|
|
except IndexError:
|
|
pass
|
|
# if we get here we didn't find a Python script with scriptname
|
|
# (other than ourselves)
|
|
return 0
|
|
|
|
|
|
def osascript(osastring):
|
|
"""Wrapper to run AppleScript commands"""
|
|
cmd = ['/usr/bin/osascript', '-e', osastring]
|
|
proc = subprocess.Popen(cmd, shell=False,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(out, err) = proc.communicate()
|
|
if proc.returncode != 0:
|
|
print('Error: ', err.decode('UTF-8'), file=sys.stderr)
|
|
if out:
|
|
return out.decode('UTF-8').rstrip('\n')
|
|
return u''
|
|
|
|
|
|
def bridgeos_update_staged():
|
|
'''Checks an undocumented nvram variable to see if a bridgeOS update
|
|
has been staged. If so, we should shut down instead of restart.
|
|
Returns a boolean.'''
|
|
cmd = ["/usr/sbin/nvram", "BOSUpdateStarted"]
|
|
proc = subprocess.Popen(cmd,
|
|
shell=False,
|
|
bufsize=-1,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
output = proc.communicate()[0].decode('UTF-8')
|
|
if proc.returncode == 0:
|
|
munkilog.log("nvram output: %s" % output)
|
|
lines = output.splitlines()
|
|
parts = lines[0].split()
|
|
try:
|
|
timestamp = int(parts[1].split(',')[0])
|
|
now = int(time.time())
|
|
seconds_ago = now - timestamp
|
|
if seconds_ago < 60 * 60:
|
|
munkilog.log(
|
|
"bridgeOS update staged %s seconds ago; shutdown required"
|
|
% seconds_ago)
|
|
return True
|
|
#else
|
|
munkilog.log(
|
|
"bridgeOS update %s seconds ago; too long ago to trust"
|
|
% seconds_ago)
|
|
return False
|
|
except (IndexError, ValueError):
|
|
munkilog.log(
|
|
"unexpected nvram output, can't detect bridgeos update")
|
|
return False
|
|
#else
|
|
munkilog.log("No bridgeOS update staged")
|
|
return False
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print('This is a library of support tools for the Munki Suite.')
|