Repository: avocado-framework/avocado

Issue:
Improve asset hash mismatch error message during fetch
In the current implementation, when an asset is fetched but there is a hash mismatch, the error message says the download failed:

```
2020-12-04 19:26:42,233 boot_xen         L0047 INFO | xen_url: https://deb.debian.org/debian-security/pool/updates/main/x/xen/xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb, hash:4326c63c9f346c6189bca1128d80c54e
2020-12-04 19:26:42,237 asset            L0320 INFO | Asset not in cache, fetching it.
2020-12-04 19:26:42,237 download         L0067 INFO | Fetching https://deb.debian.org/debian-security/pool/updates/main/x/xen/xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb -> /home/alex/avocado/data/cache/by_location/a9513b030295ed2988bdde6f076d363fb5c38217/xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb.m3568vh_
2020-12-04 19:26:42,361 download         L0051 DEBUG| Retrieved URL "https://deb.debian.org/debian-security/pool/updates/main/x/xen/xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb": content-length 3583644, date: "Fri, 04 Dec 2020 19:26:42 GMT", last-modified: "Thu, 01 Oct 2020 20:29:19 GMT"
2020-12-04 19:26:43,447 stacktrace       L0039 ERROR|
2020-12-04 19:26:43,447 stacktrace       L0042 ERROR| Reproduced traceback from: /home/alex/lsrc/qemu.git/builds/arm.all/tests/venv/lib/python3.7/site-packages/avocado/core/test.py:764
2020-12-04 19:26:43,447 stacktrace       L0045 ERROR| Traceback (most recent call last):
2020-12-04 19:26:43,447 stacktrace       L0045 ERROR|   File "/home/alex/lsrc/qemu.git/builds/arm.all/tests/venv/lib/python3.7/site-packages/avocado/core/test.py", line 1027, in fetch_asset
2020-12-04 19:26:43,447 stacktrace       L0045 ERROR|     asset_path = asset_func()
2020-12-04 19:26:43,447 stacktrace       L0045 ERROR|   File "/home/alex/lsrc/qemu.git/builds/arm.all/tests/venv/lib/python3.7/site-packages/avocado/utils/asset.py", line 360, in fetch
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|     raise OSError("Failed to fetch %s." % self.asset_name)
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR| OSError: Failed to fetch xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb.
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR| During handling of the above exception, another exception occurred:
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR| Traceback (most recent call last):
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|   File "/home/alex/lsrc/qemu.git/builds/arm.all/tests/acceptance/boot_xen.py", line 49, in test_arm64_xen_and_domU
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|     xen_deb  = self.fetch_asset(xen_url, asset_hash=xen_hash)
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|   File "/home/alex/lsrc/qemu.git/builds/arm.all/tests/acceptance/avocado_qemu/__init__.py", line 208, in fetch_asset
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|     cancel_on_missing=cancel_on_missing)
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|   File "/home/alex/lsrc/qemu.git/builds/arm.all/tests/venv/lib/python3.7/site-packages/avocado/core/test.py", line 1034, in fetch_asset
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|     self.cancel(missing_asset_message)
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|   File "/home/alex/lsrc/qemu.git/builds/arm.all/tests/venv/lib/python3.7/site-packages/avocado/core/test.py", line 984, in cancel
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR|     raise exceptions.TestCancel(message)
2020-12-04 19:26:43,448 stacktrace       L0045 ERROR| avocado.core.exceptions.TestCancel: Missing asset https://deb.debian.org/debian-security/pool/updates/main/x/xen/xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb
2020-12-04 19:26:43,448 stacktrace       L0046 ERROR|
2020-12-04 19:26:43,448 test             L0933 ERROR| CANCEL 1-./tests/acceptance/boot_xen.py:BootXen.test_arm64_xen_and_domU -> TestCancel: Missing asset https://deb.debian.org/debian-security/pool/updates/main/x/xen/xen-hypervisor-4.11-arm64_4.11.4+37-g3263f257ca-1_arm64.deb
2020-12-04 19:26:43,448 test             L0922 INFO |
2020-12-04 19:26:43,459 job              L0610 INFO | Test results available in /home/alex/avocado/job-results/job-2020-12-04T19.26-60a5a0d
```

Avocado needs to give a clear message of what was the cause of the failure.


Discussion:

Resolving patch:
diff --git a/avocado/plugins/assets.py b/avocado/plugins/assets.py
--- a/avocado/plugins/assets.py
+++ b/avocado/plugins/assets.py
@@ -18,7 +18,6 @@
 
 import ast
 import os
-import urllib.parse
 
 from avocado.core import data_dir, exit_codes, safeloader
 from avocado.core.nrunner import Task
@@ -200,41 +199,22 @@ def fetch_assets(test_file, klass=None, method=None, logger=None):
     :returns: list of names that were successfully fetched and list of
     fails.
     """
-    def validate_parameters(call):
-        """
-        Validate the parameters to make sure we have a supported case.
-
-        :param call: List of parameter to the Asset object.
-        :type call: dict
-        :returns: True or False
-        """
-        name = call.get('name', None)
-        locations = call.get('locations', None)
-        # probably, parameter name was defined as a class attribute
-        if ((name is None) or
-                # probably, parameter locations was defined as a class attribute
-                (not urllib.parse.urlparse(name).scheme and
-                 locations is None)):
-            return False
-        return True
-
     cache_dirs = data_dir.get_cache_dirs()
     success = []
     fail = []
     handler = FetchAssetHandler(test_file, klass, method)
     for call in handler.calls:
-        # validate the parameters
-        if not validate_parameters(call):
-            continue
         expire = call.pop('expire', None)
         if expire is not None:
             expire = data_structures.time_to_seconds(str(expire))
+
+        # make dictionary unpacking compatible with python 3.4 as it does
+        # not support constructions like:
+        # Asset(**call, cache_dirs=cache_dirs, expire=expire)
+        call['cache_dirs'] = cache_dirs
+        call['expire'] = expire
+
         try:
-            # make dictionary unpacking compatible with python 3.4 as it does
-            # not support constructions like:
-            # Asset(**call, cache_dirs=cache_dirs, expire=expire)
-            call['cache_dirs'] = cache_dirs
-            call['expire'] = expire
             asset_obj = Asset(**call)
             if logger is not None:
                 logger.info('Fetching asset from %s:%s.%s',
@@ -321,33 +301,92 @@ def configure(self, parser):
                                  parser=fetch_subcommand_parser,
                                  long_arg='--ignore-errors')
 
-    def run(self, config):
-        subcommand = config.get('assets_subcommand')
-        # we want to let the command caller knows about fails
-        exitcode = exit_codes.AVOCADO_ALL_OK
+        register_subcommand_parser = subcommands.add_parser(
+                'register',
+                help='Register an asset directly to the cacche')
 
-        if subcommand == 'fetch':
-            # fetch assets from instrumented tests
-            for test_file in config.get('assets.fetch.references'):
-                if os.path.isfile(test_file) and test_file.endswith('.py'):
-                    LOG_UI.debug('Fetching assets from %s.', test_file)
-                    success, fail = fetch_assets(test_file)
-
-                    for asset_file in success:
-                        LOG_UI.debug('  File %s fetched or already on'
-                                     ' cache.', asset_file)
-                    for asset_file in fail:
-                        LOG_UI.error(asset_file)
-
-                    if fail:
-                        exitcode |= exit_codes.AVOCADO_FAIL
-                else:
-                    LOG_UI.warning('No such file or file not supported: %s',
-                                   test_file)
-                    exitcode |= exit_codes.AVOCADO_FAIL
+        help_msg = "Unique name to associate with this asset."
+        settings.register_option(section='assets.register',
+                                 key='name',
+                                 help_msg=help_msg,
+                                 default=None,
+                                 key_type=str,
+                                 parser=register_subcommand_parser,
+                                 positional_arg=True)
 
-            # check if we should ignore the errors
-            if config.get('assets.fetch.ignore_errors'):
-                exitcode = exit_codes.AVOCADO_ALL_OK
+        help_msg = "Path to asset that you would like to register manually."
+        settings.register_option(section='assets.register',
+                                 key='url',
+                                 help_msg=help_msg,
+                                 default=None,
+                                 key_type=str,
+                                 parser=register_subcommand_parser,
+                                 positional_arg=True)
+
+        help_msg = "SHA1 hash of this asset."
+        settings.register_option(section='assets.register',
+                                 key='sha1_hash',
+                                 help_msg=help_msg,
+                                 default=None,
+                                 key_type=str,
+                                 metavar="SHA1",
+                                 long_arg='--hash',
+                                 parser=register_subcommand_parser)
 
+    def handle_fetch(self, config):
+        exitcode = exit_codes.AVOCADO_ALL_OK
+        # fetch assets from instrumented tests
+        for test_file in config.get('assets.fetch.references'):
+            if os.path.isfile(test_file) and test_file.endswith('.py'):
+                LOG_UI.debug('Fetching assets from %s.', test_file)
+                success, fail = fetch_assets(test_file)
+
+                for asset_file in success:
+                    LOG_UI.debug('  File %s fetched or already on'
+                                 ' cache.', asset_file)
+                for asset_file in fail:
+                    LOG_UI.error(asset_file)
+
+                if fail:
+                    exitcode |= exit_codes.AVOCADO_FAIL
+            else:
+                LOG_UI.warning('No such file or file not supported: %s',
+                               test_file)
+                exitcode |= exit_codes.AVOCADO_FAIL
+
+        # check if we should ignore the errors
+        if config.get('assets.fetch.ignore_errors'):
+            return exit_codes.AVOCADO_ALL_OK
         return exitcode
+
+    def handle_register(self, config):
+        cache_dirs = data_dir.get_cache_dirs()
+        name = config.get('assets.register.name')
+        asset_hash = config.get('assets.register.sha1_hash')
+        location = config.get('assets.register.url')
+        # Adding a twice the location is a small hack due the current logic to
+        # return "by_name". This needs to be improved soon.
+        asset = Asset(name=name,
+                      asset_hash=asset_hash,
+                      locations=[location, location],
+                      cache_dirs=cache_dirs)
+
+        try:
+            asset.find_asset_file()
+            LOG_UI.error("Asset with name %s already registered.", name)
+        except OSError:
+            try:
+                asset.fetch()
+                LOG_UI.info("Done. Now you can reference it by name %s", name)
+            except OSError as e:
+                LOG_UI.error(e)
+
+    def run(self, config):
+        subcommand = config.get('assets_subcommand')
+
+        if subcommand == 'fetch':
+            return self.handle_fetch(config)
+        elif subcommand == 'register':
+            return self.handle_register(config)
+        else:
+            return exit_codes.UTILITY_FAIL
diff --git a/avocado/utils/asset.py b/avocado/utils/asset.py
--- a/avocado/utils/asset.py
+++ b/avocado/utils/asset.py
@@ -27,7 +27,7 @@
 import sys
 import tempfile
 import time
-import urllib.parse
+from urllib.parse import urlparse
 
 from . import astring, crypto
 from . import path as utils_path
@@ -50,10 +50,9 @@ class Asset:
     Try to fetch/verify an asset file from multiple locations.
     """
 
-    def __init__(self, name, asset_hash, algorithm, locations, cache_dirs,
-                 expire=None, metadata=None):
-        """
-        Initialize the Asset() class.
+    def __init__(self, name, asset_hash=None, algorithm=None, locations=None,
+                 cache_dirs=None, expire=None, metadata=None):
+        """Initialize the Asset() class.
 
         :param name: the asset filename. url is also supported
         :param asset_hash: asset hash
@@ -66,36 +65,20 @@ def __init__(self, name, asset_hash, algorithm, locations, cache_dirs,
         self.name = name
         self.asset_hash = asset_hash
 
-        self.parsed_name = urllib.parse.urlparse(self.name)
-
-        # we currently support the following options for name and locations:
-        # 1. name is a full URI and locations is empty;
-        # 2. name is a single file name and locations is one or more entries.
-        # raise an exception if we have an unsupported use of those arguments
-        if ((self.parsed_name.scheme and locations is not None) or
-                (not self.parsed_name.scheme and locations is None)):
-            raise ValueError("Incorrect use of parameter name with parameter"
-                             " locations.")
+        if isinstance(locations, str):
+            self.locations = [locations]
+        else:
+            self.locations = locations or []
 
         if algorithm is None:
             self.algorithm = DEFAULT_HASH_ALGORITHM
         else:
             self.algorithm = algorithm
 
-        if isinstance(locations, str):
-            self.locations = [locations]
-        else:
-            self.locations = locations
         self.cache_dirs = cache_dirs
         self.expire = expire
         self.metadata = metadata
 
-        # set asset_name according to parsed_name
-        self.asset_name = os.path.basename(self.parsed_name.path)
-        # set relative_dir for the asset
-        self.relative_dir = os.path.join(self._get_relative_dir(),
-                                         self.asset_name)
-
     def _create_hash_file(self, asset_path):
         """
         Compute the hash of the asset file and add it to the CHECKSUM
@@ -104,8 +87,10 @@ def _create_hash_file(self, asset_path):
         :param asset_path: full path of the asset file.
         """
         result = crypto.hash_file(asset_path, algorithm=self.algorithm)
-        with open(self._get_hash_file(asset_path), 'w') as hash_file:
-            hash_file.write('%s %s\n' % (self.algorithm, result))
+        hash_file = self._get_hash_file(asset_path)
+        with FileLock(hash_file, 30):
+            with open(hash_file, 'w') as fp:
+                fp.write('%s %s\n' % (self.algorithm, result))
 
     def _create_metadata_file(self, asset_file):
         """
@@ -140,7 +125,10 @@ def _download(self, url_obj, asset_path):
             with FileLock(asset_path, 1):
                 shutil.copy(temp, asset_path)
                 self._create_hash_file(asset_path)
-                return self._verify_hash(asset_path)
+                if not self._verify_hash(asset_path):
+                    msg = "Hash mismatch. Ignoring asset from the cache"
+                    raise OSError(msg)
+                return True
         finally:
             try:
                 os.remove(temp)
@@ -167,20 +155,33 @@ def _get_hash_from_file(self, asset_path):
         :returns: the hash, if it exists.
         :rtype: str
         """
-        discovered = None
         hash_file = self._get_hash_file(asset_path)
         if not os.path.isfile(hash_file):
             self._create_hash_file(asset_path)
 
-        with open(hash_file, 'r') as hash_file:
-            for line in hash_file:
-                # md5 is 32 chars big and sha512 is 128 chars big.
-                # others supported algorithms are between those.
-                pattern = '%s [a-f0-9]{32,128}' % self.algorithm
-                if re.match(pattern, line):
-                    discovered = line.split()[1]
-                    break
-        return discovered
+        return Asset.read_hash_from_file(hash_file)[1]
+
+    @classmethod
+    def read_hash_from_file(cls, filename):
+        """Read the CHECKSUM file and return the hash.
+
+        This method raises a FileNotFoundError if file is missing and assumes
+        that filename is the CHECKSUM filename.
+
+        :rtype: list with algorithm and hash
+        """
+        try:
+            with FileLock(filename, 30):
+                with open(filename, 'r') as hash_file:
+                    for line in hash_file:
+                        # md5 is 32 chars big and sha512 is 128 chars big.
+                        # others supported algorithms are between those.
+                        if re.match('^.* [a-f0-9]{32,128}', line):
+                            return line.split()
+        except Exception:  # pylint: disable=W0703
+            exc_type, exc_value = sys.exc_info()[:2]
+            LOG.error('%s: %s', exc_type.__name__, exc_value)
+            return [None, None]
 
     def _get_local_file(self, url_obj, asset_path):
         """
@@ -225,12 +226,12 @@ def _get_relative_dir(self):
         :returns: target location of asset the file.
         :rtype: str
         """
-        if (not self.parsed_name.scheme and
+        if (not self.name_scheme and
                 (self.asset_hash or len(self.locations) > 1)):
             return 'by_name'
 
         # check if the URI is located on self.locations or self.parsed_name
-        if self.locations is not None:
+        if self.locations:
             # if it is on self.locations, we need to check if it has the
             # asset name on it or a trailing '/'
             if ((self.asset_name in self.locations[0]) or
@@ -283,6 +284,21 @@ def _is_expired(path, expire):
             return True
         return False
 
+    @classmethod
+    def _has_valid_hash(cls, asset_path, asset_hash=None):
+        """Checks if a file has a valid hash based on the hash parameter.
+
+        If asset_hash is None then will consider a valid asset.
+        """
+        if asset_hash is None:
+            return True
+
+        hash_path = cls._get_hash_file(asset_path)
+        _, hash_from_file = cls.read_hash_from_file(hash_path)
+        if hash_from_file == asset_hash:
+            return True
+        return False
+
     def _verify_hash(self, asset_path):
         """
         Verify if the `asset_path` hash matches the hash in the hash file.
@@ -292,10 +308,7 @@ def _verify_hash(self, asset_path):
         value as the hash of the asset_file, otherwise return False.
         :rtype: bool
         """
-        if self.asset_hash is None or (
-                self._get_hash_from_file(asset_path) == self.asset_hash):
-            return True
-        return False
+        return self._has_valid_hash(asset_path, self.asset_hash)
 
     def fetch(self):
         """
@@ -307,13 +320,9 @@ def fetch(self):
         :returns: The path for the file on the cache directory.
         :rtype: str
         """
-        urls = []
-        # If name is actually an url, it has to be included in urls list
-        if self.parsed_name.scheme:
-            urls.append(self.parsed_name.geturl())
-
         # First let's search for the file in each one of the cache locations
         asset_file = None
+        error = "unknown"
         try:
             asset_file = self.find_asset_file()
         except OSError:
@@ -329,17 +338,18 @@ def fetch(self):
         # writable cache directory will be used.
         cache_dir = self._get_writable_cache_dir()
         # Now we have a writable cache_dir. Let's get the asset.
-        # Adding the user defined locations to the urls list:
-        if self.locations is not None:
-            for item in self.locations:
-                urls.append(item)
-
-        for url in urls:
-            urlobj = urllib.parse.urlparse(url)
+        for url in self.urls:
+            if url is None:
+                continue
+            urlobj = urlparse(url)
             if urlobj.scheme in ['http', 'https', 'ftp']:
                 fetch = self._download
             elif urlobj.scheme == 'file':
                 fetch = self._get_local_file
+            # We are assuming that everything starting with './' or '/' are a
+            # file too.
+            elif url.startswith(('/', './')):
+                fetch = self._get_local_file
             else:
                 raise UnsupportedProtocolError("Unsupported protocol"
                                                ": %s" % urlobj.scheme)
@@ -356,8 +366,9 @@ def fetch(self):
             except Exception:  # pylint: disable=W0703
                 exc_type, exc_value = sys.exc_info()[:2]
                 LOG.error('%s: %s', exc_type.__name__, exc_value)
+                error = exc_value
 
-        raise OSError("Failed to fetch %s." % self.asset_name)
+        raise OSError("Failed to fetch %s (%s)." % (self.asset_name, error))
 
     def find_asset_file(self):
         """
@@ -372,19 +383,19 @@ def find_asset_file(self):
             cache_dir = os.path.expanduser(cache_dir)
             asset_file = os.path.join(cache_dir, self.relative_dir)
 
-            # To use a cached file, it must:
-            # - Exists.
-            # - Be valid (not expired).
-            # - Be verified (hash check).
-            if (os.path.isfile(asset_file) and
-                    not self._is_expired(asset_file, self.expire)):
-                try:
-                    with FileLock(asset_file, 30):
-                        if self._verify_hash(asset_file):
-                            return asset_file
-                except Exception:  # pylint: disable=W0703
-                    exc_type, exc_value = sys.exc_info()[:2]
-                    LOG.error('%s: %s', exc_type.__name__, exc_value)
+            # Ignore non-files
+            if not os.path.isfile(asset_file):
+                continue
+
+            # Ignore expired asset files
+            if self._is_expired(asset_file, self.expire):
+                continue
+
+            # Ignore mismatch hash
+            if not self._has_valid_hash(asset_file, self.asset_hash):
+                continue
+
+            return asset_file
 
         raise OSError("File %s not found in the cache." % self.asset_name)
 
@@ -406,3 +417,93 @@ def get_metadata(self):
             with open(metadata_file, "r") as f:
                 metadata = json.load(f)
                 return metadata
+
+    @property
+    def asset_name(self):
+        return os.path.basename(self.parsed_name.path)
+
+    @classmethod
+    def get_asset_by_name(cls, name, cache_dirs, expire=None, asset_hash=None):
+        """This method will return a cached asset based on name if exists.
+
+        You don't have to instantiate an object of Asset class. Just use this
+        method.
+
+        To be improved soon: cache_dirs should be not necessary.
+
+        :param name: the asset filename used during registration.
+        :param cache_dirs: list of directories to use during the search.
+        :param expire: time in seconds for the asset to expire. Expired assets
+                       will not be returned.
+        :param asset_hash: asset hash.
+
+        :return: asset path, if it exists in the cache.
+        :rtype: str
+        :raises: OSError
+        """
+
+        for cache_dir in cache_dirs:
+            asset_file = os.path.join(os.path.expanduser(cache_dir),
+                                      'by_name',
+                                      name)
+
+            # Ignore non-files
+            if not os.path.isfile(asset_file):
+                continue
+
+            # Ignore expired asset files
+            if cls._is_expired(asset_file, expire):
+                continue
+
+            # Ignore mismatch hash
+            if not cls._has_valid_hash(asset_file, asset_hash):
+                continue
+
+            return asset_file
+
+        raise OSError("File %s not found in the cache." % name)
+
+    @property
+    def name_scheme(self):
+        """This property will return the scheme part of the name if is an URL.
+
+        Otherwise, will return None.
+        """
+        parsed = self.parsed_name
+        if parsed:
+            return parsed.scheme
+
+    @property
+    def name_url(self):
+        """This property will return the full url of the name if is an URL.
+
+        Otherwise, will return None.
+        """
+        if self.name_scheme:
+            return self.parsed_name.geturl()
+
+    @staticmethod
+    def parse_name(name):
+        """Returns a ParseResult object for the given name."""
+        return urlparse(name)
+
+    @property
+    def parsed_name(self):
+        """Returns a ParseResult object for the currently set name."""
+        return self.parse_name(self.name)
+
+    @property
+    def relative_dir(self):
+        return os.path.join(self._get_relative_dir(), self.asset_name)
+
+    @property
+    def urls(self):
+        """Complete list of locations including name if is an URL."""
+        urls = []
+        if self.name_scheme:
+            urls.append(self.name_url)
+
+        if self.locations:
+            urls.extend(self.locations)
+
+        return urls
diff --git a/selftests/__init__.py b/selftests/__init__.py
--- a/selftests/__init__.py
+++ b/selftests/__init__.py
@@ -77,6 +77,7 @@ def get_temporary_config(module_name, klass, method):
                      'base_dir = %(base_dir)s\n'
                      'test_dir = %(test_dir)s\n'
                      'data_dir = %(data_dir)s\n'
+                     'cache_dirs = ["%(cache_dir)s"]\n'
                      'logs_dir = %(logs_dir)s\n') % mapping
     config_file = tempfile.NamedTemporaryFile('w', delete=False)
     config_file.write(temp_settings)


Test patch:
diff --git a/avocado/core/test.py b/avocado/core/test.py
--- a/avocado/core/test.py
+++ b/avocado/core/test.py
@@ -1011,27 +1011,38 @@ def fetch_asset(self, name, asset_hash=None, algorithm=None,
         if expire is not None:
             expire = data_structures.time_to_seconds(str(expire))
 
+        # If name has no protocol or network locations, attempt to find
+        # the asset "by name" first. This is valid use case when the
+        # asset has been previously put into any of the cache
+        # directories, either manually or by the caching process
+        # itself.
+        parsed_name = asset.Asset.parse_name(name)
+        if not (parsed_name.scheme or locations):
+            try:
+                return asset.Asset.get_asset_by_name(name,
+                                                     self.cache_dirs,
+                                                     expire,
+                                                     asset_hash)
+            except OSError as e:
+                if cancel_on_missing:
+                    self.cancel("Missing asset {}".format(name))
+                raise e
+
         asset_obj = asset.Asset(name, asset_hash, algorithm, locations,
                                 self.cache_dirs, expire)
 
-        missing_asset_message = 'Missing asset %s' % name
-
-        # decide whether we need to find only or fetch
-        if find_only:
-            asset_func = asset_obj.find_asset_file
-        else:
-            asset_func = asset_obj.fetch
-
         try:
             # return the path to the asset when it was found or fetched
-            asset_path = asset_func()
-            return asset_path
+            if find_only:
+                return asset_obj.find_asset_file()
+            else:
+                return asset_obj.fetch()
         except OSError as e:
             # if asset is not in the cache or there was a problem fetching
             # the asset
             if cancel_on_missing:
                 # cancel when requested
-                self.cancel(missing_asset_message)
+                self.cancel("Missing asset {}".format(name))
             # otherwise re-throw OSError
             raise e
 
diff --git a/selftests/functional/test_plugin_assets.py b/selftests/functional/test_plugin_assets.py
--- a/selftests/functional/test_plugin_assets.py
+++ b/selftests/functional/test_plugin_assets.py
@@ -11,7 +11,8 @@
 from avocado.core import exit_codes
 from avocado.utils import process
 
-from .. import AVOCADO, get_temporary_config
+from .. import (AVOCADO, TestCaseTmpDir, get_temporary_config,
+                skipUnlessPathExists)
 
 TEST_TEMPLATE = r"""
 from avocado import Test
@@ -43,7 +44,7 @@ def test_fetch_assets(self):
 """
 
 
-class AssetsFetchSuccess(unittest.TestCase):
+class AssetsFetchSuccess(TestCaseTmpDir):
     """
     Assets fetch with success functional test class
     """
@@ -89,6 +90,31 @@ def test_asset_fetch_success(self):
         self.assertEqual(expected_rc, result.exit_status)
         self.assertIn(expected_output, result.stdout_text)
 
+    def test_asset_register_by_name_fail(self):
+        """Test register command failure."""
+        url = "https://urlnotfound"
+        config = self.config_file.name
+        cmd_line = "%s --config %s assets register foo %s" % (AVOCADO,
+                                                              config,
+                                                              url)
+        result = process.run(cmd_line)
+
+        self.assertIn("Failed to fetch",
+                      result.stderr_text)
+
+    @skipUnlessPathExists('/etc/hosts')
+    def test_asset_register_by_name_success(self):
+        """Test register command success."""
+        url = "/etc/hosts"
+        config = self.config_file.name
+        cmd_line = "%s --config %s assets register hosts %s" % (AVOCADO,
+                                                                config,
+                                                                url)
+        result = process.run(cmd_line)
+
+        self.assertIn("Now you can reference it by name hosts",
+                      result.stdout_text)
+
     def tearDown(self):
         self.base_dir.cleanup()
 
@@ -192,7 +218,7 @@ def test_asset_fetch_invalid_url(self):
         test_file.write(test_content.encode())
         test_file.close()
 
-        expected_stderr = "Failed to fetch hello-2.9.tar.gz.\n"
+        expected_stderr = "Failed to fetch hello-2.9.tar.gz"
         expected_rc = exit_codes.AVOCADO_FAIL
 
         cmd_line = "%s --config %s assets fetch %s " % (AVOCADO,
@@ -219,7 +245,7 @@ def test_asset_fetch_ignore_errors(self):
         test_file.write(test_content.encode())
         test_file.close()
 
-        expected_stderr = "Failed to fetch hello-2.9.tar.gz.\n"
+        expected_stderr = "Failed to fetch hello-2.9.tar.gz"
         expected_rc = exit_codes.AVOCADO_ALL_OK
 
         cmd_line = "%s --config %s assets fetch --ignore-errors %s " % (
diff --git a/selftests/functional/test_utils_asset.py b/selftests/functional/test_utils_asset.py
--- a/selftests/functional/test_utils_asset.py
+++ b/selftests/functional/test_utils_asset.py
@@ -97,20 +97,6 @@ def test_fetch_expire(self):
             content2 = f.read()
         self.assertNotEqual(content1, content2)
 
-    def test_incorrect_name_locations_parameter_case1(self):
-        # 1. name is a full URI and locations is empty
-        with self.assertRaises(ValueError):
-            asset.Asset(name='file://bar.tgz', asset_hash=None, algorithm=None,
-                        locations='file://foo', cache_dirs=[self.cache_dir],
-                        expire=None)
-
-    def test_incorrect_name_locations_parameter_case2(self):
-        # 2. name is a single file name and locations is one or more entries.
-        with self.assertRaises(ValueError):
-            asset.Asset(name='bar.tgz', asset_hash=None, algorithm=None,
-                        locations=None, cache_dirs=[self.cache_dir],
-                        expire=None)
-
     def test_fetch_lockerror(self):
         dirname = os.path.join(self.cache_dir, 'by_name')
         os.makedirs(dirname)