Merge ~andersson123/autopkgtest-cloud:no_queue_duplicate_requests into autopkgtest-cloud:master
- Git
- lp:~andersson123/autopkgtest-cloud
- no_queue_duplicate_requests
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 66b81baafda8429a43fb42399e928e3660a8c6f9 | ||||
Proposed branch: | ~andersson123/autopkgtest-cloud:no_queue_duplicate_requests | ||||
Merge into: | autopkgtest-cloud:master | ||||
Diff against target: |
173 lines (+118/-1) 2 files modified
charms/focal/autopkgtest-web/webcontrol/request/app.py (+3/-1) charms/focal/autopkgtest-web/webcontrol/request/submit.py (+115/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brian Murray | Approve | ||
Review via email: mp+447108@code.launchpad.net |
Commit message
Description of the change
Tim Andersson (andersson123) wrote : | # |
Tim Andersson (andersson123) wrote : | # |
Ready for re-review.
Brian Murray (brian-murray) wrote : | # |
Is there any point in time where either json file will not exist or be empty? If the answer is yes or we aren't sure then the code should be modified to handle this situation as it currently looks like it'll produce a traceback in those situations.
Additionally, https:/
Otherwise the content of the functions you've added looks great.
Tim Andersson (andersson123) wrote : | # |
I added this too and removed it because I came to the conclusion if those two files don't exist, nothing will be working anyway and the problem will be elsewhere. It could almost be a helpful alarm bell.
Tim Andersson (andersson123) wrote : | # |
Do you think we also need to add it to validate_
Tim Andersson (andersson123) wrote : | # |
All amended based on your comments, tested and works as intended. Let me know what you think re validate_
Brian Murray (brian-murray) wrote : | # |
> I added this too and removed it because I came to the conclusion if those two
> files don't exist, nothing will be working anyway and the problem will be
> elsewhere. It could almost be a helpful alarm bell.
What would the failure scenario look like though? Would the web service completely fall over preventing any requests from being accepted? If that's the case I'd rather accept any request, including a duplicate one, rather then having the service fail.
Brian Murray (brian-murray) wrote : | # |
Please modify this to return a 403 instead of a 400 and yes I think it should be added to validate_
Tim Andersson (andersson123) wrote : | # |
Amended and rebased. What do you think about my exit code implementation?
Brian Murray (brian-murray) wrote : | # |
> Amended and rebased. What do you think about my exit code implementation?
Looking at submit.py there are multiple cases where ValueError is used that seem more like a 400 than a 403, so if we were to look at a simple solution I'd suggest using TypeError for a duplicate request and return a 403 with that.
However, that doesn't feel very clean and I think we should instead look at using a custom exception so that we can more be more granular with our exit / return codes. (I thought I also saw some cases where we'd ideally return a 401 instead of a 400.)
In the interest of just getting this done though let's do the simple solution (or something like it) now and create a card, or bug, for improving the way in which we use http status codes.
Tim Andersson (andersson123) wrote : | # |
I've added the check in validate_
Brian Murray (brian-murray) wrote : | # |
I imagine we'll hear pretty quickly if validate_
Preview Diff
1 | diff --git a/charms/focal/autopkgtest-web/webcontrol/request/app.py b/charms/focal/autopkgtest-web/webcontrol/request/app.py |
2 | index c8791a5..beeb22d 100644 |
3 | --- a/charms/focal/autopkgtest-web/webcontrol/request/app.py |
4 | +++ b/charms/focal/autopkgtest-web/webcontrol/request/app.py |
5 | @@ -9,7 +9,7 @@ from html import escape as _escape |
6 | from flask import Flask, redirect, request, session |
7 | from flask_openid import OpenID |
8 | from helpers.utils import setup_key |
9 | -from request.submit import Submit |
10 | +from request.submit import DuplicateRequestException, Submit |
11 | from werkzeug.middleware.proxy_fix import ProxyFix |
12 | |
13 | # map multiple GET vars to AMQP JSON request parameter list |
14 | @@ -236,6 +236,8 @@ def index_root(): |
15 | s.validate_distro_request(**params) |
16 | except (ValueError, TypeError) as e: |
17 | return invalid(e) |
18 | + except DuplicateRequestException as e: |
19 | + return invalid(e, 403) |
20 | |
21 | if params.get("delete"): |
22 | del params["delete"] |
23 | diff --git a/charms/focal/autopkgtest-web/webcontrol/request/submit.py b/charms/focal/autopkgtest-web/webcontrol/request/submit.py |
24 | index 04d7f61..aeeb9a9 100644 |
25 | --- a/charms/focal/autopkgtest-web/webcontrol/request/submit.py |
26 | +++ b/charms/focal/autopkgtest-web/webcontrol/request/submit.py |
27 | @@ -18,6 +18,11 @@ from urllib.error import HTTPError |
28 | import amqplib.client_0_8 as amqp |
29 | from distro_info import UbuntuDistroInfo |
30 | |
31 | + |
32 | +class DuplicateRequestException(Exception): |
33 | + pass |
34 | + |
35 | + |
36 | # Launchpad REST API base |
37 | LP = "https://api.launchpad.net/1.0/" |
38 | NAME = re.compile("^[a-z0-9][a-z0-9.+-]+$") |
39 | @@ -31,6 +36,11 @@ ALLOWED_TEAMS = ["canonical-kernel-distro-team", "autopkgtest-requestors"] |
40 | # not teams |
41 | ALLOWED_USERS_PERPACKAGE = {"snapcraft": ["snappy-m-o"]} |
42 | |
43 | +# Path to json file detailing the queue |
44 | +QUEUE_FP = "/var/lib/cache-amqp/queued.json" |
45 | +# Path to json file detailing the running tests |
46 | +RUNNING_FP = "/run/amqp-status-collector/running.json" |
47 | + |
48 | |
49 | class Submit: |
50 | def __init__(self): |
51 | @@ -77,6 +87,12 @@ class Submit: |
52 | Raise ValueError with error messsage if the request is invalid, |
53 | otherwise return. |
54 | """ |
55 | + not_needed, msg = self.is_request_queued_or_running( |
56 | + release, arch, package, triggers |
57 | + ) |
58 | + if not_needed: |
59 | + raise DuplicateRequestException(msg) |
60 | + |
61 | can_upload_any_trigger = False |
62 | |
63 | try: |
64 | @@ -213,6 +229,21 @@ class Submit: |
65 | Raise ValueError with error messsage if the request is invalid, |
66 | otherwise return. |
67 | """ |
68 | + triggers = [] |
69 | + for env_var in env: |
70 | + if "trigger" in env_var: |
71 | + if "," in env_var: |
72 | + for trig in env_var.split("=")[1].split(","): |
73 | + triggers.append(trig) |
74 | + else: |
75 | + triggers.append(env_var.split("=")[1]) |
76 | + |
77 | + not_needed, msg = self.is_request_queued_or_running( |
78 | + release, arch, package, triggers |
79 | + ) |
80 | + if not_needed: |
81 | + raise DuplicateRequestException(msg) |
82 | + |
83 | if release not in self.releases: |
84 | raise ValueError("Unknown release " + release) |
85 | if arch not in self.architectures: |
86 | @@ -509,3 +540,87 @@ class Submit: |
87 | return (500, None) |
88 | logging.debug("lp_request %s succeeded: %s", url, response) |
89 | return (code, response) |
90 | + |
91 | + def is_test_running(self, req_series, req_arch, req_package, req_triggers): |
92 | + if not os.path.isfile(RUNNING_FP): |
93 | + return False |
94 | + data = {} |
95 | + with open(RUNNING_FP, "r") as f: |
96 | + data = json.load(f) |
97 | + for pkg in data: |
98 | + for submitted in data[pkg]: |
99 | + releases = data[pkg][submitted].keys() |
100 | + for release in data[pkg][submitted]: |
101 | + architectures = data[pkg][submitted][release].keys() |
102 | + triggers = submitted[submitted.find(";triggers_") + 1 :] |
103 | + triggers = triggers[: triggers.find(";")] |
104 | + triggers = triggers[ |
105 | + triggers.find("[") + 1 : triggers.find("]") |
106 | + ] |
107 | + triggers = triggers.replace("'", "").split(", ") |
108 | + if ( |
109 | + req_arch in architectures |
110 | + and req_series in releases |
111 | + and req_package == pkg |
112 | + and sorted(triggers) == sorted(req_triggers) |
113 | + ): |
114 | + return True |
115 | + return False |
116 | + |
117 | + def is_test_in_queue( |
118 | + self, req_series, req_arch, req_package, req_triggers |
119 | + ): |
120 | + if not os.path.isfile(QUEUE_FP): |
121 | + return False |
122 | + data = {} |
123 | + with open(QUEUE_FP, "r") as f: |
124 | + data = json.load(f) |
125 | + data = data["queues"] |
126 | + this_test = { |
127 | + "release": req_series, |
128 | + "arch": req_arch, |
129 | + "package": req_package, |
130 | + "triggers": sorted(req_triggers), |
131 | + } |
132 | + for test_type in data: |
133 | + for release in data[test_type]: |
134 | + for arch in data[test_type][release]: |
135 | + packages = data[test_type][release][arch] |
136 | + if packages["size"] != 0: |
137 | + for req in packages["requests"]: |
138 | + pkg = req[: req.find("{")].rstrip() |
139 | + details = json.loads(req[req.find("{") :]) |
140 | + triggers = details["triggers"] |
141 | + test = { |
142 | + "release": release, |
143 | + "arch": arch, |
144 | + "package": pkg, |
145 | + "triggers": sorted(triggers), |
146 | + } |
147 | + if test == this_test: |
148 | + return True |
149 | + return False |
150 | + |
151 | + def is_request_queued_or_running( |
152 | + self, req_series, req_arch, req_package, req_triggers=[] |
153 | + ): |
154 | + if self.is_test_running( |
155 | + req_series, req_arch, req_package, req_triggers |
156 | + ): |
157 | + o_msg = "Requested test is already running." |
158 | + o_msg += "\nRelease: " + req_series |
159 | + o_msg += "\nArchitecture: " + req_arch |
160 | + o_msg += "\nPackage: " + req_package |
161 | + o_msg += "\nTriggers: " + ",".join(req_triggers) |
162 | + return True, o_msg |
163 | + |
164 | + if self.is_test_in_queue( |
165 | + req_series, req_arch, req_package, req_triggers |
166 | + ): |
167 | + o_msg = "Requested test is already queued." |
168 | + o_msg += "\nRelease: " + req_series |
169 | + o_msg += "\nArchitecture: " + req_arch |
170 | + o_msg += "\nPackage: " + req_package |
171 | + o_msg += "\nTriggers: " + ",".join(req_triggers) |
172 | + return True, o_msg |
173 | + return False, "" |
Just going to quickly test this in staging then it's good to go.