You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by ad...@apache.org on 2017/07/07 20:30:14 UTC

ambari git commit: AMBARI-21405. Create custom action to force-remove packages

Repository: ambari
Updated Branches:
  refs/heads/branch-feature-AMBARI-21348 85d7c196f -> b6c5d78ac


AMBARI-21405. Create custom action to force-remove packages


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b6c5d78a
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b6c5d78a
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b6c5d78a

Branch: refs/heads/branch-feature-AMBARI-21348
Commit: b6c5d78ac633e238310419e45314bd69ac4edc26
Parents: 85d7c19
Author: Attila Doroszlai <ad...@hortonworks.com>
Authored: Tue Jul 4 20:20:02 2017 +0200
Committer: Attila Doroszlai <ad...@hortonworks.com>
Committed: Fri Jul 7 22:29:38 2017 +0200

----------------------------------------------------------------------
 .../resource_management/TestPackageResource.py  | 41 ++++++++++++++
 .../core/providers/package/__init__.py          |  2 +-
 .../core/providers/package/apt.py               | 10 ++--
 .../core/providers/package/choco.py             |  4 +-
 .../core/providers/package/yumrpm.py            |  9 +++-
 .../core/providers/package/zypper.py            |  9 +++-
 .../core/resources/packaging.py                 |  6 +++
 .../system_action_definitions.xml               | 10 ++++
 .../scripts/force_remove_packages.py            | 56 ++++++++++++++++++++
 9 files changed, 137 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-agent/src/test/python/resource_management/TestPackageResource.py
----------------------------------------------------------------------
diff --git a/ambari-agent/src/test/python/resource_management/TestPackageResource.py b/ambari-agent/src/test/python/resource_management/TestPackageResource.py
index 66227c6..bc1bfeb 100644
--- a/ambari-agent/src/test/python/resource_management/TestPackageResource.py
+++ b/ambari-agent/src/test/python/resource_management/TestPackageResource.py
@@ -217,6 +217,21 @@ class TestPackageResource(TestCase):
 
   @patch.object(shell, "call", new = MagicMock(return_value=(0, None)))
   @patch.object(shell, "checked_call")
+  @patch.object(System, "os_family", new = 'redhat')
+  def test_action_remove_nodeps_rhel(self, shell_mock):
+    sys.modules['rpm'] = MagicMock()
+    sys.modules['rpm'].TransactionSet.return_value = MagicMock()
+    sys.modules['rpm'].TransactionSet.return_value.dbMatch.return_value = [{'name':'some_package'}]
+    with Environment('/') as env:
+      Package("some_package",
+              action = "remove",
+              logoutput = False,
+              ignore_dependencies = True
+      )
+    shell_mock.assert_called_with(['/usr/bin/rpm', '-e', '--nodeps', 'some_package'], logoutput=False, sudo=True)
+
+  @patch.object(shell, "call", new = MagicMock(return_value=(0, None)))
+  @patch.object(shell, "checked_call")
   @patch.object(System, "os_family", new = 'suse')
   def test_action_remove_suse(self, shell_mock):
     shell_mock.return_value = (0, '')
@@ -230,6 +245,32 @@ class TestPackageResource(TestCase):
       )
     shell_mock.assert_called_with(['/usr/bin/zypper', '--quiet', 'remove', '--no-confirm', 'some_package'], logoutput=False, sudo=True)
 
+  @patch.object(shell, "call", new = MagicMock(return_value=(0, None)))
+  @patch.object(shell, "checked_call")
+  @patch.object(System, "os_family", new = 'suse')
+  def test_action_remove_nodeps_suse(self, shell_mock):
+    shell_mock.return_value = (0, '')
+    with Environment('/') as env:
+      Package("some_package",
+              action = "remove",
+              logoutput = False,
+              ignore_dependencies = True
+      )
+    shell_mock.assert_called_with(['/usr/bin/rpm', '-e', '--nodeps', 'some_package'], logoutput=False, sudo=True)
+
+  @patch.object(shell, "call", new = MagicMock(return_value=(0, None)))
+  @patch.object(shell, "checked_call")
+  @patch.object(System, "os_family", new = 'ubuntu')
+  def test_action_remove_nodeps_ubuntu(self, shell_mock):
+    shell_mock.return_value = (0, '')
+    with Environment('/') as env:
+      Package("some-package",
+              action = "remove",
+              logoutput = False,
+              ignore_dependencies = True
+      )
+    shell_mock.assert_called_with(['/usr/bin/dpkg', '--remove', '--ignore-depends', 'some-package', 'some-package'], logoutput=False, sudo=True)
+
   @patch.object(shell, "call", new = MagicMock(return_value=(1, None)))
   @patch.object(shell, "checked_call")
   @patch.object(System, "os_family", new = 'redhat')

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-common/src/main/python/resource_management/core/providers/package/__init__.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/resource_management/core/providers/package/__init__.py b/ambari-common/src/main/python/resource_management/core/providers/package/__init__.py
index 21de183..a1d3e02 100644
--- a/ambari-common/src/main/python/resource_management/core/providers/package/__init__.py
+++ b/ambari-common/src/main/python/resource_management/core/providers/package/__init__.py
@@ -59,7 +59,7 @@ class PackageProvider(Provider):
 
   def action_remove(self):
     package_name = self.get_package_name_with_version()
-    self.remove_package(package_name)
+    self.remove_package(package_name, self.resource.ignore_dependencies)
 
   def get_package_name_with_version(self):
     if self.resource.version:

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-common/src/main/python/resource_management/core/providers/package/apt.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/resource_management/core/providers/package/apt.py b/ambari-common/src/main/python/resource_management/core/providers/package/apt.py
index d095173..aa80557 100644
--- a/ambari-common/src/main/python/resource_management/core/providers/package/apt.py
+++ b/ambari-common/src/main/python/resource_management/core/providers/package/apt.py
@@ -41,6 +41,7 @@ REMOVE_CMD = {
   False: ['/usr/bin/apt-get', '-y', '-q', 'remove'],
 }
 REPO_UPDATE_CMD = ['/usr/bin/apt-get', 'update','-qq']
+REMOVE_WITHOUT_DEPENDENCIES_CMD = ['/usr/bin/dpkg', '--remove', '--ignore-depends']
 
 APT_SOURCES_LIST_DIR = "/etc/apt/sources.list.d"
 
@@ -103,9 +104,12 @@ class AptProvider(PackageProvider):
     return self.install_package(name, use_repos, skip_repos, is_upgrade)
 
   @replace_underscores
-  def remove_package(self, name):
+  def remove_package(self, name, ignore_dependencies = False):
     if self._check_existence(name):
-      cmd = REMOVE_CMD[self.get_logoutput()] + [name]
+      if ignore_dependencies:
+        cmd = REMOVE_WITHOUT_DEPENDENCIES_CMD + [name, name] # have to specify name twice: one for --ignore-depends, one for --remove
+      else:
+        cmd = REMOVE_CMD[self.get_logoutput()] + [name]
       Logger.info("Removing package %s ('%s')" % (name, string_cmd_from_args_list(cmd)))
       self.checked_call_with_retries(cmd, sudo=True, logoutput=self.get_logoutput())
     else:
@@ -132,4 +136,4 @@ class AptProvider(PackageProvider):
     we should not rely on that.
     """
     code, out = shell.call(CHECK_CMD % name)
-    return not bool(code)
\ No newline at end of file
+    return not bool(code)

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-common/src/main/python/resource_management/core/providers/package/choco.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/resource_management/core/providers/package/choco.py b/ambari-common/src/main/python/resource_management/core/providers/package/choco.py
index db55296..1bb6abf 100644
--- a/ambari-common/src/main/python/resource_management/core/providers/package/choco.py
+++ b/ambari-common/src/main/python/resource_management/core/providers/package/choco.py
@@ -75,7 +75,7 @@ class ChocoProvider(PackageProvider):
     if res['exitCode'] != 0:
       raise Exception("Error while upgrading choco package " + name + ". " + res['error'] + res['output'])
 
-  def remove_package(self, name):
+  def remove_package(self, name, ignore_dependencies = False):
     if self._check_existence(name):
       cmd = REMOVE_CMD[self.get_logoutput()] + [name]
       cmdString = " ".join(cmd)
@@ -93,4 +93,4 @@ class ChocoProvider(PackageProvider):
     res = runner.run(cmd)
     if name in res['output']:
       return True
-    return False
\ No newline at end of file
+    return False

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-common/src/main/python/resource_management/core/providers/package/yumrpm.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/resource_management/core/providers/package/yumrpm.py b/ambari-common/src/main/python/resource_management/core/providers/package/yumrpm.py
index ea10a86..064b504 100644
--- a/ambari-common/src/main/python/resource_management/core/providers/package/yumrpm.py
+++ b/ambari-common/src/main/python/resource_management/core/providers/package/yumrpm.py
@@ -36,6 +36,8 @@ REMOVE_CMD = {
   False: ['/usr/bin/yum', '-d', '0', '-e', '0', '-y', 'erase'],
 }
 
+REMOVE_WITHOUT_DEPENDENCIES_CMD = ['/usr/bin/rpm', '-e', '--nodeps']
+
 REPO_UPDATE_CMD = ['/usr/bin/yum', 'clean','metadata']
 
 class YumProvider(PackageProvider):
@@ -55,9 +57,12 @@ class YumProvider(PackageProvider):
   def upgrade_package(self, name, use_repos=[], skip_repos=[], is_upgrade=True):
     return self.install_package(name, use_repos, skip_repos, is_upgrade)
 
-  def remove_package(self, name):
+  def remove_package(self, name, ignore_dependencies = False):
     if self._check_existence(name):
-      cmd = REMOVE_CMD[self.get_logoutput()] + [name]
+      if ignore_dependencies:
+        cmd = REMOVE_WITHOUT_DEPENDENCIES_CMD + [name]
+      else:
+        cmd = REMOVE_CMD[self.get_logoutput()] + [name]
       Logger.info("Removing package %s ('%s')" % (name, string_cmd_from_args_list(cmd)))
       shell.checked_call(cmd, sudo=True, logoutput=self.get_logoutput())
     else:

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-common/src/main/python/resource_management/core/providers/package/zypper.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/resource_management/core/providers/package/zypper.py b/ambari-common/src/main/python/resource_management/core/providers/package/zypper.py
index 265c162..c1aab60 100644
--- a/ambari-common/src/main/python/resource_management/core/providers/package/zypper.py
+++ b/ambari-common/src/main/python/resource_management/core/providers/package/zypper.py
@@ -35,6 +35,8 @@ REMOVE_CMD = {
   False: ['/usr/bin/zypper', '--quiet', 'remove', '--no-confirm'],
 }
 
+REMOVE_WITHOUT_DEPENDENCIES_CMD = ['/usr/bin/rpm', '-e', '--nodeps']
+
 REPO_UPDATE_CMD = ['/usr/bin/zypper', 'clean']
 
 LIST_ACTIVE_REPOS_CMD = ['/usr/bin/zypper', 'repos']
@@ -63,9 +65,12 @@ class ZypperProvider(PackageProvider):
   def upgrade_package(self, name, use_repos=[], skip_repos=[], is_upgrade=True):
     return self.install_package(name, use_repos, skip_repos, is_upgrade)
   
-  def remove_package(self, name):
+  def remove_package(self, name, ignore_dependencies = False):
     if self._check_existence(name):
-      cmd = REMOVE_CMD[self.get_logoutput()] + [name]
+      if ignore_dependencies:
+        cmd = REMOVE_WITHOUT_DEPENDENCIES_CMD + [name]
+      else:
+        cmd = REMOVE_CMD[self.get_logoutput()] + [name]
       Logger.info("Removing package %s ('%s')" % (name, string_cmd_from_args_list(cmd)))
       self.checked_call_with_retries(cmd, sudo=True, logoutput=self.get_logoutput())
     else:

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-common/src/main/python/resource_management/core/resources/packaging.py
----------------------------------------------------------------------
diff --git a/ambari-common/src/main/python/resource_management/core/resources/packaging.py b/ambari-common/src/main/python/resource_management/core/resources/packaging.py
index e3adc30..5febdae 100644
--- a/ambari-common/src/main/python/resource_management/core/resources/packaging.py
+++ b/ambari-common/src/main/python/resource_management/core/resources/packaging.py
@@ -52,3 +52,9 @@ class Package(Resource):
   version = ResourceArgument()
   actions = ["install", "upgrade", "remove"]
   build_vars = ForcedListArgument(default=[])
+
+  """
+  False - also remove any packages that depend on the one being removed
+  True - possibly break dependencies by keeping them installed
+  """
+  ignore_dependencies = BooleanArgument(default=False)

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-server/src/main/resources/custom_action_definitions/system_action_definitions.xml
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/custom_action_definitions/system_action_definitions.xml b/ambari-server/src/main/resources/custom_action_definitions/system_action_definitions.xml
index 0f50256..c3f97e7 100644
--- a/ambari-server/src/main/resources/custom_action_definitions/system_action_definitions.xml
+++ b/ambari-server/src/main/resources/custom_action_definitions/system_action_definitions.xml
@@ -94,4 +94,14 @@
     <description>Perform remove old stack version action</description>
     <targetType>ANY</targetType>
   </actionDefinition>
+  <actionDefinition>
+    <actionName>force_remove_packages</actionName>
+    <actionType>SYSTEM</actionType>
+    <inputs>package_list</inputs>
+    <targetService/>
+    <targetComponent/>
+    <defaultTimeout>600</defaultTimeout>
+    <description>Remove packages specified by package_list, instructing the package manager to ignore dependencies.</description>
+    <targetType>ALL</targetType>
+  </actionDefinition>
 </actionDefinitions>

http://git-wip-us.apache.org/repos/asf/ambari/blob/b6c5d78a/ambari-server/src/main/resources/custom_actions/scripts/force_remove_packages.py
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/custom_actions/scripts/force_remove_packages.py b/ambari-server/src/main/resources/custom_actions/scripts/force_remove_packages.py
new file mode 100644
index 0000000..237e135
--- /dev/null
+++ b/ambari-server/src/main/resources/custom_actions/scripts/force_remove_packages.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+"""
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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
+
+    http://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.
+
+Ambari Agent
+
+"""
+from resource_management import Script
+from resource_management.core.exceptions import Fail
+from resource_management.core.logger import Logger
+from resource_management.core.resources.packaging import Package
+
+class ForceRemovePackages(Script):
+  """
+  This script is used during cross-stack upgrade to remove packages
+  required by the old stack that are in conflict with packages from the
+  new stack (eg. stack tools).  It can be called via REST API as a custom
+  action.
+  """
+
+  def actionexecute(self, env):
+    config = Script.get_config()
+    packages_to_remove = config['roleParams']['package_list'].split(',')
+    structured_output = {'success': [], 'failure': []}
+
+    for package_name in packages_to_remove:
+      try:
+        Package(package_name, action='remove', ignore_dependencies = True)
+        Logger.info('Removed {0}'.format(package_name))
+        structured_output['success'].append(package_name)
+      except Exception, e:
+        Logger.exception('Failed to remove {0}: {1}'.format(package_name, str(e)))
+        structured_output['failure'].append(package_name)
+
+    self.put_structured_out(structured_output)
+
+    if structured_output['failure']:
+      raise Fail('Failed to remove packages: ' + ', '.join(structured_output['failure']))
+
+
+if __name__ == '__main__':
+  ForceRemovePackages().execute()