Refactor Nexus stage repo close|create cmds 36/13236/26
authorBengt Thuree <bthuree@linuxfoundation.org>
Fri, 2 Nov 2018 00:51:38 +0000 (11:51 +1100)
committerBengt Thuree <bthuree@linuxfoundation.org>
Mon, 12 Nov 2018 09:28:12 +0000 (20:28 +1100)
This is part of the work to convert the existing SHELL
scripts to Python scripts.

Nexus stage repo close
Nexus stage repo create

Issue: RELENG-1371
Change-Id: If93fda483813e2acfc3b283b08cd2de90a467629
Signed-off-by: Bengt Thuree <bthuree@linuxfoundation.org>
lftools/cli/deploy.py
lftools/deploy.py
releasenotes/notes/refactor-deploy-stage-create-close-7b3fcc911023a318.yaml [new file with mode: 0644]
requirements.txt
tests/test_deploy.py

index 224a77b..fa35bde 100644 (file)
@@ -274,13 +274,9 @@ def nexus_stage(ctx, nexus_url, staging_profile_id, deploy_dir):
 @click.pass_context
 def nexus_stage_repo_close(ctx, nexus_url, staging_profile_id, staging_repo_id):
     """Close a Nexus staging repo."""
-    status = subprocess.call([
-        'deploy', 'nexus-stage-repo-close',
-        nexus_url,
-        staging_profile_id,
-        staging_repo_id
-    ])
-    sys.exit(status)
+    deploy_sys.nexus_stage_repo_close(nexus_url,
+                                      staging_profile_id,
+                                      staging_repo_id)
 
 
 @click.command(name='nexus-stage-repo-create')
@@ -289,12 +285,9 @@ def nexus_stage_repo_close(ctx, nexus_url, staging_profile_id, staging_repo_id):
 @click.pass_context
 def nexus_stage_repo_create(ctx, nexus_url, staging_profile_id):
     """Create a Nexus staging repo."""
-    status = subprocess.call([
-        'deploy', 'nexus-stage-repo-create',
-        nexus_url,
-        staging_profile_id
-    ])
-    sys.exit(status)
+    staging_repo_id = deploy_sys.nexus_stage_repo_create(nexus_url,
+                                                         staging_profile_id)
+    log.info(staging_repo_id)
 
 
 @click.command(name='nexus-zip')
index 108652f..eb6058e 100644 (file)
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # SPDX-License-Identifier: EPL-1.0
 ##############################################################################
 # Copyright (c) 2017 The Linux Foundation and others.
@@ -19,6 +20,7 @@ import sys
 import tempfile
 import zipfile
 
+from defusedxml.minidom import parseString
 import glob2  # Switch to glob when Python < 3.5 support is dropped
 import requests
 
@@ -72,14 +74,29 @@ def _request_post(url, data, headers):
     try:
         resp = requests.post(url, data=data, headers=headers)
     except requests.exceptions.MissingSchema:
+        log.debug("in _request_post. MissingSchema")
         _log_error_and_exit("Not valid URL: {}".format(url))
     except requests.exceptions.ConnectionError:
+        log.debug("in _request_post. ConnectionError")
         _log_error_and_exit("Could not connect to URL: {}".format(url))
     except requests.exceptions.InvalidURL:
+        log.debug("in _request_post. InvalidURL")
         _log_error_and_exit("Invalid URL: {}".format(url))
     return resp
 
 
+def _get_node_from_xml(xml_data, tag_name):
+    """Extract tag data from xml data."""
+    log.debug('xml={}'.format(xml_data))
+
+    try:
+        dom1 = parseString(xml_data)
+        childnode = dom1.getElementsByTagName(tag_name)[0]
+    except:
+        _log_error_and_exit("Received bad XML, can not find tag {}".format(tag_name), xml_data)
+    return childnode.firstChild.data
+
+
 def copy_archives(workspace, pattern=None):
     """Copy files matching PATTERN in a WORKSPACE to the current directory.
 
@@ -288,3 +305,106 @@ def deploy_nexus_zip(nexus_url, nexus_repo, nexus_path, zip_file):
     if not str(resp.status_code).startswith('20'):
         raise requests.HTTPError("Failed to upload to Nexus with status code: {}.\n{}\n{}".format(
             resp.status_code, resp.text, zipfile.ZipFile(zip_file).infolist()))
+
+
+def nexus_stage_repo_create(nexus_url, staging_profile_id):
+    """Create a Nexus staging repo.
+
+    Parameters:
+    nexus_url:           URL to Nexus server. (Ex: https://nexus.example.org)
+    staging_profile_id:  The staging profile id as defined in Nexus for the
+                         staging repo.
+
+    Returns:             staging_repo_id
+
+    Sample:
+    lftools deploy nexus-stage-repo-create 192.168.1.26:8081/nexsus/ 93fb68073c18
+    """
+    nexus_url = '{0}/service/local/staging/profiles/{1}/start'.format(
+        _format_url(nexus_url),
+        staging_profile_id)
+
+    log.debug("Nexus URL           = {}".format(nexus_url))
+
+    xml = """
+        <promoteRequest>
+            <data>
+                <description>Create staging repository.</description>
+            </data>
+        </promoteRequest>
+    """
+
+    headers = {'Content-Type': 'application/xml'}
+    resp = _request_post(nexus_url, xml, headers)
+
+    log.debug("resp.status_code = {}".format(resp.status_code))
+    log.debug("resp.text = {}".format(resp.text))
+
+    if re.search('nexus-error', resp.text):
+        error_msg = _get_node_from_xml(resp.text, 'msg')
+        if re.search('.*profile with id:.*does not exist.', error_msg):
+            _log_error_and_exit("Staging profile id {} not found.".format(staging_profile_id))
+        _log_error_and_exit(error_msg)
+
+    if resp.status_code == 405:
+        _log_error_and_exit("HTTP method POST is not supported by this URL", nexus_url)
+    if resp.status_code == 404:
+        _log_error_and_exit("Did not find nexus site: {}".format(nexus_url))
+    if not resp.status_code == 201:
+        _log_error_and_exit("Failed with status code {}".format(resp.status_code), resp.text)
+
+    staging_repo_id = _get_node_from_xml(resp.text, 'stagedRepositoryId')
+    log.debug("staging_repo_id = {}".format(staging_repo_id))
+
+    return staging_repo_id
+
+
+def nexus_stage_repo_close(nexus_url, staging_profile_id, staging_repo_id):
+    """Close a Nexus staging repo.
+
+    Parameters:
+    nexus_url:          URL to Nexus server. (Ex: https://nexus.example.org)
+    staging_profile_id: The staging profile id as defined in Nexus for the
+                        staging repo.
+    staging_repo_id:    The ID of the repo to close.
+
+    Sample:
+    lftools deploy nexus-stage-repo-close 192.168.1.26:8081/nexsus/ 93fb68073c18 test1-1031
+    """
+    nexus_url = '{0}/service/local/staging/profiles/{1}/finish'.format(
+        _format_url(nexus_url),
+        staging_profile_id)
+
+    log.debug("Nexus URL           = {}".format(nexus_url))
+    log.debug("staging_repo_id     = {}".format(staging_repo_id))
+
+    xml = """
+        <promoteRequest>
+            <data>
+                <stagedRepositoryId>{0}</stagedRepositoryId>
+                <description>Close staging repository.</description>
+            </data>
+        </promoteRequest>
+    """.format(staging_repo_id)
+
+    headers = {'Content-Type': 'application/xml'}
+    resp = _request_post(nexus_url, xml, headers)
+
+    log.debug("resp.status_code = {}".format(resp.status_code))
+    log.debug("resp.text = {}".format(resp.text))
+
+    if re.search('nexus-error', resp.text):
+        error_msg = _get_node_from_xml(resp.text, 'msg')
+    else:
+        error_msg = resp.text
+
+    if resp.status_code == 404:
+        _log_error_and_exit("Did not find nexus site: {}".format(nexus_url))
+
+    if re.search('invalid state: closed', error_msg):
+        _log_error_and_exit("Staging repository is already closed.")
+    if re.search('Missing staging repository:', error_msg):
+        _log_error_and_exit("Staging repository do not exist.")
+
+    if not resp.status_code == 201:
+        _log_error_and_exit("Failed with status code {}".format(resp.status_code), resp.text)
diff --git a/releasenotes/notes/refactor-deploy-stage-create-close-7b3fcc911023a318.yaml b/releasenotes/notes/refactor-deploy-stage-create-close-7b3fcc911023a318.yaml
new file mode 100644 (file)
index 0000000..bdcd465
--- /dev/null
@@ -0,0 +1,11 @@
+---
+features:
+  - |
+    Refactored nexus_stage_repo_close(), and nexus_repo_stage_create() function
+    from shell/deploy to pure Python to be more portable with Windows systems.
+    Also added a number of unit tests to cover all executable branches of the
+    code.
+deprecations:
+  - |
+    shell/deploy script's nexus_stage_repo_close() and nexus_stage_repo_create()
+    function is now deprecated and will be removed in a future release.
index e447727..a1a78ce 100644 (file)
@@ -1,5 +1,6 @@
 click
 glob2  # Needed for Python < 3.5 recursive glob support
+defusedxml # Needed due to tox complains on parseString not safe
 pyyaml
 requests~=2.18.0
 ruamel.yaml
index be790b9..af7b4c7 100644 (file)
@@ -18,12 +18,20 @@ import requests
 from lftools import cli
 import lftools.deploy as deploy_sys
 
+
 FIXTURE_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
     'fixtures',
     )
 
 
+def test_log_and_exit():
+    """Test exit."""
+    with pytest.raises(SystemExit) as excinfo:
+        deploy_sys._log_error_and_exit("testmsg")
+    assert excinfo.type == SystemExit
+
+
 def test_format_url():
     """Test url format."""
     test_url=[["192.168.1.1", "http://192.168.1.1"],
@@ -65,6 +73,7 @@ def test_copy_archive_dir(cli_runner, datafiles):
 
     assert os.path.exists(os.path.join(stage_dir, 'test.log'))
 
+
 @pytest.mark.datafiles(
     os.path.join(FIXTURE_DIR, 'deploy'),
     )
@@ -88,6 +97,7 @@ def test_copy_archive_pattern(cli_runner, datafiles):
         stage_dir, 'aaa', 'aaa-cert', 'target', 'surefire-reports',
         'org.opendaylight.aaa.cert.test.AaaCertMdsalProviderTest-output.txt'))
 
+
 @pytest.mark.datafiles(
     os.path.join(FIXTURE_DIR, 'deploy'),
     )
@@ -138,6 +148,7 @@ def test_deploy_logs(cli_runner, datafiles, responses):
         obj={})
     assert result.exit_code == 0
 
+
 @pytest.mark.datafiles(
     os.path.join(FIXTURE_DIR, 'deploy'),
     )
@@ -194,58 +205,218 @@ def test_deploy_nexus_zip(cli_runner, datafiles, responses):
         obj={})
     assert result.exit_code == 1
 
-def mocked_log_error(msg1=None, msg2=None):
-    """Mock local_log_error_and_exit function.
+
+def test_get_node_from_xml():
+    """Test extracting from xml."""
+    document = """\
+        <slideshow>
+        <title>Demo slideshow</title>
+        <slide><title>Slide title</title>
+        <point>This is a demo</point>
+        <point>Of a program for processing slides</point>
+        </slide>
+
+        <slide><title>Another demo slide</title><stagedRepositoryId>432</stagedRepositoryId><point>It is important</point>
+        <point>To have more than</point>
+        <point>one slide</point>
+        </slide>
+        </slideshow>
+        """
+    assert deploy_sys._get_node_from_xml(document, 'stagedRepositoryId') == '432'
+    with pytest.raises(SystemExit) as excinfo:
+        deploy_sys._get_node_from_xml(document, 'NotFoundTag')
+    assert excinfo.type == SystemExit
+
+
+def mocked_log_error(*msg_list):
+    """Mock _log_error_and_exit function.
     This function is modified to simply raise an Exception.
     The original will print msg1 & msg2, then call sys.exit(1)."""
+    msg1=msg_list[0]
     if 'Could not connect to URL:' in msg1:
         raise ValueError('connection_error')
     if 'Invalid URL:' in msg1:
         raise ValueError('invalid_url')
     if 'Not valid URL:' in msg1:
         raise ValueError('missing_schema')
+    if "profile with id 'INVALID' does not exist" in msg1:
+        raise ValueError('profile.id.not.exist')
+    if "OTHER create error" in msg1:
+        raise ValueError('other.create.error')
+    if "HTTP method POST is not supported by this URL" in msg1:
+        raise ValueError('post.not.supported')
+    if "Did not find nexus site" in msg1:
+        raise ValueError('site.not.found')
+    if "Failed with status code " in msg1:
+        raise ValueError('other.error.occured')
+    if "Staging repository do not exist." in msg1:
+        raise ValueError('missing.staging.repository')
+    if "Staging repository is already closed." in msg1:
+        raise ValueError('staging.already.closed')
     raise ValueError('fail')
 
 
-def mocked_requests_post(*args, **kwargs):
-    """Mock requests.post function."""
-    class MockResponse:
-        def __init__(self, json_data, status_code):
-            self.json_data = json_data
-            self.status_code = status_code
-            self.text = json_data
+def test__request_post(responses, mocker):
+    """Test _request_post."""
+    mocker.patch('lftools.deploy._log_error_and_exit', side_effect=mocked_log_error)
+    xml_doc="""
+        <promoteRequest><data>
+            <stagedRepositoryId>test1-1027</stagedRepositoryId>
+            <description>Close staging repository.</description>
+        </data></promoteRequest>
+        """
+    headers = {'Content-Type': 'application/xml'}
+
+    test_url='http://connection.error.test'
+    exception = requests.exceptions.ConnectionError(test_url)
+    responses.add(responses.POST, test_url, body=exception)
+    with pytest.raises(ValueError) as excinfo:
+        deploy_sys._request_post(test_url, xml_doc, headers)
+    assert 'connection_error' in str(excinfo.value)
 
-        def json(self):
-            return self.json_data
+    test_url='http://invalid.url.test:8081'
+    exception = requests.exceptions.InvalidURL(test_url)
+    responses.add(responses.POST, test_url, body=exception)
+    with pytest.raises(ValueError) as excinfo:
+        deploy_sys._request_post(test_url, xml_doc, headers)
+    assert 'invalid_url' in str(excinfo.value)
 
-    if 'connection.error.test' in args[0]:
-        raise requests.exceptions.ConnectionError
-    if 'invalid.url.test' in args[0]:
-        raise requests.exceptions.InvalidURL
-    if 'missing.schema.test' in args[0]:
-        raise requests.exceptions.MissingSchema
-    return MockResponse(None, 404)
+    test_url='http://missing.schema.test:8081'
+    exception = requests.exceptions.MissingSchema(test_url)
+    responses.add(responses.POST, test_url, body=exception)
+    with pytest.raises(ValueError) as excinfo:
+        deploy_sys._request_post(test_url, xml_doc, headers)
+    assert 'missing_schema' in str(excinfo.value)
 
 
-def test_local_request_post(mocker):
-    """Test local_request_post."""
-    mocker.patch('requests.post', side_effect=mocked_requests_post)
+def test_nexus_stage_repo_close(responses, mocker):
+    """Test nexus_stage_repo_close."""
     mocker.patch('lftools.deploy._log_error_and_exit', side_effect=mocked_log_error)
+    url='service/local/staging/profiles'
+
+    responses.add(responses.POST, 'http://valid.create.post/{}/{}/finish'.format(url, '93fb68073c18' ),
+                  body=None, status=201)
+    deploy_sys.nexus_stage_repo_close('valid.create.post', '93fb68073c18', 'test1-1027')
+
+    xml_site_not_found = """
+        <html><head><title>404 - Site Not Found</title></head>
+            <body><h1>404 - Site not found</h1></body>
+        </html>
+        """
+    responses.add(responses.POST, 'http://site.not.found/{}/{}/finish'.format(url, 'INVALID'),
+                  body=xml_site_not_found, status=404)
+    with pytest.raises(ValueError) as excinfo:
+         deploy_sys.nexus_stage_repo_close('site.not.found', 'INVALID', 'test1-1027')
+    assert 'site.not.found' in str(excinfo.value)
+
+    xml_missing_staging_repository = """
+        <nexus-error><errors><error>
+            <id>*</id>
+            <msg>Unhandled: Missing staging repository: test1-1</msg>
+        </error></errors></nexus-error>
+        """
+    responses.add(responses.POST, 'http://missing.staging.repository/{}/{}/finish'.format(url, 'INVALID'),
+                  body=xml_missing_staging_repository, status=500)
+    with pytest.raises(ValueError) as excinfo:
+         deploy_sys.nexus_stage_repo_close('missing.staging.repository', 'INVALID', 'test1-1027')
+    assert 'missing.staging.repository' in str(excinfo.value)
+
+    xml_staging_already_closed = """
+        <nexus-error><errors><error>
+            <id>*</id>
+            <msg>Unhandled: Repository: test1-1000 has invalid state: closed</msg>
+        </error></errors></nexus-error>
+        """
+    responses.add(responses.POST, 'http://staging.already.closed/{}/{}/finish'.format(url, 'INVALID'),
+                  body=xml_staging_already_closed, status=500)
+    with pytest.raises(ValueError) as excinfo:
+        deploy_sys.nexus_stage_repo_close('staging.already.closed', 'INVALID',  'test1-1027')
+    assert 'staging.already.closed' in str(excinfo.value)
+
+    xml_other_error_occured = """
+        <html><head><title>303 - See Other</title></head>
+            <body><h1>303 - See Other</h1></body>
+        </html>
+        """
+    responses.add(responses.POST, 'http://other.error.occured/{}/{}/finish'.format(url, 'INVALID'),
+                  body=xml_other_error_occured, status=303)
+    with pytest.raises(ValueError) as excinfo:
+        deploy_sys.nexus_stage_repo_close('other.error.occured', 'INVALID', 'test1-1027')
+    assert 'other.error.occured' in str(excinfo.value)
 
-    xml_doc='''
-        <promoteRequest>
-            <data>
-                <stagedRepositoryId>test1-1027</stagedRepositoryId>
-                <description>Close staging repository.</description>
-            </data>
-        </promoteRequest>
-        '''
+
+def test_nexus_stage_repo_create(responses, mocker):
+    """Test nexus_stage_repo_create."""
+    mocker.patch('lftools.deploy._log_error_and_exit', side_effect=mocked_log_error)
+    url = 'service/local/staging/profiles'
+
+    xml_created = "<stagedRepositoryId>test1-1030</stagedRepositoryId>"
+    responses.add(responses.POST, 'http://valid.create.post/{}/{}/start'.format(url, '93fb68073c18' ),
+                  body=xml_created, status=201)
+    res = deploy_sys.nexus_stage_repo_create('valid.create.post', '93fb68073c18')
+    assert res == 'test1-1030'
+
+    xml_profile_id_dont_exist = """
+        <nexus-error><errors><error>
+            <id>*</id>
+            <msg>Cannot create Staging Repository, profile with id &apos;INVALID&apos; does not exist.</msg>
+        </error></errors></nexus-error>
+        """
+    responses.add(responses.POST, 'http://profile.id_not.exist/{}/{}/start'.format(url, 'INVALID' ),
+                  body=xml_profile_id_dont_exist, status=404)
     with pytest.raises(ValueError) as excinfo:
-        deploy_sys._request_post('connection.error.test', xml_doc, "{'Content-Type': 'application/xml'}")
-    assert 'connection_error' in str(excinfo.value)
+        res = deploy_sys.nexus_stage_repo_create('profile.id_not.exist', 'INVALID')
+    assert 'profile.id.not.exist' in str(excinfo.value)
+
+    xml_other_create_error = "<nexus-error><errors><error><id>*</id><msg>OTHER create error.</msg></error></errors></nexus-error>"
+    responses.add(responses.POST, 'http://other.create.error/{}/{}/start'.format(url, 'INVALID' ),
+                  body=xml_other_create_error, status=404)
     with pytest.raises(ValueError) as excinfo:
-        deploy_sys._request_post('invalid.url.test:8081nexus', xml_doc, "{'Content-Type': 'application/xml'}")
-    assert 'invalid_url' in str(excinfo.value)
+        res = deploy_sys.nexus_stage_repo_create('other.create.error', 'INVALID')
+    assert 'other.create.error' in str(excinfo.value)
+
+    xml_other_error_occured = """
+        <html>
+            <head><title>303 - See Other</title></head>
+            <body><h1>303 - See Other</h1></body>
+        </html>
+        """
+    responses.add(responses.POST, 'http://other.error.occured/{}/{}/start'.format(url, 'INVALID' ),
+                  body=xml_other_error_occured, status=303)
     with pytest.raises(ValueError) as excinfo:
-        deploy_sys._request_post('http:/missing.schema.test:8081nexus', xml_doc, "{'Content-Type': 'application/xml'}")
-    assert 'missing_schema' in str(excinfo.value)
+        res = deploy_sys.nexus_stage_repo_create('other.error.occured', 'INVALID')
+    assert 'other.error.occured' in str(excinfo.value)
+
+    xml_post_not_supported = """
+        <html>
+            <head>
+                <title>405 - HTTP method POST is not supported by this URL</title>
+                <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+                <link rel="icon" type="image/png" href="http://192.168.1.26:8081/nexus/favicon.png">
+                <!--[if IE]>
+                <link rel="SHORTCUT ICON" href="http://192.168.1.26:8081/nexus/favicon.ico"/>
+                <![endif]-->
+                <link rel="stylesheet" href="http://192.168.1.26:8081/nexus/static/css/Sonatype-content.css?2.14.10-01" type="text/css" media="screen" title="no title" charset="utf-8">
+            </head>
+            <body>
+                <h1>405 - HTTP method POST is not supported by this URL</h1>
+                <p>HTTP method POST is not supported by this URL</p>
+            </body>
+        </html>
+        """
+    responses.add(responses.POST, 'http://post.not.supported/{}/{}/start'.format(url, 'INVALID' ),
+                  body=xml_post_not_supported, status=405)
+    with pytest.raises(ValueError) as excinfo:
+         res = deploy_sys.nexus_stage_repo_create('post.not.supported', 'INVALID')
+    assert 'post.not.supported' in str(excinfo.value)
+
+    xml_site_not_found = """
+        <html><head><title>404 - Site Not Found</title></head>
+            <body><h1>404 - Site not found</h1></body>
+        </html>
+        """
+    responses.add(responses.POST, 'http://site.not.found/{}/{}/start'.format(url, 'INVALID' ),
+                  body=xml_site_not_found, status=404)
+    with pytest.raises(ValueError) as excinfo:
+        res = deploy_sys.nexus_stage_repo_create('site.not.found', 'INVALID')
+    assert 'site.not.found' in str(excinfo.value)