This command will release one or more staging repositories in Nexus.
To ensure proper functionality, this commit also includes fixes for
lftools.nexus.cmd.get_credentials and lftools.cli.nexus.list_images.
Issue: RELENG-916
Change-Id: I5fffea04e160004485e09513834825f4c08b220d
Signed-off-by: Eric Ball <eball@linuxfoundation.org>
 ^^^^
 
 .. program-output:: lftools nexus docker list --help
+
+.. _nexus-release:
+
+release
+-------
+
+.. program-output:: lftools nexus release --help
 
     Use '*' for wildcard, or begin with '!' to search for images NOT matching
     the string.
     """
-    if not server and 'NEXUS_URL_ENV' in environ:
-        server = environ['NEXUS_URL_ENV']
+    if not server and NEXUS_URL_ENV in environ:
+        server = environ[NEXUS_URL_ENV]
     images = nexuscmd.search(settings, server, repo, pattern)
     if images:
         nexuscmd.output_images(images, csv)
     if yes or click.confirm("Would you like to delete all {} images?".format(
             str(len(images)))):
         nexuscmd.delete_images(settings, server, images)
+
+
+@nexus.command()
+@click.pass_context
+@click.argument('REPOS', type=str, nargs=-1)
+@click.option(
+    '-s', '--server', type=str,
+    help=('Nexus server URL. Can also be set as {} in the environment. '
+          'This will override any URL set in settings.yaml.').format(
+              NEXUS_URL_ENV))
+def release(ctx, repos, server):
+    """Release one or more staging repositories."""
+    if not server and NEXUS_URL_ENV in environ:
+        server = environ[NEXUS_URL_ENV]
+    nexuscmd.release_staging_repos(repos, server)
 
 import logging
 import sys
 
+import requests
+from six.moves import configparser
 import yaml
 
 from lftools import config
         elif set(['nexus', 'user', 'password']).issubset(settings):
             return settings
     elif url:
-        user = config.get_setting("global", "username")
-        password = config.get_setting("global", "password")
+        try:
+            user = config.get_setting("nexus", "username")
+            password = config.get_setting("nexus", "password")
+        except (configparser.NoOptionError,
+                configparser.NoSectionError):
+            return {"nexus": url, "user": "", "password": ""}
         return {"nexus": url, "user": user, "password": password}
     log.error('Please define a settings.yaml file, or include a url if using '
               + 'lftools.ini')
 
     for image in images:
         _nexus.delete_image(image)
+
+
+def release_staging_repos(repos, nexus_url=""):
+    """Release one or more staging repos.
+
+    :arg tuple repos: A tuple containing one or more repo name strings.
+    :arg str nexus_url: Optional URL of target Nexus server.
+    """
+    credentials = get_credentials(None, nexus_url)
+    _nexus = Nexus(credentials['nexus'], credentials['user'],
+                   credentials['password'])
+
+    for repo in repos:
+        data = {"data": {"stagedRepositoryIds": [repo]}}
+        log.debug("Sending data: {}".format(data))
+        request_url = "{}/staging/bulk/promote".format(_nexus.baseurl)
+        log.debug("Request URL: {}".format(request_url))
+        response = requests.post(request_url, json=data)
+
+        if response.status_code != 201:
+            raise requests.HTTPError("Release failed with the following error:"
+                                     "\n{}: {}".format(response.status_code,
+                                                       response.text))
+        else:
+            log.debug("Successfully released {}".format(str(repo)))
+
+        request_url = "{}/staging/bulk/drop".format(_nexus.baseurl)
+        log.debug("Request URL: {}".format(request_url))
+        response = requests.post(request_url, json=data)
+
+        if response.status_code != 201:
+            raise requests.HTTPError("Drop failed with the following error:"
+                                     "\n{}: {}".format(response.status_code,
+                                                       response.text))
+        else:
+            log.debug("Successfully dropped {}".format(str(repo)))
 
--- /dev/null
+---
+features:
+  - |
+    Add Nexus command to release one or more staging repositories. Via the
+    Nexus 2 REST API, this command performs both a "release" and a "drop"
+    action on the repo(s), in order to best reproduce the action of manually
+    using the "Release" option in the Nexus UI.
+
+    Usage: lftools nexus release [OPTIONS] [REPOS]...
+
+    Options:
+      -s, --server TEXT  Nexus server URL. Can also be set as NEXUS_URL in the
+                         environment. This will override any URL set in
+                         settings.yaml.
 
 """Test nexus command."""
 
 import re
+import requests
 
 import pytest
 
+from lftools.nexus import cmd
 from lftools.nexus import util
 
 
     vpp = util.create_repo_target_regex('io.fd.vpp')
     vpp_regex = re.compile(vpp)
     assert vpp_regex.match('/io/fd/vpp/jvpp/16.06/jvpp-16.06.jar')
+
+
+def test_release_staging_repos(responses):
+    """Test release_staging_repos() command."""
+    good_url = "https://nexus.example.org"
+    # Prepare response for Nexus initialization
+    responses.add(responses.GET,
+                  "{}/service/local/repo_targets".format(good_url),
+                  json=None, status=200)
+    # The responses provide implicit assertions.
+    responses.add(responses.POST, "{}/service/local/staging/bulk/promote".format(good_url),
+                  json=None, status=201)
+    responses.add(responses.POST, "{}/service/local/staging/bulk/drop".format(good_url),
+                  json=None, status=201)
+
+    # Test successful single release.
+    cmd.release_staging_repos(("release-1",), good_url)
+
+    # Test successful multiple release.
+    cmd.release_staging_repos(("release-1", "release-2", "release-3"),
+                              good_url)
+
+    # Test promote failure
+    bad_url1 = "https://nexus-fail1.example.org"
+    responses.add(responses.GET,
+                  "{}/service/local/repo_targets".format(bad_url1),
+                  json=None, status=200)
+    responses.add(responses.POST,
+                  "{}/service/local/staging/bulk/promote".format(bad_url1),
+                  status=401)
+    with pytest.raises(requests.HTTPError):
+        cmd.release_staging_repos(("release-1",), bad_url1)
+
+    # Test drop failure
+    bad_url2 = "https://nexus-fail2.example.org"
+    responses.add(responses.GET,
+                  "{}/service/local/repo_targets".format(bad_url2),
+                  json=None, status=200)
+    responses.add(responses.POST,
+                  "{}/service/local/staging/bulk/promote".format(bad_url2),
+                  status=201)
+    responses.add(responses.POST,
+                  "{}/service/local/staging/bulk/drop".format(bad_url2),
+                  status=403)
+    with pytest.raises(requests.HTTPError):
+        cmd.release_staging_repos(("release-1",), bad_url2)