# Buggy code
def returner(ret):
    """
    Return data to an etcd server or cluster
    """
    write_profile = __opts__.get("etcd.returner_write_profile")
    if write_profile:
        ttl = __opts__.get(write_profile, {}).get("etcd.ttl")
    else:
        ttl = __opts__.get("etcd.ttl")

    client, path = _get_conn(__opts__, write_profile)
    # Make a note of this minion for the external job cache
    client.set(
        "/".join((path, "minions", ret["id"])),
        ret["jid"],
        ttl=ttl,
    )

    for field in ret:
        # Not using os.path.join because we're not dealing with file paths
        dest = "/".join((path, "jobs", ret["jid"], ret["id"], field))
        client.set(dest, salt.utils.json.dumps(ret[field]), ttl=ttl)


# Traceback (AttributeError)
test: [ERROR   ] An un-handled exception was caught by salt's global exception handler:
test: AttributeError: 'unicode' object has no attribute 'name'
test: Traceback (most recent call last):
test:   File "/bin/spm", line 12, in <module>
test:     salt_spm()
test:   File "/usr/lib/python2.7/site-packages/salt/scripts.py", line 527, in salt_spm
test:     spm.run()
test:   File "/usr/lib/python2.7/site-packages/salt/cli/spm.py", line 41, in run
test:     client.run(self.args)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 125, in run
test:     self._install(args)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 379, in _install
test:     self._install_indv_pkg(package, out_file)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 532, in _install_indv_pkg
test:     self.files_conn)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 161, in _pkgfiles_fun
test:     return self.pkgfiles['{0}.{1}'.format(self.files_prov, func)](*args, **kwargs)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/pkgfiles/local.py", line 147, in install_file
test:     member.name = new_name.name.replace('{0}/'.format(package), '')
test: AttributeError: 'unicode' object has no attribute 'name'
test: Traceback (most recent call last):
test:   File "/bin/spm", line 12, in <module>
test:     salt_spm()
test:   File "/usr/lib/python2.7/site-packages/salt/scripts.py", line 527, in salt_spm
test:     spm.run()
test:   File "/usr/lib/python2.7/site-packages/salt/cli/spm.py", line 41, in run
test:     client.run(self.args)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 125, in run
test:     self._install(args)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 379, in _install
test:     self._install_indv_pkg(package, out_file)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 532, in _install_indv_pkg
test:     self.files_conn)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/__init__.py", line 161, in _pkgfiles_fun
test:     return self.pkgfiles['{0}.{1}'.format(self.files_prov, func)](*args, **kwargs)
test:   File "/usr/lib/python2.7/site-packages/salt/spm/pkgfiles/local.py", line 147, in install_file
test:     member.name = new_name.name.replace('{0}/'.format(package), '')
test: AttributeError: 'unicode' object has no attribute 'name'

# Fixed code
def returner(ret):
    """
    Return data to an etcd profile.
    """
    write_profile = __opts__.get("etcd.returner_write_profile")
    client, path, ttl = _get_conn(__opts__, write_profile)

    # If a minion is returning a standalone job, update the jid for the load
    # when it's saved since this job came directly from a minion.
    if ret["jid"] == "req":
        new_jid = prep_jid(nocache=ret.get("nocache", False))
        log.debug(
            "sdstack_etcd returner <returner> satisfying a new job id request ({jid:s}) with id {new:s} for {data}".format(
                jid=ret["jid"], new=new_jid, data=ret
            )
        )
        ret["jid"] = new_jid
        save_load(new_jid, ret)

    # Update the given minion in the external job cache with the current (latest job)
    # This is used by get_fun() to return the last function that was called
    minionp = "/".join([path, Schema["minion-fun"], ret["id"]])

    # We can use the ttl here because our minionp is actually linked to the job
    # which will expire according to the ttl anyways..
    log.debug(
        "sdstack_etcd returner <returner> updating (last) job id of {minion:s} at {path:s} with job {jid:s}".format(
            jid=ret["jid"], minion=ret["id"], path=minionp
        )
    )
    res = client.write(minionp, ret["jid"], ttl=ttl if ttl > 0 else None)
    if not res.newKey:
        log.debug(
            "sdstack_etcd returner <returner> the previous job id {old:s} for {minion:s} at {path:s} was set to {new:s}".format(
                old=res._prev_node.value, minion=ret["id"], path=minionp, new=res.value
            )
        )

    # Figure out the path for the specified job and minion
    jobp = "/".join([path, Schema["job-cache"], ret["jid"], ret["id"]])
    log.debug(
        "sdstack_etcd returner <returner> writing job data for {jid:s} to {path:s} with {data}".format(
            jid=ret["jid"], path=jobp, data=ret
        )
    )

    # Iterate through all the fields in the return dict and dump them under the
    # jobs/$jid/id/$field key. We aggregate all the exceptions so that if an
    # error happens, the rest of the fields will still be written.
    exceptions = []
    for field in ret:
        fieldp = "/".join([jobp, field])
        data = salt.utils.json.dumps(ret[field])
        try:
            res = client.write(fieldp, data)
        except Exception as E:
            log.trace(
                "sdstack_etcd returner <returner> unable to set field {field:s} for job {jid:s} at {path:s} to {result} due to exception ({exception})".format(
                    field=field,
                    jid=ret["jid"],
                    path=fieldp,
                    result=ret[field],
                    exception=E,
                )
            )
            exceptions.append((E, field, ret[field]))
            continue
        log.trace(
            "sdstack_etcd returner <returner> set field {field:s} for job {jid:s} at {path:s} to {result}".format(
                field=field, jid=ret["jid"], path=res.key, result=ret[field]
            )
        )

    # Go back through all the exceptions that occurred while trying to write the
    # fields and log them.
    for E, field, value in exceptions:
        log.exception(
            "sdstack_etcd returner <returner> exception ({exception}) was raised while trying to set the field {field:s} for job {jid:s} to {value}".format(
                exception=E, field=field, jid=ret["jid"], value=value
            )
        )
    return