mirror of
https://github.com/munki/munki.git
synced 2025-12-30 11:09:57 -06:00
Refactor removepackages
This commit is contained in:
2
code/client/munkilib/installer/core.py
Normal file → Executable file
2
code/client/munkilib/installer/core.py
Normal file → Executable file
@@ -50,7 +50,7 @@ from .. import scriptutils
|
||||
from .. import FoundationPlist
|
||||
|
||||
from ..updatecheck import catalogs
|
||||
from ..removepackages import removepackages
|
||||
from ..removepkgslib import removepackages
|
||||
|
||||
|
||||
# initialize our report fields
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1
code/client/munkilib/removepackages.py
Symbolic link
1
code/client/munkilib/removepackages.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../removepackages
|
||||
842
code/client/munkilib/removepkgslib.py
Executable file
842
code/client/munkilib/removepkgslib.py
Executable file
@@ -0,0 +1,842 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Copyright 2009-2016 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.
|
||||
|
||||
"""
|
||||
removepkgslib
|
||||
|
||||
Code to analyze installed packages and remove
|
||||
files unique to the packages given. No attempt
|
||||
is made to revert to older versions of a file when uninstalling;
|
||||
only file removals are done.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sqlite3
|
||||
|
||||
from . import display
|
||||
from . import munkistatus
|
||||
from . import osutils
|
||||
from . import pkgutils
|
||||
from . import prefs
|
||||
from . import processes
|
||||
from . import FoundationPlist
|
||||
|
||||
|
||||
#################################################################
|
||||
# our package db schema -- a subset of Apple's schema in Leopard
|
||||
#
|
||||
# CREATE TABLE paths (path_key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
# path VARCHAR NOT NULL UNIQUE )
|
||||
# CREATE TABLE pkgs (pkg_key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
# timestamp INTEGER NOT NULL,
|
||||
# owner INTEGER NOT NULL,
|
||||
# pkgid VARCHAR NOT NULL,
|
||||
# vers VARCHAR NOT NULL,
|
||||
# ppath VARCHAR NOT NULL,
|
||||
# pkgname VARCHAR NOT NULL,
|
||||
# replaces INTEGER )
|
||||
# CREATE TABLE pkgs_paths (pkg_key INTEGER NOT NULL,
|
||||
# path_key INTEGER NOT NULL,
|
||||
# uid INTEGER,
|
||||
# gid INTEGER,
|
||||
# perms INTEGER )
|
||||
#################################################################
|
||||
|
||||
|
||||
def should_rebuild_db(pkgdbpath):
|
||||
"""
|
||||
Checks to see if our internal package DB should be rebuilt.
|
||||
If anything in /Library/Receipts, /Library/Receipts/boms, or
|
||||
/Library/Receipts/db/a.receiptdb has a newer modtime than our
|
||||
database, we should rebuild.
|
||||
"""
|
||||
def items_newer_than_pkgdb(directory, file_extensions):
|
||||
'''Return true if the directory or files inside the directory
|
||||
with the given file extensions are newer than the pkgdb'''
|
||||
if os.path.exists(directory):
|
||||
dir_modtime = os.stat(directory).st_mtime
|
||||
# was directory modified after packagedb?
|
||||
if dir_modtime > packagedb_modtime:
|
||||
return True
|
||||
for item in osutils.listdir(directory):
|
||||
if item.endswith(file_extensions):
|
||||
filepath = os.path.join(directory, item)
|
||||
file_modtime = os.stat(filepath).st_mtime
|
||||
# was file modified after packagedb?
|
||||
if file_modtime > packagedb_modtime:
|
||||
return True
|
||||
return False
|
||||
|
||||
if not os.path.exists(pkgdbpath):
|
||||
return True
|
||||
|
||||
packagedb_modtime = os.stat(pkgdbpath).st_mtime
|
||||
|
||||
if items_newer_than_pkgdb('/Library/Receipts', '.pkg'):
|
||||
return True
|
||||
if items_newer_than_pkgdb('/Library/Receipts/boms', '.bom'):
|
||||
return True
|
||||
if items_newer_than_pkgdb('/private/var/db/receipts', ('.bom', '.plist')):
|
||||
return True
|
||||
|
||||
installhistory = '/Library/Receipts/InstallHistory.plist'
|
||||
if os.path.exists(installhistory):
|
||||
installhistory_modtime = os.stat(installhistory).st_mtime
|
||||
if packagedb_modtime < installhistory_modtime:
|
||||
return True
|
||||
|
||||
# if we got this far, we don't need to update the db
|
||||
return False
|
||||
|
||||
|
||||
def create_tables(curs):
|
||||
"""
|
||||
Creates the tables needed for our internal package database.
|
||||
"""
|
||||
curs.execute('''CREATE TABLE paths
|
||||
(path_key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
path VARCHAR NOT NULL UNIQUE )''')
|
||||
curs.execute('''CREATE TABLE pkgs
|
||||
(pkg_key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp INTEGER NOT NULL,
|
||||
owner INTEGER NOT NULL,
|
||||
pkgid VARCHAR NOT NULL,
|
||||
vers VARCHAR NOT NULL,
|
||||
ppath VARCHAR NOT NULL,
|
||||
pkgname VARCHAR NOT NULL,
|
||||
replaces INTEGER )''')
|
||||
curs.execute('''CREATE TABLE pkgs_paths
|
||||
(pkg_key INTEGER NOT NULL,
|
||||
path_key INTEGER NOT NULL,
|
||||
uid INTEGER,
|
||||
gid INTEGER,
|
||||
perms INTEGER )''')
|
||||
|
||||
|
||||
def find_bundle_receipt(pkgid):
|
||||
'''Finds a bundle receipt in /Library/Receipts based on packageid.
|
||||
Some packages write bundle receipts under /Library/Receipts even on
|
||||
Snow Leopard; we need to be able to find them so we can remove them.
|
||||
Returns a path.'''
|
||||
if not pkgid:
|
||||
return ''
|
||||
receiptsdir = "/Library/Receipts"
|
||||
if os.path.isdir(receiptsdir):
|
||||
for item in osutils.listdir(receiptsdir):
|
||||
itempath = os.path.join(receiptsdir, item)
|
||||
if item.endswith('.pkg') and os.path.isdir(itempath):
|
||||
info = pkgutils.getOnePackageInfo(itempath)
|
||||
if info.get('packageid') == pkgid:
|
||||
return itempath
|
||||
|
||||
#if we get here, not found
|
||||
return ''
|
||||
|
||||
|
||||
def insert_bomvalues_into_pkgdb(bom_line, pkgkey, ppath, curs):
|
||||
'''Parses line from lsbom or pkgutil --files and inserts the paths into
|
||||
our pkgdb'''
|
||||
try:
|
||||
item = bom_line.rstrip("\n").split("\t")
|
||||
path = item[0]
|
||||
perms = item[1]
|
||||
uidgid = item[2].split("/")
|
||||
uid = uidgid[0]
|
||||
gid = uidgid[1]
|
||||
except IndexError:
|
||||
# we really only care about the path
|
||||
perms = "0000"
|
||||
uid = "0"
|
||||
gid = "0"
|
||||
|
||||
try:
|
||||
if path != ".":
|
||||
# special case for MS Office 2008 installers
|
||||
if ppath == "tmp/com.microsoft.updater/office_location":
|
||||
ppath = "Applications"
|
||||
|
||||
# prepend the ppath so the paths match the actual install locations
|
||||
path = path.lstrip("./")
|
||||
if ppath:
|
||||
path = ppath + "/" + path
|
||||
|
||||
values_t = (path, )
|
||||
row = curs.execute(
|
||||
'SELECT path_key from paths where path = ?',
|
||||
values_t).fetchone()
|
||||
if not row:
|
||||
curs.execute(
|
||||
'INSERT INTO paths (path) values (?)', values_t)
|
||||
pathkey = curs.lastrowid
|
||||
else:
|
||||
pathkey = row[0]
|
||||
|
||||
values_t = (pkgkey, pathkey, uid, gid, perms)
|
||||
curs.execute(
|
||||
'INSERT INTO pkgs_paths (pkg_key, path_key, uid, gid, '
|
||||
'perms) values (?, ?, ?, ?, ?)', values_t)
|
||||
except sqlite3.DatabaseError:
|
||||
pass
|
||||
|
||||
|
||||
def import_package(packagepath, curs):
|
||||
"""
|
||||
Imports package data from the receipt at packagepath into
|
||||
our internal package database.
|
||||
"""
|
||||
|
||||
bompath = os.path.join(packagepath, 'Contents/Archive.bom')
|
||||
infopath = os.path.join(packagepath, 'Contents/Info.plist')
|
||||
pkgname = os.path.basename(packagepath)
|
||||
|
||||
if not os.path.exists(packagepath):
|
||||
display.display_error("%s not found.", packagepath)
|
||||
return
|
||||
|
||||
if not os.path.isdir(packagepath):
|
||||
# Every machine I've seen has a bogus BSD.pkg,
|
||||
# so we won't print a warning for that specific one.
|
||||
if pkgname != "BSD.pkg":
|
||||
display.display_warning(
|
||||
"%s is not a valid receipt. Skipping.", packagepath)
|
||||
return
|
||||
|
||||
if not os.path.exists(bompath):
|
||||
# look in receipt's Resources directory
|
||||
bomname = os.path.splitext(pkgname)[0] + '.bom'
|
||||
bompath = os.path.join(
|
||||
packagepath, "Contents/Resources", bomname)
|
||||
if not os.path.exists(bompath):
|
||||
display.display_warning(
|
||||
"%s has no BOM file. Skipping.", packagepath)
|
||||
return
|
||||
|
||||
if not os.path.exists(infopath):
|
||||
display.display_warning(
|
||||
"%s has no Info.plist. Skipping.", packagepath)
|
||||
return
|
||||
|
||||
timestamp = os.stat(packagepath).st_mtime
|
||||
owner = 0
|
||||
plist = FoundationPlist.readPlist(infopath)
|
||||
# 'Bundle identifier' is a weird Casper Composer thing
|
||||
pkgid = plist.get('CFBundleIdentifier',
|
||||
plist.get('Bundle identifier', pkgname))
|
||||
# 'Bundle versions string, short' is a weird Casper Composer thing
|
||||
vers = plist.get('CFBundleShortVersionString',
|
||||
plist.get('Bundle versions string, short'), '1.0')
|
||||
ppath = plist.get('IFPkgRelocatedPath', '').lstrip('./').rstrip('/')
|
||||
|
||||
values_t = (timestamp, owner, pkgid, vers, ppath, pkgname)
|
||||
curs.execute(
|
||||
'''INSERT INTO pkgs (timestamp, owner, pkgid, vers, ppath, pkgname)
|
||||
values (?, ?, ?, ?, ?, ?)''', values_t)
|
||||
pkgkey = curs.lastrowid
|
||||
|
||||
proc = subprocess.Popen(
|
||||
['/usr/bin/lsbom', bompath], shell=False, bufsize=-1,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
while True:
|
||||
line = proc.stdout.readline().decode('UTF-8')
|
||||
if not line and (proc.poll() != None):
|
||||
break
|
||||
insert_bomvalues_into_pkgdb(line, pkgkey, ppath, curs)
|
||||
|
||||
|
||||
def import_bom(bompath, curs):
|
||||
"""
|
||||
Imports package data into our internal package database
|
||||
using a combination of the bom file and data in Apple's
|
||||
package database into our internal package database.
|
||||
"""
|
||||
# If we completely trusted the accuracy of Apple's database, we wouldn't
|
||||
# need the bom files, but in my environment at least, the bom files are
|
||||
# a better indicator of what flat packages have actually been installed
|
||||
# on the current machine.
|
||||
# We still need to consult Apple's package database
|
||||
# because the bom files are missing metadata about the package.
|
||||
|
||||
pkgname = os.path.basename(bompath)
|
||||
|
||||
timestamp = os.stat(bompath).st_mtime
|
||||
owner = 0
|
||||
pkgid = os.path.splitext(pkgname)[0]
|
||||
vers = "1.0"
|
||||
ppath = ""
|
||||
|
||||
# try to get metadata from pkginfo db
|
||||
proc = subprocess.Popen(["/usr/sbin/pkgutil", "--pkg-info-plist", pkgid],
|
||||
bufsize=-1, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(pliststr, dummy_err) = proc.communicate()
|
||||
if pliststr:
|
||||
plist = FoundationPlist.readPlistFromString(pliststr)
|
||||
if "install-location" in plist:
|
||||
ppath = plist["install-location"]
|
||||
ppath = ppath.lstrip('./').rstrip('/')
|
||||
if "pkg-version" in plist:
|
||||
vers = plist["pkg-version"]
|
||||
if "install-time" in plist:
|
||||
timestamp = plist["install-time"]
|
||||
|
||||
values_t = (timestamp, owner, pkgid, vers, ppath, pkgname)
|
||||
curs.execute(
|
||||
'''INSERT INTO pkgs (timestamp, owner, pkgid, vers, ppath, pkgname)
|
||||
values (?, ?, ?, ?, ?, ?)''', values_t)
|
||||
pkgkey = curs.lastrowid
|
||||
|
||||
cmd = ["/usr/bin/lsbom", bompath]
|
||||
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
while True:
|
||||
line = proc.stdout.readline().decode('UTF-8')
|
||||
if not line and (proc.poll() != None):
|
||||
break
|
||||
insert_bomvalues_into_pkgdb(line, pkgkey, ppath, curs)
|
||||
|
||||
def import_from_pkgutil(pkgname, curs):
|
||||
"""
|
||||
Imports package data from pkgutil into our internal package database.
|
||||
"""
|
||||
|
||||
timestamp = 0
|
||||
owner = 0
|
||||
pkgid = pkgname
|
||||
vers = "1.0"
|
||||
ppath = ""
|
||||
|
||||
#get metadata from pkginfo db
|
||||
proc = subprocess.Popen(["/usr/sbin/pkgutil", "--pkg-info-plist", pkgid],
|
||||
bufsize=-1, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(pliststr, dummy_err) = proc.communicate()
|
||||
if pliststr:
|
||||
plist = FoundationPlist.readPlistFromString(pliststr)
|
||||
if "pkg-version" in plist:
|
||||
vers = plist["pkg-version"]
|
||||
if "install-time" in plist:
|
||||
timestamp = plist["install-time"]
|
||||
if "install-location" in plist:
|
||||
ppath = plist["install-location"]
|
||||
ppath = ppath.lstrip('./').rstrip('/')
|
||||
else:
|
||||
# there _should_ be an install-location. If there's not, let's
|
||||
# check the old /Library/Receipts.
|
||||
# (Workaround for QuarkXPress 8.1 packages)
|
||||
receiptpath = find_bundle_receipt(pkgid)
|
||||
if receiptpath:
|
||||
infopath = os.path.join(receiptpath, 'Contents/Info.plist')
|
||||
if os.path.exists(infopath):
|
||||
infopl = FoundationPlist.readPlist(infopath)
|
||||
if "IFPkgRelocatedPath" in infopl:
|
||||
ppath = infopl["IFPkgRelocatedPath"]
|
||||
ppath = ppath.lstrip('./').rstrip('/')
|
||||
|
||||
values_t = (timestamp, owner, pkgid, vers, ppath, pkgname)
|
||||
curs.execute(
|
||||
'''INSERT INTO pkgs (timestamp, owner, pkgid, vers, ppath, pkgname)
|
||||
values (?, ?, ?, ?, ?, ?)''', values_t)
|
||||
pkgkey = curs.lastrowid
|
||||
|
||||
cmd = ["/usr/sbin/pkgutil", "--files", pkgid]
|
||||
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
while True:
|
||||
line = proc.stdout.readline().decode('UTF-8')
|
||||
if not line and (proc.poll() != None):
|
||||
break
|
||||
insert_bomvalues_into_pkgdb(line, pkgkey, ppath, curs)
|
||||
|
||||
|
||||
def init_database(forcerebuild=False):
|
||||
"""
|
||||
Builds or rebuilds our internal package database.
|
||||
"""
|
||||
def abort_init_database():
|
||||
'''What to do if user requests we stop'''
|
||||
curs.close()
|
||||
conn.close()
|
||||
#our package db isn't valid, so we should delete it
|
||||
os.remove(PACKAGEDB)
|
||||
return False
|
||||
|
||||
if not should_rebuild_db(PACKAGEDB) and not forcerebuild:
|
||||
return True
|
||||
|
||||
display.display_status_minor(
|
||||
'Gathering information on installed packages')
|
||||
|
||||
if os.path.exists(PACKAGEDB):
|
||||
try:
|
||||
os.remove(PACKAGEDB)
|
||||
except (OSError, IOError):
|
||||
display.display_error(
|
||||
"Could not remove out-of-date receipt database.")
|
||||
return False
|
||||
|
||||
receiptsdir = '/Library/Receipts'
|
||||
receiptlist = []
|
||||
if os.path.exists(receiptsdir):
|
||||
receiptlist = [item for item in osutils.listdir(receiptsdir)
|
||||
if item.endswith('.pkg')]
|
||||
|
||||
bomsdir = '/Library/Receipts/boms'
|
||||
bomslist = []
|
||||
if os.path.exists(bomsdir):
|
||||
bomslist = [item for item in osutils.listdir(bomsdir)
|
||||
if item.endswith('.bom')]
|
||||
|
||||
pkglist = []
|
||||
cmd = ['/usr/sbin/pkgutil', '--pkgs']
|
||||
proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
while True:
|
||||
line = proc.stdout.readline()
|
||||
if not line and (proc.poll() != None):
|
||||
break
|
||||
|
||||
pkglist.append(line.rstrip('\n'))
|
||||
|
||||
pkgcount = len(receiptlist) + len(bomslist) + len(pkglist)
|
||||
conn = sqlite3.connect(PACKAGEDB)
|
||||
conn.text_factory = str
|
||||
curs = conn.cursor()
|
||||
create_tables(curs)
|
||||
|
||||
currentpkgindex = 0
|
||||
display.display_percent_done(0, pkgcount)
|
||||
|
||||
for item in receiptlist:
|
||||
if processes.stop_requested():
|
||||
return abort_init_database()
|
||||
|
||||
receiptpath = os.path.join(receiptsdir, item)
|
||||
display.display_detail("Importing %s...", receiptpath)
|
||||
import_package(receiptpath, curs)
|
||||
currentpkgindex += 1
|
||||
display.display_percent_done(currentpkgindex, pkgcount)
|
||||
|
||||
for item in bomslist:
|
||||
if processes.stop_requested():
|
||||
return abort_init_database()
|
||||
|
||||
bompath = os.path.join(bomsdir, item)
|
||||
display.display_detail("Importing %s...", bompath)
|
||||
import_bom(bompath, curs)
|
||||
currentpkgindex += 1
|
||||
display.display_percent_done(currentpkgindex, pkgcount)
|
||||
|
||||
for pkg in pkglist:
|
||||
if processes.stop_requested():
|
||||
return abort_init_database()
|
||||
|
||||
display.display_detail("Importing %s...", pkg)
|
||||
import_from_pkgutil(pkg, curs)
|
||||
currentpkgindex += 1
|
||||
display.display_percent_done(currentpkgindex, pkgcount)
|
||||
|
||||
# in case we didn't quite get to 100% for some reason
|
||||
display.display_percent_done(pkgcount, pkgcount)
|
||||
|
||||
# commit and close the db when we're done.
|
||||
conn.commit()
|
||||
curs.close()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
|
||||
def getpkgkeys(pkgnames):
|
||||
"""
|
||||
Given a list of receipt names, bom file names, or package ids,
|
||||
gets a list of pkg_keys from the pkgs table in our database.
|
||||
"""
|
||||
# open connection and cursor to our database
|
||||
conn = sqlite3.connect(PACKAGEDB)
|
||||
curs = conn.cursor()
|
||||
|
||||
# check package names to make sure they're all in the database,
|
||||
# build our list of pkg_keys
|
||||
pkgerror = False
|
||||
pkgkeyslist = []
|
||||
for pkg in pkgnames:
|
||||
values_t = (pkg, )
|
||||
display.display_debug1(
|
||||
"select pkg_key from pkgs where pkgid = %s", pkg)
|
||||
pkg_keys = curs.execute(
|
||||
'select pkg_key from pkgs where pkgid = ?', values_t).fetchall()
|
||||
if not pkg_keys:
|
||||
# try pkgname
|
||||
display.display_debug1(
|
||||
"select pkg_key from pkgs where pkgname = %s", pkg)
|
||||
pkg_keys = curs.execute(
|
||||
'select pkg_key from pkgs where pkgname = ?',
|
||||
values_t).fetchall()
|
||||
if not pkg_keys:
|
||||
display.display_error("%s not found in database.", pkg)
|
||||
pkgerror = True
|
||||
else:
|
||||
for row in pkg_keys:
|
||||
# only want first column
|
||||
pkgkeyslist.append(row[0])
|
||||
if pkgerror:
|
||||
pkgkeyslist = []
|
||||
|
||||
curs.close()
|
||||
conn.close()
|
||||
display.display_debug1("pkgkeys: %s", pkgkeyslist)
|
||||
return pkgkeyslist
|
||||
|
||||
|
||||
def getpathstoremove(pkgkeylist):
|
||||
"""
|
||||
Queries our database for paths to remove.
|
||||
"""
|
||||
pkgkeys = tuple(pkgkeylist)
|
||||
|
||||
# open connection and cursor to our database
|
||||
conn = sqlite3.connect(PACKAGEDB)
|
||||
curs = conn.cursor()
|
||||
|
||||
# set up some subqueries:
|
||||
# all the paths that are referred to by the selected packages:
|
||||
if len(pkgkeys) > 1:
|
||||
in_selected_packages = \
|
||||
"select distinct path_key from pkgs_paths where pkg_key in %s" % \
|
||||
str(pkgkeys)
|
||||
else:
|
||||
in_selected_packages = \
|
||||
"select distinct path_key from pkgs_paths where pkg_key = %s" % \
|
||||
str(pkgkeys[0])
|
||||
|
||||
# all the paths that are referred to by every package
|
||||
# except the selected packages:
|
||||
if len(pkgkeys) > 1:
|
||||
not_in_other_packages = \
|
||||
"select distinct path_key from pkgs_paths where pkg_key not in %s" % \
|
||||
str(pkgkeys)
|
||||
else:
|
||||
not_in_other_packages = \
|
||||
"select distinct path_key from pkgs_paths where pkg_key != %s" % \
|
||||
str(pkgkeys[0])
|
||||
|
||||
# every path that is used by the selected packages and no other packages:
|
||||
combined_query = \
|
||||
"select path from paths where " + \
|
||||
"(path_key in (%s) and path_key not in (%s))" % \
|
||||
(in_selected_packages, not_in_other_packages)
|
||||
|
||||
display.display_status_minor(
|
||||
'Determining which filesystem items to remove')
|
||||
munkistatus.percent(-1)
|
||||
|
||||
curs.execute(combined_query)
|
||||
results = curs.fetchall()
|
||||
curs.close()
|
||||
conn.close()
|
||||
|
||||
removalpaths = []
|
||||
for item in results:
|
||||
removalpaths.append(item[0])
|
||||
|
||||
return removalpaths
|
||||
|
||||
|
||||
def remove_receipts(pkgkeylist, noupdateapplepkgdb):
|
||||
"""
|
||||
Removes receipt data from /Library/Receipts,
|
||||
/Library/Receipts/boms, our internal package database,
|
||||
and optionally Apple's package database.
|
||||
"""
|
||||
display.display_status_minor('Removing receipt info')
|
||||
display.display_percent_done(0, 4)
|
||||
|
||||
os_version = osutils.getOsVersion(as_tuple=True)
|
||||
conn = sqlite3.connect(PACKAGEDB)
|
||||
curs = conn.cursor()
|
||||
|
||||
display.display_percent_done(1, 4)
|
||||
|
||||
for pkgkey in pkgkeylist:
|
||||
pkgid = ''
|
||||
pkgkey_t = (pkgkey, )
|
||||
row = curs.execute(
|
||||
'SELECT pkgname, pkgid from pkgs where pkg_key = ?',
|
||||
pkgkey_t).fetchone()
|
||||
if row:
|
||||
pkgname = row[0]
|
||||
pkgid = row[1]
|
||||
receiptpath = None
|
||||
if os_version <= (10, 5):
|
||||
if pkgname.endswith('.pkg'):
|
||||
receiptpath = os.path.join('/Library/Receipts', pkgname)
|
||||
if pkgname.endswith('.bom'):
|
||||
receiptpath = os.path.join(
|
||||
'/Library/Receipts/boms', pkgname)
|
||||
else:
|
||||
# clean up /Library/Receipts in case there's stuff left there
|
||||
receiptpath = find_bundle_receipt(pkgid)
|
||||
|
||||
if receiptpath and os.path.exists(receiptpath):
|
||||
display.display_detail("Removing %s...", receiptpath)
|
||||
dummy_retcode = subprocess.call(
|
||||
["/bin/rm", "-rf", receiptpath])
|
||||
|
||||
# remove pkg info from our database
|
||||
display.display_detail(
|
||||
"Removing package data from internal database...")
|
||||
curs.execute('DELETE FROM pkgs_paths where pkg_key = ?', pkgkey_t)
|
||||
curs.execute('DELETE FROM pkgs where pkg_key = ?', pkgkey_t)
|
||||
|
||||
# then remove pkg info from Apple's database unless option is passed
|
||||
if not noupdateapplepkgdb and pkgid:
|
||||
cmd = ['/usr/sbin/pkgutil', '--forget', pkgid]
|
||||
proc = subprocess.Popen(cmd, bufsize=-1,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(output, dummy_err) = proc.communicate()
|
||||
if output:
|
||||
display.display_detail(
|
||||
str(output).decode('UTF-8').rstrip('\n'))
|
||||
|
||||
display.display_percent_done(2, 4)
|
||||
|
||||
# now remove orphaned paths from paths table
|
||||
display.display_percent_done(3, 4)
|
||||
|
||||
# we do our database last so its modtime is later than the modtime for the
|
||||
# Apple DB...
|
||||
display.display_detail(
|
||||
"Removing unused paths from internal package database...")
|
||||
curs.execute(
|
||||
'''DELETE FROM paths where path_key not in
|
||||
(select distinct path_key from pkgs_paths)''')
|
||||
conn.commit()
|
||||
curs.close()
|
||||
conn.close()
|
||||
|
||||
display.display_percent_done(4, 4)
|
||||
|
||||
|
||||
def is_bundle(pathname):
|
||||
"""
|
||||
Returns true if pathname is a bundle-style directory.
|
||||
"""
|
||||
bundle_extensions = [".action",
|
||||
".app",
|
||||
".bundle",
|
||||
".clr",
|
||||
".colorPicker",
|
||||
".component",
|
||||
".dictionary",
|
||||
".docset",
|
||||
".framework",
|
||||
".fs",
|
||||
".kext",
|
||||
".loginPlugin",
|
||||
".mdiimporter",
|
||||
".monitorPanel",
|
||||
".osax",
|
||||
".pkg",
|
||||
".plugin",
|
||||
".prefPane",
|
||||
".qlgenerator",
|
||||
".saver",
|
||||
".service",
|
||||
".slideSaver",
|
||||
".SpeechRecognizer",
|
||||
".SpeechSynthesizer",
|
||||
".SpeechVoice",
|
||||
".spreporter",
|
||||
".wdgt"]
|
||||
if os.path.isdir(pathname):
|
||||
basename = os.path.basename(pathname)
|
||||
extension = os.path.splitext(basename)[1]
|
||||
if extension in bundle_extensions:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def inside_bundle(pathname):
|
||||
'''Check the path to see if it's inside a bundle.'''
|
||||
while len(pathname) > 1:
|
||||
if is_bundle(pathname):
|
||||
return True
|
||||
else:
|
||||
# chop off last item in path
|
||||
pathname = os.path.dirname(pathname)
|
||||
#if we get here, we didn't find a bundle path
|
||||
return False
|
||||
|
||||
|
||||
def remove_filesystem_items(removalpaths, forcedeletebundles):
|
||||
"""
|
||||
Attempts to remove all the paths in the array removalpaths
|
||||
"""
|
||||
# we sort in reverse because we can delete from the bottom up,
|
||||
# clearing a directory before we try to remove the directory itself
|
||||
removalpaths.sort(reverse=True)
|
||||
removalerrors = ""
|
||||
removalcount = len(removalpaths)
|
||||
display.display_status_minor(
|
||||
'Removing %s filesystem items' % removalcount)
|
||||
|
||||
itemcount = len(removalpaths)
|
||||
itemindex = 0
|
||||
display.display_percent_done(itemindex, itemcount)
|
||||
|
||||
for item in removalpaths:
|
||||
itemindex += 1
|
||||
pathtoremove = "/" + item
|
||||
# use os.path.lexists so broken links return true
|
||||
# so we can remove them
|
||||
if os.path.lexists(pathtoremove):
|
||||
display.display_detail("Removing: " + pathtoremove)
|
||||
if (os.path.isdir(pathtoremove) and
|
||||
not os.path.islink(pathtoremove)):
|
||||
diritems = osutils.listdir(pathtoremove)
|
||||
if diritems == ['.DS_Store']:
|
||||
# If there's only a .DS_Store file
|
||||
# we'll consider it empty
|
||||
ds_storepath = pathtoremove + "/.DS_Store"
|
||||
try:
|
||||
os.remove(ds_storepath)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
diritems = osutils.listdir(pathtoremove)
|
||||
if diritems == []:
|
||||
# directory is empty
|
||||
try:
|
||||
os.rmdir(pathtoremove)
|
||||
except (OSError, IOError), err:
|
||||
msg = "Couldn't remove directory %s - %s" % (
|
||||
pathtoremove, err)
|
||||
display.display_error(msg)
|
||||
removalerrors = removalerrors + "\n" + msg
|
||||
else:
|
||||
# the directory is marked for deletion but isn't empty.
|
||||
# if so directed, if it's a bundle (like .app), we should
|
||||
# remove it anyway - no use having a broken bundle hanging
|
||||
# around
|
||||
if forcedeletebundles and is_bundle(pathtoremove):
|
||||
display.display_warning(
|
||||
"Removing non-empty bundle: %s", pathtoremove)
|
||||
retcode = subprocess.call(['/bin/rm', '-r',
|
||||
pathtoremove])
|
||||
if retcode:
|
||||
msg = "Couldn't remove bundle %s" % pathtoremove
|
||||
display.display_error(msg)
|
||||
removalerrors = removalerrors + "\n" + msg
|
||||
else:
|
||||
# if this path is inside a bundle, and we've been
|
||||
# directed to force remove bundles,
|
||||
# we don't need to warn because it's going to be
|
||||
# removed with the bundle.
|
||||
# Otherwise, we should warn about non-empty
|
||||
# directories.
|
||||
if not inside_bundle(pathtoremove) or \
|
||||
not forcedeletebundles:
|
||||
msg = \
|
||||
"Did not remove %s because it is not empty." % \
|
||||
pathtoremove
|
||||
display.display_error(msg)
|
||||
removalerrors = removalerrors + "\n" + msg
|
||||
|
||||
else:
|
||||
# not a directory, just unlink it
|
||||
try:
|
||||
os.remove(pathtoremove)
|
||||
except (OSError, IOError), err:
|
||||
msg = "Couldn't remove item %s: %s" % (pathtoremove, err)
|
||||
display.display_error(msg)
|
||||
removalerrors = removalerrors + "\n" + msg
|
||||
|
||||
display.display_percent_done(itemindex, itemcount)
|
||||
|
||||
if removalerrors:
|
||||
display.display_info(
|
||||
"---------------------------------------------------")
|
||||
display.display_info(
|
||||
"There were problems removing some filesystem items.")
|
||||
display.display_info(
|
||||
"---------------------------------------------------")
|
||||
display.display_info(removalerrors)
|
||||
|
||||
|
||||
def removepackages(pkgnames, forcedeletebundles=False, listfiles=False,
|
||||
rebuildpkgdb=False, noremovereceipts=False,
|
||||
noupdateapplepkgdb=False):
|
||||
"""
|
||||
Our main function, called by installer.py to remove items based on
|
||||
receipt info.
|
||||
"""
|
||||
if pkgnames == []:
|
||||
display.display_error(
|
||||
"You must specify at least one package to remove!")
|
||||
return -2
|
||||
|
||||
if not init_database(forcerebuild=rebuildpkgdb):
|
||||
display.display_error("Could not initialize receipt database.")
|
||||
return -3
|
||||
|
||||
pkgkeyslist = getpkgkeys(pkgnames)
|
||||
if len(pkgkeyslist) == 0:
|
||||
return -4
|
||||
|
||||
if processes.stop_requested():
|
||||
return -128
|
||||
removalpaths = getpathstoremove(pkgkeyslist)
|
||||
if processes.stop_requested():
|
||||
return -128
|
||||
|
||||
if removalpaths:
|
||||
if listfiles:
|
||||
removalpaths.sort()
|
||||
for item in removalpaths:
|
||||
print "/" + item.encode('UTF-8')
|
||||
else:
|
||||
munkistatus.disableStopButton()
|
||||
remove_filesystem_items(removalpaths, forcedeletebundles)
|
||||
else:
|
||||
display.display_status_minor('Nothing to remove.')
|
||||
|
||||
if not listfiles:
|
||||
if not noremovereceipts:
|
||||
remove_receipts(pkgkeyslist, noupdateapplepkgdb)
|
||||
munkistatus.enableStopButton()
|
||||
display.display_status_minor('Package removal complete.')
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# some globals
|
||||
PACKAGEDB = os.path.join(prefs.pref('ManagedInstallDir'), "b.receiptdb")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print 'This is a library of support tools for the Munki Suite.'
|
||||
94
code/client/removepackages
Executable file
94
code/client/removepackages
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/python
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Copyright 2009-2016 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.
|
||||
|
||||
"""
|
||||
removepackages
|
||||
munkilib/removepackages.py should be a symlink to this file for backwards
|
||||
compatibility
|
||||
|
||||
Command-line tool to analyze installed packages and remove
|
||||
files unique to the packages given. No attempt
|
||||
is made to revert to older versions of a file when uninstalling;
|
||||
only file removals are done.
|
||||
|
||||
"""
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from munkilib import display
|
||||
from munkilib import munkistatus
|
||||
from munkilib import removepkgslib
|
||||
|
||||
|
||||
def main():
|
||||
'''Used when calling removepackages.py directly from the command line.'''
|
||||
# command-line options
|
||||
parser = optparse.OptionParser()
|
||||
parser.set_usage('''Usage: %prog [options] package_id ...''')
|
||||
parser.add_option('--forcedeletebundles', '-f', action='store_true',
|
||||
help='Delete bundles even if they aren\'t empty.')
|
||||
parser.add_option('--listfiles', '-l', action='store_true',
|
||||
help='List the filesystem objects to be removed, '
|
||||
'but do not actually remove them.')
|
||||
parser.add_option('--rebuildpkgdb', action='store_true',
|
||||
help='Force a rebuild of the internal package database.')
|
||||
parser.add_option('--noremovereceipts', action='store_true',
|
||||
help='''Do not remove receipts and boms from
|
||||
/Library/Receipts and update internal package
|
||||
database.''')
|
||||
parser.add_option('--noupdateapplepkgdb', action='store_true',
|
||||
help='Do not update Apple\'s package database. '
|
||||
'If --noremovereceipts is also given, this is implied')
|
||||
parser.add_option('--munkistatusoutput', '-m', action='store_true',
|
||||
help='Output is formatted for use with MunkiStatus.')
|
||||
parser.add_option('--verbose', '-v', action='count', default=1,
|
||||
help='More verbose output. May be specified multiple '
|
||||
'times.')
|
||||
|
||||
# Get our options and our package names
|
||||
options, pkgnames = parser.parse_args()
|
||||
|
||||
# check to see if we're root
|
||||
if os.geteuid() != 0:
|
||||
print sys.stderr, "You must run this as root!"
|
||||
exit(-1)
|
||||
|
||||
# set the display globals
|
||||
display.munkistatusoutput = options.munkistatusoutput
|
||||
display.verbose = options.verbose
|
||||
|
||||
if options.munkistatusoutput:
|
||||
pkgcount = len(pkgnames)
|
||||
munkistatus.message("Removing %s packages..." % pkgcount)
|
||||
munkistatus.detail("")
|
||||
|
||||
retcode = removepkgslib.removepackages(
|
||||
pkgnames,
|
||||
forcedeletebundles=options.forcedeletebundles,
|
||||
listfiles=options.listfiles,
|
||||
rebuildpkgdb=options.rebuildpkgdb,
|
||||
noremovereceipts=options.noremovereceipts,
|
||||
noupdateapplepkgdb=options.noupdateapplepkgdb)
|
||||
if options.munkistatusoutput:
|
||||
munkistatus.quit()
|
||||
exit(retcode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user