directory.py revision 3339
1516N/A#!/usr/bin/python
49N/A#
49N/A# CDDL HEADER START
49N/A#
49N/A# The contents of this file are subject to the terms of the
49N/A# Common Development and Distribution License (the "License").
49N/A# You may not use this file except in compliance with the License.
49N/A#
49N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
49N/A# or http://www.opensolaris.org/os/licensing.
49N/A# See the License for the specific language governing permissions
49N/A# and limitations under the License.
49N/A#
49N/A# When distributing Covered Code, include this CDDL HEADER in each
49N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
49N/A# If applicable, add the following below this CDDL HEADER, with the
49N/A# fields enclosed by brackets "[]" replaced with your own identifying
49N/A# information: Portions Copyright [yyyy] [name of copyright owner]
49N/A#
49N/A# CDDL HEADER END
49N/A#
49N/A
49N/A#
1685N/A# Copyright (c) 2007, 2016, Oracle and/or its affiliates. All rights reserved.
49N/A#
49N/A
49N/A"""module describing a directory packaging object
49N/A
49N/AThis module contains the DirectoryAction class, which represents a
49N/Adirectory-type packaging object."""
49N/A
49N/Aimport errno
51N/Afrom . import generic
51N/Aimport os
1281N/Aimport pkg.portable as portable
49N/Aimport pkg.actions
289N/Aimport pkg.client.api_errors as apx
941N/Aimport stat
49N/A
49N/Aclass DirectoryAction(generic.Action):
49N/A """Class representing a directory-type packaging object."""
49N/A
51N/A __slots__ = []
51N/A
72N/A name = "dir"
1713N/A key_attr = "path"
51N/A unique_attrs = "path", "mode", "owner", "group"
49N/A globally_identical = True
49N/A refcountable = True
941N/A namespace_group = "path"
941N/A ordinality = generic._orderdict[name]
941N/A
941N/A def compare(self, other):
941N/A return (self.attrs["path"] > other.attrs["path"]) - \
941N/A (self.attrs["path"] < other.attrs["path"])
49N/A
307N/A def differences(self, other):
307N/A """Returns a list of attributes that have different values
307N/A between 'other' and 'self'. This differs from the generic
307N/A Action's differences() method in that it normalizes the 'mode'
307N/A attribute so that, say, '0755' and '755' are treated as
281N/A identical."""
136N/A
49N/A diffs = generic.Action.differences(self, other)
1755N/A
1755N/A if "mode" in diffs and \
1755N/A int(self.attrs.get("mode", "0"), 8) == int(other.attrs.get("mode", "0"), 8):
1755N/A diffs.remove("mode")
1755N/A
1755N/A return diffs
1755N/A
1755N/A def directory_references(self):
1755N/A return [os.path.normpath(self.attrs["path"])]
1784N/A
1784N/A def __create_directory(self, pkgplan, path, mode, **kwargs):
72N/A """Create a directory."""
1784N/A
1784N/A try:
1784N/A self.makedirs(path, mode=mode,
1784N/A fmri=pkgplan.destination_fmri, **kwargs)
1784N/A except OSError as e:
1784N/A if e.filename != path:
1784N/A # makedirs failed for some component
1784N/A # of the path.
72N/A raise
53N/A
1784N/A fs = os.lstat(path)
51N/A fs_mode = stat.S_IFMT(fs.st_mode)
95N/A if e.errno == errno.EROFS:
289N/A # Treat EROFS like EEXIST if both are
1507N/A # applicable, since we'll end up with
95N/A # EROFS instead.
72N/A if stat.S_ISDIR(fs_mode):
72N/A return
1755N/A raise
72N/A elif e.errno != errno.EEXIST:
72N/A raise
72N/A
95N/A if stat.S_ISLNK(fs_mode):
95N/A # User has replaced directory with a
95N/A # link, or a package has been poorly
95N/A # implemented. It isn't safe to
95N/A # simply re-create the directory as
95N/A # that won't restore the files that
95N/A # are supposed to be contained within.
95N/A err_txt = _("Unable to create "
95N/A "directory {0}; it has been "
95N/A "replaced with a link. To "
95N/A "continue, please remove the "
95N/A "link or restore the directory "
72N/A "to its original location and "
72N/A "try again.").format(path)
49N/A raise apx.ActionExecutionError(
72N/A self, details=err_txt, error=e,
72N/A fmri=pkgplan.destination_fmri)
289N/A elif stat.S_ISREG(fs_mode):
72N/A # User has replaced directory with a
271N/A # file, or a package has been poorly
271N/A # implemented. Salvage what's there,
72N/A # and drive on.
66N/A pkgplan.salvage(path)
235N/A os.mkdir(path, mode)
1685N/A elif stat.S_ISDIR(fs_mode):
1685N/A # The directory already exists, but
1685N/A # ensure that the mode matches what's
222N/A # expected.
1685N/A os.chmod(path, mode)
1281N/A
1685N/A def install(self, pkgplan, orig):
289N/A """Client-side method that installs a directory."""
136N/A
307N/A mode = None
462N/A try:
462N/A mode = int(self.attrs.get("mode", None), 8)
462N/A except (TypeError, ValueError):
462N/A # Mode isn't valid, so let validate raise a more
462N/A # informative error.
462N/A self.validate(fmri=pkgplan.destination_fmri)
462N/A
462N/A omode = oowner = ogroup = None
462N/A owner, group = self.get_fsobj_uid_gid(pkgplan,
462N/A pkgplan.destination_fmri)
462N/A if orig:
462N/A try:
462N/A omode = int(orig.attrs.get("mode", None), 8)
462N/A except (TypeError, ValueError):
140N/A # Mode isn't valid, so let validate raise a more
144N/A # informative error.
1100N/A orig.validate(fmri=pkgplan.origin_fmri)
1100N/A oowner, ogroup = orig.get_fsobj_uid_gid(pkgplan,
1100N/A pkgplan.origin_fmri)
941N/A
941N/A path = self.get_installed_path(pkgplan.image.get_root())
941N/A
941N/A # Don't allow installation through symlinks.
941N/A self.fsobj_checkpath(pkgplan, path)
941N/A
941N/A # XXX Hack! (See below comment.)
1755N/A if not portable.is_admin():
1755N/A mode |= stat.S_IWUSR
1755N/A
1755N/A if not orig:
1755N/A self.__create_directory(pkgplan, path, mode)
1755N/A
1755N/A # The downside of chmodding the directory is that as a non-root
1755N/A # user, if we set perms u-w, we won't be able to put anything in
1755N/A # it, which is often not what we want at install time. We save
1755N/A # the chmods for the postinstall phase, but it's always possible
1755N/A # that a later package install will want to place something in
1755N/A # this directory and then be unable to. So perhaps we need to
1755N/A # (in all action types) chmod the parent directory to u+w on
# failure, and chmod it back aftwards. The trick is to
# recognize failure due to missing file_dac_write in contrast to
# other failures. Or can we require that everyone simply have
# file_dac_write who wants to use the tools. Probably not.
elif mode != omode:
try:
os.chmod(path, mode)
except Exception as e:
if e.errno != errno.EPERM and e.errno != \
errno.ENOSYS:
# Assume chmod failed due to a
# recoverable error.
self.__create_directory(pkgplan, path,
mode)
omode = oowner = ogroup = None
# if we're salvaging contents, move 'em now.
# directories with "salvage-from" attribute
# set will scavenge any available contents
# that matches specified directory and
# move it underneath itself on install or update.
# This is here to support directory rename
# when old directory has unpackaged contents, or
# consolidation of content from older directories.
for salvage_from in self.attrlist("salvage-from"):
pkgplan.salvage_from(salvage_from, path)
if not orig or oowner != owner or ogroup != group:
try:
portable.chown(path, owner, group)
except OSError as e:
if e.errno != errno.EPERM and \
e.errno != errno.ENOSYS:
# Assume chown failed due to a
# recoverable error.
self.__create_directory(pkgplan, path,
mode, uid=owner, gid=group)
def verify(self, img, **args):
"""Returns a tuple of lists of the form (errors, warnings,
info). The error list will be empty if the action has been
correctly installed in the given image."""
lstat, errors, warnings, info, abort = \
self.verify_fsobj_common(img, stat.S_IFDIR)
return errors, warnings, info
def remove(self, pkgplan):
path = self.get_installed_path(pkgplan.image.get_root())
try:
os.rmdir(path)
except OSError as e:
if e.errno == errno.ENOENT:
pass
elif e.errno in (errno.EEXIST, errno.ENOTEMPTY):
# Cannot remove directory since it's
# not empty.
pkgplan.salvage(path)
elif e.errno == errno.ENOTDIR:
# Either the user or another package has changed
# this directory into a link or file. Salvage
# what's there and drive on.
pkgplan.salvage(path)
elif e.errno == errno.EBUSY and os.path.ismount(path):
# User has replaced directory with mountpoint,
# or a package has been poorly implemented.
if not self.attrs.get("implicit"):
err_txt = _("Unable to remove {0}; it is "
"in use as a mountpoint. To "
"continue, please unmount the "
"filesystem at the target "
"location and try again.").format(
path)
raise apx.ActionExecutionError(self,
details=err_txt, error=e,
fmri=pkgplan.origin_fmri)
elif e.errno == errno.EBUSY:
# os.path.ismount() is broken for lofs
# filesystems, so give a more generic
# error.
if not self.attrs.get("implicit"):
err_txt = _("Unable to remove {0}; it "
"is in use by the system, another "
"process, or as a "
"mountpoint.").format(path)
raise apx.ActionExecutionError(self,
details=err_txt, error=e,
fmri=pkgplan.origin_fmri)
elif e.errno != errno.EACCES: # this happens on Windows
raise
def generate_indices(self):
"""Generates the indices needed by the search dictionary. See
generic.py for a more detailed explanation."""
return [
(self.name, "basename",
os.path.basename(self.attrs["path"].rstrip(os.path.sep)),
None),
(self.name, "path", os.path.sep + self.attrs["path"],
None)
]
def validate(self, fmri=None):
"""Performs additional validation of action attributes that
for performance or other reasons cannot or should not be done
during Action object creation. An ActionError exception (or
subclass of) will be raised if any attributes are not valid.
This is primarily intended for use during publication or during
error handling to provide additional diagonostics.
'fmri' is an optional package FMRI (object or string) indicating
what package contained this action."""
errors = generic.Action._validate(self, fmri=fmri,
raise_errors=False, required_attrs=("owner", "group"))
errors.extend(self._validate_fsobj_common())
if errors:
raise pkg.actions.InvalidActionAttributesError(self,
errors, fmri=fmri)