aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFilipp Lepalaan <filipp@mac.com>2016-09-22 09:46:34 +0300
committerFilipp Lepalaan <filipp@mac.com>2016-09-22 09:46:34 +0300
commitf29a31746f12f006d916a4dfcc07a566be1f432a (patch)
tree71b0087e7a4bcd48fa473f7885f5709e2e333bfa
downloadmachammer-f29a31746f12f006d916a4dfcc07a566be1f432a.tar.gz
machammer-f29a31746f12f006d916a4dfcc07a566be1f432a.tar.bz2
machammer-f29a31746f12f006d916a4dfcc07a566be1f432a.zip
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--README.md50
-rw-r--r--__init__.py6
-rw-r--r--functions.py222
-rw-r--r--printers.py39
-rw-r--r--system_profiler.py93
-rw-r--r--tests.py58
7 files changed, 469 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..80221ae
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+### Introduction
+
+`machammer` is library/microframework for Macintosh system administration. The idea is to provide common, often-used functions for admins to build their own management tools.
+
+
+### System Requirements
+
+- OS X
+
+
+### system_profiler
+
+machammer includes `system_profiler` - a simple wrapper around OS X's `system_profiler (1)` tool. It provides a simple API for accessing system profile information as well as caching to improve performance (especially when dealing with application profile data).
+
+
+#### Usage
+
+Simple example to find and list all versions of ArchiCAD 19:
+
+```python
+import system_profiler
+results = system_profiler.find('Applications', '_name', 'ArchiCAD 19')
+print([x['version'] for x in results])
+['19.0.0 R1 FIN (6006)']
+```
+
+Check `tests.py` for more usage examples.
+
+
+### License
+
+ Copyright (c) 2016 Filipp Lepalaan
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..645ecea
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,6 @@
+__all__ = ['']
+__title__ = 'machammer'
+__author__ = 'Filipp Lepalaan'
+__version__ = '0.1'
+__license__ = 'MIT'
+__copyright__ = 'Copyright 2016 Filipp Lepalaan'
diff --git a/functions.py b/functions.py
new file mode 100644
index 0000000..a7002ce
--- /dev/null
+++ b/functions.py
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+
+import os
+import plistlib
+import subprocess
+import sys
+import tempfile
+
+from system_profiler import SystemProfile
+
+
+SERVICEDIR = '/Library/Services'
+
+
+def ditto(src, dst):
+ subprocess.call(['/usr/bin/ditto', src, dst])
+
+
+def rsync(src, dst, flags='auE'):
+ subprocess.call(['rsync', '-' + flags, src, dst])
+
+
+def dscl(domain='.', *args):
+ subprocess.call(['/usr/bin/dscl', domain, ])
+
+
+def exec_jar(path, user='jkmmadmin'):
+ javapath = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java'
+ if not os.path.exists(javapath):
+ raise ValueError('Looks like your machine does not have Java installed')
+
+ subprocess.call(['launchctl', 'asuser', user, javapath, '-jar', path, '-silent'])
+
+
+def osascript(s):
+ subprocess.call(['/usr/bin/osascript', '-e', s])
+
+
+def tell_app(app, s):
+ osascript('tell application "%s" to %s' % (app, s))
+
+
+def quit_app(app):
+ tell_app(app, 'quit')
+
+
+def copy_app(path):
+ """
+ Copies path to /Applications folder
+ """
+ rsync(path.rstrip('/'), '/Applications/')
+
+
+def add_login_item(path, name=None, hidden=True):
+ """
+ Adds login item to the current user
+ """
+ if not name:
+ name = os.path.basename(path)
+
+ hidden = 'true' if hidden else 'false'
+ tell_app('System Events',
+ 'make login item at end with properties {path:"%s", hidden:%s, name:"%s"}' % (path, hidden, name))
+
+
+def remove_login_item(**kwargs):
+ """
+ Removes login item from the current user
+ """
+ if not (kwargs.get('name') or kwargs.get('path')):
+ raise ValueError('Need either path or name')
+
+ if kwargs.get('name') and kwargs.get('path'):
+ raise ValueError('Please specify only path OR name')
+
+ k, v = kwargs.items()[0]
+ tell_app('System Events', 'delete every login item whose %s is "%s"' % (k, v))
+
+
+def create_user(username, realname, password):
+ """
+ Creates a user
+ """
+ os.system("""dscl . create /Users/{0}
+ dscl . create /Users/{0} RealName "{1}"
+ dscl . passwd /Users/{0} {2}
+ dscl . create /Users/{0} UniqueID 501
+ dscl . create /Users/{0} PrimaryGroupID 80
+ dscl . create /Users/{0} UserShell /bin/bash
+ dscl . create /Users/{0} NFSHomeDirectory /Users/{0}
+ cp -R /System/Library/User\ Template/English.lproj /Users/{0}
+ chown -R {0}:staff /Users/{0}""".format(username, realname, password))
+
+
+def hide_user(username, hide_home=True):
+ """
+ Hides a user
+ """
+ path = '/Users/%s' % username
+ subprocess.call(['dscl', '.', 'create', path, 'IsHidden', '1'])
+
+ if hide_home:
+ subprocess.call(['chflags', 'hidden', path])
+
+
+def delete_user(username, delete_home=True):
+ """
+ Deletes a user account
+ """
+ path = '/Users/' + username
+ dscl = subprocess.check_output(['dscl', '-plist', '.', 'read', path])
+ userinfo = plistlib.readPlistFromString(dscl)
+
+ subprocess.call(['dscl', '.', 'delete', path])
+
+ if delete_home:
+ homedir = userinfo['dsAttrTypeStandard:NFSHomeDirectory'][0]
+ os.rmdir(homedir)
+
+
+def make_admin(username):
+ subprocess.call(['dscl', '.', '-append', '/Groups/admin', 'users', username])
+
+
+def is_laptop():
+ profile = SystemProfile('SPHardwareDataType')
+ return 'Book' in profile.machine_model
+
+
+def is_desktop():
+ return not is_laptop()
+
+
+def mount_image(path):
+ """
+ Mounts disk image and returns path to mountpoint
+ """
+ r = subprocess.check_output(['/usr/bin/hdiutil', 'mount', '-plist', '-nobrowse', path])
+ plist = plistlib.readPlistFromString(r)
+ for p in [p.get('mount-point') for p in plist.get('system-entities')]:
+ if p and os.path.exists(p):
+ return p
+
+ raise ValueError('Failed to mount %s' % path)
+
+
+def mount_and_install(dmg, pkg):
+ """
+ Mounts the DMG and installs the PKG
+ """
+ p = mount_image(dmg)
+ install_pkg(os.path.join(p, pkg))
+
+
+def install_profile(path):
+ """
+ Installs a configuration profile
+ """
+ subprocess.call(['profiles', '-I', '-F', path])
+
+
+def install_pkg(pkg, target='/'):
+ """
+ Installs a package
+ """
+ subprocess.call(['/usr/sbin/installer', '-pkg', pkg, '-target', target])
+
+
+def mount_afp(username, password, url, mountpoint=None):
+ if mountpoint is None:
+ mountpoint = tempfile.mkdtemp()
+ subprocess.call(['mount_afp', 'afp://%s:%s@%s' % (username, password, url), mountpoint])
+ return mountpoint
+
+
+def umount(path):
+ """
+ Unmounts path
+ """
+ subprocess.call(['umount', path])
+
+
+def enable_ard(username):
+ """
+ Enables ARD for username
+ """
+ subprocess.call(['/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart',
+ '-activate', '-configure',
+ '-access', '-on',
+ '-users', username,
+ '-privs', '-all',
+ '-restart', '-agent'])
+
+
+def install_su(restart=True):
+ """
+ Install all Apple software Updates, restart if update requires it
+ """
+ su_results = subprocess.check_output(['softwareupdate', '-ia'])
+ if restart and 'restart' in su_results:
+ tell_app('Finder', 'restart')
+ sys.exit(0)
+
+
+def disable_wifi(port='en1'):
+ subprocess.call(['networksetup', '-setairportpower', port, 'off'])
+ subprocess.call(['networksetup', '-setnetworkserviceenabled', 'Wi-Fi', 'off'])
+
+
+def log(msg):
+ print('*** %s...' % msg)
+
+
+def install_service(src):
+ if not os.path.exists(SERVICEDIR):
+ os.mkdir(SERVICEDIR)
+
+ ditto(src, SERVICEDIR)
+
+
+if __name__ == '__main__':
+ delete_user('demouser')
diff --git a/printers.py b/printers.py
new file mode 100644
index 0000000..60afd2e
--- /dev/null
+++ b/printers.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+
+import subprocess
+
+
+def delete_printers():
+ for p in subprocess.check_output(['lpstat', '-p']).strip().split("\n"):
+ subprocess.call(['lpadmin', '-x', p[1]])
+
+
+def add_printer(printer, options={}):
+ """
+ Adds a printer
+ A printer is a tuple (name, PPD path, LPD address)
+ """
+ cmd = ['/usr/sbin/lpadmin', '-x', printer[1]]
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (lpadminxOut, lpadminxErr) = proc.communicate()
+
+ # Install the printer
+ cmd = ['/usr/sbin/lpadmin',
+ '-p', printer[0].replace(' ', '-'),
+ '-L', printer[0][0:2],
+ '-D', printer[0],
+ '-v', 'lpd://%s' % printer[2],
+ '-P', '/Library/Printers/PPDs/Contents/Resources/%s' % printer[1],
+ '-E',
+ '-o', 'printer-is-shared=false',
+ '-o', 'printer-error-policy=abort-job']
+
+ for option in options.keys():
+ cmd.append("-o")
+ cmd.append(str(option) + "=" + str(options[option]))
+
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (res, err) = proc.communicate()
+
+ if err:
+ raise Exception(err)
diff --git a/system_profiler.py b/system_profiler.py
new file mode 100644
index 0000000..81dfb69
--- /dev/null
+++ b/system_profiler.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+
+import json
+import shelve
+import logging
+import os.path
+import tempfile
+import datetime
+import plistlib
+import subprocess
+
+from datetime import datetime, timedelta
+
+
+DEFAULT_DT = 'Hardware'
+CACHE_EXPIRE = timedelta(seconds=60*60*1)
+PROFILER_PATH = '/usr/sbin/system_profiler'
+
+
+class SystemProfile(object):
+ def __init__(self, dt=DEFAULT_DT):
+ types = subprocess.check_output([PROFILER_PATH, '-listDataTypes']).strip()
+ self.types = [x[2:].replace('DataType', '') for x in types.split("\n") if x.startswith('SP')]
+ self.types.sort()
+
+ if dt not in self.types:
+ raise ValueError('Invalid type %s. Should be one of %s' % (dt, ', '.join(self.types)))
+
+ tmp = tempfile.mkstemp()
+ self.dt = 'SP%sDataType' % dt
+
+ shelf_path = os.path.join(tempfile.gettempdir(), '%s.system_profiler' % self.dt)
+ shelf = shelve.open(shelf_path)
+
+ if shelf.get(dt) and shelf.get('expires'):
+ if shelf.get('expires') > datetime.now():
+ self._data = shelf[dt]
+ return
+
+ try:
+ subprocess.call([PROFILER_PATH, self.dt, '-detaillevel', 'full', '-xml'], stdout=tmp[0])
+ except Exception as e:
+ raise Exception('Failed to fetch system profile: %s' % e)
+
+ xml = plistlib.readPlist(tmp[1])
+ self._data = xml[0]['_items']
+ logging.debug(self._data)
+
+ shelf['expires'] = datetime.now() + CACHE_EXPIRE
+ shelf[dt] = self._data
+ shelf.close()
+
+ def json(self):
+ return json.dumps(self._data)
+
+ def get_keys(self):
+ keys = self._data[0].keys()
+ keys.sort()
+ return keys
+
+ def get_types(self):
+ return self.types
+
+ def find(self, k, v):
+ """
+ Return value(s) of property with key k containing v
+ """
+ return [x for x in self._data if v in x[k]]
+
+ def __str__(self):
+ return str(self._data)
+
+ def __getattr__(self, attr):
+ try:
+ return self._data[0][attr]
+ except KeyError as e:
+ raise ValueError('Property "%s" not found' % attr)
+
+ def __getitem__(self, attr):
+ return self._data[attr]
+
+
+def types():
+ return SystemProfile().get_types()
+
+def keys(dt=DEFAULT_DT):
+ return SystemProfile(dt).get_keys()
+
+def get(dt, param):
+ return getattr(SystemProfile(dt), param)
+
+def find(dt, k, v):
+ return SystemProfile(dt).find(k, v)
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..553b5ed
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+import logging
+import subprocess
+from unittest import main, skip, TestCase
+
+import system_profiler
+
+
+class SystemProfilerTestCase(TestCase):
+ def testSerialNumber(self):
+ sn = system_profiler.get('Hardware', 'serial_number')
+ self.assertTrue(len(sn) > 8)
+
+ def testInvalidType(self):
+ with self.assertRaises(Exception):
+ system_profiler.SystemProfile('Whatever')
+
+ def testKeys(self):
+ self.assertTrue(len(system_profiler.keys()) > 3)
+
+ def testTypes(self):
+ self.assertIn('Hardware', system_profiler.types())
+
+ def testOsVersion(self):
+ """
+ Check that the OS version we get from SP is contained
+ in the output of sw_vers
+ """
+ build = subprocess.check_output(['sw_vers', '-buildVersion']).strip()
+ software = system_profiler.SystemProfile('Software')
+ self.assertIn(build, software.os_version)
+
+ def testOsVersionShortcut(self):
+ build = subprocess.check_output(['sw_vers', '-buildVersion']).strip()
+ self.assertTrue(build in system_profiler.get('Software', 'os_version'))
+
+
+class AppsTestCase(TestCase):
+ def setUp(self):
+ self.profile = system_profiler.SystemProfile('Applications')
+
+ def testFindStickes(self):
+ results = self.profile.find('_name', 'Stickies')
+ self.assertTrue(len(results) > 0)
+
+ def testStickiesVersion(self):
+ results = self.profile.find('_name', 'Stickies')
+ self.assertEquals(results[0]['version'], '10.0')
+
+ def testFindApplications(self):
+ results = self.profile.find('path', '/Applications')
+ self.assertTrue(len(results) > 10)
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.DEBUG)
+ main()