Modify one or more values for a repo.  If the repo does not exist, it will
    be created, so long as the definition is well formed.  For Ubuntu the
    ``ppa:<project>/repo`` format is acceptable. ``ppa:`` format can only be
    used to create a new repository.

    The following options are available to modify a repo definition:

    architectures
        A comma-separated list of supported architectures, e.g. ``amd64`` If
        this option is not set, all architectures (configured in the system)
        will be used.

    comps
        A comma separated list of components for the repo, e.g. ``main``

    file
        A file name to be used

    keyserver
        Keyserver to get gpg key from

    keyid
        Key ID or a list of key IDs to load with the ``keyserver`` argument

    key_url
        URL to a GPG key to add to the APT GPG keyring

    key_text
        GPG key in string form to add to the APT GPG keyring

        .. versionadded:: 2018.3.0

    consolidate : False
        If ``True``, will attempt to de-duplicate and consolidate sources

    comments
        Sometimes you want to supply additional information, but not as
        enabled configuration. All comments provided here will be joined
        into a single string and appended to the repo configuration with a
        comment marker (#) before it.

        .. versionadded:: 2015.8.9

    .. note::
        Due to the way keys are stored for APT, there is a known issue where
        the key won't be updated unless another change is made at the same
        time. Keys should be properly added on initial configuration.

    CLI Examples:

    .. code-block:: bash

        salt '*' pkg.mod_repo 'myrepo definition' uri=http://new/uri
        salt '*' pkg.mod_repo 'myrepo definition' comps=main,universe

def mod_repo(repo, saltenv='base', **kwargs):
    '''
    Modify one or more values for a repo.  If the repo does not exist, it will
    be created, so long as the definition is well formed.  For Ubuntu the
    ``ppa:<project>/repo`` format is acceptable. ``ppa:`` format can only be
    used to create a new repository.

    The following options are available to modify a repo definition:

    architectures
        A comma-separated list of supported architectures, e.g. ``amd64`` If
        this option is not set, all architectures (configured in the system)
        will be used.

    comps
        A comma separated list of components for the repo, e.g. ``main``

    file
        A file name to be used

    keyserver
        Keyserver to get gpg key from

    keyid
        Key ID or a list of key IDs to load with the ``keyserver`` argument

    key_url
        URL to a GPG key to add to the APT GPG keyring

    key_text
        GPG key in string form to add to the APT GPG keyring

        .. versionadded:: 2018.3.0

    consolidate : False
        If ``True``, will attempt to de-duplicate and consolidate sources

    comments
        Sometimes you want to supply additional information, but not as
        enabled configuration. All comments provided here will be joined
        into a single string and appended to the repo configuration with a
        comment marker (#) before it.

        .. versionadded:: 2015.8.9

    .. note::
        Due to the way keys are stored for APT, there is a known issue where
        the key won't be updated unless another change is made at the same
        time. Keys should be properly added on initial configuration.

    CLI Examples:

    .. code-block:: bash

        salt '*' pkg.mod_repo 'myrepo definition' uri=http://new/uri
        salt '*' pkg.mod_repo 'myrepo definition' comps=main,universe
    '''
    if 'refresh_db' in kwargs:
        salt.utils.versions.warn_until(
            'Neon',
            'The \'refresh_db\' argument to \'pkg.mod_repo\' has been '
            'renamed to \'refresh\'. Support for using \'refresh_db\' will be '
            'removed in the Neon release of Salt.'
        )
        refresh = kwargs['refresh_db']
    else:
        refresh = kwargs.get('refresh', True)

    _check_apt()
    # to ensure no one sets some key values that _shouldn't_ be changed on the
    # object itself, this is just a white-list of "ok" to set properties
    if repo.startswith('ppa:'):
        if __grains__['os'] in ('Ubuntu', 'Mint', 'neon'):
            # secure PPAs cannot be supported as of the time of this code
            # implementation via apt-add-repository.  The code path for
            # secure PPAs should be the same as urllib method
            if salt.utils.path.which('apt-add-repository') \
                    and 'ppa_auth' not in kwargs:
                repo_info = get_repo(repo)
                if repo_info:
                    return {repo: repo_info}
                else:
                    env = None
                    http_proxy_url = _get_http_proxy_url()
                    if http_proxy_url:
                        env = {'http_proxy': http_proxy_url,
                               'https_proxy': http_proxy_url}
                    if float(__grains__['osrelease']) < 12.04:
                        cmd = ['apt-add-repository', repo]
                    else:
                        cmd = ['apt-add-repository', '-y', repo]
                    out = _call_apt(cmd, env=env, scope=False, **kwargs)
                    if out['retcode']:
                        raise CommandExecutionError(
                            'Unable to add PPA \'{0}\'. \'{1}\' exited with '
                            'status {2!s}: \'{3}\' '.format(
                                repo[4:],
                                cmd,
                                out['retcode'],
                                out['stderr']
                            )
                        )
                    # explicit refresh when a repo is modified.
                    if refresh:
                        refresh_db()
                    return {repo: out}
            else:
                if not HAS_SOFTWAREPROPERTIES:
                    _warn_software_properties(repo)
                else:
                    log.info('Falling back to urllib method for private PPA')

                # fall back to urllib style
                try:
                    owner_name, ppa_name = repo[4:].split('/', 1)
                except ValueError:
                    raise CommandExecutionError(
                        'Unable to get PPA info from argument. '
                        'Expected format "<PPA_OWNER>/<PPA_NAME>" '
                        '(e.g. saltstack/salt) not found.  Received '
                        '\'{0}\' instead.'.format(repo[4:])
                    )
                dist = __grains__['lsb_distrib_codename']
                # ppa has a lot of implicit arguments. Make them explicit.
                # These will defer to any user-defined variants
                kwargs['dist'] = dist
                ppa_auth = ''
                if 'file' not in kwargs:
                    filename = '/etc/apt/sources.list.d/{0}-{1}-{2}.list'
                    kwargs['file'] = filename.format(owner_name, ppa_name,
                                                     dist)
                try:
                    launchpad_ppa_info = _get_ppa_info_from_launchpad(
                        owner_name, ppa_name)
                    if 'ppa_auth' not in kwargs:
                        kwargs['keyid'] = launchpad_ppa_info[
                            'signing_key_fingerprint']
                    else:
                        if 'keyid' not in kwargs:
                            error_str = 'Private PPAs require a ' \
                                        'keyid to be specified: {0}/{1}'
                            raise CommandExecutionError(
                                error_str.format(owner_name, ppa_name)
                            )
                except HTTPError as exc:
                    raise CommandExecutionError(
                        'Launchpad does not know about {0}/{1}: {2}'.format(
                            owner_name, ppa_name, exc)
                    )
                except IndexError as exc:
                    raise CommandExecutionError(
                        'Launchpad knows about {0}/{1} but did not '
                        'return a fingerprint. Please set keyid '
                        'manually: {2}'.format(owner_name, ppa_name, exc)
                    )

                if 'keyserver' not in kwargs:
                    kwargs['keyserver'] = 'keyserver.ubuntu.com'
                if 'ppa_auth' in kwargs:
                    if not launchpad_ppa_info['private']:
                        raise CommandExecutionError(
                            'PPA is not private but auth credentials '
                            'passed: {0}'.format(repo)
                        )
                # assign the new repo format to the "repo" variable
                # so we can fall through to the "normal" mechanism
                # here.
                if 'ppa_auth' in kwargs:
                    ppa_auth = '{0}@'.format(kwargs['ppa_auth'])
                    repo = LP_PVT_SRC_FORMAT.format(ppa_auth, owner_name,
                                                    ppa_name, dist)
                else:
                    repo = LP_SRC_FORMAT.format(owner_name, ppa_name, dist)
        else:
            raise CommandExecutionError(
                'cannot parse "ppa:" style repo definitions: {0}'
                .format(repo)
            )

    sources = sourceslist.SourcesList()
    if kwargs.get('consolidate', False):
        # attempt to de-dup and consolidate all sources
        # down to entries in sources.list
        # this option makes it easier to keep the sources
        # list in a "sane" state.
        #
        # this should remove duplicates, consolidate comps
        # for a given source down to one line
        # and eliminate "invalid" and comment lines
        #
        # the second side effect is removal of files
        # that are not the main sources.list file
        sources = _consolidate_repo_sources(sources)

    repos = [s for s in sources if not s.invalid]
    mod_source = None
    try:
        repo_type, \
            repo_architectures, \
            repo_uri, \
            repo_dist, \
            repo_comps = _split_repo_str(repo)
    except SyntaxError:
        raise SyntaxError(
            'Error: repo \'{0}\' not a well formatted definition'.format(repo)
        )

    full_comp_list = set(repo_comps)
    no_proxy = __salt__['config.option']('no_proxy')

    if 'keyid' in kwargs:
        keyid = kwargs.pop('keyid', None)
        keyserver = kwargs.pop('keyserver', None)
        if not keyid or not keyserver:
            error_str = 'both keyserver and keyid options required.'
            raise NameError(error_str)
        if not isinstance(keyid, list):
            keyid = [keyid]

        for key in keyid:
            if isinstance(key, int):  # yaml can make this an int, we need the hex version
                key = hex(key)
            cmd = ['apt-key', 'export', key]
            output = __salt__['cmd.run_stdout'](cmd, python_shell=False, **kwargs)
            imported = output.startswith('-----BEGIN PGP')
            if keyserver:
                if not imported:
                    http_proxy_url = _get_http_proxy_url()
                    if http_proxy_url and keyserver not in no_proxy:
                        cmd = ['apt-key', 'adv', '--batch', '--keyserver-options', 'http-proxy={0}'.format(http_proxy_url),
                               '--keyserver', keyserver, '--logger-fd', '1', '--recv-keys', key]
                    else:
                        cmd = ['apt-key', 'adv', '--batch', '--keyserver', keyserver,
                               '--logger-fd', '1', '--recv-keys', key]
                    ret = _call_apt(cmd, scope=False, **kwargs)
                    if ret['retcode'] != 0:
                        raise CommandExecutionError(
                            'Error: key retrieval failed: {0}'.format(ret['stdout'])
                        )

    elif 'key_url' in kwargs:
        key_url = kwargs['key_url']
        fn_ = __salt__['cp.cache_file'](key_url, saltenv)
        if not fn_:
            raise CommandExecutionError(
                'Error: file not found: {0}'.format(key_url)
            )
        cmd = ['apt-key', 'add', fn_]
        out = __salt__['cmd.run_stdout'](cmd, python_shell=False, **kwargs)
        if not out.upper().startswith('OK'):
            raise CommandExecutionError(
                'Error: failed to add key from {0}'.format(key_url)
            )

    elif 'key_text' in kwargs:
        key_text = kwargs['key_text']
        cmd = ['apt-key', 'add', '-']
        out = __salt__['cmd.run_stdout'](cmd, stdin=key_text,
                                         python_shell=False, **kwargs)
        if not out.upper().startswith('OK'):
            raise CommandExecutionError(
                'Error: failed to add key:\n{0}'.format(key_text)
            )

    if 'comps' in kwargs:
        kwargs['comps'] = kwargs['comps'].split(',')
        full_comp_list |= set(kwargs['comps'])
    else:
        kwargs['comps'] = list(full_comp_list)

    if 'architectures' in kwargs:
        kwargs['architectures'] = kwargs['architectures'].split(',')
    else:
        kwargs['architectures'] = repo_architectures

    if 'disabled' in kwargs:
        kwargs['disabled'] = salt.utils.data.is_true(kwargs['disabled'])

    kw_type = kwargs.get('type')
    kw_dist = kwargs.get('dist')

    for source in repos:
        # This series of checks will identify the starting source line
        # and the resulting source line.  The idea here is to ensure
        # we are not returning bogus data because the source line
        # has already been modified on a previous run.
        repo_matches = source.type == repo_type and source.uri.rstrip('/') == repo_uri.rstrip('/') and source.dist == repo_dist
        kw_matches = source.dist == kw_dist and source.type == kw_type

        if repo_matches or kw_matches:
            for comp in full_comp_list:
                if comp in getattr(source, 'comps', []):
                    mod_source = source
            if not source.comps:
                mod_source = source
            if kwargs['architectures'] != source.architectures:
                mod_source = source
            if mod_source:
                break

    if 'comments' in kwargs:
        kwargs['comments'] = \
            salt.utils.pkg.deb.combine_comments(kwargs['comments'])

    if not mod_source:
        mod_source = sourceslist.SourceEntry(repo)
        if 'comments' in kwargs:
            mod_source.comment = kwargs['comments']
        sources.list.append(mod_source)
    elif 'comments' in kwargs:
        mod_source.comment = kwargs['comments']

    for key in kwargs:
        if key in _MODIFY_OK and hasattr(mod_source, key):
            setattr(mod_source, key, kwargs[key])
    sources.save()
    # on changes, explicitly refresh
    if refresh:
        refresh_db()
    return {
        repo: {
            'architectures': getattr(mod_source, 'architectures', []),
            'comps': mod_source.comps,
            'disabled': mod_source.disabled,
            'file': mod_source.file,
            'type': mod_source.type,
            'uri': mod_source.uri,
            'line': mod_source.line
        }
    }