def determine_next_openstack_release(release):
    """Determine the next release after the one passed as a str.

    The returned value is a tuple of the form: ('2020.1', 'ussuri')

    :param release: the release to use as the base
    :type release: str
    :returns: the release tuple immediately after the current one.
    :rtype: Tuple[str, str]
    :raises: KeyError if the current release doesn't actually exist
    """
    old_index = list(OPENSTACK_CODENAMES.values()).index(release)
    new_index = old_index + 1
    return list(OPENSTACK_CODENAMES.items())[new_index]
def next_release(release):
    old_index = list(OPENSTACK_CODENAMES.values()).index(release)
    new_index = old_index + 1
    return list(OPENSTACK_CODENAMES.items())[new_index]
def determine_new_source(ubuntu_version,
                         current_source,
                         new_release,
                         single_increment=True):
    """Determine the new source/openstack-origin value  based on new release.

    This takes the ubuntu_version and the current_source (in the form of
    'distro' or 'cloud:xenial-mitaka') and either converts it to a new source,
    or returns None if the new_release will match the current_source (i.e. it's
    already at the right release), or it's simply not possible.

    If single_increment is set, then the returned source will only be returned
    if the new_release is one more than the release in the current source.

    :param ubuntu_version: the ubuntu version that the app is installed on.
    :type ubuntu_version: str
    :param current_source: a source in the form of 'distro' or
        'cloud:xenial-mitaka'
    :type current_source: str
    :param new_release: a new OpenStack version codename. e.g. 'stein'
    :type new_release: str
    :param single_increment: If True, only allow single increment upgrade.
    :type single_increment: boolean
    :returns: The new source in the form of 'cloud:bionic-train' or None if not
        possible
    :rtype: Optional[str]
    :raises: KeyError if any of the strings don't correspond to known values.
    """
    logging.warn("determine_new_source: locals: %s", locals())
    if current_source == 'distro':
        # convert to a ubuntu-openstack pair
        current_source = "cloud:{}-{}".format(
            ubuntu_version, UBUNTU_OPENSTACK_RELEASE[ubuntu_version])
    # strip out the current openstack version
    if ':' not in current_source:
        current_source = "cloud:{}-{}".format(ubuntu_version, current_source)
    pair = current_source.split(':')[1]
    u_version, os_version = pair.split('-', 2)
    if u_version != ubuntu_version:
        logging.warn("determine_new_source: ubuntu_versions don't match: "
                     "%s != %s" % (ubuntu_version, u_version))
        return None
    # determine versions
    openstack_codenames = list(OPENSTACK_CODENAMES.values())
    old_index = openstack_codenames.index(os_version)
    try:
        new_os_version = openstack_codenames[old_index + 1]
    except IndexError:
        logging.warn("determine_new_source: no OpenStack version after "
                     "'%s'" % os_version)
        return None
    if single_increment and new_release != new_os_version:
        logging.warn("determine_new_source: requested version '%s' not a "
                     "single increment from '%s' which is '%s'" %
                     (new_release, os_version, new_os_version))
        return None
    # now check that there is a combination of u_version-new_os_version
    new_pair = "{}_{}".format(u_version, new_os_version)
    if new_pair not in OPENSTACK_RELEASES_PAIRS:
        logging.warn("determine_new_source: now release pair candidate for "
                     " combination cloud:%s-%s" % (u_version, new_os_version))
        return None
    return "cloud:{}-{}".format(u_version, new_os_version)