knotctl 0.1.0__tar.gz → 0.1.1__tar.gz

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.
@@ -0,0 +1,162 @@
1
+ # ---> Python
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ #Pipfile.lock
97
+
98
+ # poetry
99
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103
+ #poetry.lock
104
+
105
+ # pdm
106
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107
+ #pdm.lock
108
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109
+ # in version control.
110
+ # https://pdm.fming.dev/#use-with-ide
111
+ .pdm.toml
112
+
113
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114
+ __pypackages__/
115
+
116
+ # Celery stuff
117
+ celerybeat-schedule
118
+ celerybeat.pid
119
+
120
+ # SageMath parsed files
121
+ *.sage.py
122
+
123
+ # Environments
124
+ .env
125
+ .venv
126
+ env/
127
+ venv/
128
+ ENV/
129
+ env.bak/
130
+ venv.bak/
131
+
132
+ # Spyder project settings
133
+ .spyderproject
134
+ .spyproject
135
+
136
+ # Rope project settings
137
+ .ropeproject
138
+
139
+ # mkdocs documentation
140
+ /site
141
+
142
+ # mypy
143
+ .mypy_cache/
144
+ .dmypy.json
145
+ dmypy.json
146
+
147
+ # Pyre type checker
148
+ .pyre/
149
+
150
+ # pytype static type analyzer
151
+ .pytype/
152
+
153
+ # Cython debug symbols
154
+ cython_debug/
155
+
156
+ # PyCharm
157
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
160
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161
+ #.idea/
162
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: knotctl
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A CLI for knotapi.
5
5
  Author-email: Micke Nordin <hej@mic.ke>
6
6
  Requires-Python: >=3.9
@@ -208,9 +208,9 @@ options:
208
208
  ### LIST
209
209
 
210
210
  ```
211
- usage: knotctl list [-h] [-d DATA] [-n NAME] [-r RTYPE] -z ZONE
211
+ usage: knotctl list [-h] [-d DATA] [-n NAME] [-r RTYPE] [-z ZONE]
212
212
 
213
- List records in the zone.
213
+ List records.
214
214
 
215
215
  options:
216
216
  -h, --help show this help message and exit
@@ -249,3 +249,26 @@ Available arguments are:
249
249
  ttl: New record time to live (TTL).
250
250
  ```
251
251
 
252
+ ### USER
253
+ ```
254
+ usage: knotctl user [-h] [-u USERNAME]
255
+
256
+ View user information.
257
+
258
+ options:
259
+ -h, --help show this help message and exit
260
+ -u USERNAME, --username USERNAME
261
+ ```
262
+
263
+ ### ZONE
264
+
265
+ ```
266
+ usage: knotctl zone
267
+
268
+ List zones.
269
+
270
+ options:
271
+ -h, --help show this help message and exit
272
+ ```
273
+
274
+
@@ -191,9 +191,9 @@ options:
191
191
  ### LIST
192
192
 
193
193
  ```
194
- usage: knotctl list [-h] [-d DATA] [-n NAME] [-r RTYPE] -z ZONE
194
+ usage: knotctl list [-h] [-d DATA] [-n NAME] [-r RTYPE] [-z ZONE]
195
195
 
196
- List records in the zone.
196
+ List records.
197
197
 
198
198
  options:
199
199
  -h, --help show this help message and exit
@@ -231,3 +231,26 @@ Available arguments are:
231
231
  rtype: New record type.
232
232
  ttl: New record time to live (TTL).
233
233
  ```
234
+
235
+ ### USER
236
+ ```
237
+ usage: knotctl user [-h] [-u USERNAME]
238
+
239
+ View user information.
240
+
241
+ options:
242
+ -h, --help show this help message and exit
243
+ -u USERNAME, --username USERNAME
244
+ ```
245
+
246
+ ### ZONE
247
+
248
+ ```
249
+ usage: knotctl zone
250
+
251
+ List zones.
252
+
253
+ options:
254
+ -h, --help show this help message and exit
255
+ ```
256
+
@@ -7,7 +7,7 @@ import os
7
7
  import sys
8
8
  import urllib.parse
9
9
  from os import environ, mkdir
10
- from os.path import isdir, isfile, islink, join, split
10
+ from os.path import isdir, isfile, join
11
11
  from typing import Union
12
12
  from urllib.parse import urlparse
13
13
 
@@ -36,6 +36,9 @@ def error(description: str, error: str) -> list[dict]:
36
36
 
37
37
 
38
38
  def get_config(config_filename: str):
39
+ if not isfile(config_filename):
40
+ print("You need to configure knotctl before proceeding")
41
+ run_config(config_filename)
39
42
  with open(config_filename, "r") as fh:
40
43
  return yaml.safe_load(fh.read())
41
44
 
@@ -46,7 +49,8 @@ def nested_out(input, tabs="") -> str:
46
49
  string += "{}\n".format(input)
47
50
  elif isinstance(input, dict):
48
51
  for key, value in input.items():
49
- string += "{}{}: {}".format(tabs, key, nested_out(value, tabs + " "))
52
+ string += "{}{}: {}".format(tabs, key,
53
+ nested_out(value, tabs + " "))
50
54
  elif isinstance(input, list):
51
55
  for entry in input:
52
56
  string += "{}\n{}".format(tabs, nested_out(entry, tabs + " "))
@@ -70,11 +74,9 @@ def run_add(url: str, jsonout: bool, headers: dict):
70
74
  out = response.json()
71
75
  if isinstance(out, list):
72
76
  for record in out:
73
- if (
74
- record["data"] == parsed["data"]
75
- and record["name"] == parsed["name"]
76
- and record["rtype"] == parsed["rtype"]
77
- ):
77
+ if (record["data"] == parsed["data"]
78
+ and record["name"] == parsed["name"]
79
+ and record["rtype"] == parsed["rtype"]):
78
80
  output(record, jsonout)
79
81
  break
80
82
  else:
@@ -94,29 +96,28 @@ def run_log(url: str, jsonout: bool, headers: dict):
94
96
  line = lines[index]
95
97
  index += 1
96
98
  cur_has_timestamp = line.startswith("[")
97
- next_has_timestamp = index < len(lines) and lines[index].startswith(
98
- "["
99
- )
99
+ next_has_timestamp = index < len(
100
+ lines) and lines[index].startswith("[")
100
101
  # Simple case, just one line with timestamp
101
102
  if cur_has_timestamp and next_has_timestamp:
102
- timestamp = line.split(']')[0].split('[')[1]
103
- text = line.split(']')[1].lstrip(':').strip()
104
- out.append({'timestamp': timestamp, 'text': text})
103
+ timestamp = line.split("]")[0].split("[")[1]
104
+ text = line.split("]")[1].lstrip(":").strip()
105
+ out.append({"timestamp": timestamp, "text": text})
105
106
  text = ""
106
107
  timestamp = ""
107
108
  # Start of multiline
108
109
  elif cur_has_timestamp:
109
- timestamp = line.split(']')[0].split('[')[1]
110
- text = line.split(']')[1].lstrip(':').strip()
110
+ timestamp = line.split("]")[0].split("[")[1]
111
+ text = line.split("]")[1].lstrip(":").strip()
111
112
  # End of multiline
112
113
  elif next_has_timestamp:
113
- text += f'\n{line.strip()}'
114
- out.append({'timestamp': timestamp, 'text': text})
114
+ text += f"\n{line.strip()}"
115
+ out.append({"timestamp": timestamp, "text": text})
115
116
  text = ""
116
117
  timestamp = ""
117
118
  # Middle of multiline
118
119
  else:
119
- text += f'\n{line.strip()}'
120
+ text += f"\n{line.strip()}"
120
121
 
121
122
  else:
122
123
  out = string
@@ -144,7 +145,7 @@ def run_config(
144
145
  if current:
145
146
  if os.path.islink(config_filename):
146
147
  actual_path = os.readlink(config_filename)
147
- print(actual_path.split('-')[-1])
148
+ print(actual_path.split("-")[-1])
148
149
  else:
149
150
  print("none")
150
151
  return
@@ -171,8 +172,7 @@ def run_config(
171
172
  error(
172
173
  "Can not configure without {}".format(need),
173
174
  "No {}".format(need),
174
- )
175
- )
175
+ ))
176
176
  sys.exit(1)
177
177
  config[need] = input("Enter {}: ".format(need))
178
178
 
@@ -196,7 +196,10 @@ def run_delete(url: str, jsonout: bool, headers: dict):
196
196
  output(reply, jsonout)
197
197
 
198
198
 
199
- def run_list(url: str, jsonout: bool, headers: dict, ret=False) -> Union[None, str]:
199
+ def run_list(url: str,
200
+ jsonout: bool,
201
+ headers: dict,
202
+ ret=False) -> Union[None, str]:
200
203
  response = requests.get(url, headers=headers)
201
204
  string = response.json()
202
205
  if ret:
@@ -210,6 +213,22 @@ def run_update(url: str, jsonout: bool, headers: dict):
210
213
  output(response.json(), jsonout)
211
214
 
212
215
 
216
+ def run_zone(url: str,
217
+ jsonout: bool,
218
+ headers: dict,
219
+ ret=False) -> Union[None, str]:
220
+ response = requests.get(url, headers=headers)
221
+ zones = response.json()
222
+ for zone in zones:
223
+ del zone["records"]
224
+ string = zones
225
+
226
+ if ret:
227
+ return string
228
+ else:
229
+ output(string, jsonout)
230
+
231
+
213
232
  # Set up the url
214
233
  def setup_url(
215
234
  baseurl: str,
@@ -248,16 +267,14 @@ def setup_url(
248
267
  error(
249
268
  "ttl only makes sense with rtype, name and zone",
250
269
  "Missing parameter",
251
- )
252
- )
270
+ ))
253
271
  sys.exit(1)
254
272
  if rtype and (not name or not zone):
255
273
  output(
256
274
  error(
257
275
  "rtype only makes sense with name and zone",
258
276
  "Missing parameter",
259
- )
260
- )
277
+ ))
261
278
  sys.exit(1)
262
279
  if name and not zone:
263
280
  output(error("name only makes sense with a zone", "Missing parameter"))
@@ -297,8 +314,7 @@ def split_url(url: str) -> dict:
297
314
  }
298
315
 
299
316
 
300
- # Entry point to program
301
- def main() -> int:
317
+ def get_parser() -> dict:
302
318
  description = """Manage DNS records with knot dns rest api:
303
319
  * https://gitlab.nic.cz/knot/knot-dns-rest"""
304
320
 
@@ -366,18 +382,22 @@ def main() -> int:
366
382
  subparsers.add_parser("auditlog", description=auditlog_description)
367
383
 
368
384
  changelog_description = "View the changelog of a zone."
369
- changelogcmd = subparsers.add_parser("changelog", description=changelog_description)
385
+ changelogcmd = subparsers.add_parser("changelog",
386
+ description=changelog_description)
370
387
  changelogcmd.add_argument("-z", "--zone", required=True)
371
388
 
372
389
  complete_description = "Generate shell completion script."
373
- completecmd = subparsers.add_parser("completion", description=complete_description)
390
+ completecmd = subparsers.add_parser("completion",
391
+ description=complete_description)
374
392
  completecmd.add_argument("-s", "--shell")
375
393
 
376
394
  config_description = "Configure access to knot-dns-rest-api."
377
395
  configcmd = subparsers.add_parser("config", description=config_description)
378
396
  configcmd.add_argument("-b", "--baseurl")
379
397
  configcmd.add_argument("-c", "--context")
380
- configcmd.add_argument("-C", "--current", action=argparse.BooleanOptionalAction)
398
+ configcmd.add_argument("-C",
399
+ "--current",
400
+ action=argparse.BooleanOptionalAction)
381
401
  configcmd.add_argument("-p", "--password")
382
402
  configcmd.add_argument("-u", "--username")
383
403
 
@@ -388,22 +408,23 @@ def main() -> int:
388
408
  deletecmd.add_argument("-r", "--rtype")
389
409
  deletecmd.add_argument("-z", "--zone", required=True)
390
410
 
391
- list_description = "List records in the zone."
411
+ list_description = "List records."
392
412
  listcmd = subparsers.add_parser("list", description=list_description)
393
413
  listcmd.add_argument("-d", "--data")
394
414
  listcmd.add_argument("-n", "--name")
395
415
  listcmd.add_argument("-r", "--rtype")
396
- listcmd.add_argument("-z", "--zone", required=True)
416
+ listcmd.add_argument("-z", "--zone", required=False)
417
+
418
+ user_description = "View user information."
419
+ usercmd = subparsers.add_parser("user", description=user_description)
420
+ usercmd.add_argument("-u", "--username", default=None)
397
421
 
398
422
  update_description = (
399
- "Update a record in the zone. The record must exist in the zone.\n"
400
- )
423
+ "Update a record in the zone. The record must exist in the zone.\n")
401
424
  update_description += (
402
- "In this case --data, --name, --rtype and --ttl switches are used\n"
403
- )
425
+ "In this case --data, --name, --rtype and --ttl switches are used\n")
404
426
  update_description += (
405
- "for searching for the appropriate record, while the --argument\n"
406
- )
427
+ "for searching for the appropriate record, while the --argument\n")
407
428
  update_description += "switches are used for updating the record."
408
429
  update_epilog = """Available arguments are:
409
430
  data: New record data.
@@ -421,7 +442,8 @@ def main() -> int:
421
442
  "--argument",
422
443
  action="append",
423
444
  metavar="KEY=VALUE",
424
- help="Specify key - value pairs to be updated: name=dns1.example.com. or data=127.0.0.1 for example. --argument can be repeated",
445
+ help="Specify key - value pairs to be updated: name=dns1.example.com."
446
+ + " or data=127.0.0.1 for example. --argument can be repeated",
425
447
  required=True,
426
448
  )
427
449
  updatecmd.add_argument("-d", "--data", required=True)
@@ -430,7 +452,68 @@ def main() -> int:
430
452
  updatecmd.add_argument("-t", "--ttl")
431
453
  updatecmd.add_argument("-z", "--zone", required=True)
432
454
 
455
+ zone_description = "View zones."
456
+ subparsers.add_parser("zone", description=zone_description)
457
+
433
458
  argcomplete.autocomplete(parser)
459
+
460
+ return parser
461
+
462
+
463
+ def get_token(config) -> str:
464
+ # Authenticate
465
+ baseurl = config["baseurl"]
466
+ username = config["username"]
467
+ password = config["password"]
468
+ basic = HTTPBasicAuth(username, password)
469
+ response = requests.get(baseurl + "/user/login", auth=basic)
470
+ token = ""
471
+ try:
472
+ token = response.json()["token"]
473
+ except KeyError:
474
+ output(response.json())
475
+ except requests.exceptions.JSONDecodeError:
476
+ output(
477
+ error("Could not decode api response as JSON", "Could not decode"))
478
+ return token
479
+
480
+
481
+ def run(url, args, headers, baseurl, parser, username):
482
+ try:
483
+ if args.command == "add":
484
+ run_add(url, args.json, headers)
485
+ elif args.command == "delete":
486
+ run_delete(url, args.json, headers)
487
+ elif args.command == "list":
488
+ run_list(url, args.json, headers)
489
+ elif args.command == "update":
490
+ run_update(url, args.json, headers)
491
+ elif args.command == "user":
492
+ url = baseurl + f"/user/info/{username}"
493
+ run_list(url, args.json, headers)
494
+ elif args.command == "auditlog":
495
+ url = baseurl + "/user/auditlog"
496
+ run_log(url, args.json, headers)
497
+ elif args.command == "changelog":
498
+ url = baseurl + f"/zones/changelog/{args.zone.rstrip('.')}"
499
+ run_log(url, args.json, headers)
500
+ elif args.command == "zone":
501
+ url = baseurl + "/zones"
502
+ run_zone(url, args.json, headers)
503
+ else:
504
+ parser.print_help(sys.stderr)
505
+ return 2
506
+ except requests.exceptions.RequestException as e:
507
+ output(error(e, "Could not connect to server"))
508
+ except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
509
+ output(
510
+ error("Could not decode api response as JSON", "Could not decode"))
511
+ return 0
512
+
513
+
514
+ # Entry point to program
515
+ def main() -> int:
516
+ parser = get_parser()
434
517
  args = parser.parse_args()
435
518
  if args.command == "completion":
436
519
  run_complete(args.shell)
@@ -445,35 +528,27 @@ def main() -> int:
445
528
 
446
529
  if args.command == "config":
447
530
  run_config(
448
- config_filename, args.context, args.baseurl, args.username, args.password, args.current
531
+ config_filename,
532
+ args.context,
533
+ args.baseurl,
534
+ args.username,
535
+ args.password,
536
+ args.current,
449
537
  )
450
538
  return 0
451
539
 
452
- if not isfile(config_filename):
453
- print("You need to configure knotctl before proceeding")
454
- run_config(config_filename)
455
-
456
540
  config = get_config(config_filename)
457
541
  baseurl = config["baseurl"]
458
- username = config["username"]
459
- password = config["password"]
460
-
461
- # Authenticate
462
- basic = HTTPBasicAuth(username, password)
463
- response = requests.get(baseurl + "/user/login", auth=basic)
464
- try:
465
- token = response.json()["token"]
466
- except KeyError:
467
- output(response.json())
468
- return 1
469
- except requests.exceptions.JSONDecodeError:
470
- output(error("Could not decode api response as JSON", "Could not decode"))
542
+ token = get_token(config)
543
+ if token == "":
544
+ print("Could not get token, exiting")
471
545
  return 1
472
546
  headers = {"Authorization": "Bearer {}".format(token)}
473
547
 
474
548
  # Route based on command
475
549
  url = ""
476
550
  ttl = None
551
+ user = config["username"]
477
552
  if "ttl" in args:
478
553
  ttl = args.ttl
479
554
  if args.command != "update":
@@ -486,7 +561,10 @@ def main() -> int:
486
561
  soa_url = setup_url(baseurl, None, None, zname, "SOA", None, args.zone)
487
562
  soa_json = run_list(soa_url, True, headers, ret=True)
488
563
  ttl = soa_json[0]["ttl"]
489
- if args.command in ["auditlog", "changelog"]:
564
+ if args.command == "user":
565
+ if args.username:
566
+ user = args.username
567
+ if args.command in ["auditlog", "changelog", "user", "zone"]:
490
568
  pass
491
569
  else:
492
570
  try:
@@ -503,29 +581,7 @@ def main() -> int:
503
581
  parser.print_help(sys.stderr)
504
582
  return 1
505
583
 
506
- try:
507
- if args.command == "add":
508
- run_add(url, args.json, headers)
509
- elif args.command == "delete":
510
- run_delete(url, args.json, headers)
511
- elif args.command == "list":
512
- run_list(url, args.json, headers)
513
- elif args.command == "update":
514
- run_update(url, args.json, headers)
515
- elif args.command == "auditlog":
516
- url = baseurl + "/user/auditlog"
517
- run_log(url, args.json, headers)
518
- elif args.command == "changelog":
519
- url = baseurl + f"/zones/changelog/{args.zone.rstrip('.')}"
520
- run_log(url, args.json, headers)
521
- else:
522
- parser.print_help(sys.stderr)
523
- return 2
524
- except requests.exceptions.RequestException as e:
525
- output(error(e, "Could not connect to server"))
526
- except (RequestsJSONDecodeError, SimplejsonJSONDecodeError):
527
- output(error("Could not decode api response as JSON", "Could not decode"))
528
- return 0
584
+ return run(url, args, headers, baseurl, parser, user)
529
585
 
530
586
 
531
587
  if __name__ == "__main__":
@@ -16,7 +16,7 @@ classifiers=[
16
16
  "Operating System :: OS Independent",
17
17
  ]
18
18
  requires-python= ">=3.9"
19
- version = "0.1.0"
19
+ version = "0.1.1"
20
20
 
21
21
  dependencies = [
22
22
  "argcomplete==2.0.0",
@@ -0,0 +1,4 @@
1
+ argcomplete==2.0.0
2
+ pyyaml==6.0.1
3
+ requests==2.27.1
4
+ simplejson==3.17.6
@@ -0,0 +1,4 @@
1
+ [DEFAULT]
2
+ Depends3: python3-argcomplete, python3-requests, python3-simplejson, python3-yaml
3
+ Debian-Version: 1
4
+ Package3: knotctl
File without changes