summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Trowbridge <trown@redhat.com>2018-05-11 16:07:16 -0400
committerJohn Trowbridge <trown@redhat.com>2018-05-18 14:35:41 +0000
commit53e40920386f337ee2a1fcca4cfc3ea928836fa2 (patch)
treed6d52a96c6cd712f90b61a7cce1804be888168fb
parent7ca3fcd68583d37e4e2270944582e5152a73c4f0 (diff)
Add CLI argument parser and YAML file parser
This adds a CLI interface with the following options: --stable-release --featureset-file --output-file --log-file This also adds the code and tests for the YAML file parsing. I tried adding a specific error case for when the YAML file fails to parse vs the more generic case when the file just cannot be opened. However, mock would not behave for testing the specific case. We log and raise the exception in both cases though, and the tests cover both as well. Change-Id: I0834a0e9b3193c664b377ae7e066fe15239bbfb1
Notes
Notes (review): Code-Review+2: wes hayutin <weshayutin@gmail.com> Code-Review+2: Ronelle Landy <rlandy@redhat.com> Workflow+1: Ronelle Landy <rlandy@redhat.com> Verified-1: RDO Third Party CI <dmsimard+rdothirdparty@redhat.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Sat, 19 May 2018 00:34:17 +0000 Reviewed-on: https://review.openstack.org/567936 Project: openstack-infra/tripleo-ci Branch: refs/heads/master
-rw-r--r--scripts/emit_releases_file/emit_releases_file.py89
-rw-r--r--scripts/emit_releases_file/test_yaml_parsing.py78
-rw-r--r--test-requirements.txt1
3 files changed, 149 insertions, 19 deletions
diff --git a/scripts/emit_releases_file/emit_releases_file.py b/scripts/emit_releases_file/emit_releases_file.py
index c0479cf..7efe7a7 100644
--- a/scripts/emit_releases_file/emit_releases_file.py
+++ b/scripts/emit_releases_file/emit_releases_file.py
@@ -1,6 +1,10 @@
1import argparse
1import logging 2import logging
3import logging.handlers
4import os
2import re 5import re
3import requests 6import requests
7import yaml
4 8
5# Define releases 9# Define releases
6RELEASES = ['newton', 'ocata', 'pike', 'queens', 'master'] 10RELEASES = ['newton', 'ocata', 'pike', 'queens', 'master']
@@ -13,7 +17,30 @@ def get_relative_release(release, relative_idx):
13 return RELEASES[absolute_idx] 17 return RELEASES[absolute_idx]
14 18
15 19
20def setup_logging(log_file):
21 '''Setup logging for the script'''
22 logger = logging.getLogger('emit-releases')
23 logger.setLevel(logging.DEBUG)
24 log_handler = logging.handlers.WatchedFileHandler(
25 os.path.expanduser(log_file))
26 logger.addHandler(log_handler)
27
28
29def load_featureset_file(featureset_file):
30 logger = logging.getLogger('emit-releases')
31 try:
32 with open(featureset_file, 'r') as stream:
33 featureset = yaml.safe_load(stream)
34 except Exception as e:
35 logger.error("The featureset file: {} can not be "
36 "opened.".format(featureset_file))
37 logger.exception(e)
38 raise e
39 return featureset
40
41
16def get_dlrn_hash(release, hash_name, retries=10): 42def get_dlrn_hash(release, hash_name, retries=10):
43 logger = logging.getLogger('emit-releases')
17 full_hash_pattern = re.compile('[a-z,0-9]{40}_[a-z,0-9]{8}') 44 full_hash_pattern = re.compile('[a-z,0-9]{40}_[a-z,0-9]{8}')
18 repo_url = ('https://trunk.rdoproject.org/centos7-%s/%s/delorean.repo' 45 repo_url = ('https://trunk.rdoproject.org/centos7-%s/%s/delorean.repo'
19 % (release, hash_name)) 46 % (release, hash_name))
@@ -25,7 +52,7 @@ def get_dlrn_hash(release, hash_name, retries=10):
25 try: 52 try:
26 repo_file = requests.get(repo_url, timeout=(3.05, 27)) 53 repo_file = requests.get(repo_url, timeout=(3.05, 27))
27 except Exception as e: 54 except Exception as e:
28 # TODO(trown): Handle exceptions 55 logger.exception(e)
29 pass 56 pass
30 else: 57 else:
31 if repo_file is not None and repo_file.ok: 58 if repo_file is not None and repo_file.ok:
@@ -40,7 +67,7 @@ def get_dlrn_hash(release, hash_name, retries=10):
40 67
41 68
42def compose_releases_dictionary(stable_release, featureset): 69def compose_releases_dictionary(stable_release, featureset):
43 70 logger = logging.getLogger('emit-releases')
44 if stable_release not in RELEASES: 71 if stable_release not in RELEASES:
45 raise RuntimeError("The {} release is not supported by this tool" 72 raise RuntimeError("The {} release is not supported by this tool"
46 "Supported releases: {}".format( 73 "Supported releases: {}".format(
@@ -81,29 +108,29 @@ def compose_releases_dictionary(stable_release, featureset):
81 108
82 if featureset.get('mixed_upgrade'): 109 if featureset.get('mixed_upgrade'):
83 if featureset.get('overcloud_upgrade'): 110 if featureset.get('overcloud_upgrade'):
84 logging.info('Doing an overcloud upgrade') 111 logger.info('Doing an overcloud upgrade')
85 deploy_release = get_relative_release(stable_release, -1) 112 deploy_release = get_relative_release(stable_release, -1)
86 releases_dictionary['overcloud_deploy_release'] = deploy_release 113 releases_dictionary['overcloud_deploy_release'] = deploy_release
87 114
88 elif featureset.get('ffu_overcloud_upgrade'): 115 elif featureset.get('ffu_overcloud_upgrade'):
89 logging.info('Doing an overcloud fast forward upgrade') 116 logger.info('Doing an overcloud fast forward upgrade')
90 deploy_release = get_relative_release(stable_release, -3) 117 deploy_release = get_relative_release(stable_release, -3)
91 releases_dictionary['overcloud_deploy_release'] = deploy_release 118 releases_dictionary['overcloud_deploy_release'] = deploy_release
92 119
93 elif featureset.get('undercloud_upgrade'): 120 elif featureset.get('undercloud_upgrade'):
94 logging.info('Doing an undercloud upgrade') 121 logger.info('Doing an undercloud upgrade')
95 install_release = get_relative_release(stable_release, -1) 122 install_release = get_relative_release(stable_release, -1)
96 releases_dictionary['undercloud_install_release'] = install_release 123 releases_dictionary['undercloud_install_release'] = install_release
97 124
98 elif featureset.get('overcloud_update'): 125 elif featureset.get('overcloud_update'):
99 logging.info('Doing an overcloud update') 126 logger.info('Doing an overcloud update')
100 releases_dictionary['overcloud_deploy_hash'] = \ 127 releases_dictionary['overcloud_deploy_hash'] = \
101 'previous-current-tripleo' 128 'previous-current-tripleo'
102 129
103 logging.debug("stable_release: %s, featureset: %s", stable_release, 130 logger.debug("stable_release: %s, featureset: %s", stable_release,
104 featureset) 131 featureset)
105 132
106 logging.info('output releases: %s', releases_dictionary) 133 logger.info('output releases: %s', releases_dictionary)
107 134
108 return releases_dictionary 135 return releases_dictionary
109 136
@@ -129,16 +156,40 @@ def shim_convert_old_release_names(releases_names):
129 156
130if __name__ == '__main__': 157if __name__ == '__main__':
131 158
132 # TODO read the feature set from a file path passed in the arguments 159 default_log_file = '{}.log'.format(os.path.basename(__file__))
133 featureset = { 160 default_output_file = '{}.out'.format(os.path.basename(__file__))
134 'mixed_upgrade': True, 161
135 'overcloud_upgrade': True, 162 parser = argparse.ArgumentParser(
136 } 163 formatter_class=argparse.RawTextHelpFormatter,
137 164 description='Get a dictionary of releases from a release '
138 # TODO read this from an argumment 165 'and a featureset file.')
139 stable_release = 'queens' 166 parser.add_argument('--stable-release',
140 167 choices=RELEASES,
141 releases_dictionary = compose_releases_dictionary(stable_release, 168 required=True,
169 help='Release that the change being tested is from.\n'
170 'All other releases are calculated from this\n'
171 'basis.')
172 parser.add_argument('--featureset-file',
173 required=True,
174 help='Featureset file which will be introspected to\n'
175 'infer what type of upgrade is being performed\n'
176 '(if any).')
177 parser.add_argument('--output-file', default=default_output_file,
178 help='Output file containing dictionary of releases\n'
179 'for the provided featureset and release.\n'
180 '(default: %(default)s)')
181 parser.add_argument('--log-file', default=default_log_file,
182 help='log file to print debug information from\n'
183 'running the script.\n'
184 '(default: %(default)s)')
185 args = parser.parse_args()
186
187 setup_logging(args.log_file)
188 logger = logging.getLogger('emit-releases')
189
190 featureset = load_featureset_file(args.featureset_file)
191
192 releases_dictionary = compose_releases_dictionary(args.stable_release,
142 featureset) 193 featureset)
143 194
144 releases_dictionary = shim_convert_old_release_names( 195 releases_dictionary = shim_convert_old_release_names(
diff --git a/scripts/emit_releases_file/test_yaml_parsing.py b/scripts/emit_releases_file/test_yaml_parsing.py
new file mode 100644
index 0000000..5714469
--- /dev/null
+++ b/scripts/emit_releases_file/test_yaml_parsing.py
@@ -0,0 +1,78 @@
1from emit_releases_file import load_featureset_file
2
3import mock
4import pytest
5import yaml
6from six import PY2
7
8
9if PY2:
10 BUILTINS_OPEN = "__builtin__.open"
11else:
12 BUILTINS_OPEN = "builtins.open"
13
14
15@mock.patch('yaml.safe_load')
16@mock.patch('logging.getLogger')
17def test_featureset_file_with_bad_file_path(mock_logging, mock_yaml):
18 mock_logger = mock.MagicMock()
19 mock_logging.return_value = mock_logger
20 mock_log_exception = mock.MagicMock()
21 mock_log_error = mock.MagicMock()
22 mock_logger.exception = mock_log_exception
23 mock_logger.error = mock_log_error
24 bad_file_exception = IOError("Dude where's my YAML!")
25 mo = mock.mock_open()
26 with pytest.raises(IOError):
27 with mock.patch(BUILTINS_OPEN, mo, create=True) as mock_file:
28 mock_file.side_effect = bad_file_exception
29 featureset = load_featureset_file('some_non_existent.yaml')
30 mock_yaml.assert_not_called()
31 mock_file.assert_called_with('some_non_existent.yaml', 'r')
32 mock_logging.assert_called_with('emit-releases')
33 mock_log_error.assert_called()
34 mock_log_exception.assert_called_with(bad_file_exception)
35 assert featureset is None
36
37
38@mock.patch('yaml.safe_load')
39@mock.patch('logging.getLogger')
40def test_featureset_file_with_bad_yaml(mock_logging, mock_yaml):
41 mock_logger = mock.MagicMock()
42 mock_logging.return_value = mock_logger
43 mock_log_exception = mock.MagicMock()
44 mock_log_error = mock.MagicMock()
45 mock_logger.exception = mock_log_exception
46 mock_logger.error = mock_log_error
47 mo = mock.mock_open()
48 mock_yaml.side_effect = yaml.YAMLError()
49 with pytest.raises(yaml.YAMLError):
50 with mock.patch(BUILTINS_OPEN, mo, create=True) as mock_file:
51 featureset = load_featureset_file('some_badly_formatted.yaml')
52 mock_yaml.assert_called()
53 mock_file.assert_called_with('some_badly_formatted.yaml', 'r')
54 mock_logging.assert_called_with('emit-releases')
55 mock_log_exception.assert_called()
56 mock_log_error.assert_called()
57 assert featureset is None
58
59
60@mock.patch('yaml.safe_load')
61@mock.patch('logging.getLogger')
62def test_featureset_file_loaded_ok(mock_logging, mock_yaml):
63 mock_logger = mock.MagicMock()
64 mock_logging.return_value = mock_logger
65 mock_log_exception = mock.MagicMock()
66 mock_log_error = mock.MagicMock()
67 mock_logger.exception = mock_log_exception
68 mock_logger.error = mock_log_error
69 ok_yaml_dict = {'some_featureset_keys': 'some_featureset_values'}
70 mock_yaml.return_value = ok_yaml_dict
71 mo = mock.mock_open()
72 with mock.patch(BUILTINS_OPEN, mo, create=True) as mock_file:
73 featureset = load_featureset_file('featureset999.yaml')
74 mock_file.assert_called_with('featureset999.yaml', 'r')
75 mock_yaml.assert_called()
76 mock_log_exception.assert_not_called()
77 mock_log_error.assert_not_called()
78 assert featureset == ok_yaml_dict
diff --git a/test-requirements.txt b/test-requirements.txt
index 7a1141b..6167416 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,3 +5,4 @@ pytest-cov
5mock 5mock
6requests 6requests
7pprint 7pprint
8PyYAML