From 8aa95360e93db3d8122920313786794215a158eb Mon Sep 17 00:00:00 2001 From: Bengt Thuree Date: Fri, 2 Nov 2018 11:51:38 +1100 Subject: [PATCH] Refactor Nexus stage repo close|create cmds 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 --- lftools/cli/deploy.py | 19 +- lftools/deploy.py | 120 ++++++++++ ...deploy-stage-create-close-7b3fcc911023a318.yaml | 11 + requirements.txt | 1 + tests/test_deploy.py | 241 ++++++++++++++++++--- 5 files changed, 344 insertions(+), 48 deletions(-) create mode 100644 releasenotes/notes/refactor-deploy-stage-create-close-7b3fcc911023a318.yaml diff --git a/lftools/cli/deploy.py b/lftools/cli/deploy.py index 224a77b1..fa35bdee 100644 --- a/lftools/cli/deploy.py +++ b/lftools/cli/deploy.py @@ -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') diff --git a/lftools/deploy.py b/lftools/deploy.py index 108652f6..eb6058e3 100644 --- a/lftools/deploy.py +++ b/lftools/deploy.py @@ -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 = """ + + + Create staging repository. + + + """ + + 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 = """ + + + {0} + Close staging repository. + + + """.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 index 00000000..bdcd4653 --- /dev/null +++ b/releasenotes/notes/refactor-deploy-stage-create-close-7b3fcc911023a318.yaml @@ -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. diff --git a/requirements.txt b/requirements.txt index e447727d..a1a78ce8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/test_deploy.py b/tests/test_deploy.py index be790b9a..af7b4c72 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -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 = """\ + + Demo slideshow + Slide title + This is a demo + Of a program for processing slides + + + Another demo slide432It is important + To have more than + one slide + + + """ + 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=""" + + test1-1027 + Close staging repository. + + """ + 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 = """ + 404 - Site Not Found +

404 - Site not found

+ + """ + 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 = """ + + * + Unhandled: Missing staging repository: test1-1 + + """ + 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 = """ + + * + Unhandled: Repository: test1-1000 has invalid state: closed + + """ + 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 = """ + 303 - See Other +

303 - See Other

+ + """ + 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=''' - - - test1-1027 - Close staging repository. - - - ''' + +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 = "test1-1030" + 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 = """ + + * + Cannot create Staging Repository, profile with id 'INVALID' does not exist. + + """ + 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 = "*OTHER create 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 = """ + + 303 - See Other +

303 - See Other

+ + """ + 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 = """ + + + 405 - HTTP method POST is not supported by this URL + + + + + + +

405 - HTTP method POST is not supported by this URL

+

HTTP method POST is not supported by this URL

+ + + """ + 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 = """ + 404 - Site Not Found +

404 - Site not found

+ + """ + 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) -- 2.16.6