diff --git a/README.md b/README.md
index 785176f..df56c32 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,10 @@ with all the sensible defaults in place.
 $ conjure-up canonical-kubernetes localhost
 ```
 
+Note that some spells require sudo for certain steps.  When running in headless
+mode, conjure-up should be run with a user with passwordless sudo enabled (or
+sudo should be pre-authorized before invoking conjure-up).
+
 ## Destroying deployments
 
 ```
diff --git a/conjureup/controllers/clouds/tui.py b/conjureup/controllers/clouds/tui.py
index 3b2cd5d..d7c9c9f 100644
--- a/conjureup/controllers/clouds/tui.py
+++ b/conjureup/controllers/clouds/tui.py
@@ -33,7 +33,8 @@ class CloudsController:
                        "please wait.".format(app.current_model))
             juju.add_model(app.current_model,
                            app.current_controller,
-                           app.current_cloud)
+                           app.current_cloud,
+                           allow_exists=True)
             return controllers.use('deploy').render()
 
         utils.error("Something happened with the controller or model, "
diff --git a/conjureup/controllers/steps/common.py b/conjureup/controllers/steps/common.py
index d990ade..d082321 100644
--- a/conjureup/controllers/steps/common.py
+++ b/conjureup/controllers/steps/common.py
@@ -1,10 +1,20 @@
 import json
 import os
+import os.path as path
 from glob import glob
 
+import yaml
+
 from conjureup import juju, utils
 from conjureup.api.models import model_info
 from conjureup.app_config import app
+from conjureup.models.step import StepModel
+
+
+class ValidationError(Exception):
+    def __init__(self, msg, *args, **kwargs):
+        self.msg = msg
+        super().__init__(msg, *args, **kwargs)
 
 
 def set_env(inputs):
@@ -38,6 +48,22 @@ def get_step_metadata_filenames(steps_dir):
     return sorted(glob(os.path.join(steps_dir, 'step-*.yaml')))
 
 
+def load_step(step_meta_path):
+    step_ex_path, ext = path.splitext(step_meta_path)
+    short_path = '/'.join(step_ex_path.split('/')[-3:])
+    if not path.isfile(step_ex_path):
+        raise ValidationError(
+            'Step {} has no implementation'.format(short_path))
+    elif not os.access(step_ex_path, os.X_OK):
+        raise ValidationError(
+            'Step {} is not executable, make sure it has '
+            'the executable bit set'.format(short_path))
+    with open(step_meta_path) as fp:
+        step_metadata = yaml.load(fp.read())
+        model = StepModel(step_metadata, step_ex_path)
+        return model
+
+
 def do_step(step_model, step_widget, message_cb, gui=False):
     """ Processes steps in the background
 
@@ -50,6 +76,13 @@ def do_step(step_model, step_widget, message_cb, gui=False):
     Step title and results message
     """
 
+    if step_model.needs_sudo:
+        password = None
+        if step_widget and step_widget.sudo_input:
+            password = step_widget.sudo_input.value
+        if not step_model.can_sudo(password):
+            raise Exception('Sudo failed')
+
     # merge the step_widget input data into our step model
     if gui:
         step_widget.clear_button()
diff --git a/conjureup/controllers/steps/gui.py b/conjureup/controllers/steps/gui.py
index cc23212..d583f0b 100644
--- a/conjureup/controllers/steps/gui.py
+++ b/conjureup/controllers/steps/gui.py
@@ -1,14 +1,10 @@
-import os
 import os.path as path
 from collections import OrderedDict, deque
 from functools import partial
 
-import yaml
-
 from conjureup import async, controllers
 from conjureup.app_config import app
 from conjureup.controllers.steps import common
-from conjureup.models.step import StepModel
 from conjureup.telemetry import track_exception, track_screen
 from conjureup.ui.views.steps import StepsView
 from conjureup.ui.widgets.step import StepWidget
@@ -35,6 +31,7 @@ class StepsController:
     def get_result(self, future):
         if future.exception():
             self.__handle_exception('E002', future.exception())
+            return
 
         step_model, step_widget = future.result()
 
@@ -104,38 +101,20 @@ class StepsController:
         step_widgets = deque()
         self.n_completed_steps = 0
         for step_meta_path in self.step_metas:
-            step_ex_path, ext = path.splitext(step_meta_path)
-            short_path = '/'.join(step_ex_path.split('/')[-3:])
-            err_msg = None
-            if not path.isfile(step_ex_path):
-                err_msg = (
-                    'Step {} has no implementation'.format(short_path))
-            elif not os.access(step_ex_path, os.X_OK):
-                err_msg = (
-                    'Step {} is not executable, make sure it has '
-                    'the executable bit set'.format(short_path))
-            if err_msg:
-                app.log.error(err_msg)
-                self.__handle_exception('E002', Exception(err_msg))
-                return
-            step_metadata = {}
-            with open(step_meta_path) as fp:
-                step_metadata = yaml.load(fp.read())
-
             try:
                 # Store step model and its widget
-                model = StepModel(step_metadata, step_meta_path)
+                model = common.load_step(step_meta_path)
+                if not model.viewable:
+                    app.log.debug("Skipping step: {}".format(model.title))
+                    continue
                 step_widget = StepWidget(
                     app,
                     model,
                     self.finish)
-                if not step_widget.model.viewable:
-                    app.log.debug("Skipping step: {}".format(step_widget))
-                    continue
-                model.path = step_ex_path
                 step_widgets.append(step_widget)
                 app.log.debug("Queueing step: {}".format(step_widget))
             except Exception as e:
+                app.log.exception(e)
                 self.__handle_exception('E002', e)
                 return
 
diff --git a/conjureup/controllers/steps/tui.py b/conjureup/controllers/steps/tui.py
index 18ce191..76fa355 100644
--- a/conjureup/controllers/steps/tui.py
+++ b/conjureup/controllers/steps/tui.py
@@ -1,14 +1,10 @@
-import os
 import os.path as path
 import sys
 from collections import OrderedDict
 
-import yaml
-
 from conjureup import controllers, utils
 from conjureup.app_config import app
 from conjureup.controllers.steps import common
-from conjureup.models.step import StepModel
 
 
 class StepsController:
@@ -26,25 +22,16 @@ class StepsController:
 
     def render(self):
         for step_meta_path in self.step_metas:
-            step_ex_path, ext = path.splitext(step_meta_path)
-            short_path = '/'.join(step_ex_path.split('/')[-3:])
-            err_msg = None
-            if not path.isfile(step_ex_path):
-                err_msg = (
-                    'Step {} has no implementation'.format(short_path))
-            elif not os.access(step_ex_path, os.X_OK):
-                err_msg = (
-                    'Step {} is not executable, make sure it has '
-                    'the executable bit set'.format(short_path))
-            if err_msg:
-                app.log.error(err_msg)
-                utils.error(err_msg)
+            try:
+                model = common.load_step(step_meta_path)
+            except common.ValidationError as e:
+                app.log.error(e.msg)
+                utils.error(e.msg)
+                sys.exit(1)
+            if model.needs_sudo and not model.can_sudo():
+                utils.error("Step requires passwordless sudo: {}".format(
+                    model.title))
                 sys.exit(1)
-            step_metadata = {}
-            with open(step_meta_path) as fp:
-                step_metadata = yaml.load(fp.read())
-            model = StepModel(step_metadata, step_meta_path)
-            model.path = step_ex_path
             app.log.debug("Running step: {}".format(model))
             try:
                 step_model, _ = common.do_step(model,
diff --git a/conjureup/juju.py b/conjureup/juju.py
index 6f16517..a1f129d 100644
--- a/conjureup/juju.py
+++ b/conjureup/juju.py
@@ -756,12 +756,16 @@ def get_model(controller, name):
         "Unable to find model: {}".format(name))
 
 
-def add_model(name, controller, cloud):
+def add_model(name, controller, cloud, allow_exists=False):
     """ Adds a model to current controller
 
     Arguments:
     controller: controller to add model in
+    allow_exists: re-use an existing model, if one exists.
     """
+    if allow_exists and model_available():
+        return
+
     sh = run('juju add-model {} -c {} {}'.format(name, controller, cloud),
              shell=True, stdout=DEVNULL, stderr=PIPE)
     if sh.returncode > 0:
diff --git a/conjureup/models/step.py b/conjureup/models/step.py
index a221100..1e20a27 100644
--- a/conjureup/models/step.py
+++ b/conjureup/models/step.py
@@ -1,5 +1,7 @@
 """ Step model
 """
+import subprocess
+
 from conjureup.app_config import app
 
 
@@ -10,8 +12,9 @@ class StepModel:
         self.description = step.get('description', '')
         self.result = ''
         self.viewable = step.get('viewable', False)
-
+        self.needs_sudo = step.get('sudo', False)
         self.additional_input = step.get('additional-input', [])
+        self.path = path
 
     def __getattr__(self, attr):
         """
@@ -34,3 +37,16 @@ class StepModel:
                                                self.description,
                                                self.viewable,
                                                self.path)
+
+    def can_sudo(self, password=None):
+        if not password:
+            result = subprocess.run(['sudo', '-nv'],
+                                    stdout=subprocess.DEVNULL,
+                                    stderr=subprocess.DEVNULL)
+        else:
+            password = '{}\n'.format(password).encode('utf8')
+            result = subprocess.run(['sudo', '-Sv'],
+                                    input=password,
+                                    stdout=subprocess.DEVNULL,
+                                    stderr=subprocess.DEVNULL)
+        return result.returncode == 0
diff --git a/conjureup/ui/widgets/step.py b/conjureup/ui/widgets/step.py
index e9c2568..c173152 100644
--- a/conjureup/ui/widgets/step.py
+++ b/conjureup/ui/widgets/step.py
@@ -37,6 +37,7 @@ class StepWidget(WidgetWrap):
         self.result = Text(step_model.result)
         self.output = Text(('info_minor', ''))
         self.icon = Text(("info_minor", "\N{BALLOT BOX}"))
+        self.sudo_input = None
 
         self.additional_input = []
         if len(step_model.additional_input) > 0:
@@ -156,6 +157,23 @@ class StepWidget(WidgetWrap):
             'pending_icon',
             self.icon.get_text()[0]
         ))
+        if self.model.needs_sudo:
+            self.step_pile.contents.append((Padding.line_break(""),
+                                            self.step_pile.options()))
+            can_sudo = self.model.can_sudo()
+            label = 'This step requires sudo.'
+            if not can_sudo:
+                label += '  Please enter sudo password:'
+            columns = [
+                ('weight', 0.5, Padding.left(Text(('body', label)), left=5)),
+            ]
+            if not can_sudo:
+                self.sudo_input = PasswordEditor()
+                columns.append(('weight', 1, Color.string_input(
+                    self.sudo_input, focus_map='string_input focus')))
+            self.step_pile.contents.append((Columns(columns, dividechars=3),
+                                            self.step_pile.options()))
+
         for i in self.additional_input:
             self.app.log.debug(i)
             self.step_pile.contents.append((Padding.line_break(""),

Add sudo support for spells
Some spell steps will require sudo.  They need a way to indicate that need, the GUI needs to detect if prompting for the password is necessary, and the TUI needs to document that passwordless sudo is required.
diff --git a/test/test_controllers_clouds_tui.py b/test/test_controllers_clouds_tui.py
index e6e7386..d5193b2 100644
--- a/test/test_controllers_clouds_tui.py
+++ b/test/test_controllers_clouds_tui.py
@@ -96,7 +96,8 @@ class CloudsTUIFinishTestCase(unittest.TestCase):
         self.mock_app.current_cloud = 'cloud'
         self.controller.finish()
         self.mock_juju.assert_has_calls([
-            call.add_model(ANY, 'testcontroller', 'cloud')])
+            call.add_model(ANY, 'testcontroller', 'cloud',
+                           allow_exists=True)])
 
     def test_finish_no_controller(self):
         "clouds.finish without existing controller"

pip install -e .
parse_log_pytest
requirements.txt
nose tox pytest
apt-get update apt-get install -y gcc
3.5
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
-e git+https://github.com/conjure-up/conjure-up.git@9e24a368a5a50bd8493484373829d5b8f3d2fb8e#egg=conjure_up
distlib==0.3.9
filelock==3.4.1
idna==3.10
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
Jinja2==3.0.3
juju-wait==2.8.4
MarkupSafe==2.0.1
nose==1.3.7
oauthlib==3.2.2
packaging==21.3
platformdirs==2.4.0
pluggy==1.0.0
prettytable==2.5.0
progressbar2==3.55.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
python-utils==3.5.2
PyYAML==6.0.1
q==2.7
requests==2.27.1
requests-oauthlib==2.0.0
six==1.17.0
termcolor==1.1.0
toml==0.10.2
tomli==1.2.3
tox==3.28.0
typing_extensions==4.1.1
urllib3==1.26.20
urwid==2.1.2
virtualenv==20.17.1
wcwidth==0.2.13
ws4py==0.3.4
zipp==3.6.0

name: conjure-up
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - distlib==0.3.9
      - filelock==3.4.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - jinja2==3.0.3
      - juju-wait==2.8.4
      - markupsafe==2.0.1
      - nose==1.3.7
      - oauthlib==3.2.2
      - packaging==21.3
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - prettytable==2.5.0
      - progressbar2==3.55.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - python-utils==3.5.2
      - pyyaml==6.0.1
      - q==2.7
      - requests==2.27.1
      - requests-oauthlib==2.0.0
      - six==1.17.0
      - termcolor==1.1.0
      - toml==0.10.2
      - tomli==1.2.3
      - tox==3.28.0
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - urwid==2.1.2
      - virtualenv==20.17.1
      - wcwidth==0.2.13
      - ws4py==0.3.4
      - zipp==3.6.0
prefix: /opt/conda/envs/conjure-up

test/test_controllers_clouds_tui.py::CloudsTUIFinishTestCase::test_finish_w_controller
test/test_controllers_clouds_tui.py::CloudsTUIRenderTestCase::test_render test/test_controllers_clouds_tui.py::CloudsTUIRenderTestCase::test_render_unknown test/test_controllers_clouds_tui.py::CloudsTUIFinishTestCase::test_finish_no_controller
conjureup/juju.py conjureup/controllers/clouds/tui.py conjureup/controllers/steps/gui.py conjureup/controllers/steps/common.py conjureup/controllers/steps/tui.py conjureup/ui/widgets/step.py README.md conjureup/models/step.py
conjureup/juju.py conjureup/controllers/clouds/tui.py conjureup/controllers/steps/gui.py conjureup/controllers/steps/common.py conjureup/controllers/steps/tui.py conjureup/ui/widgets/step.py README.md conjureup/models/step.py