Merge lp:~adeuring/juju-ci-tools/add-missing-results-files-to-s3 into lp:juju-ci-tools

Proposed by Abel Deuring
Status: Merged
Merged at revision: 692
Proposed branch: lp:~adeuring/juju-ci-tools/add-missing-results-files-to-s3
Merge into: lp:juju-ci-tools
Diff against target: 189 lines (+148/-11)
3 files modified
add-missing-result-yaml-files.py (+135/-0)
backup-to-s3.py (+2/-11)
utility.py (+11/-0)
To merge this branch: bzr merge lp:~adeuring/juju-ci-tools/add-missing-results-files-to-s3
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+236008@code.launchpad.net

Description of the change

This branch adds a new script to check if the files stored in S3 for all test runs contain a file result.yaml or result.json.

Additionally it checks for existing files "result.(json|yaml)" if the contain the final result information (key "result" in the main dicitonary). The final result is added if it missing.

Being a bit paranoid, I added checks that the dat recorded in S3 matches that in the main state file (lines 92..111).

In a dry run of the script on my laptop the second check results in warnings for some revisions that the compared dictionaries do not match. Some details:

revision 1855

S3 file:
  "complete_failure_info": {'functional-ha-recovery': 835, 'hp-deploy-trusty-amd64': 1715, 'run-unit-tests-utopic-amd64': 690}
state file:
  "complete_failure_info": {'functional-ha-recovery': 836, 'hp-deploy-trusty-amd64': 1715, 'run-unit-tests-utopic-amd64': 690}

revision 1856:

S3 file
  "result": {"status": "curse", "build": 1268}
state file:
      result: {build: 1269, status: bless}

Somewhat worrying... But the S3 files for both revisions contain a final result, so the script will not change anything.

I'm a bit too tired to dive into the other failures; can do this tomorrow. But it makes probably sense to explicitly prevent changes to result files that show this type of inconsistency.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you very much for this script. I have a question. I think this branch is really valuable and would like it merged quickly.

I am not surprised by divergent result informaiton. juju-reports didn't try to get revised results for many months. This also accounts for retries of tests that still failed.

review: Needs Information (code)
686. By Abel Deuring

If two result files exist for one revision, use the timestamp of the older file but the data from the newer file.

Revision history for this message
Abel Deuring (adeuring) wrote :

Am 26.09.2014 um 16:33 schrieb Curtis Hovey:
> Review: Needs Information code
>
> Thank you very much for this script. I have a question. I think this branch is really valuable and would like it merged quickly.
>
> I am not surprised by divergent result informaiton. juju-reports didn't try to get revised results for many months. This also accounts for retries of tests that still failed.

I think I understand meanwhile a bit better how the result diverged. But
that's a discussion about possible improvements of ci-director.

>
> Diff comments:
>
>> + # The most recent version may currently be building, hence a check
>> + # if the result file is useless.
>> + del all_revisions[max(all_revisions)]
>> + for revision_number, revision_data in sorted(all_revisions.items()):
>> + if not revision_data['result']:
>> + result_file_name = None
>> + else:
>> + # If both a result.yaml and a result.json file exist, use
>> + # the older one.
>> + older = min(revision_data['result'])
>
> The older one? Isn't the older one more likely to be wrong? I think the newer one is the replacement with revised data. If this is the case, we want the newer file with the older files's timestamp

Argh, right. Fixed.

>> +def s3_cmd(params, drop_output=False):
>> + s3cfg_path = os.path.join(
>> + os.environ['HOME'], 'cloud-city/juju-qa.s3cfg')
>> + if drop_output:
>> + return subprocess.check_call(
>> + ['s3cmd', '-c', s3cfg_path] + params, stdout=open('/dev/null', 'w'))
>
> Maybe --no-progress should be the default. I think most of the cron spam I get is because juju-reports never uses this option.

Right, I've added "--no-progress" as a non-removable parameter.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'add-missing-result-yaml-files.py'
2--- add-missing-result-yaml-files.py 1970-01-01 00:00:00 +0000
3+++ add-missing-result-yaml-files.py 2014-09-26 15:12:24 +0000
4@@ -0,0 +1,135 @@
5+#!/usr/bin/env python
6+
7+"""Add missing result.yaml in S3; ensue that existing files contain
8+the final result.
9+"""
10+
11+from __future__ import print_function
12+from argparse import ArgumentParser
13+from datetime import datetime
14+import json
15+import os
16+import re
17+from tempfile import NamedTemporaryFile
18+import yaml
19+
20+from utility import (
21+ s3_cmd,
22+ temp_dir,
23+)
24+
25+ARCHIVE_URL = 's3://juju-qa-data/juju-ci/products/'
26+ISO_8601_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
27+LONG_AGO = datetime(2000, 1, 1)
28+
29+
30+def get_ci_director_state():
31+ state_file_path = os.path.join(
32+ os.environ['HOME'], '.config/ci-director-state')
33+ with open(state_file_path) as state_file:
34+ return yaml.load(state_file)['versions']
35+
36+
37+def list_s3_files():
38+ text = s3_cmd(['ls', '-r', ARCHIVE_URL])
39+ for line in text.strip().split('\n'):
40+ file_date, file_time, size, url = re.split(r'\s+', line)
41+ file_date = [int(part) for part in file_date.split('-')]
42+ file_time = [int(part) for part in file_time.split(':')]
43+ file_time = datetime(*(file_date + file_time))
44+ revision_number, filename = re.search(
45+ r'^{}version-(\d+)/(.*)$'.format(ARCHIVE_URL), url).groups()
46+ yield int(revision_number), filename, file_time
47+
48+
49+def get_s3_revision_info():
50+ all_revisions = {}
51+ for revision_number, file_name, file_time in list_s3_files():
52+ revision = all_revisions.setdefault(revision_number, {
53+ 'result': {},
54+ 'artifact_time': LONG_AGO,
55+ })
56+ if file_name in ('result.yaml', 'result.json'):
57+ # Many result.json files were added on 2014-08-14 for older
58+ # builds, so we may have both a result.yaml file and a
59+ # result.json file.
60+ revision['result'][file_time] = file_name
61+ else:
62+ revision['artifact_time'] = max(
63+ revision['artifact_time'], file_time)
64+ # The most recent version may currently be building, hence a check
65+ # if the result file exists is useless.
66+ del all_revisions[max(all_revisions)]
67+ result_file_time = revision['artifact_time']
68+ for revision_number, revision_data in sorted(all_revisions.items()):
69+ if not revision_data['result']:
70+ result_file_name = None
71+ else:
72+ result_file_time = min(revision_data['result'])
73+ # If both a result.yaml and a result.json file exist, use
74+ # the newer one.
75+ newer = max(revision_data['result'])
76+ result_file_name = revision_data['result'][newer]
77+ yield revision_number, result_file_name, result_file_time
78+
79+def main(args):
80+ ci_director_state = get_ci_director_state()
81+ for revision_number, result_file, artifact_time in get_s3_revision_info():
82+ state_file_result = ci_director_state.get(revision_number)
83+ if state_file_result is None:
84+ print(
85+ "Warning: No state file data available for revision",
86+ revision_number)
87+ continue
88+ if result_file is not None:
89+ with temp_dir() as workspace:
90+ copy_from = '{}version-{}/{}'.format(
91+ ARCHIVE_URL, revision_number, result_file)
92+ copy_to = os.path.join(workspace, result_file)
93+ s3_cmd(['--no-progress', 'get', copy_from, copy_to])
94+ with open(copy_to) as f:
95+ s3_result = yaml.load(f)
96+ # For paranoids: Check that the data from S3 is a subset
97+ # of the data from the state file
98+ s3_keys = set(s3_result)
99+ state_keys = set(ci_director_state[revision_number])
100+ if not s3_keys.issubset(state_keys):
101+ print(
102+ "Warning: S3 result file for {} contains keys that do "
103+ "not exist in the main state file: {}".format(
104+ revision_number, s3_keys.difference(state_keys)))
105+ continue
106+ comparable_state_data = dict(
107+ (k, v)
108+ for k, v in ci_director_state[revision_number].items()
109+ if k in s3_keys)
110+ if comparable_state_data != s3_result:
111+ # This can happen when the result file was written
112+ # when a -devel job is still running.
113+ print(
114+ "Warning: Diverging data for revision {} in S3 ({}) "
115+ "and in state file ({}).".format(
116+ revision_number, s3_result,
117+ ci_director_state[revision_number]))
118+ if 'result' in s3_result:
119+ continue
120+
121+ if 'finished' not in state_file_result:
122+ state_file_result['finished'] = artifact_time.strftime(
123+ ISO_8601_FORMAT)
124+ with NamedTemporaryFile() as new_result_file:
125+ json.dump(state_file_result, new_result_file)
126+ new_result_file.flush()
127+ dest_url = '{}version-{}/result.json'.format(
128+ ARCHIVE_URL, revision_number)
129+ params = ['put', new_result_file.name, dest_url]
130+ if args.dry_run:
131+ print(*(['s3cmd'] + params))
132+ else:
133+ s3_cmd(params)
134+
135+if __name__ == '__main__':
136+ parser = ArgumentParser()
137+ parser.add_argument('--dry-run', action='store_true')
138+ args = parser.parse_args()
139+ main(args)
140
141=== modified file 'backup-to-s3.py'
142--- backup-to-s3.py 2014-07-25 12:02:32 +0000
143+++ backup-to-s3.py 2014-09-26 15:12:24 +0000
144@@ -6,6 +6,8 @@
145 import re
146 import subprocess
147
148+from utility import s3_cmd
149+
150
151 MAX_BACKUPS = 10
152 BACKUP_URL = 's3://juju-qa-data/juju-ci/backups/'
153@@ -26,17 +28,6 @@
154 ]
155
156
157-def s3_cmd(params, drop_output=False):
158- s3cfg_path = os.path.join(
159- os.environ['HOME'], 'cloud-city/juju-qa.s3cfg')
160- if drop_output:
161- return subprocess.check_call(
162- ['s3cmd', '-c', s3cfg_path] + params, stdout=open('/dev/null', 'w'))
163- else:
164- return subprocess.check_output(
165- ['s3cmd', '-c', s3cfg_path] + params)
166-
167-
168 def current_backups():
169 """Return a list of S3 URLs of existing backups."""
170 # We expect lines like
171
172=== modified file 'utility.py'
173--- utility.py 2014-09-20 00:05:37 +0000
174+++ utility.py 2014-09-26 15:12:24 +0000
175@@ -133,3 +133,14 @@
176 'path': path, 'mount': df_result[5], 'required': required,
177 'available': available, 'purpose': purpose
178 })
179+
180+
181+def s3_cmd(params, drop_output=False):
182+ s3cfg_path = os.path.join(
183+ os.environ['HOME'], 'cloud-city/juju-qa.s3cfg')
184+ command = ['s3cmd', '-c', s3cfg_path, '--no-progress'] + params
185+ if drop_output:
186+ return subprocess.check_call(
187+ command, stdout=open('/dev/null', 'w'))
188+ else:
189+ return subprocess.check_output(command)

Subscribers

People subscribed via source and target branches