# Buggy code
def sync(opts, form, saltenv=None):
    """
    Sync custom modules into the extension_modules directory
    """
    if saltenv is None:
        saltenv = ["base"]
    if isinstance(saltenv, six.string_types):
        saltenv = saltenv.split(",")
    ret = []
    remote = set()
    source = salt.utils.url.create("_" + form)
    mod_dir = os.path.join(opts["extension_modules"], "{0}".format(form))
    cumask = os.umask(0o77)
    try:
        if not os.path.isdir(mod_dir):
            log.info("Creating module dir '{0}'".format(mod_dir))
            try:
                os.makedirs(mod_dir)
            except (IOError, OSError):
                log.error(
                    "Cannot create cache module directory {0}. Check "
                    "permissions.".format(mod_dir)
                )
        fileclient = salt.fileclient.get_file_client(opts)
        for sub_env in saltenv:
            log.info("Syncing {0} for environment '{1}'".format(form, sub_env))
            cache = []
            log.info("Loading cache from {0}, for {1})".format(source, sub_env))
            # Grab only the desired files (.py, .pyx, .so)
            cache.extend(
                fileclient.cache_dir(
                    source,
                    sub_env,
                    include_empty=False,
                    include_pat=r"E@\.(pyx?|so|zip)$",
                    exclude_pat=None,
                )
            )
            local_cache_dir = os.path.join(
                opts["cachedir"], "files", sub_env, "_{0}".format(form)
            )
            log.debug("Local cache dir: '{0}'".format(local_cache_dir))
            for fn_ in cache:
                relpath = os.path.relpath(fn_, local_cache_dir)
                relname = os.path.splitext(relpath)[0].replace(os.sep, ".")
                remote.add(relpath)
                dest = os.path.join(mod_dir, relpath)
                log.info("Copying '{0}' to '{1}'".format(fn_, dest))
                if os.path.isfile(dest):
                    # The file is present, if the sum differs replace it
                    hash_type = opts.get("hash_type", "md5")
                    src_digest = salt.utils.get_hash(fn_, hash_type)
                    dst_digest = salt.utils.get_hash(dest, hash_type)
                    if src_digest != dst_digest:
                        # The downloaded file differs, replace!
                        shutil.copyfile(fn_, dest)
                        ret.append("{0}.{1}".format(form, relname))
                else:
                    dest_dir = os.path.dirname(dest)
                    if not os.path.isdir(dest_dir):
                        os.makedirs(dest_dir)
                    shutil.copyfile(fn_, dest)
                    ret.append("{0}.{1}".format(form, relname))

        touched = bool(ret)
        if opts.get("clean_dynamic_modules", True):
            current = set(_listdir_recursively(mod_dir))
            for fn_ in current - remote:
                full = os.path.join(mod_dir, fn_)
                if os.path.isfile(full):
                    touched = True
                    os.remove(full)
            # Cleanup empty dirs
            while True:
                emptydirs = _list_emptydirs(mod_dir)
                if not emptydirs:
                    break
                for emptydir in emptydirs:
                    touched = True
                    shutil.rmtree(emptydir, ignore_errors=True)
    except Exception as exc:
        log.error("Failed to sync {0} module: {1}".format(form, exc))
    finally:
        os.umask(cumask)
    return ret, touched


# Traceback (UnboundLocalError)
server03:
The minion function caused an exception: Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/salt/minion.py", line 1318, in _thread_return
return_data = executor.execute()
File "/usr/lib/python2.7/site-packages/salt/executors/direct_call.py", line 28, in execute
return self.func(*self.args, **self.kwargs)
File "/usr/lib/python2.7/site-packages/salt/modules/state.py", line 722, in highstate
whitelist=kwargs.get('whitelist')
File "/usr/lib/python2.7/site-packages/salt/state.py", line 3216, in call_highstate
self.load_dynamic(matches)
File "/usr/lib/python2.7/site-packages/salt/state.py", line 2745, in load_dynamic
refresh=False)
File "/usr/lib/python2.7/site-packages/salt/modules/saltutil.py", line 562, in sync_all
ret['states'] = sync_states(saltenv, False)
File "/usr/lib/python2.7/site-packages/salt/modules/saltutil.py", line 291, in sync_states
ret = _sync('states', saltenv)
File "/usr/lib/python2.7/site-packages/salt/modules/saltutil.py", line 93, in _sync
ret, touched = salt.utils.extmods.sync(__opts__, form, saltenv=saltenv)
File "/usr/lib/python2.7/site-packages/salt/utils/extmods.py", line 127, in sync
return ret, touched
UnboundLocalError: local variable 'touched' referenced before assignment

# Fixed code
def sync(opts, form, saltenv=None):
    """
    Sync custom modules into the extension_modules directory
    """
    if saltenv is None:
        saltenv = ["base"]
    if isinstance(saltenv, six.string_types):
        saltenv = saltenv.split(",")
    ret = []
    remote = set()
    source = salt.utils.url.create("_" + form)
    mod_dir = os.path.join(opts["extension_modules"], "{0}".format(form))
    cumask = os.umask(0o77)
    touched = False
    try:
        if not os.path.isdir(mod_dir):
            log.info("Creating module dir '{0}'".format(mod_dir))
            try:
                os.makedirs(mod_dir)
            except (IOError, OSError):
                log.error(
                    "Cannot create cache module directory {0}. Check "
                    "permissions.".format(mod_dir)
                )
        fileclient = salt.fileclient.get_file_client(opts)
        for sub_env in saltenv:
            log.info("Syncing {0} for environment '{1}'".format(form, sub_env))
            cache = []
            log.info("Loading cache from {0}, for {1})".format(source, sub_env))
            # Grab only the desired files (.py, .pyx, .so)
            cache.extend(
                fileclient.cache_dir(
                    source,
                    sub_env,
                    include_empty=False,
                    include_pat=r"E@\.(pyx?|so|zip)$",
                    exclude_pat=None,
                )
            )
            local_cache_dir = os.path.join(
                opts["cachedir"], "files", sub_env, "_{0}".format(form)
            )
            log.debug("Local cache dir: '{0}'".format(local_cache_dir))
            for fn_ in cache:
                relpath = os.path.relpath(fn_, local_cache_dir)
                relname = os.path.splitext(relpath)[0].replace(os.sep, ".")
                remote.add(relpath)
                dest = os.path.join(mod_dir, relpath)
                log.info("Copying '{0}' to '{1}'".format(fn_, dest))
                if os.path.isfile(dest):
                    # The file is present, if the sum differs replace it
                    hash_type = opts.get("hash_type", "md5")
                    src_digest = salt.utils.get_hash(fn_, hash_type)
                    dst_digest = salt.utils.get_hash(dest, hash_type)
                    if src_digest != dst_digest:
                        # The downloaded file differs, replace!
                        shutil.copyfile(fn_, dest)
                        ret.append("{0}.{1}".format(form, relname))
                else:
                    dest_dir = os.path.dirname(dest)
                    if not os.path.isdir(dest_dir):
                        os.makedirs(dest_dir)
                    shutil.copyfile(fn_, dest)
                    ret.append("{0}.{1}".format(form, relname))

        touched = bool(ret)
        if opts.get("clean_dynamic_modules", True):
            current = set(_listdir_recursively(mod_dir))
            for fn_ in current - remote:
                full = os.path.join(mod_dir, fn_)
                if os.path.isfile(full):
                    touched = True
                    os.remove(full)
            # Cleanup empty dirs
            while True:
                emptydirs = _list_emptydirs(mod_dir)
                if not emptydirs:
                    break
                for emptydir in emptydirs:
                    touched = True
                    shutil.rmtree(emptydir, ignore_errors=True)
    except Exception as exc:
        log.error("Failed to sync {0} module: {1}".format(form, exc))
    finally:
        os.umask(cumask)
    return ret, touched