-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathsave.py
216 lines (191 loc) · 7.64 KB
/
save.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
from datetime import datetime
import requests
from django.utils.timezone import make_aware
from collectors.bzimport.collectors import BugzillaQuerier, FlawCollector
from osidb.constants import DATETIME_FMT
from osidb.exceptions import DataInconsistencyException
from osidb.models import Flaw
from .constants import SYNC_FLAWS_TO_BZ_ASYNCHRONOUSLY
from .exceptions import UnsaveableFlawError
from .query import FlawBugzillaQueryBuilder
class BugzillaSaver(BugzillaQuerier):
"""
Bugzilla bug save handler underlying model instance
the instance validity is assumed and not checked
"""
@property
def model(self):
"""
instance model class getter
needs to be defined in the subclasses
"""
raise NotImplementedError
@property
def query_builder(self):
"""
query builder class getter
needs to be defined in the subclasses
"""
raise NotImplementedError
def __init__(self, instance, bz_api_key=None):
"""
init stuff
"""
super().__init__()
self.instance = instance
# substitute the default service Bugzilla API key
# so the resulting Bugzilla audit log corresponds
# to the acutal user requesting the operation
if bz_api_key is not None:
self._bz_api_key = bz_api_key
def save(self):
"""
generic save serving as class entry point
which calls create or update handler to continue
returns an updated instance (without saving)
"""
return self.create() if self.instance.bz_id is None else self.update()
def create(self):
"""
create a bug underlying the model instance in Bugilla
"""
bugzilla_query_builder = self.query_builder(self.instance)
response = self.bz_conn.createbug(bugzilla_query_builder.query)
self.instance.bz_id = str(response.id)
if isinstance(self.instance, Flaw):
# update the meta_attr according to the changes
# since in the async mode we do not fetch them
self.model.objects.filter(uuid=self.instance.uuid).update(
meta_attr=bugzilla_query_builder.meta_attr
)
return self.instance
def update(self):
"""
update a bug underlying the model instance in Bugilla
"""
# switch of sync/async processing of flaws
if not isinstance(self.instance, Flaw) or not SYNC_FLAWS_TO_BZ_ASYNCHRONOUSLY:
try:
bugzilla_query_builder = self.query_builder(self.instance)
self.check_collisions() # check for collisions right before the update
except DataInconsistencyException:
if not isinstance(self.instance, Flaw):
raise
# to mitigate user discomfort and also possible service errors
# we handle the flaws more gently here attempting to resync
flaw_collector = FlawCollector()
flaw_collector.sync_flaw(self.instance.bz_id)
# resync from the DB and repeat the query building
self.instance.meta_attr = Flaw.objects.get(
pk=self.instance.pk
).meta_attr # update metadata after refresh
bugzilla_query_builder = self.query_builder(self.instance)
self.check_collisions() # if still colliding then something is very wrong
try:
self.bz_conn.update_bugs(
[self.instance.bz_id], bugzilla_query_builder.query
)
except requests.exceptions.HTTPError as e:
# this is a heuristic at best, we know that the data we submit to
# bugzilla has already been validated and are pretty sure that the
# error is not due to the request being malformed, but it could be.
# bugzilla returns a 400 error on concurrent updates even though
# this is not the client's fault, and the HTTPError bubbled up
# by requests / python-bugzilla doesn't contain the response
# embedded into it, so all we can do is a string comparison.
if "400" in str(e):
raise DataInconsistencyException(
"Failed to write back to Bugzilla, this is likely due to a "
"concurrent update which Bugzilla does not support, "
"try again later."
) from e
# reraise otherwise
raise e
return self.instance
else:
bugzilla_query_builder = self.query_builder(self.instance)
self.bz_conn.update_bugs(
[self.instance.bz_id], bugzilla_query_builder.query
)
# update the meta_attr according to the changes
# since in the async mode we do not fetch them
self.model.objects.filter(uuid=self.instance.uuid).update(
meta_attr=bugzilla_query_builder.meta_attr
)
return self.instance
def check_collisions(self):
"""
one last preventative check that Bugzilla last_change_time
really corresponds to the stored one so there was no collision
"""
if self.actual_last_change != self.stored_last_change:
raise DataInconsistencyException(
"Save operation based on an outdated model instance: "
f"Bugzilla last change time {self.actual_last_change} "
f"differs from OSIDB {self.stored_last_change}. "
"You need to wait a minute for the data refresh."
)
@property
def actual_last_change(self):
"""
retrieve the actual last change timestamp from Bugzilla
"""
return make_aware(
datetime.strptime(
self.get_bug_data(
self.instance.bz_id, include_fields=["last_change_time"]
)["last_change_time"],
DATETIME_FMT,
)
)
@property
def stored_last_change(self):
"""
retrieve the stored last change timestamp from DB
"""
last_change_time = (
self.instance.meta_attr["last_change_time"]
if "last_change_time" in self.instance.meta_attr
else self.instance.meta_attr["updated_dt"]
)
# let us support multiple date formats
return datetime.fromisoformat(last_change_time.replace("Z", "+00:00"))
class FlawBugzillaSaver(BugzillaSaver):
"""
Bugzilla flaw bug save handler
"""
@property
def flaw(self):
"""
concrete name shortcut
"""
return self.instance
@property
def model(self):
"""
Flaw model class getter
"""
return Flaw
@property
def query_builder(self):
"""
query builder class getter
"""
return FlawBugzillaQueryBuilder
def update(self):
"""
update flaw in Bugzilla
"""
# TODO flaws with multiple CVEs introduce a paradox behavior
# when modifying a flaw the way that the CVE ID is removed as
# in OSIDB it basically results in a flaw removal
# so let us restrict it for now - should be rare
if (
self.model.objects.filter(meta_attr__bz_id=self.flaw.bz_id).count() > 1
and not self.flaw.cve_id
):
raise UnsaveableFlawError(
"Unable to remove a CVE ID from a flaw with multiple CVEs "
"due to an ambigous N to 1 OSIDB to Buzilla flaw mapping"
)
return super().update()