Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

g.extension: doesn't return list of available extensions in the official GRASS GIS Addons repository #2025

Closed
tmszi opened this issue Dec 19, 2021 · 20 comments
Labels
bug Something isn't working
Milestone

Comments

@tmszi
Copy link
Member

tmszi commented Dec 19, 2021

Describe the bug
g.extension doesn't return list of available extensions in the official GRASS GIS Addons repository.

To Reproduce
Steps to reproduce the behavior:

  1. Launch g.extension -l
  2. Doesn't return list of available extensions

Expected behavior
g.extension should return a list of available extensions

System description (please complete the following information):

  • GRASS GIS version dev 8.0

Additional context
g.extension tries find module.xml file at this URL https://grass.osgeo.org/addons/grass8/modules.xml, but grass8 directory doesn't exists on the server, only grass6 and grass7 exists. Please check them using this URL https://grass.osgeo.org/addons/.

@tmszi tmszi added the bug Something isn't working label Dec 19, 2021
@neteler neteler added this to the 8.0.0 milestone Dec 19, 2021
@neteler
Copy link
Member

neteler commented Dec 19, 2021

Additional context
g.extension tries find module.xml file at this URL https://grass.osgeo.org/addons/grass8/modules.xml, but grass8 directory doesn't exists on the server, only grass6 and grass7 exists. Please check them using this URL https://grass.osgeo.org/addons/.

This is stuck at OSGeo/grass-addons#613 which we need to implement asap.

@veroandreo
Copy link
Contributor

Related to #1960

@tmszi tmszi closed this as completed Dec 20, 2021
@tmszi tmszi reopened this Dec 20, 2021
@tmszi
Copy link
Member Author

tmszi commented Dec 20, 2021

Sorry I closed this issue prematurely, my mistake.

@gregNS
Copy link

gregNS commented Dec 26, 2021

This report relates to grass 7.8.6 but it seems related to this issue for v8.0

The works-for-me solution was to edit g.extension.py based on a posting found here. I do not have sufficient understanding of proper usage of SSL and python networking to suggest this is a good solution, only that it worked.

# gds: added import ssl
import ssl

def urlopen(url, *args, **kwargs):
    """Wrapper around urlopen. Same function as 'urlopen', but with the
    ability to define headers.
    """
    request = urlrequest.Request(url, headers=HEADERS)
    # gds edit: added ssl._create_default_https_context = ssl._create_unverified_context
    ssl._create_default_https_context = ssl._create_unverified_context
    return urlrequest.urlopen(request, *args, **kwargs)

Details:

I recently installed a completely new and updated osgeo4w bundle on my Windows 10 laptop.
Attempting to install extensions in grass failed with an error message indicating the package url could not be opened. i.e:

ERROR: Cannot open URL:
http://wingrass.fsv.cvut.cz/grass78/x86_64/addons/grass-7.8.6/r.in.pdal.zip

I'm not experienced with python networking code so added a bunch of grass.message(...) statements to identify the failure was occurring with return urlrequest.urlopen(request, *args, **kwargs) called from the urlopen in g.extension.py.

Running python from the grass command window and trying to retrieve the url directly using urllib in the interpreter produced an error related to an SSL failure...

>>> import urllib
>>> import urllib.request
>>> urllib.request.urlopen('http://wingrass.fsv.cvut.cz/grass78/x86_64/addons/grass-7.8.6/r.in.pdal.zip')
Traceback (most recent call last):
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 1346, in do_open
    h.request(req.get_method(), req.selector, req.data, headers,
  File "C:\OSGeo4W\apps\Python39\lib\http\client.py", line 1253, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\OSGeo4W\apps\Python39\lib\http\client.py", line 1299, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\OSGeo4W\apps\Python39\lib\http\client.py", line 1248, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\OSGeo4W\apps\Python39\lib\http\client.py", line 1008, in _send_output
    self.send(msg)
  File "C:\OSGeo4W\apps\Python39\lib\http\client.py", line 948, in send
    self.connect()
  File "C:\OSGeo4W\apps\Python39\lib\http\client.py", line 1422, in connect
    self.sock = self._context.wrap_socket(self.sock,
  File "C:\OSGeo4W\apps\Python39\lib\ssl.py", line 500, in wrap_socket
    return self.sslsocket_class._create(
  File "C:\OSGeo4W\apps\Python39\lib\ssl.py", line 1040, in _create
    self.do_handshake()
  File "C:\OSGeo4W\apps\Python39\lib\ssl.py", line 1309, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: A failure in the SSL library occurred (_ssl.c:1129)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 214, in urlopen
    return opener.open(url, data, timeout)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 523, in open
    response = meth(req, response)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 632, in http_response
    response = self.parent.error(
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 555, in error
    result = self._call_chain(*args)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 494, in _call_chain
    result = func(*args)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 747, in http_error_302
    return self.parent.open(new, timeout=req.timeout)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 517, in open
    response = self._open(req, data)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 534, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 494, in _call_chain
    result = func(*args)
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 1389, in https_open
    return self.do_open(http.client.HTTPSConnection, req,
  File "C:\OSGeo4W\apps\Python39\lib\urllib\request.py", line 1349, in do_open
    raise URLError(err)
urllib.error.URLError: <urlopen error A failure in the SSL library occurred (_ssl.c:1129)>

@hellik
Copy link
Member

hellik commented Dec 26, 2021

ERROR: Cannot open URL:
http://wingrass.fsv.cvut.cz/grass78/x86_64/addons/grass-7.8.6/r.in.pdal.zip

IIRC, modules/addons linking to pdal aren't available in winGRASS due to C++ compiling issues on incompatible build environment.

@gregNS
Copy link

gregNS commented Dec 26, 2021

Sorry if I distracted you...
g.extension -l failed to return any result and no extension would install when named on the command line.

After the previous noted change for the SSL failure I am able to have g.extension return the list and all extensions I attempted to install were successful. (including r.in.pdal)

@veroandreo
Copy link
Contributor

I just updated the main branch and when running g.extension -l, I get:

List of available extensions (modules):
Fetching list of extensions from GRASS-Addons SVN repository (be patient)...
Traceback (most recent call last):
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 681, in list_available_modules
    tree = etree_fromurl(file_url)
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 383, in etree_fromurl
    file_ = urlopen(url)
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 235, in urlopen
    return urlrequest.urlopen(request, *args, **kwargs)
  File "/usr/lib64/python3.9/urllib/request.py", line 214, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/lib64/python3.9/urllib/request.py", line 523, in open
    response = meth(req, response)
  File "/usr/lib64/python3.9/urllib/request.py", line 632, in http_response
    response = self.parent.error(
  File "/usr/lib64/python3.9/urllib/request.py", line 561, in error
    return self._call_chain(*args)
  File "/usr/lib64/python3.9/urllib/request.py", line 494, in _call_chain
    result = func(*args)
  File "/usr/lib64/python3.9/urllib/request.py", line 641, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 2570, in <module>
    sys.exit(main())
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 2517, in main
    list_available_extensions(xmlurl)
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 556, in list_available_extensions
    list_available_modules(url)
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 693, in list_available_modules
    list_available_extensions_svn(url)
  File "/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-gnu/scripts/g.extension", line 761, in list_available_extensions_svn
    sline = pattern.search(line)
TypeError: cannot use a string pattern on a bytes-like object

When I try to do the same in the GUI, i.e., go to Settings-> Addons extensions-> Install extension, I get this in the GUI console tab:

Exception in thread
Thread-4
:
Traceback (most recent call last):
  File "/usr/lib64/python3.9/threading.py", line 973, in
_bootstrap_inner

self.run()
  File
"/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-
gnu/gui/wxpython/core/gthread.py", line 154, in __run

self.__run_backup()
  File
"/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-
gnu/gui/wxpython/core/gthread.py", line 117, in run

ret = vars()["callable"](*args, **kwds)
  File
"/home/veroandreo/software/grass79-dev/dist.x86_64-pc-linux-
gnu/gui/wxpython/modules/extensions.py", line 369, in Load

raise GException(_("Unable to load extensions. %s") % msg)
core.gcmd
.
GException
:
Unable to load extensions. Flag 'g' ignored, addons metadata
file not available

Installing extensions from the terminal works just fine though.

System Info:

GRASS version: 8.0.dev                                                          
Code revision: 563797cd3                                                        
Build date: 2021-12-27                                                          
Build platform: x86_64-pc-linux-gnu                                             
GDAL: 3.2.2                                                                     
PROJ: 7.2.1                                                                     
GEOS: 3.9.0                                                                     
SQLite: 3.34.1                                                                  
Python: 3.9.7                                                                   
wxPython: 4.0.7                                                                 
Platform: Linux-5.15.6-100.fc34.x86_64-x86_64-with-glibc2.33

@tmszi
Copy link
Member Author

tmszi commented Dec 27, 2021

It seems, that problem is missing build GRASS GIS 8 version addons on CTU server, missing modules.xml and toolboxes.xml file, see this URL directory https://grass.osgeo.org/addons/grass8/. g.extension -l command point to this URL server.

In this line code is missing version 8, same here and here and etc., it seems that building GRASS GIS Addons (matching addons xml files) on CTU server doesn't count with version 8.

@neteler
Copy link
Member

neteler commented Dec 27, 2021

It seems, that problem is missing build GRASS GIS 8 version addons on CTU server, missing modules.xml and toolboxes.xml file, see this URL directory https://grass.osgeo.org/addons/grass8/. g.extension -l command point to this URL server.

Could we generate these (missing) files on grass.osgeo.org itself to not be dependent on remote files?

In this line code is missing version 8, same here and here and etc., it seems that building GRASS GIS Addons (matching addons xml files) on CTU server doesn't count with version 8.

Good catch! Maybe we need to introduce a config file which defines GRASS_VERSION_MIN and GRASS_VERSION_MAX or so, for better loops and a central place to configure it (with ´source config.txt` in the beginning of the respective shell files).

@tmszi
Copy link
Member Author

tmszi commented Dec 27, 2021

Could we generate these (missing) files on grass.osgeo.org itself to not be dependent on remote files?

Yes we could. We will have to complete this PR and at the same time fix this issue #1960.

Good catch! Maybe we need to introduce a config file which defines GRASS_VERSION_MIN and GRASS_VERSION_MAX or so, for better loops and a central place to configure it (with ´source config.txt` in the beginning of the respective shell files).

Yes I agree a good idea.

@neteler
Copy link
Member

neteler commented Dec 29, 2021

Maybe we need to introduce a config file which defines GRASS_VERSION_MIN and GRASS_VERSION_MAX or so, for better loops and a central place to configure it (with ´source config.txt` in the beginning of the respective shell files).

Since I didn't find a sh/bash/... compatible way to parse a config.cfg file without defining an absolute path to it, for now I changed the scripts to use variables and define them on top of the scripts. See #655.

@tmszi
Copy link
Member Author

tmszi commented Dec 30, 2021

I looked at the metadata of that module.xml file (require for g.extension -l module GRASS GIS 7) and the date creation is 2020-11-19, it looks like the update isn't going on CTU server.

@neteler
Copy link
Member

neteler commented Dec 30, 2021

Yes, so I wonder if we can generate that on the GRASS server itself?

@tmszi
Copy link
Member Author

tmszi commented Dec 30, 2021

Yes, so I wonder if we can generate that on the GRASS server itself?

I'm going to look at it.

@hellik
Copy link
Member

hellik commented Dec 30, 2021

Yes, so I wonder if we can generate that on the GRASS >server itself?

On github?

@neteler
Copy link
Member

neteler commented Dec 30, 2021

Yes, so I wonder if we can generate that on the GRASS >server itself?

On github?

No, I thought of grass.osgeo.org where the addon manual pages are already built.

@echoix
Copy link
Member

echoix commented Dec 31, 2021

This report relates to grass 7.8.6 but it seems related to this issue for v8.0

The works-for-me solution was to edit g.extension.py based on a posting found here. I do not have sufficient understanding of proper usage of SSL and python networking to suggest this is a good solution, only that it worked.

# gds: added import ssl
import ssl

def urlopen(url, *args, **kwargs):
    """Wrapper around urlopen. Same function as 'urlopen', but with the
    ability to define headers.
    """
    request = urlrequest.Request(url, headers=HEADERS)
    # gds edit: added ssl._create_default_https_context = ssl._create_unverified_context
    ssl._create_default_https_context = ssl._create_unverified_context
    return urlrequest.urlopen(request, *args, **kwargs)

I tried to overcome the issue too by myself two weeks ago, but, couldn't get pas the SSL error. I have difficulty understanding the root of the issue, since this is some urllib Python 2 code ported to Python 3, by mimicking the same behaviour as Python 2. What is clear with the approach in this comment is that disabling the ssl verification is not a solution and too risky as a real workaround.

I took a second look, to try to understand the issue. I'm on Windows 10, using a OSGeo4W install (of QGIS 3.22.2, including GRASS 7.8.6). I found out that how the code is written, a https website should be working normally, since it works when using a different hardcoded URL than the http://wingrass.fsv.cvut.cz/ URL. On Chrome (at least), the http://wingrass.fsv.cvut.cz/ URL is redirected with a 301 response to https://wingrass.fsv.cvut.cz/. Certificates are valid. When using a different https URL, there is no SSL error (and the requests themselves are successful, but they aren't the good content). So this might mean that by default, there are some outdated certificates, but I couldn't find a way to verify that. Is there a website configuration issue? Either way, the g.extension code must be able to handle this.

So I continued to try and find a solution and found a solution, but I don't think it's a valid fix, since I introduce a dependency, and I can't figure out where the python requirements are listed. The solution works on Windows from the OSGeo4W QGIS install, since some other packages are installed too. Using urllib for web requests is quite challenging to do right, and isn't recommended even by the Python docs, which refer to using the ubiquitous requests package if we only need a high level interface (which is already in the libraries of the OSGeo4W install). I however managed something using urllib, but adding the built-in ssl library (only available if Python was built with SSL support, that is a potential problem), and using the certifi package (already present in the install), that manages certificates, or gets updated certificates. Often outdated certificates errors are fixed by pip install --upgrade certifi, so that's why I thought of using certifi if I suspected outdated certificates in the default behaviour.

So my fix is in two parts: Using a default urllib opener (urllib.request.build_opener()) to handle common scenarios, like redirections and HTTPS (from the docs: ProxyHandler (if proxy settings are detected), UnknownHandler, HTTPHandler, HTTPDefaultErrorHandler, HTTPRedirectHandler, FTPHandler, FileHandler, HTTPErrorProcessor, and HTTPSHandler if available). However, the HTTPS handler by itself is not enough, nor by setting the context=ssl.create_default_context() in its initialization (context introduced in Python 3.2 in 2011, create_default_context introduced in Python 3.4 in 2014). The context=ssl.create_default_context(cafile=certifi.where()) is needed to be able to make the wingrass.fsv.cvut.cz URLs work.

Finally, adding everything together, and adding a protection so an import error doesn't fail and the extension continues to work, gives the following:

At the imports, add

try:
    from ssl import create_default_context
    import certifi
except ImportError:
    # If not built with SSL or certifi not present, do not configure
    # the HTTPSHandler and only use default opener.
    create_default_context = None
    certifi = None

and in the main() function at the end of the file, replace the proxy section with the following (since there are some changes before and after):

    if create_default_context and certifi:
        ssl_context = create_default_context(cafile=certifi.where())
        https_handler = urlrequest.HTTPSHandler(context=ssl_context)
        opener = urlrequest.build_opener(https_handler)
    else:
        opener = urlrequest.build_opener()

    # manage proxies
    global PROXIES
    if options["proxy"]:
        PROXIES = {}
        for ptype, purl in (p.split("=") for p in options["proxy"].split(",")):
            PROXIES[ptype] = purl
        proxy = urlrequest.ProxyHandler(PROXIES)
        # opener = urlrequest.build_opener(proxy)
        # urlrequest.install_opener(opener)

        # Since urllib.request.build_opener() creates many types of handlers when possible,
        # including ProxyHandler, but it wouldn,t be configured correctly, we are need to replace it.
        for handler in opener.handlers:
            if isinstance(handler, urlrequest.ProxyHandler):
                opener.handlers.remove(handler)
        opener.add_handler(proxy)

    urlrequest.install_opener(opener)

So, the complete import section looks like:

from __future__ import print_function
import fileinput
import http
import os
import codecs
import sys
import re
import atexit
import shutil
import zipfile
import tempfile
import json
import xml.etree.ElementTree as etree
from distutils.dir_util import copy_tree

try:
    from ssl import create_default_context
    import certifi
except ImportError:
    # If not built with SSL or certifi not present, do not configure
    # the HTTPSHandler and only use default opener.
    create_default_context = None
    certifi = None
from six.moves.urllib import request as urlrequest
from six.moves.urllib.error import HTTPError, URLError
from six.moves.urllib.parse import urlparse

# Get the XML parsing exceptions to catch. The behavior changed with Python 2.7
# and ElementTree 1.3.
from xml.parsers import expat  # TODO: works for any Python?

if hasattr(etree, "ParseError"):
    ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
else:
    ETREE_EXCEPTIONS = expat.ExpatError

import grass.script as gscript
from grass.script.utils import try_rmdir
from grass.script import core as grass
from grass.script import task as gtask

# temp dir
REMOVE_TMPDIR = True
PROXIES = {}
HEADERS = {
    "User-Agent": "Mozilla/5.0",
}
HTTP_STATUS_CODES = list(http.HTTPStatus)
GIT_URL = "https://github.com/OSGeo/grass-addons"

and the main() function looks like:

def main():
    # check dependencies
    if not flags["a"] and sys.platform != "win32":
        check_progs()

    original_url = options["url"]
    branch = options["branch"]

    if create_default_context and certifi:
        ssl_context = create_default_context(cafile=certifi.where())
        https_handler = urlrequest.HTTPSHandler(context=ssl_context)
        opener = urlrequest.build_opener(https_handler)
    else:
        opener = urlrequest.build_opener()

    # manage proxies
    global PROXIES
    if options["proxy"]:
        PROXIES = {}
        for ptype, purl in (p.split("=") for p in options["proxy"].split(",")):
            PROXIES[ptype] = purl
        proxy = urlrequest.ProxyHandler(PROXIES)

        # Since urllib.request.build_opener() creates many types of handlers when possible,
        # including ProxyHandler, but it wouldn,t be configured correctly, we are need to replace it.
        for handler in opener.handlers:
            if isinstance(handler, urlrequest.ProxyHandler):
                opener.handlers.remove(handler)
        opener.add_handler(proxy)

    urlrequest.install_opener(opener)

    # define path
    options["prefix"] = resolve_install_prefix(
        path=options["prefix"], to_system=flags["s"]
    )

    if flags["j"]:
        get_addons_paths(gg_addons_base_dir=options["prefix"])
        return 0

    # list available extensions
    if flags["l"] or flags["c"] or (flags["g"] and not flags["a"]):
        # using dummy extension, we don't need any extension URL now,
        # but will work only as long as the function does not check
        # if the URL is actually valid or something
        source, url = resolve_source_code(
            name="dummy", url=original_url, branch=branch, fork=flags["o"]
        )
        xmlurl = resolve_xmlurl_prefix(original_url, source=source)
        list_available_extensions(xmlurl)
        return 0
    elif flags["a"]:
        list_installed_extensions(toolboxes=flags["t"])
        return 0

    if flags["d"] or flags["i"]:
        flag = "d" if flags["d"] else "i"
        if options["operation"] != "add":
            grass.warning(
                _(
                    "Flag '{}' is relevant only to"
                    " 'operation=add'. Ignoring this flag."
                ).format(flag)
            )
        else:
            global REMOVE_TMPDIR
            REMOVE_TMPDIR = False

    if options["operation"] == "add":
        check_dirs()
        if original_url == "" or flags["o"]:
            """
            Query GitHub API only if extension will be downloaded
            from official GRASS GIS addon repository
            """
            get_addons_paths(gg_addons_base_dir=options["prefix"])
        source, url = resolve_source_code(
            name=options["extension"], url=original_url, branch=branch, fork=flags["o"]
        )
        xmlurl = resolve_xmlurl_prefix(original_url, source=source)
        install_extension(source=source, url=url, xmlurl=xmlurl, branch=branch)
    else:  # remove
        remove_extension(force=flags["f"])

    return 0

@neteler
Copy link
Member

neteler commented Jan 1, 2022

See also: OSGeo/grass-addons#656

@neteler
Copy link
Member

neteler commented Jan 2, 2022

Yes, so I wonder if we can generate that on the GRASS server itself?

Update: developed and deployed on grass.osgeo.org (may take up to 24hs to get to the next cronjob cycle), for both G7 and G8.

@tmszi
Copy link
Member Author

tmszi commented Jan 9, 2022

Fixed with GRASS GIS Addons PR OSGeo/grass-addons#656.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants