Repository: conda/conda

Issue:
conda clean -pt as non-root user with root anaconda install
I have installed root miniconda at /opt/anaconda.  When running 

```
conda clean -pt
```

as a lesser user than root, I am seeing errors indicating conda is not checking permissions before attempting to delete package dirs:

```
conda clean -pt
Cache location: /opt/anaconda/pkgs
Will remove the following tarballs:

/opt/anaconda/pkgs
------------------
conda-3.18.3-py27_0.tar.bz2                  175 KB
conda-env-2.4.4-py27_0.tar.bz2                24 KB
itsdangerous-0.24-py27_0.tar.bz2              16 KB
markupsafe-0.23-py27_0.tar.bz2                30 KB
flask-0.10.1-py27_1.tar.bz2                  129 KB
jinja2-2.8-py27_0.tar.bz2                    263 KB
anaconda-build-0.12.0-py27_0.tar.bz2          69 KB
flask-wtf-0.8.4-py27_1.tar.bz2                12 KB
flask-ldap-login-0.3.0-py27_1.tar.bz2         13 KB

---------------------------------------------------
Total:                                       730 KB

removing conda-3.18.3-py27_0.tar.bz2
An unexpected error has occurred, please consider sending the
following traceback to the conda GitHub issue tracker at:

    https://github.com/conda/conda/issues

Include the output of the command 'conda info' in your report.


Traceback (most recent call last):
  File "/opt/anaconda/envs/anaconda.org/bin/conda", line 5, in <module>
    sys.exit(main())
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main.py", line 195, in main
    args_func(args, p)
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main.py", line 202, in args_func
    args.func(args, p)
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main_clean.py", line 331, in execute
    rm_tarballs(args, pkgs_dirs, totalsize, verbose=not args.json)
  File "/opt/anaconda/lib/python2.7/site-packages/conda/cli/main_clean.py", line 156, in rm_tarballs
    os.unlink(os.path.join(pkgs_dir, fn))
OSError: [Errno 13] Permission denied: '/opt/anaconda/pkgs/conda-3.18.3-py27_0.tar.bz2'

```



Discussion:
Hi @csoja - this issue is blocking some key Build System stability fixes. LMK if this can be prioritized (along with #1752 above)

@stephenakearns and @PeterDSteinberg and @csoja this issue is also now preventing us from moving forward with the anaconda-cluster build scripts.

@csoja - I know you're strapped for resources and may have a new person taking a look at this. Can we bring it up in the platform meeting to discuss a possible solution 


Resolving patch:
diff --git a/conda/cli/main_clean.py b/conda/cli/main_clean.py
--- a/conda/cli/main_clean.py
+++ b/conda/cli/main_clean.py
@@ -151,9 +151,13 @@ def rm_tarballs(args, pkgs_dirs, totalsize, verbose=True):
 
     for pkgs_dir in pkgs_dirs:
         for fn in pkgs_dirs[pkgs_dir]:
-            if verbose:
-                print("removing %s" % fn)
-            os.unlink(os.path.join(pkgs_dir, fn))
+            if os.access(os.path.join(pkgs_dir, fn), os.W_OK):
+                if verbose:
+                    print("Removing %s" % fn)
+                os.unlink(os.path.join(pkgs_dir, fn))
+            else:
+                if verbose:
+                    print("WARNING: cannot remove, file permissions: %s" % fn)
 
 
 def find_pkgs():
diff --git a/conda/install.py b/conda/install.py
--- a/conda/install.py
+++ b/conda/install.py
@@ -138,6 +138,13 @@ def _remove_readonly(func, path, excinfo):
     os.chmod(path, stat.S_IWRITE)
     func(path)
 
+def warn_failed_remove(function, path, exc_info):
+    if exc_info[1].errno == errno.EACCES:
+        log.warn("Cannot remove, permission denied: {0}".format(path))
+    elif exc_info[1].errno == errno.ENOTEMPTY:
+        log.warn("Cannot remove, not empty: {0}".format(path))
+    else:
+        log.warn("Cannot remove, unknown reason: {0}".format(path))
 
 def rm_rf(path, max_retries=5, trash=True):
     """
@@ -152,12 +159,15 @@ def rm_rf(path, max_retries=5, trash=True):
         # Note that we have to check if the destination is a link because
         # exists('/path/to/dead-link') will return False, although
         # islink('/path/to/dead-link') is True.
-        os.unlink(path)
+        if os.access(path, os.W_OK):
+            os.unlink(path)
+        else:
+            log.warn("Cannot remove, permission denied: {0}".format(path))
 
     elif isdir(path):
         for i in range(max_retries):
             try:
-                shutil.rmtree(path)
+                shutil.rmtree(path, ignore_errors=False, onerror=warn_failed_remove)
                 return
             except OSError as e:
                 msg = "Unable to delete %s\n%s\n" % (path, e)
@@ -189,7 +199,7 @@ def rm_rf(path, max_retries=5, trash=True):
                 log.debug(msg + "Retrying after %s seconds..." % i)
                 time.sleep(i)
         # Final time. pass exceptions to caller.
-        shutil.rmtree(path)
+        shutil.rmtree(path, ignore_errors=False, onerror=warn_failed_remove)
 
 def rm_empty_dir(path):
     """


Test patch:
diff --git a/tests/test_install.py b/tests/test_install.py
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -8,7 +8,7 @@
 
 
 from conda import install
-from conda.install import PaddingError, binary_replace, update_prefix
+from conda.install import PaddingError, binary_replace, update_prefix, warn_failed_remove
 
 from .decorators import skip_if_no_mock
 from .helpers import mock
@@ -140,6 +140,11 @@ def generate_mock_isfile(self, value):
         with patch.object(install, 'isfile', return_value=value) as isfile:
             yield isfile
 
+    @contextmanager
+    def generate_mock_os_access(self, value):
+        with patch.object(install.os, 'access', return_value=value) as os_access:
+            yield os_access
+
     @contextmanager
     def generate_mock_unlink(self):
         with patch.object(install.os, 'unlink') as unlink:
@@ -173,25 +178,27 @@ def generate_mock_check_call(self):
             yield check_call
 
     @contextmanager
-    def generate_mocks(self, islink=True, isfile=True, isdir=True, on_win=False):
+    def generate_mocks(self, islink=True, isfile=True, isdir=True, on_win=False, os_access=True):
         with self.generate_mock_islink(islink) as mock_islink:
             with self.generate_mock_isfile(isfile) as mock_isfile:
-                with self.generate_mock_isdir(isdir) as mock_isdir:
-                    with self.generate_mock_unlink() as mock_unlink:
-                        with self.generate_mock_rmtree() as mock_rmtree:
-                            with self.generate_mock_sleep() as mock_sleep:
-                                with self.generate_mock_log() as mock_log:
-                                    with self.generate_mock_on_win(on_win):
-                                        with self.generate_mock_check_call() as check_call:
-                                            yield {
-                                                'islink': mock_islink,
-                                                'isfile': mock_isfile,
-                                                'isdir': mock_isdir,
-                                                'unlink': mock_unlink,
-                                                'rmtree': mock_rmtree,
-                                                'sleep': mock_sleep,
-                                                'log': mock_log,
-                                                'check_call': check_call,
+                with self.generate_mock_os_access(os_access) as mock_os_access:
+                    with self.generate_mock_isdir(isdir) as mock_isdir:
+                        with self.generate_mock_unlink() as mock_unlink:
+                            with self.generate_mock_rmtree() as mock_rmtree:
+                                with self.generate_mock_sleep() as mock_sleep:
+                                    with self.generate_mock_log() as mock_log:
+                                        with self.generate_mock_on_win(on_win):
+                                            with self.generate_mock_check_call() as check_call:
+                                                yield {
+                                                    'islink': mock_islink,
+                                                    'isfile': mock_isfile,
+                                                    'isdir': mock_isdir,
+                                                    'os_access': mock_os_access,
+                                                    'unlink': mock_unlink,
+                                                    'rmtree': mock_rmtree,
+                                                    'sleep': mock_sleep,
+                                                    'log': mock_log,
+                                                    'check_call': check_call,
                                             }
 
     def generate_directory_mocks(self, on_win=False):
@@ -219,6 +226,13 @@ def test_calls_unlink_on_true_islink(self):
             install.rm_rf(some_path)
         mocks['unlink'].assert_called_with(some_path)
 
+    @skip_if_no_mock
+    def test_does_not_call_unlink_on_os_access_false(self):
+        with self.generate_mocks(os_access=False) as mocks:
+            some_path = self.generate_random_path
+            install.rm_rf(some_path)
+        self.assertFalse(mocks['unlink'].called)
+
     @skip_if_no_mock
     def test_does_not_call_isfile_if_islink_is_true(self):
         with self.generate_mocks() as mocks:
@@ -259,7 +273,8 @@ def test_calls_rmtree_at_least_once_on_isdir_true(self):
         with self.generate_directory_mocks() as mocks:
             some_path = self.generate_random_path
             install.rm_rf(some_path)
-        mocks['rmtree'].assert_called_with(some_path)
+        mocks['rmtree'].assert_called_with(
+            some_path, onerror=warn_failed_remove, ignore_errors=False)
 
     @skip_if_no_mock
     def test_calls_rmtree_only_once_on_success(self):
@@ -342,7 +357,7 @@ def test_tries_extra_kwarg_on_windows(self):
             install.rm_rf(random_path)
 
         expected_call_list = [
-            mock.call(random_path),
+            mock.call(random_path, ignore_errors=False, onerror=warn_failed_remove),
             mock.call(random_path, onerror=install._remove_readonly)
         ]
         mocks['rmtree'].assert_has_calls(expected_call_list)