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