/
models.py
4573 lines (3824 loc) · 176 KB
/
models.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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# Copyright © 2011-2019 Red Hat, Inc. and others.
#
# This file is part of Bodhi.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Bodhi's database models."""
from collections import defaultdict
from datetime import datetime
from textwrap import wrap
import copy
import hashlib
import json
import os
import re
import rpm
import time
import uuid
from simplemediawiki import MediaWiki
from six.moves.urllib.parse import quote
from sqlalchemy import (and_, Boolean, Column, DateTime, event, ForeignKey,
Integer, or_, Table, Unicode, UnicodeText, UniqueConstraint)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import class_mapper, relationship, backref, validates
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.properties import RelationshipProperty
from sqlalchemy.sql import text
from sqlalchemy.types import SchemaType, TypeDecorator, Enum
import requests.exceptions
import six
from bodhi.server import bugs, buildsys, log, mail, notifications, Session, util
from bodhi.server.config import config
from bodhi.server.exceptions import BodhiException, LockedUpdateException
from bodhi.server.util import (
avatar as get_avatar, build_evr, flash_log, get_critpath_components,
get_rpm_header, header, tokenize, pagure_api_get)
if six.PY2:
from pkgdb2client import PkgDB
# http://techspot.zzzeek.org/2011/01/14/the-enum-recipe
class EnumSymbol(object):
"""Define a fixed symbol tied to a parent class."""
def __init__(self, cls_, name, value, description):
"""
Initialize the EnumSymbol.
Args:
cls_ (EnumMeta): The metaclass this symbol is tied to.
name (basestring): The name of this symbol.
value (basestring): The value used in the database to represent this symbol.
description (basestring): A human readable description of this symbol.
"""
self.cls_ = cls_
self.name = name
self.value = value
self.description = description
def __lt__(self, other):
"""
Return True if self.value is less than other.value.
Args:
other (EnumSymbol): The other EnumSymbol we are being compared to.
Returns:
bool: True if self.value is less than other.value, False otherwise.
"""
return self.value < other.value
def __reduce__(self):
"""
Allow unpickling to return the symbol linked to the DeclEnum class.
Returns:
tuple: A 2-tuple of the ``getattr`` function, and a 2-tuple of the EnumSymbol's member
class and name.
"""
return getattr, (self.cls_, self.name)
def __iter__(self):
"""
Iterate over this EnumSymbol's value and description.
Returns:
iterator: An iterator over the value and description.
"""
return iter([self.value, self.description])
def __repr__(self):
"""
Return a string representation of this EnumSymbol.
Returns:
basestring: A string representation of this EnumSymbol's value.
"""
return "<%s>" % self.name
def __unicode__(self):
"""
Return a string representation of this EnumSymbol.
Returns:
unicode: A string representation of this EnumSymbol's value.
"""
return six.text_type(self.value)
__str__ = __unicode__
def __json__(self, request=None):
"""
Return a JSON representation of this EnumSymbol.
Args:
request (pyramid.util.Request): The current request.
Returns:
basestring: A string representation of this EnumSymbol's value.
"""
return self.value
class EnumMeta(type):
"""Generate new DeclEnum classes."""
def __init__(cls, classname, bases, dict_):
"""
Initialize the metaclass.
Args:
classname (basestring): The name of the enum.
bases (list): A list of base classes for the enum.
dict_ (dict): A key-value mapping for the new enum's attributes.
Returns:
DeclEnum: A new DeclEnum.
"""
cls._reg = reg = cls._reg.copy()
for k, v in dict_.items():
if isinstance(v, tuple):
sym = reg[v[0]] = EnumSymbol(cls, k, *v)
setattr(cls, k, sym)
return type.__init__(cls, classname, bases, dict_)
def __iter__(cls):
"""
Iterate the enum values.
Returns:
iterator: An iterator for the enum values.
"""
return iter(cls._reg.values())
class DeclEnum(six.with_metaclass(EnumMeta, object)):
"""Declarative enumeration."""
_reg = {}
@classmethod
def from_string(cls, value):
"""
Convert a string version of the enum to its enum type.
Args:
value (basestring): A string that you wish to convert to an Enum value.
Returns:
EnumSymbol: The symbol corresponding to the value.
Raises:
ValueError: If no symbol matches the given value.
"""
try:
return cls._reg[value]
except KeyError:
raise ValueError("Invalid value for %r: %r" % (cls.__name__, value))
@classmethod
def values(cls):
"""
Return the possible values that this enum can take on.
Returns:
list: A list of strings of the values that this enum can represent.
"""
return list(cls._reg.keys())
@classmethod
def db_type(cls):
"""
Return a database column type to be used for this enum.
Returns:
DeclEnumType: A DeclEnumType to be used for this enum.
"""
return DeclEnumType(cls)
class DeclEnumType(SchemaType, TypeDecorator):
"""A database column type for an enum."""
def __init__(self, enum):
"""
Initialize with the given enum.
Args:
enum (bodhi.server.models.EnumMeta): The enum metaclass.
"""
self.enum = enum
self.impl = Enum(
*enum.values(),
name="ck%s" % re.sub('([A-Z])', lambda m: "_" + m.group(1).lower(), enum.__name__))
def _set_table(self, table, column):
"""
Set the table for this object.
Args:
table (sqlalchemy.sql.schema.Table): The table that uses this Enum.
column (sqlalchemy.sql.schema.Column): The column that uses this Enum.
"""
self.impl._set_table(table, column)
def copy(self):
"""
Return a copy of self.
Returns:
DeclEnumType: A copy of self.
"""
return DeclEnumType(self.enum)
def process_bind_param(self, value, dialect):
"""
Return the value of the enum.
Args:
value (bodhi.server.models.enum.EnumSymbol): The enum symbol we are resolving the value
of.
dialect (sqlalchemy.engine.default.DefaultDialect): Unused.
Returns:
basestring: The EnumSymbol's value.
"""
if value is None:
return None
return value.value
def process_result_value(self, value, dialect):
"""
Return the enum that matches the given string.
Args:
value (basestring): The name of an enum.
dialect (sqlalchemy.engine.default.DefaultDialect): Unused.
Returns:
EnumSymbol or None: The enum that matches value, or ``None`` if ``value`` is ``None``.
"""
if value is None:
return None
return self.enum.from_string(value.strip())
def create(self, bind=None, checkfirst=False):
"""Issue CREATE ddl for this type, if applicable."""
super(DeclEnumType, self).create(bind, checkfirst)
t = self.dialect_impl(bind.dialect)
if t.impl.__class__ is not self.__class__ and isinstance(t, SchemaType):
t.impl.create(bind=bind, checkfirst=checkfirst)
def drop(self, bind=None, checkfirst=False):
"""Issue DROP ddl for this type, if applicable."""
super(DeclEnumType, self).drop(bind, checkfirst)
t = self.dialect_impl(bind.dialect)
if t.impl.__class__ is not self.__class__ and isinstance(t, SchemaType):
t.impl.drop(bind=bind, checkfirst=checkfirst)
class BodhiBase(object):
"""
Base class for the SQLAlchemy model base class.
Attributes:
__exclude_columns__ (tuple): A list of columns to exclude from JSON
__include_extras__ (tuple): A list of methods or attrs to include in JSON
__get_by__ (tuple): A list of columns that :meth:`.get` will query.
id (int): An integer id that serves as the default primary key.
query (sqlalchemy.orm.query.Query): a class property which produces a
Query object against the class and the current Session when called.
"""
__exclude_columns__ = ('id',)
__include_extras__ = tuple()
__get_by__ = ()
id = Column(Integer, primary_key=True)
query = Session.query_property()
@classmethod
def get(cls, id):
"""
Return an instance of the model by using its __get_by__ attribute with id.
Args:
id (object): An attribute to look up the model by.
Returns:
BodhiBase or None: An instance of the model that matches the id, or ``None`` if no match
was found.
"""
return cls.query.filter(or_(
getattr(cls, col) == id for col in cls.__get_by__
)).first()
def __getitem__(self, key):
"""
Define a dictionary like interface for the models.
Args:
key (string): The name of an attribute you wish to retrieve from the model.
Returns:
object: The value of the attribute represented by key.
"""
return getattr(self, key)
def __repr__(self):
"""
Return a string representation of this model.
Returns:
basestring: A string representation of this model.
"""
return '<{0} {1}>'.format(self.__class__.__name__, self.__json__())
def __json__(self, request=None, anonymize=False, exclude=None, include=None):
"""
Return a JSON representation of this model.
Args:
request (pyramid.util.Request or None): The current web request, or None.
anonymize (bool): If True, scrub out some information from the JSON blob using
the model's ``__anonymity_map__``. Defaults to False.
exclude (iterable or None): An iterable of strings naming the attributes to exclude from
the JSON representation of the model. If None (the default), the class's
__exclude_columns__ attribute will be used.
include (iterable or None): An iterable of strings naming the extra attributes to
include in the JSON representation of the model. If None (the default), the class's
__include_extras__ attribute will be used.
Returns:
dict: A dict representation of the model suitable for serialization as JSON.
"""
return self._to_json(self, request=request, anonymize=anonymize, exclude=exclude,
include=include)
@classmethod
def _to_json(cls, obj, seen=None, request=None, anonymize=False, exclude=None, include=None):
"""
Return a JSON representation of obj.
Args:
obj (BodhiBase): The model to serialize.
seen (list or None): A list of attributes we have already serialized. Used by this
method to keep track of its state, as it uses recursion.
request (pyramid.util.Request or None): The current web request, or None.
anonymize (bool): If True, scrub out some information from the JSON blob using
the model's ``__anonymity_map__``. Defaults to False.
exclude (iterable or None): An iterable of strings naming the attributes to exclude from
the JSON representation of the model. If None (the default), the class's
__exclude_columns__ attribute will be used.
include (iterable or None): An iterable of strings naming the extra attributes to
include in the JSON representation of the model. If None (the default), the class's
__include_extras__ attribute will be used.
Returns:
dict: A dict representation of the model suitable for serialization.
"""
if not seen:
seen = []
if not obj:
return
if exclude is None:
exclude = getattr(obj, '__exclude_columns__', [])
properties = list(class_mapper(type(obj)).iterate_properties)
rels = [p.key for p in properties if isinstance(p, RelationshipProperty)]
attrs = [p.key for p in properties if p.key not in rels]
d = dict([(attr, getattr(obj, attr)) for attr in attrs
if attr not in exclude and not attr.startswith('_')])
if include is None:
include = getattr(obj, '__include_extras__', [])
for name in include:
attribute = getattr(obj, name)
if callable(attribute):
attribute = attribute(request)
d[name] = attribute
for attr in rels:
if attr in exclude:
continue
target = getattr(type(obj), attr).property.mapper.class_
if target in seen:
continue
d[attr] = cls._expand(obj, getattr(obj, attr), seen, request)
for key, value in six.iteritems(d):
if isinstance(value, datetime):
d[key] = value.strftime('%Y-%m-%d %H:%M:%S')
if isinstance(value, EnumSymbol):
d[key] = six.text_type(value)
# If explicitly asked to, we will overwrite some fields if the
# corresponding condition of each evaluates to True.
# This is primarily for anonymous Comments. We want to serialize
# authenticated FAS usernames in the 'author' field, but we want to
# scrub out anonymous users' email addresses.
if anonymize:
for key1, key2 in getattr(obj, '__anonymity_map__', {}).items():
if getattr(obj, key2):
d[key1] = 'anonymous'
return d
@classmethod
def _expand(cls, obj, relation, seen, req):
"""
Return the to_json or id of a sqlalchemy relationship.
Args:
obj (BodhiBase): The object we are trying to describe a relationship on.
relation (object): A relationship attribute on obj we are trying to learn about.
seen (list): A list of objects we have already recursed over.
req (pyramid.util.Request): The current request.
Returns:
object: The to_json() or the id of a sqlalchemy relationship.
"""
if hasattr(relation, 'all'):
relation = relation.all()
if hasattr(relation, '__iter__'):
return [cls._expand(obj, item, seen, req) for item in relation]
if type(relation) not in seen:
return cls._to_json(relation, seen + [type(obj)], req)
else:
return relation.id
@classmethod
def grid_columns(cls):
"""
Return the column names for the model, except for the excluded ones.
Returns:
list: A list of column names, with excluded ones removed.
"""
columns = []
exclude = getattr(cls, '__exclude_columns__', [])
for col in cls.__table__.columns:
if col.name in exclude:
continue
columns.append(col.name)
return columns
@classmethod
def find_polymorphic_child(cls, identity):
"""
Find a child of a polymorphic base class.
For example, given the base Package class and the 'rpm' identity, this
class method should return the RpmPackage class.
This is accomplished by iterating over all classes in scope.
Limiting that to only those which are an extension of the given base
class. Among those, return the one whose polymorphic_identity matches
the value given. If none are found, then raise a NameError.
Args:
identity (EnumSymbol): An instance of EnumSymbol used to identify the child.
Returns:
BodhiBase: The type-specific child class.
Raises:
KeyError: If this class is not polymorphic.
NameError: If no child class is found for the given identity.
TypeError: If identity is not an EnumSymbol.
"""
if not isinstance(identity, EnumSymbol):
raise TypeError("%r is not an instance of EnumSymbol" % identity)
if 'polymorphic_on' not in getattr(cls, '__mapper_args__', {}):
raise KeyError("%r is not a polymorphic model." % cls)
classes = (c for c in globals().values() if isinstance(c, type))
children = (c for c in classes if issubclass(c, cls))
for child in children:
candidate = child.__mapper_args__.get('polymorphic_identity')
if candidate is identity:
return child
error = "Found no child of %r with identity %r"
raise NameError(error % (cls, identity))
def update_relationship(self, name, model, data, db): # pragma: no cover
"""
Add items to or remove items from a many-to-many relationship.
pragma: no cover is on this method because it is only used by Stacks, which is not used by
Fedora and will likely be removed in the future.
See https://github.com/fedora-infra/bodhi/issues/2241
Args:
name (basestring): The name of the relationship column on self, as well as the key in
``data``.
model (BodhiBase): The model class of the relationship that we're updating.
data (dict): A dict containing the key `name` with a list of values.
db (sqlalchemy.orm.session.Session): A database session.
Return:
tuple: A three-tuple of lists, `new`, `same`, and `removed`, indicating which items have
been added and removed, and which remain unchanged.
"""
rel = getattr(self, name)
items = data.get(name)
new, same, removed = [], copy.copy(items), []
if items:
for item in items:
obj = model.get(item)
if not obj:
obj = model(name=item)
db.add(obj)
if obj not in rel:
rel.append(obj)
new.append(item)
same.remove(item)
for item in rel:
if item.name not in items:
log.info('Removing %r from %r', item, self)
rel.remove(item)
removed.append(item.name)
return new, same, removed
Base = declarative_base(cls=BodhiBase)
metadata = Base.metadata
##
# Enumerated type declarations
##
class ContentType(DeclEnum):
"""
Used to differentiate between different kinds of content in various models.
This enum is used to mark objects as pertaining to particular kinds of content type, such as
RPMs or Modules.
Attributes:
base (EnumSymbol): This is used to represent base classes that are shared between specific
content types.
rpm (EnumSymbol): Used to represent RPM related objects.
module (EnumSymbol): Used to represent Module related objects.
container (EnumSymbol): Used to represent Container related objects.
flatpak (EnumSymbol): Used to represent Flatpak related objects.
"""
base = 'base', 'Base'
rpm = 'rpm', 'RPM'
module = 'module', 'Module'
container = 'container', 'Container'
flatpak = 'flatpak', 'Flatpak'
@classmethod
def infer_content_class(cls, base, build):
"""
Identify and return the child class associated with the appropriate ContentType.
For example, given the Package base class and a normal koji build, return
the RpmPackage model class. Or, given the Build base class and a container
build, return the ContainerBuild model class.
Args:
base (BodhiBase): A base model class, such as :class:`Build` or :class:`Package`.
build (dict): Information about the build from the build system (koji).
Returns:
BodhiBase: The type-specific child class of base that is appropriate to use with the
given koji build.
"""
# Default value. Overridden below if we find markers in the build info
identity = cls.rpm
extra = build.get('extra') or {}
if 'module' in extra.get('typeinfo', {}):
identity = cls.module
elif 'container_koji_task_id' in extra:
if 'flatpak' in extra['image']:
identity = cls.flatpak
else:
identity = cls.container
return base.find_polymorphic_child(identity)
class UpdateStatus(DeclEnum):
"""
An enum used to describe the current state of an update.
Attributes:
pending (EnumSymbol): The update is not in any repository.
testing (EnumSymbol): The update is in the testing repository.
stable (EnumSymbol): The update is in the stable repository.
unpushed (EnumSymbol): The update had been in a testing repository, but has been removed.
obsolete (EnumSymbol): The update has been obsoleted by another update.
processing (EnumSymbol): Unused.
side_tag_active (EnumSymbol): The update's side tag is currently active.
side_tag_expired (EnumSymbol): The update's side tag has expired.
"""
pending = 'pending', 'pending'
testing = 'testing', 'testing'
stable = 'stable', 'stable'
unpushed = 'unpushed', 'unpushed'
obsolete = 'obsolete', 'obsolete'
processing = 'processing', 'processing'
side_tag_active = 'side_tag_active', 'Side tag active'
side_tag_expired = 'side_tag_expired', 'Side tag expired'
class TestGatingStatus(DeclEnum):
"""
This class lists the different status the ``Update.test_gating_status`` flag can have.
Attributes:
waiting (EnumSymbol): Bodhi is waiting to hear about the test gating status of the update.
ignored (EnumSymbol): Greenwave said that the update does not require any tests.
queued (EnumSymbol): Greenwave said that the required tests for this update have been
queued.
running (EnumSymbol): Greenwave said that the required tests for this update are running.
passed (EnumSymbol): Greenwave said that the required tests for this update have passed.
failed (EnumSymbol): Greenwave said that the required tests for this update have failed.
"""
waiting = 'waiting', 'Waiting'
ignored = 'ignored', 'Ignored'
queued = 'queued', 'Queued'
running = 'running', 'Running'
passed = 'passed', 'Passed'
failed = 'failed', 'Failed'
greenwave_failed = 'greenwave_failed', 'Greenwave failed to respond'
class UpdateType(DeclEnum):
"""
An enum used to classify the type of the update.
Attributes:
bugfix (EnumSymbol): The update fixes bugs only.
security (EnumSymbol): The update addresses security issues.
newpackage (EnumSymbol): The update introduces new packages to the release.
enhancement (EnumSymbol): The update introduces new features.
"""
bugfix = 'bugfix', 'bugfix'
security = 'security', 'security'
newpackage = 'newpackage', 'newpackage'
enhancement = 'enhancement', 'enhancement'
class UpdateRequest(DeclEnum):
"""
An enum used to specify an update requesting to change states.
Attributes:
testing (EnumSymbol): The update is requested to change to testing.
batched (EnumSymbol): The update is requested to be pushed to stable during the next batch
push.
obsolete (EnumSymbol): The update has been obsoleted by another update.
unpush (EnumSymbol): The update no longer needs to be released.
revoke (EnumSymbol): The unpushed update will no longer be mashed in any repository.
stable (EnumSymbol): The update is ready to be pushed to the stable repository.
"""
testing = 'testing', 'testing'
batched = 'batched', 'batched'
obsolete = 'obsolete', 'obsolete'
unpush = 'unpush', 'unpush'
revoke = 'revoke', 'revoke'
stable = 'stable', 'stable'
class UpdateSeverity(DeclEnum):
"""
An enum used to specify the severity of the update.
Attributes:
unspecified (EnumSymbol): The packager has not specified a severity.
urgent (EnumSymbol): The update is urgent, and will skip the batched state automatically.
high (EnumSymbol): The update is high severity.
medium (EnumSymbol): The update is medium severity.
low (EnumSymbol): The update is low severity.
"""
unspecified = 'unspecified', 'unspecified'
urgent = 'urgent', 'urgent'
high = 'high', 'high'
medium = 'medium', 'medium'
low = 'low', 'low'
class UpdateSuggestion(DeclEnum):
"""
An enum used to tell the user whether they need to reboot or logout after applying an update.
Attributes:
unspecified (EnumSymbol): No action is needed.
reboot (EnumSymbol): The user should reboot after applying the update.
logout (EnumSymbol): The user should logout after applying the update.
"""
unspecified = 'unspecified', 'unspecified'
reboot = 'reboot', 'reboot'
logout = 'logout', 'logout'
class ReleaseState(DeclEnum):
"""
An enum that describes the state of a :class:`Release`.
Attributes:
disabled (EnumSymbol): Indicates that the release is disabled.
pending (EnumSymbol): Indicates that the release is pending.
current (EnumSymbol): Indicates that the release is current.
archived (EnumSymbol): Indicates taht the release is archived.
"""
disabled = 'disabled', 'disabled'
pending = 'pending', 'pending'
current = 'current', 'current'
archived = 'archived', 'archived'
class ComposeState(DeclEnum):
"""
Define the various states that a :class:`Compose` can be in.
Attributes:
requested (EnumSymbol): A compose has been requested, but it has not started yet.
pending (EnumSymbol): The request for the compose has been received by the backend worker,
but the compose has not started yet.
initializing (EnumSymbol): The compose is initializing.
updateinfo (EnumSymbol): The updateinfo.xml is being generated.
punging (EnumSymbol): A Pungi soldier has been deployed to deal with the situation.
syncing_repo (EnumSymbol): The repo is being synced to the master mirror.
notifying (EnumSymbol): Pungi has finished successfully, and we are now sending out various
forms of notifications, such as e-mail, fedmsgs, and bugzilla.
success (EnumSymbol): The Compose has completed successfully.
failed (EnumSymbol): The compose has failed, abandon hope.
signing_repo (EnumSymbol): Waiting for the repo to be signed.
cleaning (EnumSymbol): Cleaning old Composes after successful completion.
"""
requested = 'requested', 'Requested'
pending = 'pending', 'Pending'
initializing = 'initializing', 'Initializing'
updateinfo = 'updateinfo', 'Generating updateinfo.xml'
punging = 'punging', 'Waiting for Pungi to finish'
syncing_repo = 'syncing_repo', 'Wait for the repo to hit the master mirror'
notifying = 'notifying', 'Sending notifications'
success = 'success', 'Success'
failed = 'failed', 'Failed'
signing_repo = 'signing_repo', 'Signing repo'
cleaning = 'cleaning', 'Cleaning old composes'
##
# Association tables
##
update_bug_table = Table(
'update_bug_table', metadata,
Column('update_id', Integer, ForeignKey('updates.id')),
Column('bug_id', Integer, ForeignKey('bugs.id')))
update_cve_table = Table(
'update_cve_table', metadata,
Column('update_id', Integer, ForeignKey('updates.id')),
Column('cve_id', Integer, ForeignKey('cves.id')))
bug_cve_table = Table(
'bug_cve_table', metadata,
Column('bug_id', Integer, ForeignKey('bugs.id')),
Column('cve_id', Integer, ForeignKey('cves.id')))
user_package_table = Table(
'user_package_table', metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('package_id', Integer, ForeignKey('packages.id')))
class Release(Base):
"""
Represent a distribution release, such as Fedora 27.
Attributes:
name (unicode): The name of the release, such as 'F27'.
long_name (unicode): A human readable name for the release, such as 'Fedora 27'.
version (unicode): The version of the release, such as '27'.
id_prefix (unicode): The prefix to use when forming update aliases for this release, such as
'FEDORA'.
branch (unicode): The dist-git branch associated with this release, such as 'f27'.
dist_tag (unicode): The koji dist_tag associated with this release, such as 'f27'.
stable_tag (unicode): The koji tag to be used for stable builds in this release, such as
'f27-updates'.
testing_tag (unicode): The koji tag to be used for testing builds in this release, such as
'f27-updates-testing'.
candidate_tag (unicode): The koji tag used for builds that are candidates to be updates,
such as 'f27-updates-candidate'.
pending_signing_tag (unicode): The koji tag that specifies that a build is waiting to be
signed, such as 'f27-signing-pending'.
pending_testing_tag (unicode): The koji tag that indicates that a build is waiting to be
mashed into the testing repository, such as 'f27-updates-testing-pending'.
pending_stable_tag (unicode): The koji tag that indicates that a build is waiting to be
mashed into the stable repository, such as 'f27-updates-pending'.
override_tag (unicode): The koji tag that is used when a build is added as a buildroot
override, such as 'f27-override'.
mail_template (unicode): The notification mail template.
state (:class:`ReleaseState`): The current state of the release. Defaults to
``ReleaseState.disabled``.
id (int): The primary key of this release.
builds (sqlalchemy.orm.collections.InstrumentedList): An iterable of :class:`Builds <Build>`
associated with this release.
composed_by_bodhi (bool): The flag that indicates whether the release is composed by
Bodhi or not. Defaults to True.
"""
__tablename__ = 'releases'
__exclude_columns__ = ('id', 'builds')
__get_by__ = ('name', 'long_name', 'dist_tag')
name = Column(Unicode(10), unique=True, nullable=False)
long_name = Column(Unicode(25), unique=True, nullable=False)
version = Column(Unicode(5), nullable=False)
id_prefix = Column(Unicode(25), nullable=False)
branch = Column(Unicode(10), nullable=False)
dist_tag = Column(Unicode(20), nullable=False)
stable_tag = Column(UnicodeText, nullable=False)
testing_tag = Column(UnicodeText, nullable=False)
candidate_tag = Column(UnicodeText, nullable=False)
pending_signing_tag = Column(UnicodeText, nullable=False)
pending_testing_tag = Column(UnicodeText, nullable=False)
pending_stable_tag = Column(UnicodeText, nullable=False)
override_tag = Column(UnicodeText, nullable=False)
mail_template = Column(UnicodeText, default=u'fedora_errata_template', nullable=False)
state = Column(ReleaseState.db_type(), default=ReleaseState.disabled, nullable=False)
composed_by_bodhi = Column(Boolean, default=True)
@property
def version_int(self):
"""
Return an integer representation of the version of this release.
Returns:
int: The version of the release.
"""
regex = re.compile(r'\D+(\d+)[CM]?$')
return int(regex.match(self.name).groups()[0])
@property
def mandatory_days_in_testing(self):
"""
Return the number of days that updates in this release must spend in testing.
Returns:
int or None: The number of days in testing that updates in this release must spend in
testing. If the release isn't configured to have mandatory testing time, ``None`` is
returned.
"""
name = self.name.lower().replace('-', '')
status = config.get('%s.status' % name, None)
if status:
days = int(config.get(
'%s.%s.mandatory_days_in_testing' % (name, status)))
if days:
return days
days = config.get('%s.mandatory_days_in_testing' %
self.id_prefix.lower().replace('-', '_'))
if days is None:
log.warning('No mandatory days in testing defined for %s. Defaulting to 0.' % self.name)
return 0
else:
return int(days)
@property
def collection_name(self):
"""
Return the collection name of this release (eg: Fedora EPEL).
Returns:
basestring: The collection name of this release.
"""
return ' '.join(self.long_name.split()[:-1])
@classmethod
def all_releases(cls):
"""
Return a mapping of release states to a list of dictionaries describing the releases.
Returns:
defaultdict: Mapping strings of :class:`ReleaseState` names to lists of dictionaries
that describe the releases in those states.
"""
if cls._all_releases:
return cls._all_releases
releases = defaultdict(list)
for release in cls.query.order_by(cls.name.desc()).all():
releases[release.state.value].append(release.__json__())
cls._all_releases = releases
return cls._all_releases
_all_releases = None
@classmethod
def get_tags(cls, session):
"""
Return a 2-tuple mapping tags to releases.
Args:
session (sqlalchemy.orm.session.Session): A database session.
Returns:
tuple: A 2-tuple. The first element maps the keys 'candidate', 'testing', 'stable',
'override', 'pending_testing', and 'pending_stable' each to a list of tags for various
releases that correspond to those tag semantics. The second element maps each koji tag
to the release's name that uses it.
"""
if cls._tag_cache:
return cls._tag_cache
data = {'candidate': [], 'testing': [], 'stable': [], 'override': [],
'pending_testing': [], 'pending_stable': []}
tags = {} # tag -> release lookup
for release in session.query(cls).all():
for key in data:
tag = getattr(release, '%s_tag' % key)
data[key].append(tag)
tags[tag] = release.name
cls._tag_cache = (data, tags)
return cls._tag_cache
_tag_cache = None
@classmethod
def from_tags(cls, tags, session):
"""
Find a release associated with one of the given koji tags.
Args:
tags (list): A list of koji tags for which an associated release is desired.
session (sqlalchemy.orm.session.Session): A database session.
Returns:
Release or None: The first release found that matches the first tag. If no release is
found, ``None`` is returned.
"""
tag_types, tag_rels = cls.get_tags(session)
for tag in tags:
if tag not in tag_rels:
continue
release = session.query(cls).filter_by(name=tag_rels[tag]).first()
if release:
return release
class TestCase(Base):
"""
Represents test cases from the wiki.
Attributes:
name (unicode): The name of the test case.
package_id (int): The primary key of the :class:`Package` associated with this test case.
package (Package): The package associated with this test case.
"""
__tablename__ = 'testcases'
__get_by__ = ('name',)
name = Column(UnicodeText, nullable=False)
package_id = Column(Integer, ForeignKey('packages.id'))
# package backref
class Package(Base):
"""