Merge ubuntu-cve-tracker:usn-publishing into ubuntu-cve-tracker:master

Proposed by Mark Morlino
Status: Merged
Merged at revision: 9cfade128d2ffa78e77d69df4f0d33ba2fdd1e98
Proposed branch: ubuntu-cve-tracker:usn-publishing
Merge into: ubuntu-cve-tracker:master
Diff against target: 247 lines (+114/-57)
3 files modified
scripts/convert-pickle.py (+1/-1)
scripts/publish-usn-to-website (+19/-5)
scripts/publish-usn-to-website-api.py (+94/-51)
Reviewer Review Type Date Requested Status
Alex Murray Approve
Review via email: mp+386455@code.launchpad.net

Description of the change

* improved error handling
* add ability to remove usns
* add option to upsert
* add option to control behavior during batch operations (exit on fail or keep going)

To post a comment you must log in.
Revision history for this message
Alex Murray (alexmurray) wrote :

LGTM - I tested this during the publication of USN 4405-1 - https://ubuntu.com/security/notices/USN-4405-1 - and it seemed to work ok.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/scripts/convert-pickle.py b/scripts/convert-pickle.py
2index a3133b7..88258de 100755
3--- a/scripts/convert-pickle.py
4+++ b/scripts/convert-pickle.py
5@@ -60,7 +60,7 @@ if __name__ == "__main__":
6 parser.add_option("-i", "--input-file", dest="infile", help="pickle data file", metavar="FILE")
7 parser.add_option("-o", "--output-file", dest="outfile", help="target json data file", metavar="FILE")
8 parser.add_option("-p", "--prefix", dest="prefix",
9- help="prefix for value of id field. eg: 'USN-'", default=None, metavar="FILE")
10+ help="prefix for value of id field. eg: 'USN-'", default=None)
11 (opt, args) = parser.parse_args()
12
13 if not opt.infile:
14diff --git a/scripts/publish-usn-to-website b/scripts/publish-usn-to-website
15index 33f63fd..20d2b58 100755
16--- a/scripts/publish-usn-to-website
17+++ b/scripts/publish-usn-to-website
18@@ -77,19 +77,33 @@ push_to_website_api() {
19 TMP_PICKLE=$(mktemp -u)
20 TMP_JSON=$(mktemp -u)
21
22+ # shellcheck disable=SC2064
23+ trap "rm -f \"$TMP_YAML\" \"$TMP_PICKLE\" \"$TMP_JSON\"" EXIT
24+
25 # export the single USN from the pickle to a yaml file
26- "$usn_tool"/usn.py --db "$database" --export "$usn" >"$TMP_YAML"
27+ if ! "$usn_tool"/usn.py --db "$database" --export "$usn" >"$TMP_YAML" ; then
28+ perr "Failed to export $USN from pickle to yaml"
29+ exit 1
30+ fi
31
32 # import the single USN from the yaml into a new pickle file
33- "$usn_tool"/usn.py --db "$TMP_PICKLE" --import < "$TMP_YAML"
34+ if ! "$usn_tool"/usn.py --db "$TMP_PICKLE" --import < "$TMP_YAML" ; then
35+ perr "Failed to create temporary pickle from $USN yaml"
36+ exit 1
37+ fi
38
39 # convert the single USN pickle to a single USN json file
40- "$UCT"/scripts/convert-pickle.py --input-file "$TMP_PICKLE" --output-file "$TMP_JSON" --prefix "USN-"
41+ if ! "$UCT"/scripts/convert-pickle.py --input-file "$TMP_PICKLE" --output-file "$TMP_JSON" --prefix "USN-" ; then
42+ perr "Failed to convert $USN pickle to json"
43+ exit 1
44+ fi
45
46 # post the single USN json file to the website API
47- "$UCT"/scripts/publish-usn-to-website-api.py --action "$action" --json "$TMP_JSON"
48+ if ! "$UCT"/scripts/publish-usn-to-website-api.py --action "$action" --json "$TMP_JSON" --stop --no-upsert; then
49+ perr "Failed to publish $USN pickle to website API"
50+ exit 1
51+ fi
52
53- rm -f "$TMP_YAML" "$TMP_PICKLE" "$TMP_JSON"
54 }
55
56 # $1: The action string (add, update, or remove)
57diff --git a/scripts/publish-usn-to-website-api.py b/scripts/publish-usn-to-website-api.py
58index cd3c92f..1185739 100755
59--- a/scripts/publish-usn-to-website-api.py
60+++ b/scripts/publish-usn-to-website-api.py
61@@ -5,15 +5,23 @@
62 #
63 # initial upload of all USNs can be done as follows
64 # cd $UCT && ./scripts/fetch-db database.pickle.bz2
65-# ./scripts/convert-pickle.py --input database.pickle --output database.json
66+# ./scripts/convert-pickle.py --input database.pickle --output database.json --prefix 'USN-'
67 # ./scripts/publish-usn-to-website-api.py --json ./database.json --action add
68+#
69+# to test with a single USN you can use these commands:
70+# export USN-9999
71+# ../usn-tool/usn.py --db database.pickle --export $USN > $USN.yaml
72+# ../usn-tool/usn.py --db $USN.pickle --import < $USN.yaml
73+# ./scripts/convert-pickle.py --input-file $USN.pickle --output-file $USN.json --prefix USN-
74+# ./scripts/publish-usn-to-website-api.py --action add --json $USN.json
75+
76
77 # Standard library
78 import argparse
79 import json
80 import sys
81 import os
82-#import re
83+import re
84 from datetime import datetime
85 from http.cookiejar import MozillaCookieJar
86
87@@ -66,45 +74,60 @@ def guess_binary_links(binary, version, sources):
88
89 parser = argparse.ArgumentParser(description="CLI to post USNs",)
90 parser.add_argument(
91- "--json", action="store", type=str,
92- required=True,
93- help='path to json file',
94- )
95-parser.add_argument(
96 "--action", action="store", type=str,
97 default="add",
98 choices=['add', 'update', 'remove'],
99- help='api action to perform',
100+ help='API action to perform',
101+ )
102+parser.add_argument(
103+ "--json", action="store", type=str,
104+ help='Path to json file. Required for --action add/update. Can be used instead of --usns for --action remove',
105+ )
106+parser.add_argument(
107+ "--usns", action="store", nargs='+',
108+ help='Can be used instead of --json with --action remove',
109 )
110 parser.add_argument(
111 "--endpoint", action="store", type=str,
112 default="https://ubuntu.com/security/notices",
113- help='api endpoint url',
114+ help='API endpoint url. default is https://ubuntu.com/security/notices',
115+ )
116+parser.add_argument(
117+ "--stop", action="store_true",
118+ help="Exit after non-200 status. If upserting, exit after both attempted add/update operations fail",
119+ )
120+parser.add_argument(
121+ "--no-upsert", action="store_true",
122+ help="No update after non-200 status on add, No add after non-200 status on update",
123 )
124-#parser.add_argument(
125- #"--errors", action="store", type=str,
126- #default="exit",
127- #choices=['continue', 'exit'],
128- #help="what to do when the api returns a non-200 status",
129- #)
130
131 args = parser.parse_args()
132
133-if args.action == 'add':
134- http_method = 'POST'
135-elif args.action == 'update':
136- http_method = 'PUT'
137-elif args.action == 'remove':
138- sys.exit(f"Error: {args.action} action not implemented yet")
139-else:
140- sys.exit(f"Error: {args.action} action not implemented yet")
141+if args.json and not os.path.exists(args.json):
142+ sys.exit(f"Error: {args.json} not found")
143+if (args.action == 'add' or args.action == 'update') and not args.json:
144+ sys.exit(f"Error: --action {args.action} requires --json")
145+if args.action == 'remove' and not args.json and not args.usns:
146+ sys.exit("Error: --action remove requires --json or --usns")
147
148 client = httpbakery.Client(cookies=MozillaCookieJar(".login"))
149
150 # Make a first call to make sure we are logged in
151-response = client.request(http_method, url=args.endpoint)
152-
153-
154+response = client.request('GET', url=args.endpoint)
155+
156+# if we have a list of USNs instead of a json file
157+# we can just remove/DELETE them
158+if args.action == 'remove' and args.usns:
159+ http_method = 'DELETE'
160+ for usn in args.usns:
161+ endpoint = f"{args.endpoint}/{usn}"
162+ response = client.request( http_method, url=endpoint, json=None)
163+ print(response, response.text[0:60])
164+ if args.stop and not re.match(r'^<Response \[2..\]>$',str(response)):
165+ sys.exit(1)
166+ sys.exit(0)
167+
168+# read in the json
169 with open(args.json) as usn_json:
170 payload = json.load(usn_json).items()
171
172@@ -146,30 +169,50 @@ with open(args.json) as usn_json:
173 else:
174 references.append(reference)
175
176+ # Build json payload
177+ json_data = {
178+ "id": notice["id"],
179+ "description": notice["description"],
180+ "references": references,
181+ "cves": cves,
182+ "release_packages": release_packages,
183+ "title": notice["title"],
184+ "published": datetime.fromtimestamp(
185+ notice["timestamp"]
186+ ).isoformat(),
187+ "summary": notice["summary"],
188+ "instructions": notice.get("action"),
189+ }
190+
191 # Build endpoint
192- endpoint = args.endpoint
193- if http_method == "PUT":
194+ if args.action == 'add':
195+ upsert = not args.no_upsert
196+ http_method = 'POST'
197+ endpoint = args.endpoint
198+ elif args.action == 'update':
199+ upsert = not args.no_upsert
200+ http_method = 'PUT'
201 endpoint = f"{args.endpoint}/{notice['id']}"
202-
203- response = client.request(
204- http_method,
205- url=endpoint,
206- json={
207- "id": notice["id"],
208- "description": notice["description"],
209- "references": references,
210- "cves": cves,
211- "release_packages": release_packages,
212- "title": notice["title"],
213- "published": datetime.fromtimestamp(
214- notice["timestamp"]
215- ).isoformat(),
216- "summary": notice["summary"],
217- "instructions": notice.get("action"),
218- },
219- )
220-
221- print(response, response.text)
222-
223- #if args.errors == 'exit' and not re.match("2\d\d",str(response)):
224- #sys.exit(1)
225+ elif args.action == 'remove':
226+ upsert = False
227+ http_method = 'DELETE'
228+ endpoint = f"{args.endpoint}/{notice['id']}"
229+ json_data = None
230+
231+ response = client.request( http_method, url=endpoint, json=json_data)
232+ print(response, response.text[0:60])
233+
234+ if upsert and not re.match(r'^<Response \[2..\]>$',str(response)):
235+ if args.action == 'add':
236+ print("add failed, trying update")
237+ http_method = 'PUT'
238+ endpoint = f"{args.endpoint}/{notice['id']}"
239+ elif args.action == 'update':
240+ print("update failed, trying add")
241+ http_method = 'POST'
242+ endpoint = args.endpoint
243+ response = client.request( http_method, url=endpoint, json=json_data)
244+ print(response, response.text[0:60])
245+
246+ if args.stop and not re.match(r'^<Response \[2..\]>$',str(response)):
247+ sys.exit(1)

Subscribers

People subscribed via source and target branches