trigger 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. trigger/__init__.py +7 -0
  2. trigger/acl/__init__.py +32 -0
  3. trigger/acl/autoacl.py +70 -0
  4. trigger/acl/db.py +324 -0
  5. trigger/acl/dicts.py +357 -0
  6. trigger/acl/grammar.py +112 -0
  7. trigger/acl/ios.py +222 -0
  8. trigger/acl/junos.py +422 -0
  9. trigger/acl/models.py +118 -0
  10. trigger/acl/parser.py +168 -0
  11. trigger/acl/queue.py +296 -0
  12. trigger/acl/support.py +1431 -0
  13. trigger/acl/tools.py +746 -0
  14. trigger/bin/__init__.py +0 -0
  15. trigger/bin/acl.py +233 -0
  16. trigger/bin/acl_script.py +574 -0
  17. trigger/bin/aclconv.py +82 -0
  18. trigger/bin/check_access.py +93 -0
  19. trigger/bin/check_syntax.py +66 -0
  20. trigger/bin/fe.py +197 -0
  21. trigger/bin/find_access.py +191 -0
  22. trigger/bin/gnng.py +434 -0
  23. trigger/bin/gong.py +86 -0
  24. trigger/bin/load_acl.py +841 -0
  25. trigger/bin/load_config.py +18 -0
  26. trigger/bin/netdev.py +317 -0
  27. trigger/bin/optimizer.py +638 -0
  28. trigger/bin/run_cmds.py +18 -0
  29. trigger/changemgmt/__init__.py +352 -0
  30. trigger/changemgmt/bounce.py +57 -0
  31. trigger/cmds.py +1217 -0
  32. trigger/conf/__init__.py +94 -0
  33. trigger/conf/global_settings.py +674 -0
  34. trigger/contrib/__init__.py +7 -0
  35. trigger/exceptions.py +307 -0
  36. trigger/gorc.py +172 -0
  37. trigger/netdevices/__init__.py +1288 -0
  38. trigger/netdevices/loader.py +174 -0
  39. trigger/netscreen.py +1030 -0
  40. trigger/packages/__init__.py +6 -0
  41. trigger/packages/peewee.py +8084 -0
  42. trigger/rancid.py +463 -0
  43. trigger/tacacsrc.py +584 -0
  44. trigger/twister.py +2203 -0
  45. trigger/twister2.py +745 -0
  46. trigger/utils/__init__.py +88 -0
  47. trigger/utils/cli.py +349 -0
  48. trigger/utils/importlib.py +77 -0
  49. trigger/utils/network.py +157 -0
  50. trigger/utils/rcs.py +178 -0
  51. trigger/utils/templates.py +81 -0
  52. trigger/utils/url.py +78 -0
  53. trigger/utils/xmltodict.py +298 -0
  54. trigger-2.0.0.dist-info/METADATA +146 -0
  55. trigger-2.0.0.dist-info/RECORD +61 -0
  56. trigger-2.0.0.dist-info/WHEEL +5 -0
  57. trigger-2.0.0.dist-info/entry_points.txt +15 -0
  58. trigger-2.0.0.dist-info/licenses/AUTHORS.md +20 -0
  59. trigger-2.0.0.dist-info/licenses/LICENSE.md +28 -0
  60. trigger-2.0.0.dist-info/top_level.txt +2 -0
  61. twisted/plugins/trigger_xmlrpc.py +124 -0
trigger/acl/junos.py ADDED
@@ -0,0 +1,422 @@
1
+ """
2
+ This code is originally from parser.py. This file is not meant to by used by itself.
3
+ This is for JunOS like ACLs
4
+
5
+ #Constants
6
+ junos_match_types
7
+ rules - this is really from the grammar.py
8
+ # Classes
9
+ Policer
10
+ PolicerGroup
11
+ QuotedString
12
+ # Functions
13
+ braced_list
14
+ keyword_match
15
+ range_match
16
+ handle_junos_acl
17
+ handle_junos_family_acl
18
+ handle_junos_policers
19
+ handle_junos_term
20
+ juniper_multiline_comments
21
+ """
22
+
23
+ # Copied metadata from parser.py
24
+ __author__ = "Jathan McCollum, Mike Biancaniello, Michael Harding, Michael Shields"
25
+ __editor__ = "Joseph Malone"
26
+ __maintainer__ = "Jathan McCollum"
27
+ __email__ = "jathanism@aol.com"
28
+ __copyright__ = "Copyright 2006-2013, AOL Inc.; 2013 Saleforce.com"
29
+
30
+ from trigger.conf import settings
31
+
32
+ from .grammar import *
33
+
34
+ # Temporary resting place for comments, so the rest of the parser can
35
+ # ignore them. Yes, this makes the library not thread-safe.
36
+ Comments = []
37
+
38
+
39
+ class Policer:
40
+ """
41
+ Container class for policer policy definitions. This is a dummy class for
42
+ now, that just passes it through as a string.
43
+ """
44
+
45
+ def __init__(self, name, data):
46
+ if not name:
47
+ raise exceptions.ActionError("Policer requres name")
48
+ self.name = name
49
+ self.exceedings = []
50
+ self.actions = []
51
+ for elt in data:
52
+ for k, v in elt.items():
53
+ if k == "if-exceeding":
54
+ for entry in v:
55
+ type, value = entry
56
+ if type == "bandwidth-limit":
57
+ limit = self.str2bits(value)
58
+ if limit > 32000000000 or limit < 32000:
59
+ raise "bandwidth-limit must be between 32000bps and 32000000000bps"
60
+ self.exceedings.append((type, limit))
61
+ elif type == "burst-size-limit":
62
+ limit = self.str2bits(value)
63
+ if limit > 100000000 or limit < 1500:
64
+ raise "burst-size-limit must be between 1500B and 100,000,000B"
65
+ self.exceedings.append((type, limit))
66
+ elif type == "bandwidth-percent":
67
+ limit = int(value)
68
+ if limit < 1 or limit > 100:
69
+ raise "bandwidth-percent must be between 1 and 100"
70
+ else:
71
+ raise f"Unknown policer if-exceeding tag: {type}"
72
+ elif k == "action":
73
+ for i in v:
74
+ self.actions.append(i)
75
+
76
+ def str2bits(self, str):
77
+ try:
78
+ val = int(str)
79
+ except:
80
+ if str[-1] == "k":
81
+ return int(str[0:-1]) * 1024
82
+ if str[-1] == "m":
83
+ return int(str[0:-1]) * 1048576
84
+ else:
85
+ raise f"invalid bit definition {str}"
86
+ return val
87
+
88
+ def __repr__(self):
89
+ return f"<{self.__class__.__name__}: {repr(self.name)}>"
90
+
91
+ def __str__(self):
92
+ return self.data
93
+
94
+ def output(self):
95
+ output = [f"policer {self.name} {{"]
96
+ if self.exceedings:
97
+ output.append(" if-exceeding {")
98
+ for x in self.exceedings:
99
+ output.append(f" {x[0]} {x[1]};")
100
+ if self.exceedings:
101
+ output.append(" }")
102
+ if self.actions:
103
+ output.append(" then {")
104
+ for x in self.actions:
105
+ output.append(f" {x};")
106
+
107
+ if self.actions:
108
+ output.append(" }")
109
+ output.append("}")
110
+ return output
111
+
112
+
113
+ class PolicerGroup:
114
+ """Container for Policer objects. Juniper only."""
115
+
116
+ def __init__(self, format=None):
117
+ self.policers = []
118
+ self.format = format
119
+ global Comments
120
+ self.comments = Comments
121
+ Comments = []
122
+
123
+ def output(self, format=None, *largs, **kwargs):
124
+ if format is None:
125
+ format = self.format
126
+ return getattr(self, "output_" + format)(*largs, **kwargs)
127
+
128
+ def output_junos(self, replace=False):
129
+ output = []
130
+ for ent in self.policers:
131
+ for x in ent.output():
132
+ output.append(x)
133
+
134
+ if replace:
135
+ return ["firewall {", "replace:"] + [" " + x for x in output] + ["}"]
136
+ else:
137
+ return output
138
+
139
+
140
+ class QuotedString(str):
141
+ def __str__(self):
142
+ return '"' + self + '"'
143
+
144
+
145
+ junos_match_types = []
146
+
147
+
148
+ def braced_list(arg):
149
+ """Returned braced output. Will alert if comment is malformed."""
150
+ # return '("{", jws?, (%s, jws?)*, "}")' % arg
151
+ return '("{{", jws?, ({}, jws?)*, "}}"!{})'.format(arg, errs["comm_start"])
152
+
153
+
154
+ def keyword_match(keyword, arg=None):
155
+ for k in keyword, keyword + "-except":
156
+ prod = "junos_" + k.replace("-", "_")
157
+ junos_match_types.append(prod)
158
+ if arg is None:
159
+ rules[prod] = (f'"{k}", jsemi', {k: True})
160
+ else:
161
+ tokens = f'"{k}", jws, '
162
+ if k in address_matches:
163
+ tokens += braced_list(arg + ", jsemi")
164
+ else:
165
+ tokens += arg + ", jsemi"
166
+ rules[S(prod)] = (tokens, lambda x, k=k: {k: x})
167
+
168
+
169
+ keyword_match("address", "cidr / ipaddr")
170
+ keyword_match("destination-address", "cidr / ipaddr")
171
+ keyword_match("destination-prefix-list", "jword")
172
+ keyword_match("first-fragment")
173
+ keyword_match("fragment-flags", "fragment_flag")
174
+ keyword_match("ip-options", "ip_option")
175
+ keyword_match("is-fragment")
176
+ keyword_match("prefix-list", "jword")
177
+ keyword_match("source-address", "cidr / ipaddr")
178
+ keyword_match("source-prefix-list", "jword")
179
+ keyword_match("tcp-established")
180
+ keyword_match("tcp-flags", "tcp_flag")
181
+ keyword_match("tcp-initial")
182
+
183
+
184
+ def range_match(key, arg):
185
+ rules[S(arg + "_range")] = (f'{arg}, "-", {arg}', tuple)
186
+ match = f"{arg}_range / {arg}"
187
+ keyword_match(key, f'{match} / ("[", jws?, ({match}, jws?)*, "]")')
188
+
189
+
190
+ range_match("ah-spi", "alphanums")
191
+ range_match("destination-mac-address", "macaddr")
192
+ range_match("destination-port", "port")
193
+ range_match("dscp", "dscp")
194
+ range_match("ether-type", "alphanums")
195
+ range_match("esp-spi", "alphanums")
196
+ range_match("forwarding-class", "jword")
197
+ range_match("fragment-offset", "port")
198
+ range_match("icmp-code", "icmp_code")
199
+ range_match("icmp-type", "icmp_type")
200
+ range_match("interface-group", "digits")
201
+ range_match("packet-length", "digits")
202
+ range_match("port", "port")
203
+ range_match("precedence", "jword")
204
+ range_match("protocol", "protocol")
205
+ range_match("source-mac-address", "macaddr")
206
+ range_match("source-port", "port")
207
+ range_match("vlan-ether-type", "alphanums")
208
+
209
+
210
+ def handle_junos_acl(x):
211
+ """
212
+ Parse JUNOS ACL and return an ACL object populated with Term and Policer
213
+ objects.
214
+
215
+ It's expected that x is a 2-tuple of (name, terms) returned from the
216
+ parser.
217
+
218
+ Don't forget to wrap your token in S()!
219
+ """
220
+ a = ACL(name=x[0], format="junos")
221
+ for elt in x[1:]:
222
+ # Handle dictionary args we throw at the constructor
223
+ if isinstance(elt, dict):
224
+ a.__dict__.update(elt)
225
+ elif isinstance(elt, Term):
226
+ a.terms.append(elt)
227
+ elif isinstance(elt, Policer):
228
+ a.policers.append(elt)
229
+ else:
230
+ raise RuntimeError(f"Bad Object: {repr(elt)}")
231
+ return a
232
+
233
+
234
+ def handle_junos_family_acl(x):
235
+ """
236
+ Parses a JUNOS acl that contains family information and sets the family
237
+ attribute for the ACL object.
238
+
239
+ It's expected that x is a 2-tuple of (family, aclobj) returned from the
240
+ parser.
241
+
242
+ Don't forget to wrap your token in S()!
243
+ """
244
+ family, aclobj = x
245
+ setattr(aclobj, "family", family)
246
+ return aclobj
247
+
248
+
249
+ def handle_junos_policers(x):
250
+ """Parse JUNOS policers and return a PolicerGroup object"""
251
+ p = PolicerGroup(format="junos")
252
+ for elt in x:
253
+ if isinstance(elt, Policer):
254
+ p.policers.append(elt)
255
+ else:
256
+ raise RuntimeError(f"bad object: {repr(elt)} in policer")
257
+ return p
258
+
259
+
260
+ def handle_junos_term(d):
261
+ """Parse a JUNOS term and return a Term object"""
262
+ if "modifiers" in d:
263
+ d["modifiers"] = Modifiers(d["modifiers"])
264
+ return Term(**d)
265
+
266
+
267
+ # For multiline comments
268
+ def juniper_multiline_comments():
269
+ """
270
+ Return appropriate multi-line comment grammar for Juniper ACLs.
271
+
272
+ This depends on ``settings.ALLOW_JUNIPER_MULTLIINE_COMMENTS``.
273
+ """
274
+ single = '-("*/" / "\n")*' # single-line comments only
275
+ multi = '-"*/"*' # syntactically correct multi-line support
276
+ if settings.ALLOW_JUNIPER_MULTILINE_COMMENTS:
277
+ return multi
278
+ return single
279
+
280
+
281
+ rules.update(
282
+ {
283
+ "jword": "double_quoted / word",
284
+ "double_quoted": ('"\\"", -[\\"]+, "\\""', lambda x: QuotedString(x[1:-1])),
285
+ #'>jws<': '(ws / jcomment)+',
286
+ # S('jcomment'): ('"/*", ws?, jcomment_body, ws?, "*/"',
287
+ # lambda x: Comment(x[0])),
288
+ #'jcomment_body': '-(ws?, "*/")*',
289
+ ">jws<": "(ws / jcomment)+",
290
+ S("jcomment"): ("jslashbang_comment", lambda x: Comment(x[0])),
291
+ "<comment_start>": '"/*"',
292
+ "<comment_stop>": '"*/"',
293
+ ">jslashbang_comment<": "comment_start, jcomment_body, !{}, comment_stop".format(
294
+ errs["comm_stop"]
295
+ ),
296
+ "jcomment_body": juniper_multiline_comments(),
297
+ # Errors on missing ';', ignores multiple ;; and normalizes to one.
298
+ "<jsemi>": "jws?, [;]+!{}".format(errs["semicolon"]),
299
+ "fragment_flag": literals(fragment_flag_names),
300
+ "ip_option": "digits / " + literals(ip_option_names),
301
+ "tcp_flag": literals(tcp_flag_names),
302
+ }
303
+ )
304
+
305
+ # Note there cannot be jws (including comments) before or after the "filter"
306
+ # section of the config. It's wrong to do this anyway, since if you load
307
+ # that config onto the router, the comments will not remain in place on
308
+ # the next load of a similar config (e.g., another ACL). I had a workaround
309
+ # for this but it made the parser substantially slower.
310
+ rules.update(
311
+ {
312
+ S("junos_raw_acl"): (
313
+ 'jws?, "filter", jws, jword, jws?, '
314
+ + braced_list("junos_iface_specific / junos_term / junos_policer"),
315
+ handle_junos_acl,
316
+ ),
317
+ "junos_iface_specific": (
318
+ '("interface-specific", jsemi)',
319
+ lambda x: {"interface_specific": len(x) > 0},
320
+ ),
321
+ "junos_replace_acl": (
322
+ 'jws?, "firewall", jws?, "{", jws?, "replace:", jws?, (junos_raw_acl, jws?)*, "}"'
323
+ ),
324
+ S("junos_replace_family_acl"): (
325
+ 'jws?, "firewall", jws?, "{", jws?, junos_filter_family, jws?, "{", jws?, "replace:", jws?, (junos_raw_acl, jws?)*, "}", jws?, "}"',
326
+ handle_junos_family_acl,
327
+ ),
328
+ S("junos_replace_policers"): (
329
+ '"firewall", jws?, "{", jws?, "replace:", jws?, (junos_policer, jws?)*, "}"',
330
+ handle_junos_policers,
331
+ ),
332
+ "junos_filter_family": ('"family", ws, junos_family_type'),
333
+ "junos_family_type": ('"inet" / "inet6" / "ethernet-switching"'),
334
+ "opaque_braced_group": (
335
+ '"{", jws?, (jword / "[" / "]" / ";" / opaque_braced_group / jws)*, "}"',
336
+ lambda x: x,
337
+ ),
338
+ S("junos_term"): (
339
+ 'maybe_inactive, "term", jws, junos_term_name, '
340
+ "jws?, " + braced_list("junos_from / junos_then"),
341
+ lambda x: handle_junos_term(dict_sum(x)),
342
+ ),
343
+ S("junos_term_name"): ("jword", lambda x: {"name": x[0]}),
344
+ "maybe_inactive": ('("inactive:", jws)?', lambda x: {"inactive": len(x) > 0}),
345
+ S("junos_from"): (
346
+ '"from", jws?, ' + braced_list("junos_match"),
347
+ lambda x: {"match": Matches(dict_sum(x))},
348
+ ),
349
+ S("junos_then"): ("junos_basic_then / junos_braced_then", dict_sum),
350
+ S("junos_braced_then"): (
351
+ '"then", jws?, ' + braced_list("junos_action/junos_modifier, jsemi"),
352
+ dict_sum,
353
+ ),
354
+ S("junos_basic_then"): ('"then", jws?, junos_action, jsemi', dict_sum),
355
+ S("junos_policer"): (
356
+ '"policer", jws, junos_term_name, jws?, '
357
+ + braced_list("junos_exceeding / junos_policer_then"),
358
+ lambda x: Policer(x[0]["name"], x[1:]),
359
+ ),
360
+ S("junos_policer_then"): (
361
+ '"then", jws?, ' + braced_list("junos_policer_action, jsemi")
362
+ ),
363
+ S("junos_policer_action"): (
364
+ 'junos_discard / junos_fwd_class / ("loss-priority", jws, jword)',
365
+ lambda x: {"action": x},
366
+ ),
367
+ "junos_discard": ('"discard"'),
368
+ "junos_loss_pri": (
369
+ '"loss-priority", jws, jword',
370
+ lambda x: {"loss-priority": x[0]},
371
+ ),
372
+ "junos_fwd_class": (
373
+ '"forwarding-class", jws, jword',
374
+ lambda x: {"forwarding-class": x[0]},
375
+ ),
376
+ "junos_filter_specific": ('"filter-specific"'),
377
+ S("junos_exceeding"): (
378
+ '"if-exceeding", jws?, '
379
+ + braced_list("junos_bw_limit/junos_bw_perc/junos_burst_limit"),
380
+ lambda x: {"if-exceeding": x},
381
+ ),
382
+ S("junos_bw_limit"): (
383
+ '"bandwidth-limit", jws, word, jsemi',
384
+ lambda x: ("bandwidth-limit", x[0]),
385
+ ),
386
+ S("junos_bw_perc"): (
387
+ '"bandwidth-percent", jws, alphanums, jsemi',
388
+ lambda x: ("bandwidth-percent", x[0]),
389
+ ),
390
+ S("junos_burst_limit"): (
391
+ '"burst-size-limit", jws, alphanums, jsemi',
392
+ lambda x: ("burst-size-limit", x[0]),
393
+ ),
394
+ S("junos_match"): (" / ".join(junos_match_types), dict_sum),
395
+ S("junos_action"): (
396
+ "junos_one_action / junos_reject_action /"
397
+ "junos_reject_action / junos_ri_action",
398
+ lambda x: {"action": x[0]},
399
+ ),
400
+ "junos_one_action": ('"accept" / "discard" / "reject" / ("next", jws, "term")'),
401
+ "junos_reject_action": (
402
+ '"reject", jws, ' + literals(icmp_reject_codes),
403
+ lambda x: ("reject", x),
404
+ ),
405
+ S("junos_ri_action"): (
406
+ '"routing-instance", jws, jword',
407
+ lambda x: ("routing-instance", x[0]),
408
+ ),
409
+ S("junos_modifier"): (
410
+ "junos_one_modifier / junos_arg_modifier",
411
+ lambda x: {"modifiers": x},
412
+ ),
413
+ "junos_one_modifier": (
414
+ '"log" / "sample" / "syslog" / "port-mirror"',
415
+ lambda x: (x, True),
416
+ ),
417
+ S("junos_arg_modifier"): "junos_arg_modifier_kw, jws, jword",
418
+ "junos_arg_modifier_kw": (
419
+ '"count" / "forwarding-class" / "ipsec-sa" /"loss-priority" / "policer"'
420
+ ),
421
+ }
422
+ )
trigger/acl/models.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ Database models for the task queue.
3
+ """
4
+
5
+ import datetime
6
+
7
+ import peewee as pw
8
+
9
+ from trigger.conf import settings
10
+
11
+ engine = settings.DATABASE_ENGINE
12
+ if not engine:
13
+ raise RuntimeError("You must specify a database engine in settings.DATABASE_ENGINE")
14
+
15
+ # We're hard-coding support for the BIG THREE database solutions for now,
16
+ # because that's what the ``peewee`` library we are using as the ORM supports.
17
+ if engine == "sqlite3":
18
+ database = pw.SqliteDatabase(database=settings.DATABASE_NAME)
19
+ elif engine == "mysql":
20
+ if not settings.DATABASE_PORT:
21
+ settings.DATABASE_PORT = 3306
22
+ database = pw.MySQLDatabase(
23
+ host=settings.DATABASE_HOST,
24
+ database=settings.DATABASE_NAME,
25
+ port=settings.DATABASE_PORT,
26
+ user=settings.DATABASE_USER,
27
+ passwd=settings.DATABASE_PASSWORD,
28
+ )
29
+ elif engine == "postgresql":
30
+ database = pw.PostgresqlDatabase(
31
+ host=settings.DATABASE_HOST,
32
+ database=settings.DATABASE_NAME,
33
+ port=settings.DATABASE_PORT,
34
+ user=settings.DATABASE_USER,
35
+ password=settings.DATABASE_PASSWORD,
36
+ )
37
+ else:
38
+ raise RuntimeError(f"Unsupported database engine: {engine}")
39
+
40
+
41
+ class BaseModel(pw.Model):
42
+ """
43
+ Base model that inherits the database object determined above.
44
+ """
45
+
46
+ class Meta:
47
+ database = database
48
+
49
+
50
+ class CustomCharField(pw.CharField):
51
+ """Overload default CharField to always return strings vs. UTF-8"""
52
+
53
+ def coerce(self, value):
54
+ return str(value or "")
55
+
56
+
57
+ class IntegratedTask(BaseModel):
58
+ """
59
+ Tasks for "integrated" queue used by `~trigger.acl.queue.Queue`.
60
+
61
+ e.g. ``acl -l``
62
+ """
63
+
64
+ # Python 3 / peewee v3+: PrimaryKeyField renamed to AutoField
65
+ id = pw.AutoField()
66
+ acl = CustomCharField(null=False, default="")
67
+ router = CustomCharField(null=False, default="")
68
+ queued = pw.DateTimeField(default=datetime.datetime.now)
69
+ loaded = pw.DateTimeField(null=True)
70
+ escalation = pw.BooleanField(default=False)
71
+
72
+ class Meta:
73
+ # Python 3 / peewee v3+: db_table renamed to table_name
74
+ table_name = "acl_queue"
75
+
76
+
77
+ class ManualTask(BaseModel):
78
+ """
79
+ Tasks for "manual" queue used by `~trigger.acl.queue.Queue`.
80
+
81
+ e.g. ``acl -m``
82
+ """
83
+
84
+ q_id = pw.AutoField()
85
+ q_ts = pw.DateTimeField(default=datetime.datetime.now)
86
+ q_name = CustomCharField(null=False)
87
+ q_routers = CustomCharField(null=False, default="")
88
+ done = pw.BooleanField(default=False)
89
+ q_sr = pw.IntegerField(null=False, default=0)
90
+ login = CustomCharField(null=False, default="")
91
+
92
+ class Meta:
93
+ table_name = "queue"
94
+
95
+
96
+ MODEL_MAP = {
97
+ "integrated": IntegratedTask,
98
+ "manual": ManualTask,
99
+ }
100
+
101
+
102
+ def create_tables():
103
+ """Connect to the database and create the tables for each model."""
104
+ database.connect()
105
+ IntegratedTask.create_table()
106
+ ManualTask.create_table()
107
+
108
+
109
+ def confirm_tables():
110
+ """Ensure the table exists for each model."""
111
+ print("Checking tables...")
112
+ width = max(len(q_name) for q_name in MODEL_MAP)
113
+ for q_name, model in MODEL_MAP.items():
114
+ print(q_name.ljust(width), end=" ")
115
+ print(model.table_exists())
116
+ else:
117
+ return True
118
+ return False