-
Notifications
You must be signed in to change notification settings - Fork 28
/
upgrade_config.py
executable file
·268 lines (212 loc) · 9.1 KB
/
upgrade_config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import argparse
import yaml
# Constant values for changeset types
ADD = 'ADD'
MOVE = 'MOVE'
EDIT = 'EDIT'
DELETE = 'DELETE'
# Latest version of config. Maybe keep this value in a central location like
# __version__.py?
CURRENT_CONFIG_VERSION = '1.0.0'
# 'Patent pending', ingenius way to update config. Keep a list of changes
# between config version updates, map the config version for the old config to
# the changes needed to be made to upgrade an old config of that version to the
# config of the next version. There are 4 types of changes that can be made:
# 1. ADD - add a new field to the config
# 2. MOVE - move a value to a different field, getting rid of the old field
# 3. EDIT - make a change to value of an old config
# 4. DELETE - delete a field
CONFIG_CHANGESET = {
'0.0.0': {
ADD: {
('logs', 'log_format'): 'JSON',
('servers',): [{}],
('transport', 'id'): 'Server 1',
('duoclient', 'endpoint_server_mappings'): [{}],
('duoclient', 'endpoint_server_mappings', 0, 'server'): 'Server 1'
},
MOVE: {
('duoclient',): ('account',),
('account', 'host'): ('account', 'hostname'),
('logs', 'logDir'): ('logs', 'log_filepath'),
('logs', 'endpoints', 'enabled'): (
'logs', 'endpoints', 'endpoints'),
('logs', 'polling'): ('logs', 'api'),
('logs', 'api', 'duration'): ('logs', 'api', 'timeout'),
('logs', 'api', 'daysinpast'): ('logs', 'api', 'offset'),
('logs', 'checkpointDir'): ('logs', 'directory'),
('logs',): ('dls_settings',),
('transport', 'host'): ('transport', 'hostname'),
('transport', 'certFileDir'): ('transport', 'cert_filepath'),
('recoverFromCheckpoint',): ('checkpointing',),
('checkpointing',): ('dls_settings', 'checkpointing'),
('dls_settings', 'directory'): (
'dls_settings', 'checkpointing', 'directory'),
('dls_settings', 'endpoints', 'endpoints'): (
'account', 'endpoint_server_mappings', 0, 'endpoints'),
('transport',): ('servers', 0)
},
EDIT: {
('version',): (lambda _ : '1.0.0'),
('dls_settings', 'api', 'timeout'): (lambda timeout : timeout * 60),
},
DELETE: [
('dls_settings', 'endpoints'),
('servers', 0, 'certFileName')
]
}
}
def main():
"""
Parse command line arguments (such as to retrieve the old and new config
filepaths) and call functions to upgrade the old config and write the
upgraded config to a new file.
"""
arg_parser = argparse.ArgumentParser(
prog='Config Upgrader',
description='Automatically generate a new config from an old version')
arg_parser.add_argument(
'old_config_path',
metavar='old_config_path',
type=str,
help='Path to the old config for upgrading')
arg_parser.add_argument(
'new_config_path',
metavar='new_config_path',
type=str,
help='Filepath where the upgraded config file should be saved')
args = arg_parser.parse_args()
upgraded_config = upgrade_config(args.old_config_path)
write_config(upgraded_config, args.new_config_path)
def upgrade_config(old_config_path):
"""
Create a new config based off of a config located at the given filepath.
@param old_config_path Location of the old config to upgrade
@return an upgraded version of the old config
"""
with open(old_config_path) as config_file:
config_file_data = config_file.read()
config = yaml.full_load(config_file_data)
# Both cert related parameters are optional in old config and may not exist. Initializing
# those to '' so that upgrade script can work properly
if not config.get('transport').get('certFileDir'):
config['transport']['certFileDir'] = ''
if not config.get('transport').get('certFileName'):
config['transport']['certFileName'] = ''
# Check if the config has a version, if it doesn't then it is the
# oldest config, and give it a version of '0.0.0'
if not config.get('version', False):
config['version'] = '0.0.0'
# Apply changesets to the config until it is of the newest version
while config.get('version') != CURRENT_CONFIG_VERSION:
config = apply_changeset(config)
return config
def write_config(config, config_path):
"""
Write config to the filepath given. If there is no filepath given then
config will be printed to stdout.
@param config The config to write
@param config_path Location where config should be written to
"""
with open(config_path, 'w') as config_file:
yaml.dump(config, config_file)
def apply_changeset(config):
"""
Given a config and its version number, apply the appropriate set of changes
as defined in the version to changeset mapping in CONFIG_CHANGESET.
@param config The config for which changeset should be applied
@return a version of the config with the changeset applied
"""
version = config.get('version')
config = apply_hard_coded_changes(config, version)
changeset = CONFIG_CHANGESET.get(version)
config = apply_add_changeset(config, changeset.get(ADD))
config = apply_move_changeset(config, changeset.get(MOVE))
config = apply_edit_changeset(config, changeset.get(EDIT))
config = apply_delete_changeset(config, changeset.get(DELETE))
return config
def apply_hard_coded_changes(config, version):
"""
Unfortunately, some changes made between config version are too complex
to implement using the versioning dict to make it worth while. Best to
hard code changes like this for each version and update the config as
needed.
@param config Dictionary that changes should be applied to
@param version What changes should be applied to the config
@return config with updates according to the version specified
"""
if version == '0.0.0':
slash = '/'
# Catch the case where Windows paths exist
if '\\' in config['transport']['certFileDir']:
slash = '\\'
cert_filepath = config['transport']['certFileDir'] + slash + config[
'transport']['certFileName']
config['transport']['certFileDir'] = cert_filepath
config['logs']['logDir'] += slash + 'duologsync.log'
return config
def apply_add_changeset(config, add_changeset):
"""
Add new fields to the config as specified by the add changeset.
@param config Config to add fields to
@param add_changeset Dictionary specifying what fields to add and with
what values
@return config with the add changeset applied
"""
for keys, value in add_changeset.items():
elem_to_add = get_elem(config, keys[:-1])
elem_to_add[keys[-1]] = value
return config
def apply_move_changeset(config, move_changeset):
"""
Move fields to different locations within config as specified by the
move_changeset.
@param config Config to move fields for
@param move_changeset Dictionary specifying where to move certain fields
to
@return config with the move changeset applied
"""
for keys, new_path in move_changeset.items():
elem_to_move = get_elem(config, keys[:-1])
temp = elem_to_move.pop(keys[-1])
if new_path is not None:
new_elem = get_elem(config, new_path[:-1])
new_elem[new_path[-1]] = temp
return config
def apply_edit_changeset(config, edit_changeset):
"""
Edit certain values within config as specified by the edit_changeset.
@param config Config to edit values for
@param edit_changeset Dictionary of fields and lambdas to change the
values of those fields
@return config with the edit changeset applied
"""
for keys, func in edit_changeset.items():
elem_to_edit = get_elem(config, keys[:-1])
elem_to_edit[keys[-1]] = func(elem_to_edit[keys[-1]])
return config
def apply_delete_changeset(config, delete_changeset):
"""
Delete certain elements within config as specified by the delete_changeset.
@param config Config to delete elements for
@param delete_changeset List of elements to delete
@return config with the delete changeset applied
"""
for keys in delete_changeset:
elem_to_delete = get_elem(config, keys[:-1])
del elem_to_delete[keys[-1]]
return config
def get_elem(dictionary, keys):
"""
Given a dictionary and a tuple of keys, retrieve the element found by
following the tuple of keys.
@param dictionary Dictionary to retrieve an element from
@param keys Tuple of keys to follow to find a element
@return the elemetn found in the dictionary by following the tuple of keys
"""
curr_elem = dictionary
for key in keys:
curr_elem = curr_elem[key]
return curr_elem
if __name__ == '__main__':
main()