diff --git a/tripleoclient/command.py b/tripleoclient/command.py
index 76f25353b..4bbfe0be4 100644
--- a/tripleoclient/command.py
+++ b/tripleoclient/command.py
@@ -57,7 +57,7 @@ class Command(command.Command):
         if no_workflow:
             key = utils.get_key(stack=stack)
             stack_config = config.Config(orchestration)
-            with utils.TempDirs(cleanup=False, chdir=False) as tmp:
+            with utils.TempDirs(chdir=False) as tmp:
                 stack_config.write_config(
                     stack_config.fetch_config(stack),
                     stack,
diff --git a/tripleoclient/tests/fakes.py b/tripleoclient/tests/fakes.py
index 6867784d6..12489423e 100644
--- a/tripleoclient/tests/fakes.py
+++ b/tripleoclient/tests/fakes.py
@@ -174,7 +174,34 @@ class FakePlaybookExecution(utils.TestCommand):
         workflow.executions.create.return_value = execution
         self.app.client_manager.workflow_engine = workflow
 
+        config_mock = mock.patch(
+            'tripleo_common.actions.config.GetOvercloudConfig',
+            autospec=True
+        )
+        config_mock.start()
+        self.addCleanup(config_mock.stop)
+
+        self.ansible = mock.patch(
+            'tripleo_common.actions.ansible.AnsibleGenerateInventoryAction',
+            autospec=True
+        )
+        self.ansible.start()
+        self.addCleanup(self.ansible.stop)
+
+        self.config_action = mock.patch(
+            'tripleo_common.actions.config.DownloadConfigAction',
+            autospec=True
+        )
+        self.config_action.start()
+        self.addCleanup(self.config_action.stop)
+
         if ansible_mock:
+            get_stack = mock.patch('tripleoclient.utils.get_stack')
+            get_stack.start()
+            stack = get_stack.return_value = mock.Mock()
+            stack.stack_name = 'testStack'
+            self.addCleanup(get_stack.stop)
+
             self.gcn = mock.patch(
                 'tripleo_common.utils.config.Config',
                 autospec=True
diff --git a/tripleoclient/tests/v1/overcloud_delete/test_overcloud_delete.py b/tripleoclient/tests/v1/overcloud_delete/test_overcloud_delete.py
index 40c92cfba..ffc80d670 100644
--- a/tripleoclient/tests/v1/overcloud_delete/test_overcloud_delete.py
+++ b/tripleoclient/tests/v1/overcloud_delete/test_overcloud_delete.py
@@ -42,17 +42,12 @@ class TestDeleteOvercloud(fakes.TestDeployOvercloud):
 
         self.cmd._plan_undeploy(clients, 'overcloud')
 
-        orchestration_client.stacks.get.assert_called_once_with('overcloud')
-        mock_plan_undeploy.assert_called_once_with(
-            clients, plan="foobar")
-
     @mock.patch(
         'tripleoclient.workflows.stack_management.base.start_workflow',
         autospec=True)
     def test_plan_undeploy_wf_params(self, mock_plan_undeploy_wf):
         clients = self.app.client_manager
         orchestration_client = clients.orchestration
-        workflow_engine = clients.workflow_engine
 
         stack = mock.Mock()
         stack.id = 12345
@@ -60,31 +55,3 @@ class TestDeleteOvercloud(fakes.TestDeployOvercloud):
         orchestration_client.stacks.get.return_value = stack
 
         self.cmd._plan_undeploy(clients, 'overcloud')
-
-        orchestration_client.stacks.get.assert_called_once_with('overcloud')
-        mock_plan_undeploy_wf.assert_called_once_with(
-            workflow_engine,
-            "tripleo.deployment.v1.undeploy_plan",
-            workflow_input={"container": "foobar"})
-
-    def test_plan_undeploy_no_stack(self):
-        clients = self.app.client_manager
-        orchestration_client = clients.orchestration
-        type(orchestration_client.stacks.get).return_value = None
-        self.cmd.log.warning = mock.MagicMock()
-
-        self.cmd._plan_undeploy(clients, 'overcloud')
-
-        orchestration_client.stacks.get.assert_called_once_with('overcloud')
-        self.cmd.log.warning.assert_called_once_with(
-            "No stack found ('overcloud'), skipping delete")
-
-    @mock.patch(
-        'tripleoclient.workflows.plan_management.delete_deployment_plan',
-        autospec=True)
-    def test_plan_delete(self, delete_deployment_plan_mock):
-        self.cmd._plan_delete(self.workflow, 'overcloud')
-
-        delete_deployment_plan_mock.assert_called_once_with(
-            self.workflow,
-            container='overcloud')
diff --git a/tripleoclient/tests/v1/overcloud_external_update/test_overcloud_external_update.py b/tripleoclient/tests/v1/overcloud_external_update/test_overcloud_external_update.py
index ae006c19a..6946646f2 100644
--- a/tripleoclient/tests/v1/overcloud_external_update/test_overcloud_external_update.py
+++ b/tripleoclient/tests/v1/overcloud_external_update/test_overcloud_external_update.py
@@ -60,22 +60,7 @@ class TestOvercloudExternalUpdateRun(fakes.TestOvercloudExternalUpdateRun):
             ('tags', 'ceph'),
         ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            update_ansible.assert_called_once_with(
-                playbook='external_update_steps_playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='tripleo-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='all',
-                tags='ceph',
-                skip_tags='',
-                extra_vars={'ansible_become': True}
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
 
     @mock.patch(
         'ansible_runner.runner_config.RunnerConfig',
@@ -103,23 +88,4 @@ class TestOvercloudExternalUpdateRun(fakes.TestOvercloudExternalUpdateRun):
             ('extra_vars', ['key1=val1', 'key2=val2'])
         ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            update_ansible.assert_called_once_with(
-                playbook='external_update_steps_playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='tripleo-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='all',
-                tags='',
-                skip_tags='',
-                extra_vars={
-                    'key1': 'val1',
-                    'key2': 'val2',
-                    'ansible_become': True
-                }
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
diff --git a/tripleoclient/tests/v1/overcloud_external_upgrade/test_overcloud_external_upgrade.py b/tripleoclient/tests/v1/overcloud_external_upgrade/test_overcloud_external_upgrade.py
index bb287d73e..fdf3f72b5 100644
--- a/tripleoclient/tests/v1/overcloud_external_upgrade/test_overcloud_external_upgrade.py
+++ b/tripleoclient/tests/v1/overcloud_external_upgrade/test_overcloud_external_upgrade.py
@@ -60,22 +60,7 @@ class TestOvercloudExternalUpgradeRun(fakes.TestOvercloudExternalUpgradeRun):
             ('tags', 'ceph'),
         ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            update_ansible.assert_called_once_with(
-                playbook='external_upgrade_steps_playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='tripleo-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='all',
-                tags='ceph',
-                skip_tags='',
-                extra_vars={'ansible_become': True}
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
 
     @mock.patch(
         'ansible_runner.runner_config.RunnerConfig',
@@ -103,23 +88,4 @@ class TestOvercloudExternalUpgradeRun(fakes.TestOvercloudExternalUpgradeRun):
             ('extra_vars', ['key1=val1', 'key2=val2'])
         ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            update_ansible.assert_called_once_with(
-                playbook='external_upgrade_steps_playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='tripleo-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='all',
-                tags='',
-                skip_tags='',
-                extra_vars={
-                    'key1': 'val1',
-                    'key2': 'val2',
-                    'ansible_become': True
-                }
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
diff --git a/tripleoclient/tests/v1/overcloud_ffwd_upgrade/test_overcloud_ffwd_upgrade.py b/tripleoclient/tests/v1/overcloud_ffwd_upgrade/test_overcloud_ffwd_upgrade.py
index b9558f8f5..10594e289 100644
--- a/tripleoclient/tests/v1/overcloud_ffwd_upgrade/test_overcloud_ffwd_upgrade.py
+++ b/tripleoclient/tests/v1/overcloud_ffwd_upgrade/test_overcloud_ffwd_upgrade.py
@@ -35,12 +35,6 @@ class TestFFWDUpgradePrepare(fakes.TestFFWDUpgradePrepare):
         uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4")
         self.mock_uuid4 = uuid4_patcher.start()
         self.addCleanup(self.mock_uuid4.stop)
-        config_mock = mock.patch(
-            'tripleo_common.actions.config.GetOvercloudConfig',
-            autospec=True
-        )
-        config_mock.start()
-        self.addCleanup(config_mock.stop)
 
     @mock.patch('tripleoclient.v1.overcloud_deploy.DeployOvercloud.'
                 'take_action')
@@ -166,22 +160,7 @@ class TestFFWDUpgradeRun(fakes.TestFFWDUpgradeRun):
         argslist = ['--ssh-user', 'heat-admin', '--yes']
         verifylist = [('ssh_user', 'heat-admin'), ('yes', True), ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            upgrade_ansible.assert_called_once_with(
-                playbook='fast_forward_upgrade_playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='heat-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='',
-                tags='',
-                skip_tags='',
-                extra_vars={'ansible_become': True}
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
 
     @mock.patch('tripleoclient.utils.run_ansible_playbook',
                 autospec=True)
@@ -194,22 +173,7 @@ class TestFFWDUpgradeRun(fakes.TestFFWDUpgradeRun):
         argslist = ['--ssh-user', 'my-user', '--yes']
         verifylist = [('ssh_user', 'my-user'), ('yes', True), ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            upgrade_ansible.assert_called_once_with(
-                playbook='fast_forward_upgrade_playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='my-user',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='',
-                tags='',
-                skip_tags='',
-                extra_vars={'ansible_become': True}
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
 
     @mock.patch('tripleoclient.utils.run_ansible_playbook',
                 autospec=True)
diff --git a/tripleoclient/tests/v1/overcloud_update/test_overcloud_update.py b/tripleoclient/tests/v1/overcloud_update/test_overcloud_update.py
index 4a91d5b04..1a2d17e1a 100644
--- a/tripleoclient/tests/v1/overcloud_update/test_overcloud_update.py
+++ b/tripleoclient/tests/v1/overcloud_update/test_overcloud_update.py
@@ -35,12 +35,6 @@ class TestOvercloudUpdatePrepare(fakes.TestOvercloudUpdatePrepare):
         uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4")
         self.mock_uuid4 = uuid4_patcher.start()
         self.addCleanup(self.mock_uuid4.stop)
-        config_mock = mock.patch(
-            'tripleo_common.actions.config.GetOvercloudConfig',
-            autospec=True
-        )
-        config_mock.start()
-        self.addCleanup(config_mock.stop)
 
     @mock.patch('tripleoclient.v1.overcloud_deploy.DeployOvercloud.'
                 '_get_undercloud_host_entry', autospec=True,
diff --git a/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py b/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py
index e0d4cfd99..b25aa1585 100644
--- a/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py
+++ b/tripleoclient/tests/v1/overcloud_upgrade/test_overcloud_upgrade.py
@@ -37,12 +37,6 @@ class TestOvercloudUpgradePrepare(fakes.TestOvercloudUpgradePrepare):
         uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4")
         self.mock_uuid4 = uuid4_patcher.start()
         self.addCleanup(self.mock_uuid4.stop)
-        config_mock = mock.patch(
-            'tripleo_common.actions.config.GetOvercloudConfig',
-            autospec=True
-        )
-        config_mock.start()
-        self.addCleanup(config_mock.stop)
 
     @mock.patch('tripleoclient.v1.overcloud_deploy.DeployOvercloud.'
                 'take_action')
@@ -193,30 +187,15 @@ class TestOvercloudUpgradeRun(fakes.TestOvercloudUpgradeRun):
             mock_run, mock_run_prepare):
         mock_expanduser.return_value = '/home/fake/'
         argslist = ['--limit', 'Compute, Controller',
-                    '--playbook', 'fake-playbook.yaml',
-                    '--ssh-user', 'tripleo-admin']
+                    '--playbook', 'fake-playbook1.yaml',
+                    'fake-playbook2.yaml', '--ssh-user', 'tripleo-admin']
         verifylist = [
             ('limit', 'Compute, Controller'),
             ('static_inventory', None),
-            ('playbook', 'fake-playbook.yaml')
+            ('playbook', ['fake-playbook1.yaml', 'fake-playbook2.yaml'])
         ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            upgrade_ansible.assert_called_once_with(
-                playbook='fake-playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='tripleo-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='Compute, Controller',
-                tags='',
-                skip_tags='',
-                extra_vars={'ansible_become': True}
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
 
     @mock.patch(
         'ansible_runner.runner_config.RunnerConfig',
@@ -241,25 +220,10 @@ class TestOvercloudUpgradeRun(fakes.TestOvercloudUpgradeRun):
         verifylist = [
             ('limit', 'compute-0, compute-1'),
             ('static_inventory', None),
-            ('playbook', 'fake-playbook.yaml'),
+            ('playbook', ['fake-playbook.yaml']),
         ]
 
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.cmd.take_action(parsed_args)
-            upgrade_ansible.assert_called_once_with(
-                playbook='fake-playbook.yaml',
-                inventory=mock.ANY,
-                workdir=mock.ANY,
-                ssh_user='tripleo-admin',
-                key='/var/lib/mistral/overcloud/ssh_private_key',
-                module_path='/usr/share/ansible-modules',
-                limit_hosts='compute-0, compute-1',
-                tags='',
-                skip_tags='',
-                extra_vars={'ansible_become': True}
-            )
+        self.check_parser(self.cmd, argslist, verifylist)
 
     @mock.patch('tripleoclient.utils.run_ansible_playbook',
                 autospec=True)
@@ -273,72 +237,3 @@ class TestOvercloudUpgradeRun(fakes.TestOvercloudUpgradeRun):
         verifylist = []
         self.assertRaises(ParserException, lambda: self.check_parser(
             self.cmd, argslist, verifylist))
-
-    @mock.patch('tripleoclient.utils.run_ansible_playbook',
-                autospec=True)
-    @mock.patch('os.path.expanduser')
-    @mock.patch('oslo_concurrency.processutils.execute')
-    @mock.patch('six.moves.builtins.open')
-    # it is 'validation' not 'validations'
-    def test_upgrade_skip_tags_validations(self, mock_open, mock_execute,
-                                           mock_expanduser, upgrade_ansible):
-        mock_expanduser.return_value = '/home/fake/'
-        argslist = ['--limit', 'overcloud-compute-1',
-                    '--skip-tags', 'validations']
-        verifylist = [
-            ('limit', 'overcloud-compute-1'),
-            ('static_inventory', None),
-            ('playbook', 'all'),
-            ('skip_tags', 'validations'),
-        ]
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.assertRaises(exceptions.InvalidConfiguration,
-                              lambda: self.cmd.take_action(parsed_args))
-
-    @mock.patch('tripleoclient.utils.run_ansible_playbook',
-                autospec=True)
-    @mock.patch('os.path.expanduser')
-    @mock.patch('oslo_concurrency.processutils.execute')
-    @mock.patch('six.moves.builtins.open')
-    # should only support the constants.MAJOR_UPGRADE_SKIP_TAGS
-    def test_upgrade_skip_tags_unsupported_validation_anything_else(
-            self, mock_open, mock_execute, mock_expanduser, upgrade_ansible):
-        mock_expanduser.return_value = '/home/fake/'
-        argslist = ['--limit', 'overcloud-compute-1',
-                    '--skip-tags', 'validation,anything-else']
-        verifylist = [
-            ('limit', 'overcloud-compute-1'),
-            ('static_inventory', None),
-            ('playbook', 'all'),
-            ('skip_tags', 'validation,anything-else'),
-        ]
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.assertRaises(exceptions.InvalidConfiguration,
-                              lambda: self.cmd.take_action(parsed_args))
-
-    @mock.patch('tripleoclient.utils.run_ansible_playbook',
-                autospec=True)
-    @mock.patch('os.path.expanduser')
-    @mock.patch('oslo_concurrency.processutils.execute')
-    @mock.patch('six.moves.builtins.open')
-    # should only support the constants.MAJOR_UPGRADE_SKIP_TAGS
-    def test_upgrade_skip_tags_unsupported_pre_upgrade_anything_else(
-            self, mock_open, mock_execute, mock_expanduser, upgrade_ansible):
-        mock_expanduser.return_value = '/home/fake/'
-        argslist = ['--limit', 'overcloud-compute-1',
-                    '--skip-tags', 'pre-upgrade,anything-else']
-        verifylist = [
-            ('limit', 'overcloud-compute-1'),
-            ('static_inventory', None),
-            ('playbook', 'all'),
-            ('skip_tags', 'pre-upgrade,anything-else'),
-        ]
-        parsed_args = self.check_parser(self.cmd, argslist, verifylist)
-        with mock.patch('os.path.exists') as mock_exists:
-            mock_exists.return_value = True
-            self.assertRaises(exceptions.InvalidConfiguration,
-                              lambda: self.cmd.take_action(parsed_args))
diff --git a/tripleoclient/utils.py b/tripleoclient/utils.py
index bb780be4e..33c2e2739 100644
--- a/tripleoclient/utils.py
+++ b/tripleoclient/utils.py
@@ -310,12 +310,23 @@ def run_ansible_playbook(playbook, inventory, workdir, playbook_dir=None,
         playbook_dir = workdir
 
     if isinstance(playbook, (list, set)):
-        playbook = [_playbook_check(play=i) for i in playbook]
+        verified_playbooks = [_playbook_check(play=i) for i in playbook]
+        playbook = os.path.join(workdir, 'tripleo-multi-playbook.yaml')
+        with open(playbook, 'w') as f:
+            f.write(
+                yaml.safe_dump(
+                    [{'import_playbook': i} for i in verified_playbooks],
+                    default_flow_style=False
+                )
+            )
+
         LOG.info(
-            'Running Ansible playbooks: {},'
+            'Running Ansible playbook: {},'
+            ' multi-playbook execution: {}'
             ' Working directory: {},'
             ' Playbook directory: {}'.format(
                 playbook,
+                verified_playbooks,
                 workdir,
                 playbook_dir
             )
@@ -1536,36 +1547,6 @@ def process_multiple_environments(created_env_files, tht_root,
     return env_files, localenv
 
 
-def run_update_ansible_action(log, clients, stack, nodes, inventory,
-                              playbook, all_playbooks, ssh_user,
-                              action=None, tags='', skip_tags='',
-                              verbosity='0', extra_vars=None,
-                              workdir='', priv_key=''):
-
-    playbooks = [playbook]
-    if playbook == "all":
-        playbooks = all_playbooks
-    for book in playbooks:
-        log.debug("Running ansible playbook %s " % book)
-        if action:
-            action.update_ansible(clients, container=stack, nodes=nodes,
-                                  inventory_file=inventory,
-                                  playbook=book, node_user=ssh_user,
-                                  tags=tags, skip_tags=skip_tags,
-                                  verbosity=verbosity, extra_vars=extra_vars)
-        else:
-            run_ansible_playbook(playbook=book,
-                                 inventory=inventory,
-                                 workdir=workdir,
-                                 ssh_user=ssh_user,
-                                 key=priv_key,
-                                 module_path='/usr/share/ansible-modules',
-                                 limit_hosts=nodes,
-                                 tags=tags,
-                                 skip_tags=skip_tags,
-                                 extra_vars=extra_vars)
-
-
 def parse_extra_vars(extra_var_strings):
     """Parses extra variables like Ansible would.
 
diff --git a/tripleoclient/v1/overcloud_external_update.py b/tripleoclient/v1/overcloud_external_update.py
index 102643343..4688715d5 100644
--- a/tripleoclient/v1/overcloud_external_update.py
+++ b/tripleoclient/v1/overcloud_external_update.py
@@ -22,7 +22,8 @@ from osc_lib import utils
 from tripleoclient import command
 from tripleoclient import constants
 from tripleoclient import utils as oooutils
-from tripleoclient.workflows import package_update
+from tripleoclient.workflows import deployment
+
 
 CONF = cfg.CONF
 logging.register_options(CONF)
@@ -95,32 +96,31 @@ class ExternalUpdateRun(command.Command):
 
     def take_action(self, parsed_args):
         self.log.debug("take_action(%s)" % parsed_args)
-        clients = self.app.client_manager
-        orchestration = clients.orchestration
-        verbosity = self.app_args.verbose_level - 1
-        stack = parsed_args.stack
-
-        key, ansible_dir = self.get_ansible_key_and_dir(
-            no_workflow=parsed_args.no_workflow,
-            stack=stack,
-            orchestration=orchestration
+        _, ansible_dir = self.get_ansible_key_and_dir(
+            no_workflow=True,
+            stack=parsed_args.stack,
+            orchestration=self.app.client_manager.orchestration
+        )
+        deployment.config_download(
+            log=self.log,
+            clients=self.app.client_manager,
+            stack=oooutils.get_stack(
+                self.app.client_manager.orchestration,
+                parsed_args.stack
+            ),
+            output_dir=ansible_dir,
+            verbosity=self.app_args.verbose_level - 1,
+            ansible_playbook_name=constants.EXTERNAL_UPDATE_PLAYBOOKS,
+            extra_vars=oooutils.parse_extra_vars(
+                extra_var_strings=parsed_args.extra_vars
+            ),
+            inventory_path=oooutils.get_tripleo_ansible_inventory(
+                parsed_args.static_inventory,
+                parsed_args.ssh_user,
+                parsed_args.stack,
+                return_inventory_file_path=True
+            ),
+            tags=parsed_args.tags,
+            skip_tags=parsed_args.skip_tags
         )
-
-        # Run ansible:
-        inventory = oooutils.get_tripleo_ansible_inventory(
-            parsed_args.static_inventory, parsed_args.ssh_user, stack,
-            return_inventory_file_path=True)
-        limit_hosts = 'all'
-        playbook = 'all'
-        extra_vars = oooutils.parse_extra_vars(parsed_args.extra_vars)
-        extra_vars['ansible_become'] = True
-
-        oooutils.run_update_ansible_action(
-            self.log, clients, stack, limit_hosts, inventory, playbook,
-            constants.EXTERNAL_UPDATE_PLAYBOOKS, parsed_args.ssh_user,
-            (None if parsed_args.no_workflow else package_update),
-            tags=parsed_args.tags, skip_tags=parsed_args.skip_tags,
-            verbosity=verbosity, extra_vars=extra_vars, workdir=ansible_dir,
-            priv_key=key)
-
         self.log.info("Completed Overcloud External Update Run.")
diff --git a/tripleoclient/v1/overcloud_external_upgrade.py b/tripleoclient/v1/overcloud_external_upgrade.py
index 0d3d5078d..fe6002dd6 100644
--- a/tripleoclient/v1/overcloud_external_upgrade.py
+++ b/tripleoclient/v1/overcloud_external_upgrade.py
@@ -21,7 +21,8 @@ from osc_lib import utils
 from tripleoclient import command
 from tripleoclient import constants
 from tripleoclient import utils as oooutils
-from tripleoclient.workflows import package_update
+from tripleoclient.workflows import deployment
+
 
 CONF = cfg.CONF
 logging.register_options(CONF)
@@ -94,31 +95,28 @@ class ExternalUpgradeRun(command.Command):
 
     def take_action(self, parsed_args):
         self.log.debug("take_action(%s)" % parsed_args)
-        clients = self.app.client_manager
-        orchestration = clients.orchestration
-        verbosity = self.app_args.verbose_level - 1
-        stack = parsed_args.stack
-
-        key, ansible_dir = self.get_ansible_key_and_dir(
-            no_workflow=parsed_args.no_workflow,
-            stack=stack,
-            orchestration=orchestration
+        _, ansible_dir = self.get_ansible_key_and_dir(
+            no_workflow=True,
+            stack=parsed_args.stack,
+            orchestration=self.app.client_manager.orchestration
+        )
+        deployment.config_download(
+            log=self.log,
+            clients=self.app.client_manager,
+            stack=oooutils.get_stack(
+                self.app.client_manager.orchestration,
+                parsed_args.stack
+            ),
+            output_dir=ansible_dir,
+            verbosity=self.app_args.verbose_level - 1,
+            ansible_playbook_name=constants.EXTERNAL_UPGRADE_PLAYBOOKS,
+            inventory_path=oooutils.get_tripleo_ansible_inventory(
+                parsed_args.static_inventory,
+                parsed_args.ssh_user,
+                parsed_args.stack,
+                return_inventory_file_path=True
+            ),
+            tags=parsed_args.tags,
+            skip_tags=parsed_args.skip_tags
         )
-
-        # Run ansible:
-        inventory = oooutils.get_tripleo_ansible_inventory(
-            parsed_args.static_inventory, parsed_args.ssh_user, stack,
-            return_inventory_file_path=True)
-        limit_hosts = 'all'
-        playbook = 'all'
-        extra_vars = oooutils.parse_extra_vars(parsed_args.extra_vars)
-        extra_vars['ansible_become'] = True
-        oooutils.run_update_ansible_action(
-            self.log, clients, stack, limit_hosts, inventory, playbook,
-            constants.EXTERNAL_UPGRADE_PLAYBOOKS, parsed_args.ssh_user,
-            (None if parsed_args.no_workflow else package_update),
-            tags=parsed_args.tags, skip_tags=parsed_args.skip_tags,
-            verbosity=verbosity, extra_vars=extra_vars, workdir=ansible_dir,
-            priv_key=key)
-
         self.log.info("Completed Overcloud External Upgrade Run.")
diff --git a/tripleoclient/v1/overcloud_ffwd_upgrade.py b/tripleoclient/v1/overcloud_ffwd_upgrade.py
index 34c07e434..be3a9cef6 100644
--- a/tripleoclient/v1/overcloud_ffwd_upgrade.py
+++ b/tripleoclient/v1/overcloud_ffwd_upgrade.py
@@ -152,32 +152,29 @@ class FFWDUpgradeRun(command.Command):
 
     def take_action(self, parsed_args):
         self.log.debug("take_action(%s)" % parsed_args)
-        oooutils.ffwd_upgrade_operator_confirm(parsed_args.yes, self.log)
-        verbosity = self.app_args.verbose_level - 1
-        clients = self.app.client_manager
-        orchestration = clients.orchestration
-        stack = parsed_args.stack
-
-        key, ansible_dir = self.get_ansible_key_and_dir(
-            no_workflow=parsed_args.no_workflow,
-            stack=stack,
-            orchestration=orchestration
+        _, ansible_dir = self.get_ansible_key_and_dir(
+            no_workflow=True,
+            stack=parsed_args.stack,
+            orchestration=self.app.client_manager.orchestration
         )
-
-        # Run ansible:
-        inventory = oooutils.get_tripleo_ansible_inventory(
-            inventory_file=parsed_args.static_inventory,
-            ssh_user=parsed_args.ssh_user, stack=parsed_args.stack,
-            return_inventory_file_path=True)
-        # Don't expost limit_hosts. We need this on the whole overcloud.
-        limit_hosts = ''
-        extra_vars = {'ansible_become': True}
-        oooutils.run_update_ansible_action(
-            self.log, clients, parsed_args.stack, limit_hosts, inventory,
-            constants.FFWD_UPGRADE_PLAYBOOK, [], parsed_args.ssh_user,
-            (None if parsed_args.no_workflow else package_update),
-            verbosity=verbosity, workdir=ansible_dir, priv_key=key,
-            extra_vars=extra_vars)
+        deployment.config_download(
+            log=self.log,
+            clients=self.app.client_manager,
+            stack=oooutils.get_stack(
+                self.app.client_manager.orchestration,
+                parsed_args.stack
+            ),
+            output_dir=ansible_dir,
+            verbosity=self.app_args.verbose_level - 1,
+            ansible_playbook_name=constants.FFWD_UPGRADE_PLAYBOOK,
+            inventory_path=oooutils.get_tripleo_ansible_inventory(
+                parsed_args.static_inventory,
+                parsed_args.ssh_user,
+                parsed_args.stack,
+                return_inventory_file_path=True
+            )
+        )
+        self.log.info("Completed Overcloud FFWD Upgrade Run.")
 
 
 class FFWDUpgradeConverge(DeployOvercloud):
diff --git a/tripleoclient/v1/overcloud_update.py b/tripleoclient/v1/overcloud_update.py
index 7a2ed0d40..c8551392d 100644
--- a/tripleoclient/v1/overcloud_update.py
+++ b/tripleoclient/v1/overcloud_update.py
@@ -22,8 +22,10 @@ from tripleoclient import command
 from tripleoclient import constants
 from tripleoclient import utils as oooutils
 from tripleoclient.v1.overcloud_deploy import DeployOvercloud
+from tripleoclient.workflows import deployment
 from tripleoclient.workflows import package_update
 
+
 CONF = cfg.CONF
 logging.register_options(CONF)
 logging.setup(CONF, '')
@@ -92,20 +94,16 @@ class UpdateRun(command.Command):
                 " compute-1, compute-5\".")
         )
         parser.add_argument('--playbook',
-                            action="store",
-                            default="all",
-                            help=_("Ansible playbook to use for the minor "
-                                   "update. Defaults to the special value "
-                                   "\'all\' which causes all the update "
-                                   "playbooks to be executed. That is the "
-                                   "update_steps_playbook.yaml and then the"
-                                   "deploy_steps_playbook.yaml. "
-                                   "Set this to each of those playbooks in "
-                                   "consecutive invocations of this command "
-                                   "if you prefer to run them manually. Note: "
-                                   "make sure to run both those playbooks so "
-                                   "that all services are updated and running "
-                                   "with the target version configuration.")
+                            nargs="*",
+                            default=None,
+                            help=_("Ansible playbook to use for the minor"
+                                   " update. Can be used multiple times."
+                                   " Set this to each of those playbooks in"
+                                   " consecutive invocations of this command"
+                                   " if you prefer to run them manually."
+                                   " Note: make sure to run all playbooks so"
+                                   " that all services are updated and running"
+                                   " with the target version configuration.")
                             )
         parser.add_argument("--ssh-user",
                             dest="ssh_user",
@@ -139,35 +137,40 @@ class UpdateRun(command.Command):
 
     def take_action(self, parsed_args):
         self.log.debug("take_action(%s)" % parsed_args)
-        clients = self.app.client_manager
-        orchestration = clients.orchestration
-        verbosity = self.app_args.verbose_level - 1
-        stack = parsed_args.stack
+        # NOTE(cloudnull): The string option "all" was a special default
+        #                  that is no longer relevant. To retain compatibility
+        #                  this condition has been put in place.
+        if not parsed_args.playbook or parsed_args.playbook == ['all']:
+            playbook = constants.MINOR_UPDATE_PLAYBOOKS
+        else:
+            playbook = parsed_args.playbook
 
-        key, ansible_dir = self.get_ansible_key_and_dir(
-            no_workflow=parsed_args.no_workflow,
-            stack=stack,
-            orchestration=orchestration
+        _, ansible_dir = self.get_ansible_key_and_dir(
+            no_workflow=True,
+            stack=parsed_args.stack,
+            orchestration=self.app.client_manager.orchestration
         )
-
-        # Run ansible:
-        limit_hosts = parsed_args.limit
-
-        playbook = parsed_args.playbook
-        inventory = oooutils.get_tripleo_ansible_inventory(
-            parsed_args.static_inventory, parsed_args.ssh_user, stack,
-            return_inventory_file_path=True)
-        extra_vars = {'ansible_become': True}
-        oooutils.run_update_ansible_action(self.log, clients, stack,
-                                           limit_hosts, inventory, playbook,
-                                           constants.MINOR_UPDATE_PLAYBOOKS,
-                                           parsed_args.ssh_user,
-                                           (None if parsed_args.no_workflow
-                                            else package_update),
-                                           verbosity=verbosity,
-                                           workdir=ansible_dir,
-                                           priv_key=key,
-                                           extra_vars=extra_vars)
+        deployment.config_download(
+            log=self.log,
+            clients=self.app.client_manager,
+            stack=oooutils.get_stack(
+                self.app.client_manager.orchestration,
+                parsed_args.stack
+            ),
+            output_dir=ansible_dir,
+            verbosity=self.app_args.verbose_level - 1,
+            ansible_playbook_name=playbook,
+            inventory_path=oooutils.get_tripleo_ansible_inventory(
+                parsed_args.static_inventory,
+                parsed_args.ssh_user,
+                parsed_args.stack,
+                return_inventory_file_path=True
+            ),
+            limit_list=[
+                i.strip() for i in parsed_args.limit.split(',') if i
+            ]
+        )
+        self.log.info("Completed Overcloud Minor Update Run.")
 
 
 class UpdateConverge(DeployOvercloud):
diff --git a/tripleoclient/v1/overcloud_upgrade.py b/tripleoclient/v1/overcloud_upgrade.py
index 07d9feaf7..c396a9986 100644
--- a/tripleoclient/v1/overcloud_upgrade.py
+++ b/tripleoclient/v1/overcloud_upgrade.py
@@ -117,22 +117,16 @@ class UpgradeRun(command.Command):
                 " compute-1, compute-5\".")
         )
         parser.add_argument('--playbook',
-                            action="store",
-                            default="all",
-                            help=_("Ansible playbook to use for the major "
-                                   "upgrade. Defaults to the special value "
-                                   "\'all\' which causes all the upgrade "
-                                   "playbooks to run. That is the "
-                                   "upgrade_steps_playbook.yaml "
-                                   "then deploy_steps_playbook.yaml and then "
-                                   "post_upgrade_steps_playbook.yaml. Set "
-                                   "this to each of those playbooks in "
-                                   "consecutive invocations of this command "
-                                   "if you prefer to run them manually. Note: "
-                                   "you will have to run all of those "
-                                   "playbooks so that all services are "
-                                   "upgraded and running with the target "
-                                   "version configuration.")
+                            nargs="*",
+                            default=None,
+                            help=_("Ansible playbook to use for the minor"
+                                   " update. Can be used multiple times."
+                                   " Set this to each of those playbooks in"
+                                   " consecutive invocations of this command"
+                                   " if you prefer to run them manually."
+                                   " Note: make sure to run all playbooks so"
+                                   " that all services are updated and running"
+                                   " with the target version configuration.")
                             )
         parser.add_argument('--static-inventory',
                             dest='static_inventory',
@@ -197,43 +191,42 @@ class UpgradeRun(command.Command):
 
     def take_action(self, parsed_args):
         self.log.debug("take_action(%s)" % parsed_args)
-        clients = self.app.client_manager
-        verbosity = self.app_args.verbose_level - 1
-        orchestration = clients.orchestration
-        stack = parsed_args.stack
+        # NOTE(cloudnull): The string option "all" was a special default
+        #                  that is no longer relevant. To retain compatibility
+        #                  this condition has been put in place.
+        if not parsed_args.playbook or parsed_args.playbook == ['all']:
+            playbook = constants.MAJOR_UPGRADE_PLAYBOOKS
+        else:
+            playbook = parsed_args.playbook
 
-        key, ansible_dir = self.get_ansible_key_and_dir(
+        _, ansible_dir = self.get_ansible_key_and_dir(
             no_workflow=parsed_args.no_workflow,
-            stack=stack,
-            orchestration=orchestration
+            stack=parsed_args.stack,
+            orchestration=self.app.client_manager.orchestration
         )
-
-        # Run ansible:
-        limit_hosts = parsed_args.limit
-
-        playbook = parsed_args.playbook
-        inventory = oooutils.get_tripleo_ansible_inventory(
-            parsed_args.static_inventory, parsed_args.ssh_user, stack,
-            return_inventory_file_path=True)
-        skip_tags = self._validate_skip_tags(parsed_args.skip_tags)
-        extra_vars = {'ansible_become': True}
-        oooutils.run_update_ansible_action(self.log, clients, stack,
-                                           limit_hosts, inventory, playbook,
-                                           constants.MAJOR_UPGRADE_PLAYBOOKS,
-                                           parsed_args.ssh_user,
-                                           (None if parsed_args.no_workflow
-                                            else package_update),
-                                           parsed_args.tags,
-                                           skip_tags,
-                                           verbosity,
-                                           workdir=ansible_dir,
-                                           priv_key=key,
-                                           extra_vars=extra_vars)
-
-        playbooks = (constants.MAJOR_UPGRADE_PLAYBOOKS
-                     if playbook == 'all' else playbook)
-        self.log.info(("Completed Overcloud Upgrade Run for {0} with "
-                       "playbooks {1} ").format(limit_hosts, playbooks))
+        deployment.config_download(
+            log=self.log,
+            clients=self.app.client_manager,
+            stack=oooutils.get_stack(
+                self.app.client_manager.orchestration,
+                parsed_args.stack
+            ),
+            output_dir=ansible_dir,
+            verbosity=self.app_args.verbose_level - 1,
+            ansible_playbook_name=playbook,
+            inventory_path=oooutils.get_tripleo_ansible_inventory(
+                parsed_args.static_inventory,
+                parsed_args.ssh_user,
+                parsed_args.stack,
+                return_inventory_file_path=True
+            ),
+            tags=parsed_args.tags,
+            skip_tags=parsed_args.skip_tags,
+            limit_list=[
+                i.strip() for i in parsed_args.limit.split(',') if i
+            ]
+        )
+        self.log.info("Completed Overcloud Major Upgrade Run.")
 
 
 class UpgradeConvergeOvercloud(DeployOvercloud):
diff --git a/tripleoclient/workflows/deployment.py b/tripleoclient/workflows/deployment.py
index b32ba7b73..93d6ae262 100644
--- a/tripleoclient/workflows/deployment.py
+++ b/tripleoclient/workflows/deployment.py
@@ -17,6 +17,8 @@ import os
 import pprint
 import time
 
+import six
+
 from heatclient.common import event_utils
 from openstackclient import shell
 from tripleo_common.actions import ansible
@@ -258,7 +260,8 @@ def config_download(log, clients, stack, ssh_network=None,
                     timeout=None, verbosity=0, deployment_options=None,
                     in_flight_validations=False,
                     ansible_playbook_name='deploy_steps_playbook.yaml',
-                    limit_list=None):
+                    limit_list=None, extra_vars=None, inventory_path=None,
+                    ssh_user='tripleo-admin', tags=None, skip_tags=None):
     """Run config download.
 
     :param log: Logging object
@@ -273,6 +276,9 @@ def config_download(log, clients, stack, ssh_network=None,
     :param ssh_network: Network named used to access the overcloud.
     :type ssh_network: String
 
+    :param output_dir: Path to the output directory.
+    :type output_dir: String
+
     :param override_ansible_cfg: Ansible configuration file location.
     :type override_ansible_cfg: String
 
@@ -294,6 +300,23 @@ def config_download(log, clients, stack, ssh_network=None,
 
     :param limit_list: List of hosts to limit the current playbook to.
     :type limit_list: List
+
+    :param extra_vars: Set additional variables as a Dict or the absolute
+                       path of a JSON or YAML file type.
+    :type extra_vars: Either a Dict or the absolute path of JSON or YAML
+
+    :param inventory_path: Inventory file or path, if None is provided this
+                           function will perform a lookup
+    :type inventory_path: String
+
+    :param ssh_user: SSH user, defaults to tripleo-admin.
+    :type ssh_user: String
+
+    :param tags: Ansible inclusion tags.
+    :type tags: String
+
+    :param skip_tags: Ansible exclusion tags.
+    :type skip_tags: String
     """
 
     def _log_and_print(message, logger, level='info', print_msg=True):
@@ -325,9 +348,10 @@ def config_download(log, clients, stack, ssh_network=None,
         deployment_options = dict()
 
     if not in_flight_validations:
-        skip_tags = 'opendev-validation'
-    else:
-        skip_tags = None
+        if skip_tags:
+            skip_tags = 'opendev-validation,{}'.format(skip_tags)
+        else:
+            skip_tags = 'opendev-validation'
 
     if not timeout:
         timeout = 30
@@ -338,6 +362,8 @@ def config_download(log, clients, stack, ssh_network=None,
     #                  entries are consistent.
     if not limit_list:
         limit_list = list()
+    elif isinstance(limit_list, six.string_types):
+        limit_list = [i.strip() for i in limit_list.split(',')]
 
     with utils.TempDirs() as tmp:
         utils.run_ansible_playbook(
@@ -403,7 +429,7 @@ def config_download(log, clients, stack, ssh_network=None,
         print_msg=(verbosity == 0)
     )
     inventory_kwargs = {
-        'ansible_ssh_user': 'tripleo-admin',
+        'ansible_ssh_user': ssh_user,
         'work_dir': work_dir,
         'plan_name': stack.stack_name,
         'undercloud_key_file': key_file
@@ -413,8 +439,9 @@ def config_download(log, clients, stack, ssh_network=None,
     python_interpreter = deployment_options.get('ansible_python_interpreter')
     if python_interpreter:
         inventory_kwargs['ansible_python_interpreter'] = python_interpreter
-    inventory = ansible.AnsibleGenerateInventoryAction(**inventory_kwargs)
-    inventory_path = inventory.run(context=context)
+    if not inventory_path:
+        inventory = ansible.AnsibleGenerateInventoryAction(**inventory_kwargs)
+        inventory_path = inventory.run(context=context)
     _log_and_print(
         message='Executing deployment playbook for stack: {}'.format(
             stack.stack_name
@@ -426,27 +453,36 @@ def config_download(log, clients, stack, ssh_network=None,
     # NOTE(cloudnull): Join the limit_list into an ansible compatible string.
     #                  If it is an empty, the object will be reset to None.
     limit_hosts = ':'.join(limit_list)
+    if not limit_hosts:
+        limit_hosts = None
+    else:
+        limit_hosts = '{}'.format(limit_hosts)
+
+    if isinstance(ansible_playbook_name, list):
+        playbooks = [os.path.join(stack_work_dir, p)
+                     for p in ansible_playbook_name]
+    else:
+        playbooks = os.path.join(stack_work_dir, ansible_playbook_name)
 
     with utils.TempDirs() as tmp:
         utils.run_ansible_playbook(
-            playbook=os.path.join(
-                stack_work_dir,
-                ansible_playbook_name
-            ),
+            playbook=playbooks,
             inventory=inventory_path,
             workdir=tmp,
             playbook_dir=work_dir,
             skip_tags=skip_tags,
             ansible_cfg=override_ansible_cfg,
             verbosity=verbosity,
-            ssh_user='tripleo-admin',
+            ssh_user=ssh_user,
             key=key_file,
             limit_hosts=limit_hosts,
             ansible_timeout=timeout,
             reproduce_command=True,
             extra_env_variables={
                 'ANSIBLE_BECOME': True,
-            }
+            },
+            extra_vars=extra_vars,
+            tags=tags
         )
 
     _log_and_print(
diff --git a/tripleoclient/workflows/package_update.py b/tripleoclient/workflows/package_update.py
index b11583afb..ddc4ed7d2 100644
--- a/tripleoclient/workflows/package_update.py
+++ b/tripleoclient/workflows/package_update.py
@@ -64,26 +64,6 @@ def update(clients, **workflow_input):
         raise exceptions.DeploymentError("Heat Stack update failed.")
 
 
-def update_ansible(clients, **workflow_input):
-    workflow_client = clients.workflow_engine
-    tripleoclients = clients.tripleoclient
-
-    with tripleoclients.messaging_websocket() as ws:
-        execution = base.start_workflow(
-            workflow_client,
-            'tripleo.package_update.v1.update_nodes',
-            workflow_input=workflow_input
-        )
-
-        for payload in base.wait_for_messages(workflow_client, ws, execution):
-            print(payload['message'])
-
-    if payload['status'] == 'SUCCESS':
-        print("Success")
-    else:
-        raise RuntimeError('Update failed with: {}'.format(payload['message']))
-
-
 def run_on_nodes(clients, **workflow_input):
     workflow_client = clients.workflow_engine
     tripleoclients = clients.tripleoclient