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

Subscribers

People subscribed via source and target branches