Browse Source

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
master
John Trowbridge 1 year ago
parent
commit
53e4092038

+ 70
- 19
scripts/emit_releases_file/emit_releases_file.py View File

@@ -1,6 +1,10 @@
1
+import argparse
1 2
 import logging
3
+import logging.handlers
4
+import os
2 5
 import re
3 6
 import requests
7
+import yaml
4 8
 
5 9
 # Define releases
6 10
 RELEASES = ['newton', 'ocata', 'pike', 'queens', 'master']
@@ -13,7 +17,30 @@ def get_relative_release(release, relative_idx):
13 17
     return RELEASES[absolute_idx]
14 18
 
15 19
 
20
+def 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
+
29
+def 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
+
16 42
 def get_dlrn_hash(release, hash_name, retries=10):
43
+    logger = logging.getLogger('emit-releases')
17 44
     full_hash_pattern = re.compile('[a-z,0-9]{40}_[a-z,0-9]{8}')
18 45
     repo_url = ('https://trunk.rdoproject.org/centos7-%s/%s/delorean.repo'
19 46
                 % (release, hash_name))
@@ -25,7 +52,7 @@ def get_dlrn_hash(release, hash_name, retries=10):
25 52
         try:
26 53
             repo_file = requests.get(repo_url, timeout=(3.05, 27))
27 54
         except Exception as e:
28
-            # TODO(trown): Handle exceptions
55
+            logger.exception(e)
29 56
             pass
30 57
         else:
31 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
 
42 69
 def compose_releases_dictionary(stable_release, featureset):
43
-
70
+    logger = logging.getLogger('emit-releases')
44 71
     if stable_release not in RELEASES:
45 72
         raise RuntimeError("The {} release is not supported by this tool"
46 73
                            "Supported releases: {}".format(
@@ -81,29 +108,29 @@ def compose_releases_dictionary(stable_release, featureset):
81 108
 
82 109
     if featureset.get('mixed_upgrade'):
83 110
         if featureset.get('overcloud_upgrade'):
84
-            logging.info('Doing an overcloud upgrade')
111
+            logger.info('Doing an overcloud upgrade')
85 112
             deploy_release = get_relative_release(stable_release, -1)
86 113
             releases_dictionary['overcloud_deploy_release'] = deploy_release
87 114
 
88 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 117
             deploy_release = get_relative_release(stable_release, -3)
91 118
             releases_dictionary['overcloud_deploy_release'] = deploy_release
92 119
 
93 120
     elif featureset.get('undercloud_upgrade'):
94
-        logging.info('Doing an undercloud upgrade')
121
+        logger.info('Doing an undercloud upgrade')
95 122
         install_release = get_relative_release(stable_release, -1)
96 123
         releases_dictionary['undercloud_install_release'] = install_release
97 124
 
98 125
     elif featureset.get('overcloud_update'):
99
-        logging.info('Doing an overcloud update')
126
+        logger.info('Doing an overcloud update')
100 127
         releases_dictionary['overcloud_deploy_hash'] = \
101 128
             'previous-current-tripleo'
102 129
 
103
-    logging.debug("stable_release: %s, featureset: %s", stable_release,
104
-                  featureset)
130
+    logger.debug("stable_release: %s, featureset: %s", stable_release,
131
+                 featureset)
105 132
 
106
-    logging.info('output releases: %s', releases_dictionary)
133
+    logger.info('output releases: %s', releases_dictionary)
107 134
 
108 135
     return releases_dictionary
109 136
 
@@ -129,16 +156,40 @@ def shim_convert_old_release_names(releases_names):
129 156
 
130 157
 if __name__ == '__main__':
131 158
 
132
-    # TODO read the feature set from a file path passed in the arguments
133
-    featureset = {
134
-        'mixed_upgrade': True,
135
-        'overcloud_upgrade': True,
136
-    }
137
-
138
-    # TODO read this from an argumment
139
-    stable_release = 'queens'
140
-
141
-    releases_dictionary = compose_releases_dictionary(stable_release,
159
+    default_log_file = '{}.log'.format(os.path.basename(__file__))
160
+    default_output_file = '{}.out'.format(os.path.basename(__file__))
161
+
162
+    parser = argparse.ArgumentParser(
163
+             formatter_class=argparse.RawTextHelpFormatter,
164
+             description='Get a dictionary of releases from a release '
165
+                         'and a featureset file.')
166
+    parser.add_argument('--stable-release',
167
+                        choices=RELEASES,
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 193
                                                       featureset)
143 194
 
144 195
     releases_dictionary = shim_convert_old_release_names(

+ 78
- 0
scripts/emit_releases_file/test_yaml_parsing.py View File

@@ -0,0 +1,78 @@
1
+from emit_releases_file import load_featureset_file
2
+
3
+import mock
4
+import pytest
5
+import yaml
6
+from six import PY2
7
+
8
+
9
+if PY2:
10
+    BUILTINS_OPEN = "__builtin__.open"
11
+else:
12
+    BUILTINS_OPEN = "builtins.open"
13
+
14
+
15
+@mock.patch('yaml.safe_load')
16
+@mock.patch('logging.getLogger')
17
+def 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')
40
+def 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')
62
+def 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

+ 1
- 0
test-requirements.txt View File

@@ -5,3 +5,4 @@ pytest-cov
5 5
 mock
6 6
 requests
7 7
 pprint
8
+PyYAML

Loading…
Cancel
Save