OLD | NEW |
| (Empty) |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import json | |
6 import logging | |
7 import os | |
8 | |
9 from catapult_base import cloud_storage | |
10 from catapult_base.dependency_manager import archive_info | |
11 from catapult_base.dependency_manager import cloud_storage_info | |
12 from catapult_base.dependency_manager import dependency_info | |
13 from catapult_base.dependency_manager import exceptions | |
14 from catapult_base.dependency_manager import local_path_info | |
15 from catapult_base.dependency_manager import uploader | |
16 | |
17 | |
18 class BaseConfig(object): | |
19 """A basic config class for use with the DependencyManager. | |
20 | |
21 Initiated with a json file in the following format: | |
22 | |
23 { "config_type": "BaseConfig", | |
24 "dependencies": { | |
25 "dep_name1": { | |
26 "cloud_storage_base_folder": "base_folder1", | |
27 "cloud_storage_bucket": "bucket1", | |
28 "file_info": { | |
29 "platform1": { | |
30 "cloud_storage_hash": "hash_for_platform1", | |
31 "download_path": "download_path111", | |
32 "version_in_cs": "1.11.1.11." | |
33 "local_paths": ["local_path1110", "local_path1111"] | |
34 }, | |
35 "platform2": { | |
36 "cloud_storage_hash": "hash_for_platform2", | |
37 "download_path": "download_path2", | |
38 "local_paths": ["local_path20", "local_path21"] | |
39 }, | |
40 ... | |
41 } | |
42 }, | |
43 "dependency_name_2": { | |
44 ... | |
45 }, | |
46 ... | |
47 } | |
48 } | |
49 | |
50 Required fields: "dependencies" and "config_type". | |
51 Note that config_type must be "BaseConfig" | |
52 | |
53 Assumptions: | |
54 "cloud_storage_base_folder" is a top level folder in the given | |
55 "cloud_storage_bucket" where all of the dependency files are stored | |
56 at "dependency_name"_"cloud_storage_hash". | |
57 | |
58 "download_path" and all paths in "local_paths" are relative to the | |
59 config file's location. | |
60 | |
61 All or none of the following cloud storage related fields must be | |
62 included in each platform dictionary: | |
63 "cloud_storage_hash", "download_path", "cs_remote_path" | |
64 | |
65 "version_in_cs" is an optional cloud storage field, but is dependent | |
66 on the above cloud storage related fields. | |
67 | |
68 | |
69 Also note that platform names are often of the form os_architechture. | |
70 Ex: "win_AMD64" | |
71 | |
72 More information on the fields can be found in dependencies_info.py | |
73 """ | |
74 def __init__(self, file_path, writable=False): | |
75 """ Initialize a BaseConfig for the DependencyManager. | |
76 | |
77 Args: | |
78 writable: False: This config will be used to lookup information. | |
79 True: This config will be used to update information. | |
80 | |
81 file_path: Path to a file containing a json dictionary in the expected | |
82 json format for this config class. Base format expected: | |
83 | |
84 { "config_type": config_type, | |
85 "dependencies": dependencies_dict } | |
86 | |
87 config_type: must match the return value of GetConfigType. | |
88 dependencies: A dictionary with the information needed to | |
89 create dependency_info instances for the given | |
90 dependencies. | |
91 | |
92 See dependency_info.py for more information. | |
93 """ | |
94 self._config_path = file_path | |
95 self._writable = writable | |
96 self._is_dirty = False | |
97 self._pending_uploads = [] | |
98 if not self._config_path: | |
99 raise ValueError('Must supply config file path.') | |
100 if not os.path.exists(self._config_path): | |
101 if not writable: | |
102 raise exceptions.EmptyConfigError(file_path) | |
103 self._config_data = {} | |
104 self._WriteConfigToFile(self._config_path, dependencies=self._config_data) | |
105 else: | |
106 with open(file_path, 'r') as f: | |
107 config_data = json.load(f) | |
108 if not config_data: | |
109 raise exceptions.EmptyConfigError(file_path) | |
110 config_type = config_data.pop('config_type', None) | |
111 if config_type != self.GetConfigType(): | |
112 raise ValueError( | |
113 'Supplied config_type (%s) is not the expected type (%s) in file ' | |
114 '%s' % (config_type, self.GetConfigType(), file_path)) | |
115 self._config_data = config_data.get('dependencies', {}) | |
116 | |
117 def IterDependencyInfo(self): | |
118 """ Yields a DependencyInfo for each dependency/platform pair. | |
119 | |
120 Raises: | |
121 ReadWriteError: If called when the config is writable. | |
122 ValueError: If any of the dependencies contain partial information for | |
123 downloading from cloud_storage. (See dependency_info.py) | |
124 """ | |
125 if self._writable: | |
126 raise exceptions.ReadWriteError( | |
127 'Trying to read dependency info from a writable config. File for ' | |
128 'config: %s' % self._config_path) | |
129 base_path = os.path.dirname(self._config_path) | |
130 for dependency in self._config_data: | |
131 dependency_dict = self._config_data.get(dependency) | |
132 platforms_dict = dependency_dict.get('file_info', {}) | |
133 for platform in platforms_dict: | |
134 platform_info = platforms_dict.get(platform) | |
135 | |
136 local_info = None | |
137 local_paths = platform_info.get('local_paths', []) | |
138 if local_paths: | |
139 paths = [] | |
140 for path in local_paths: | |
141 path = self._FormatPath(path) | |
142 paths.append(os.path.abspath(os.path.join(base_path, path))) | |
143 local_info = local_path_info.LocalPathInfo(paths) | |
144 | |
145 cs_info = None | |
146 cs_bucket = dependency_dict.get('cloud_storage_bucket') | |
147 cs_base_folder = dependency_dict.get('cloud_storage_base_folder', '') | |
148 download_path = platform_info.get('download_path') | |
149 if download_path: | |
150 download_path = self._FormatPath(download_path) | |
151 download_path = os.path.abspath( | |
152 os.path.join(base_path, download_path)) | |
153 | |
154 cs_hash = platform_info.get('cloud_storage_hash') | |
155 if not cs_hash: | |
156 raise exceptions.ConfigError( | |
157 'Dependency %s has cloud storage info on platform %s, but is ' | |
158 'missing a cloud storage hash.', dependency, platform) | |
159 cs_remote_path = self._CloudStorageRemotePath( | |
160 dependency, cs_hash, cs_base_folder) | |
161 version_in_cs = platform_info.get('version_in_cs') | |
162 | |
163 zip_info = None | |
164 path_within_archive = platform_info.get('path_within_archive') | |
165 if path_within_archive: | |
166 unzip_path = os.path.abspath( | |
167 os.path.join(os.path.dirname(download_path), | |
168 '%s_%s_%s' % (dependency, platform, cs_hash))) | |
169 zip_info = archive_info.ArchiveInfo( | |
170 download_path, unzip_path, path_within_archive) | |
171 | |
172 cs_info = cloud_storage_info.CloudStorageInfo( | |
173 cs_bucket, cs_hash, download_path, cs_remote_path, | |
174 version_in_cs=version_in_cs, archive_info=zip_info) | |
175 | |
176 dep_info = dependency_info.DependencyInfo( | |
177 dependency, platform, self._config_path, local_path_info=local_info, | |
178 cloud_storage_info=cs_info) | |
179 yield dep_info | |
180 | |
181 @classmethod | |
182 def GetConfigType(cls): | |
183 return 'BaseConfig' | |
184 | |
185 @property | |
186 def config_path(self): | |
187 return self._config_path | |
188 | |
189 def AddCloudStorageDependencyUpdateJob( | |
190 self, dependency, platform, dependency_path, version=None, | |
191 execute_job=True): | |
192 """Update the file downloaded from cloud storage for a dependency/platform. | |
193 | |
194 Upload a new file to cloud storage for the given dependency and platform | |
195 pair and update the cloud storage hash and the version for the given pair. | |
196 | |
197 Example usage: | |
198 The following should update the default platform for 'dep_name': | |
199 UpdateCloudStorageDependency('dep_name', 'default', 'path/to/file') | |
200 | |
201 The following should update both the mac and win platforms for 'dep_name', | |
202 or neither if either update fails: | |
203 UpdateCloudStorageDependency( | |
204 'dep_name', 'mac_x86_64', 'path/to/mac/file', execute_job=False) | |
205 UpdateCloudStorageDependency( | |
206 'dep_name', 'win_AMD64', 'path/to/win/file', execute_job=False) | |
207 ExecuteUpdateJobs() | |
208 | |
209 Args: | |
210 dependency: The dependency to update. | |
211 platform: The platform to update the dependency info for. | |
212 dependency_path: Path to the new dependency to be used. | |
213 version: Version of the updated dependency, for checking future updates | |
214 against. | |
215 execute_job: True if the config should be written to disk and the file | |
216 should be uploaded to cloud storage after the update. False if | |
217 multiple updates should be performed atomically. Must call | |
218 ExecuteUpdateJobs after all non-executed jobs are added to complete | |
219 the update. | |
220 | |
221 Raises: | |
222 ReadWriteError: If the config was not initialized as writable, or if | |
223 |execute_job| is True but the config has update jobs still pending | |
224 execution. | |
225 ValueError: If no information exists in the config for |dependency| on | |
226 |platform|. | |
227 """ | |
228 self._ValidateIsConfigUpdatable( | |
229 execute_job=execute_job, dependency=dependency, platform=platform) | |
230 self._is_dirty = True | |
231 cs_hash = cloud_storage.CalculateHash(dependency_path) | |
232 if version: | |
233 self._SetPlatformData(dependency, platform, 'version_in_cs', version) | |
234 self._SetPlatformData(dependency, platform, 'cloud_storage_hash', cs_hash) | |
235 | |
236 cs_base_folder = self._GetPlatformData( | |
237 dependency, platform, 'cloud_storage_base_folder') | |
238 cs_bucket = self._GetPlatformData( | |
239 dependency, platform, 'cloud_storage_bucket') | |
240 cs_remote_path = self._CloudStorageRemotePath( | |
241 dependency, cs_hash, cs_base_folder) | |
242 self._pending_uploads.append(uploader.CloudStorageUploader( | |
243 cs_bucket, cs_remote_path, dependency_path)) | |
244 if execute_job: | |
245 self.ExecuteUpdateJobs() | |
246 | |
247 def ExecuteUpdateJobs(self, force=False): | |
248 """Write all config changes to the config_path specified in __init__. | |
249 | |
250 Upload all files pending upload and then write the updated config to | |
251 file. Attempt to remove all uploaded files on failure. | |
252 | |
253 Args: | |
254 force: True if files should be uploaded to cloud storage even if a | |
255 file already exists in the upload location. | |
256 | |
257 Returns: | |
258 True: if the config was dirty and the upload succeeded. | |
259 False: if the config was not dirty. | |
260 | |
261 Raises: | |
262 CloudStorageUploadConflictError: If |force| is False and the potential | |
263 upload location of a file already exists. | |
264 CloudStorageError: If copying an existing file to the backup location | |
265 or uploading a new file fails. | |
266 """ | |
267 self._ValidateIsConfigUpdatable() | |
268 if not self._is_dirty: | |
269 logging.info('ExecuteUpdateJobs called on clean config') | |
270 return False | |
271 if not self._pending_uploads: | |
272 logging.debug('No files needing upload.') | |
273 else: | |
274 try: | |
275 for item_pending_upload in self._pending_uploads: | |
276 item_pending_upload.Upload(force) | |
277 self._WriteConfigToFile(self._config_path, self._config_data) | |
278 self._pending_uploads = [] | |
279 self._is_dirty = False | |
280 except: | |
281 # Attempt to rollback the update in any instance of failure, even user | |
282 # interrupt via Ctrl+C; but don't consume the exception. | |
283 logging.error('Update failed, attempting to roll it back.') | |
284 for upload_item in reversed(self._pending_uploads): | |
285 upload_item.Rollback() | |
286 raise | |
287 return True | |
288 | |
289 def GetVersion(self, dependency, platform): | |
290 """Return the Version information for the given dependency.""" | |
291 return self._GetPlatformData( | |
292 dependency, platform, data_type='version_in_cs') | |
293 | |
294 def _SetPlatformData(self, dependency, platform, data_type, data): | |
295 self._ValidateIsConfigWritable() | |
296 dependency_dict = self._config_data.get(dependency, {}) | |
297 platform_dict = dependency_dict.get('file_info', {}).get(platform) | |
298 if not platform_dict: | |
299 raise ValueError('No platform data for platform %s on dependency %s' % | |
300 (platform, dependency)) | |
301 if (data_type == 'cloud_storage_bucket' or | |
302 data_type == 'cloud_storage_base_folder'): | |
303 self._config_data[dependency][data_type] = data | |
304 else: | |
305 self._config_data[dependency]['file_info'][platform][data_type] = data | |
306 | |
307 def _GetPlatformData(self, dependency, platform, data_type=None): | |
308 dependency_dict = self._config_data.get(dependency, {}) | |
309 if not dependency_dict: | |
310 raise ValueError('Dependency %s is not in config.' % dependency) | |
311 platform_dict = dependency_dict.get('file_info', {}).get(platform) | |
312 if not platform_dict: | |
313 raise ValueError('No platform data for platform %s on dependency %s' % | |
314 (platform, dependency)) | |
315 if data_type: | |
316 if (data_type == 'cloud_storage_bucket' or | |
317 data_type == 'cloud_storage_base_folder'): | |
318 return dependency_dict.get(data_type) | |
319 return platform_dict.get(data_type) | |
320 return platform_dict | |
321 | |
322 def _ValidateIsConfigUpdatable( | |
323 self, execute_job=False, dependency=None, platform=None): | |
324 self._ValidateIsConfigWritable() | |
325 if self._is_dirty and execute_job: | |
326 raise exceptions.ReadWriteError( | |
327 'A change has already been made to this config. Either call without' | |
328 'using the execute_job option or first call ExecuteUpdateJobs().') | |
329 if dependency and not self._config_data.get(dependency): | |
330 raise ValueError('Cannot update information because dependency %s does ' | |
331 'not exist.' % dependency) | |
332 if platform and not self._GetPlatformData(dependency, platform): | |
333 raise ValueError('No dependency info is available for the given ' | |
334 'dependency: %s' % dependency) | |
335 | |
336 def _ValidateIsConfigWritable(self): | |
337 if not self._writable: | |
338 raise exceptions.ReadWriteError( | |
339 'Trying to update the information from a read-only config. ' | |
340 'File for config: %s' % self._config_path) | |
341 | |
342 @staticmethod | |
343 def _CloudStorageRemotePath(dependency, cs_hash, cs_base_folder): | |
344 cs_remote_file = '%s_%s' % (dependency, cs_hash) | |
345 cs_remote_path = cs_remote_file if not cs_base_folder else ( | |
346 '%s/%s' % (cs_base_folder, cs_remote_file)) | |
347 return cs_remote_path | |
348 | |
349 @classmethod | |
350 def _FormatPath(cls, file_path): | |
351 """ Format |file_path| for the current file system. | |
352 | |
353 We may be downloading files for another platform, so paths must be | |
354 downloadable on the current system. | |
355 """ | |
356 if not file_path: | |
357 return file_path | |
358 if os.path.sep != '\\': | |
359 return file_path.replace('\\', os.path.sep) | |
360 elif os.path.sep != '/': | |
361 return file_path.replace('/', os.path.sep) | |
362 return file_path | |
363 | |
364 @classmethod | |
365 def _WriteConfigToFile(cls, file_path, dependencies=None): | |
366 json_dict = cls._GetJsonDict(dependencies) | |
367 file_dir = os.path.dirname(file_path) | |
368 if not os.path.exists(file_dir): | |
369 os.makedirs(file_dir) | |
370 with open(file_path, 'w') as outfile: | |
371 json.dump( | |
372 json_dict, outfile, indent=2, sort_keys=True, separators=(',', ': ')) | |
373 return json_dict | |
374 | |
375 @classmethod | |
376 def _GetJsonDict(cls, dependencies=None): | |
377 dependencies = dependencies or {} | |
378 json_dict = {'config_type': cls.GetConfigType(), | |
379 'dependencies': dependencies} | |
380 return json_dict | |
OLD | NEW |