kaqing 2.0.115__py3-none-any.whl → 2.0.172__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.

Potentially problematic release.


This version of kaqing might be problematic. Click here for more details.

Files changed (187) hide show
  1. adam/__init__.py +0 -2
  2. adam/app_session.py +8 -11
  3. adam/batch.py +3 -3
  4. adam/checks/check_utils.py +14 -46
  5. adam/checks/cpu.py +7 -1
  6. adam/checks/cpu_metrics.py +52 -0
  7. adam/checks/disk.py +2 -3
  8. adam/columns/columns.py +3 -1
  9. adam/columns/cpu.py +3 -1
  10. adam/columns/cpu_metrics.py +22 -0
  11. adam/columns/memory.py +3 -4
  12. adam/commands/__init__.py +18 -0
  13. adam/commands/alter_tables.py +43 -47
  14. adam/commands/audit/audit.py +24 -25
  15. adam/commands/audit/audit_repair_tables.py +14 -17
  16. adam/commands/audit/audit_run.py +15 -23
  17. adam/commands/audit/show_last10.py +10 -13
  18. adam/commands/audit/show_slow10.py +10 -13
  19. adam/commands/audit/show_top10.py +10 -14
  20. adam/commands/audit/utils_show_top10.py +2 -3
  21. adam/commands/bash/__init__.py +5 -0
  22. adam/commands/bash/bash.py +8 -96
  23. adam/commands/bash/utils_bash.py +16 -0
  24. adam/commands/cat.py +14 -19
  25. adam/commands/cd.py +12 -100
  26. adam/commands/check.py +20 -21
  27. adam/commands/cli_commands.py +2 -3
  28. adam/commands/code.py +20 -23
  29. adam/commands/command.py +123 -39
  30. adam/commands/commands_utils.py +8 -17
  31. adam/commands/cp.py +33 -39
  32. adam/commands/cql/cql_completions.py +28 -10
  33. adam/commands/cql/cqlsh.py +10 -30
  34. adam/commands/cql/utils_cql.py +343 -0
  35. adam/commands/deploy/code_start.py +7 -10
  36. adam/commands/deploy/code_stop.py +4 -21
  37. adam/commands/deploy/code_utils.py +3 -3
  38. adam/commands/deploy/deploy.py +4 -27
  39. adam/commands/deploy/deploy_frontend.py +14 -17
  40. adam/commands/deploy/deploy_pg_agent.py +2 -5
  41. adam/commands/deploy/deploy_pod.py +65 -73
  42. adam/commands/deploy/deploy_utils.py +14 -24
  43. adam/commands/deploy/undeploy.py +4 -27
  44. adam/commands/deploy/undeploy_frontend.py +4 -7
  45. adam/commands/deploy/undeploy_pg_agent.py +5 -7
  46. adam/commands/deploy/undeploy_pod.py +11 -12
  47. adam/commands/devices/__init__.py +0 -0
  48. adam/commands/devices/device.py +118 -0
  49. adam/commands/devices/device_app.py +173 -0
  50. adam/commands/devices/device_auit_log.py +49 -0
  51. adam/commands/devices/device_cass.py +185 -0
  52. adam/commands/devices/device_export.py +86 -0
  53. adam/commands/devices/device_postgres.py +144 -0
  54. adam/commands/devices/devices.py +25 -0
  55. adam/commands/exit.py +1 -4
  56. adam/commands/export/clean_up_all_export_sessions.py +37 -0
  57. adam/commands/export/clean_up_export_sessions.py +51 -0
  58. adam/commands/export/drop_export_database.py +55 -0
  59. adam/commands/export/drop_export_databases.py +43 -0
  60. adam/commands/export/export.py +19 -26
  61. adam/commands/export/export_databases.py +174 -0
  62. adam/commands/export/export_handlers.py +71 -0
  63. adam/commands/export/export_select.py +48 -22
  64. adam/commands/export/export_select_x.py +54 -0
  65. adam/commands/export/export_use.py +19 -23
  66. adam/commands/export/exporter.py +353 -0
  67. adam/commands/export/import_session.py +40 -0
  68. adam/commands/export/importer.py +67 -0
  69. adam/commands/export/importer_athena.py +77 -0
  70. adam/commands/export/importer_sqlite.py +39 -0
  71. adam/commands/export/show_column_counts.py +54 -0
  72. adam/commands/export/show_export_databases.py +36 -0
  73. adam/commands/export/show_export_session.py +48 -0
  74. adam/commands/export/show_export_sessions.py +44 -0
  75. adam/commands/export/utils_export.py +223 -162
  76. adam/commands/help.py +1 -1
  77. adam/commands/intermediate_command.py +49 -0
  78. adam/commands/issues.py +11 -43
  79. adam/commands/kubectl.py +3 -6
  80. adam/commands/login.py +22 -24
  81. adam/commands/logs.py +3 -6
  82. adam/commands/ls.py +11 -128
  83. adam/commands/medusa/medusa.py +4 -22
  84. adam/commands/medusa/medusa_backup.py +20 -24
  85. adam/commands/medusa/medusa_restore.py +29 -33
  86. adam/commands/medusa/medusa_show_backupjobs.py +14 -18
  87. adam/commands/medusa/medusa_show_restorejobs.py +11 -18
  88. adam/commands/nodetool.py +6 -15
  89. adam/commands/param_get.py +11 -12
  90. adam/commands/param_set.py +9 -10
  91. adam/commands/postgres/postgres.py +41 -34
  92. adam/commands/postgres/postgres_context.py +57 -24
  93. adam/commands/postgres/postgres_ls.py +4 -8
  94. adam/commands/postgres/postgres_preview.py +5 -9
  95. adam/commands/postgres/psql_completions.py +1 -1
  96. adam/commands/postgres/utils_postgres.py +66 -0
  97. adam/commands/preview_table.py +5 -44
  98. adam/commands/pwd.py +14 -47
  99. adam/commands/reaper/reaper.py +4 -27
  100. adam/commands/reaper/reaper_forward.py +48 -55
  101. adam/commands/reaper/reaper_forward_session.py +6 -0
  102. adam/commands/reaper/reaper_forward_stop.py +10 -16
  103. adam/commands/reaper/reaper_restart.py +7 -14
  104. adam/commands/reaper/reaper_run_abort.py +11 -30
  105. adam/commands/reaper/reaper_runs.py +42 -57
  106. adam/commands/reaper/reaper_runs_abort.py +29 -49
  107. adam/commands/reaper/reaper_schedule_activate.py +11 -30
  108. adam/commands/reaper/reaper_schedule_start.py +10 -29
  109. adam/commands/reaper/reaper_schedule_stop.py +10 -29
  110. adam/commands/reaper/reaper_schedules.py +4 -14
  111. adam/commands/reaper/reaper_status.py +8 -16
  112. adam/commands/reaper/utils_reaper.py +196 -0
  113. adam/commands/repair/repair.py +4 -22
  114. adam/commands/repair/repair_log.py +5 -11
  115. adam/commands/repair/repair_run.py +27 -34
  116. adam/commands/repair/repair_scan.py +32 -38
  117. adam/commands/repair/repair_stop.py +5 -11
  118. adam/commands/report.py +27 -29
  119. adam/commands/restart.py +25 -26
  120. adam/commands/rollout.py +19 -24
  121. adam/commands/shell.py +10 -4
  122. adam/commands/show/show.py +10 -25
  123. adam/commands/show/show_cassandra_repairs.py +35 -0
  124. adam/commands/show/show_cassandra_status.py +32 -43
  125. adam/commands/show/show_cassandra_version.py +5 -18
  126. adam/commands/show/show_commands.py +19 -24
  127. adam/commands/show/show_host.py +1 -1
  128. adam/commands/show/show_login.py +20 -27
  129. adam/commands/show/show_processes.py +15 -19
  130. adam/commands/show/show_storage.py +10 -20
  131. adam/commands/watch.py +26 -29
  132. adam/config.py +5 -14
  133. adam/embedded_params.py +1 -1
  134. adam/log.py +4 -4
  135. adam/pod_exec_result.py +3 -3
  136. adam/repl.py +40 -103
  137. adam/repl_commands.py +32 -16
  138. adam/repl_state.py +57 -28
  139. adam/sql/sql_completer.py +44 -28
  140. adam/sql/sql_state_machine.py +89 -28
  141. adam/sso/authn_ad.py +6 -8
  142. adam/sso/authn_okta.py +4 -6
  143. adam/sso/cred_cache.py +3 -5
  144. adam/sso/idp.py +9 -12
  145. adam/utils.py +435 -6
  146. adam/utils_athena.py +57 -37
  147. adam/utils_audits.py +12 -14
  148. adam/utils_issues.py +32 -0
  149. adam/utils_k8s/app_clusters.py +13 -18
  150. adam/utils_k8s/app_pods.py +2 -0
  151. adam/utils_k8s/cassandra_clusters.py +22 -19
  152. adam/utils_k8s/cassandra_nodes.py +2 -2
  153. adam/utils_k8s/custom_resources.py +16 -17
  154. adam/utils_k8s/ingresses.py +2 -2
  155. adam/utils_k8s/jobs.py +7 -11
  156. adam/utils_k8s/k8s.py +87 -0
  157. adam/utils_k8s/pods.py +40 -77
  158. adam/utils_k8s/secrets.py +4 -4
  159. adam/utils_k8s/service_accounts.py +5 -4
  160. adam/utils_k8s/services.py +2 -2
  161. adam/utils_k8s/statefulsets.py +1 -12
  162. adam/utils_net.py +4 -4
  163. adam/utils_repl/__init__.py +0 -0
  164. adam/utils_repl/automata_completer.py +48 -0
  165. adam/utils_repl/repl_completer.py +46 -0
  166. adam/utils_repl/state_machine.py +173 -0
  167. adam/utils_sqlite.py +137 -0
  168. adam/version.py +1 -1
  169. {kaqing-2.0.115.dist-info → kaqing-2.0.172.dist-info}/METADATA +1 -1
  170. kaqing-2.0.172.dist-info/RECORD +230 -0
  171. adam/commands/app.py +0 -67
  172. adam/commands/app_ping.py +0 -44
  173. adam/commands/cql/cql_utils.py +0 -204
  174. adam/commands/devices.py +0 -147
  175. adam/commands/export/export_on_x.py +0 -76
  176. adam/commands/export/export_rmdbs.py +0 -65
  177. adam/commands/postgres/postgres_utils.py +0 -31
  178. adam/commands/reaper/reaper_session.py +0 -159
  179. adam/commands/show/show_app_actions.py +0 -56
  180. adam/commands/show/show_app_id.py +0 -47
  181. adam/commands/show/show_app_queues.py +0 -45
  182. adam/commands/show/show_repairs.py +0 -47
  183. adam/utils_export.py +0 -42
  184. kaqing-2.0.115.dist-info/RECORD +0 -203
  185. {kaqing-2.0.115.dist-info → kaqing-2.0.172.dist-info}/WHEEL +0 -0
  186. {kaqing-2.0.115.dist-info → kaqing-2.0.172.dist-info}/entry_points.txt +0 -0
  187. {kaqing-2.0.115.dist-info → kaqing-2.0.172.dist-info}/top_level.txt +0 -0
adam/utils_athena.py CHANGED
@@ -1,30 +1,32 @@
1
- from datetime import datetime
1
+ from collections.abc import Callable
2
2
  import functools
3
3
  import time
4
4
  import boto3
5
+ import botocore
5
6
 
6
7
  from adam.config import Config
7
- from adam.utils import lines_to_tabular, log, log2
8
+ from adam.utils import lines_to_tabular, log, log2, log_exc, wait_log
8
9
 
9
10
  # no state utility class
10
11
  class Athena:
11
12
  @functools.lru_cache()
12
- def database_names(prefix: str = 'export_'):
13
+ def database_names(like: str = None):
13
14
  # this function is called only from export currently
14
- Config().wait_log(f'Inspecting export database schema...')
15
+ wait_log(f'Inspecting export database schema...')
15
16
 
16
17
  query = f"SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema'"
17
- if prefix:
18
- query = f"{query} AND schema_name like '{prefix}%'"
18
+ if like:
19
+ query = f"{query} AND schema_name like '{like}'"
19
20
 
20
- state, reason, rs = Athena.query(query)
21
- if rs:
22
- names = []
23
- for row in rs[1:]:
24
- row_data = [col.get('VarCharValue') if col else '' for col in row['Data']]
25
- names.append(row_data[0])
21
+ with log_exc():
22
+ state, reason, rs = Athena.query(query)
23
+ if rs:
24
+ names = []
25
+ for row in rs[1:]:
26
+ row_data = [col.get('VarCharValue') if col else '' for col in row['Data']]
27
+ names.append(row_data[0])
26
28
 
27
- return names
29
+ return names
28
30
 
29
31
  return []
30
32
 
@@ -38,43 +40,52 @@ class Athena:
38
40
 
39
41
  @functools.lru_cache()
40
42
  def table_names(database: str = 'audit', function: str = 'audit'):
41
- region_name = Config().get(f'{function}.athena.region', 'us-west-2')
42
- database_name = Config().get(f'{function}.athena.database', database)
43
- catalog_name = Config().get(f'{function}.athena.catalog', 'AwsDataCatalog')
44
-
45
- athena_client = boto3.client('athena', region_name=region_name)
46
- paginator = athena_client.get_paginator('list_table_metadata')
47
-
48
43
  table_names = []
49
- for page in paginator.paginate(CatalogName=catalog_name, DatabaseName=database_name):
50
- for table_metadata in page.get('TableMetadataList', []):
51
- table_names.append(table_metadata['Name'])
44
+ try:
45
+ region_name = Config().get(f'{function}.athena.region', 'us-west-2')
46
+ database_name = Config().get(f'{function}.athena.database', database)
47
+ catalog_name = Config().get(f'{function}.athena.catalog', 'AwsDataCatalog')
48
+
49
+ athena_client = boto3.client('athena', region_name=region_name)
50
+ paginator = athena_client.get_paginator('list_table_metadata')
51
+
52
+ for page in paginator.paginate(CatalogName=catalog_name, DatabaseName=database_name):
53
+ for table_metadata in page.get('TableMetadataList', []):
54
+ table_names.append(table_metadata['Name'])
55
+ except botocore.exceptions.NoCredentialsError as e:
56
+ # aws credentials not found
57
+ if function == 'audit':
58
+ log2(f'Please configure AWS credentials to Audit Log Database.')
59
+ except:
60
+ pass
52
61
 
53
62
  return table_names
54
63
 
55
64
  @functools.lru_cache()
56
65
  def column_names(tables: list[str] = [], database: str = None, function: str = 'audit', partition_cols_only = False):
57
- if not database:
58
- database = Config().get(f'{function}.athena.database', 'audit')
66
+ with log_exc():
67
+ if not database:
68
+ database = Config().get(f'{function}.athena.database', 'audit')
59
69
 
60
- if not tables:
61
- tables = Config().get(f'{function}.athena.tables', 'audit').split(',')
70
+ if not tables:
71
+ tables = Config().get(f'{function}.athena.tables', 'audit').split(',')
62
72
 
63
- table_names = "'" + "','".join([table.strip() for table in tables]) + "'"
73
+ table_names = "'" + "','".join([table.strip() for table in tables]) + "'"
64
74
 
65
- query = f"select column_name from information_schema.columns where table_name in ({table_names}) and table_schema = '{database}'"
66
- if partition_cols_only:
67
- query = f"{query} and extra_info = 'partition key'"
75
+ query = f"select column_name from information_schema.columns where table_name in ({table_names}) and table_schema = '{database}'"
76
+ if partition_cols_only:
77
+ query = f"{query} and extra_info = 'partition key'"
68
78
 
69
- _, _, rs = Athena.query(query)
70
- if rs:
71
- return [row['Data'][0].get('VarCharValue') for row in rs[1:]]
79
+ _, _, rs = Athena.query(query)
80
+ if rs:
81
+ return [row['Data'][0].get('VarCharValue') for row in rs[1:]]
72
82
 
73
83
  return []
74
84
 
75
- def run_query(sql: str, database: str = None):
85
+ def run_query(sql: str, database: str = None, output: Callable[[str], str] = None):
76
86
  state, reason, rs = Athena.query(sql, database)
77
87
 
88
+ log_file = None
78
89
  if state == 'SUCCEEDED':
79
90
  if rs:
80
91
  column_info = rs[0]['Data']
@@ -84,13 +95,22 @@ class Athena:
84
95
  row_data = [col.get('VarCharValue') if col else '' for col in row['Data']]
85
96
  lines.append('\t'.join(row_data))
86
97
 
87
- log(lines_to_tabular(lines, header='\t'.join(columns), separator='\t'))
98
+ out = lines_to_tabular(lines, header='\t'.join(columns), separator='\t')
99
+ if output:
100
+ log_file = output(out)
101
+ else:
102
+ log(out)
103
+
104
+ return len(lines), log_file
88
105
  else:
89
106
  log2(f"Query failed or was cancelled. State: {state}")
90
107
  log2(f"Reason: {reason}")
91
108
 
109
+ return 0, log_file
110
+
92
111
  def query(sql: str, database: str = None, function: str = 'audit') -> tuple[str, str, list]:
93
- athena_client = boto3.client('athena')
112
+ region_name = Config().get(f'{function}.athena.region', 'us-west-2')
113
+ athena_client = boto3.client('athena', region_name=region_name)
94
114
 
95
115
  if not database:
96
116
  database = Config().get(f'{function}.athena.database', 'audit')
adam/utils_audits.py CHANGED
@@ -1,12 +1,11 @@
1
1
  from datetime import datetime
2
- import functools
3
2
  import getpass
4
3
  import time
5
- import boto3
6
4
  import requests
7
5
 
8
6
  from adam.config import Config
9
- from adam.utils import lines_to_tabular, log, log2
7
+ from adam.utils import OffloadHandler, debug, log2, log_exc, offload
8
+ from adam.utils_athena import Athena
10
9
  from adam.utils_net import get_my_host
11
10
 
12
11
  class AuditMeta:
@@ -34,7 +33,7 @@ class Audits:
34
33
  try:
35
34
  response = requests.post(audit_endpoint, json=payload, timeout=Config().get("audit.timeout", 10))
36
35
  if response.status_code in [200, 201]:
37
- Config().debug(response.text)
36
+ debug(response.text)
38
37
  else:
39
38
  log2(f"Error: {response.status_code} {response.text}")
40
39
  except requests.exceptions.Timeout as e:
@@ -44,15 +43,13 @@ class Audits:
44
43
  checked_in = 0.0
45
44
  cluster_last_checked = 0.0
46
45
 
47
- state, _, rs = Audits.audit_query(f'select partitions_last_checked, clusters_last_checked from meta')
46
+ state, _, rs = Athena.query(f'select partitions_last_checked, clusters_last_checked from meta')
48
47
  if state == 'SUCCEEDED':
49
48
  if len(rs) > 1:
50
- try:
49
+ with log_exc():
51
50
  row = rs[1]['Data']
52
51
  checked_in = float(row[0]['VarCharValue'])
53
52
  cluster_last_checked = float(row[1]['VarCharValue'])
54
- except:
55
- pass
56
53
 
57
54
  return AuditMeta(checked_in, cluster_last_checked)
58
55
 
@@ -69,7 +66,7 @@ class Audits:
69
66
  try:
70
67
  response = requests.post(audit_endpoint, json=payload, timeout=Config().get("audit.timeout", 10))
71
68
  if response.status_code in [200, 201]:
72
- Config().debug(response.text)
69
+ debug(response.text)
73
70
  else:
74
71
  log2(f"Error: {response.status_code} {response.text}")
75
72
  except requests.exceptions.Timeout as e:
@@ -86,13 +83,11 @@ class Audits:
86
83
  f'(select distinct c as name from audit where {Audits.date_from(dt_object)}) as c2',
87
84
  'on c1.name = c2.name where c1.name is null'])
88
85
  log2(query)
89
- state, _, rs = Audits.audit_query(query)
86
+ state, _, rs = Athena.query(query)
90
87
  if state == 'SUCCEEDED':
91
88
  if len(rs) > 1:
92
- try:
89
+ with log_exc():
93
90
  return [r['Data'][0]['VarCharValue'] for r in rs[1:]]
94
- except:
95
- pass
96
91
 
97
92
  return []
98
93
 
@@ -101,4 +96,7 @@ class Audits:
101
96
  m = dt_object.strftime("%m")
102
97
  d = dt_object.strftime("%d")
103
98
 
104
- return f"y = '{y}' and m = '{m}' and d >= '{d}' or y = '{y}' and m > '{m}' or y > '{y}'"
99
+ return f"y = '{y}' and m = '{m}' and d >= '{d}' or y = '{y}' and m > '{m}' or y > '{y}'"
100
+
101
+ def offload() -> OffloadHandler:
102
+ return offload(max_workers=Config().get('audit.workers', 3))
adam/utils_issues.py ADDED
@@ -0,0 +1,32 @@
1
+ from adam.checks.check_result import CheckResult
2
+ from adam.checks.issue import Issue
3
+ from adam.repl_session import ReplSession
4
+ from adam.utils import lines_to_tabular, log, log2
5
+
6
+ class IssuesUtils:
7
+ def show(check_results: list[CheckResult], in_repl = False):
8
+ IssuesUtils.show_issues(CheckResult.collect_issues(check_results), in_repl=in_repl)
9
+
10
+ def show_issues(issues: list[Issue], in_repl = False):
11
+ if not issues:
12
+ log2('No issues found.')
13
+ else:
14
+ suggested = 0
15
+ log2(f'* {len(issues)} issues found.')
16
+ lines = []
17
+ for i, issue in enumerate(issues, start=1):
18
+ lines.append(f"{i}||{issue.category}||{issue.desc}")
19
+ lines.append(f"||statefulset||{issue.statefulset}@{issue.namespace}")
20
+ lines.append(f"||pod||{issue.pod}@{issue.namespace}")
21
+ if issue.details:
22
+ lines.append(f"||details||{issue.details}")
23
+
24
+ if issue.suggestion:
25
+ lines.append(f'||suggestion||{issue.suggestion}')
26
+ if in_repl:
27
+ ReplSession().prompt_session.history.append_string(issue.suggestion)
28
+ suggested += 1
29
+ log(lines_to_tabular(lines, separator='||'))
30
+ if suggested:
31
+ log2()
32
+ log2(f'* {suggested} suggested commands are added to history. Press <Up> arrow to access them.')
@@ -1,10 +1,9 @@
1
- from concurrent.futures import ThreadPoolExecutor
2
1
  import sys
3
2
  from typing import TypeVar
4
3
 
5
4
  from adam.utils_k8s.app_pods import AppPods
6
5
  from adam.pod_exec_result import PodExecResult
7
- from adam.utils import log2
6
+ from adam.utils import log, log2
8
7
  from adam.utils_k8s.pods import Pods
9
8
  from .kube_context import KubeContext
10
9
 
@@ -14,20 +13,16 @@ T = TypeVar('T')
14
13
  class AppClusters:
15
14
  def exec(pods: list[str], namespace: str, command: str, action: str = 'action',
16
15
  max_workers=0, show_out=True, on_any = False, shell = '/bin/sh', background = False) -> list[PodExecResult]:
17
- def body(executor: ThreadPoolExecutor, pod: str, namespace: str, show_out: bool):
18
- if executor:
19
- return executor.submit(AppPods.exec, pod, namespace, command, False, False, shell, background)
16
+ samples = 1 if on_any else sys.maxsize
17
+ msg = 'd`Running|Ran ' + action + ' command onto {size} pods'
18
+ with Pods.parallelize(pods, max_workers, samples, msg, action=action) as exec:
19
+ results: list[PodExecResult] = exec.map(lambda pod: AppPods.exec(pod, namespace, command, False, False, shell, background))
20
+ for result in results:
21
+ if KubeContext.show_out(show_out):
22
+ log(result.command)
23
+ if result.stdout:
24
+ log(result.stdout)
25
+ if result.stderr:
26
+ log2(result.stderr, file=sys.stderr)
20
27
 
21
- return AppPods.exec(pod, namespace, command, show_out=show_out, background=background)
22
-
23
- def post(result, show_out: bool):
24
- if KubeContext.show_out(show_out):
25
- print(result.command)
26
- if result.stdout:
27
- print(result.stdout)
28
- if result.stderr:
29
- log2(result.stderr, file=sys.stderr)
30
-
31
- return result
32
-
33
- return Pods.on_pods(pods, namespace, body, post=post, action=action, max_workers=max_workers, show_out=show_out, on_any=on_any, background=background)
28
+ return results
@@ -1,3 +1,4 @@
1
+ import functools
1
2
  from typing import List
2
3
  from kubernetes import client
3
4
 
@@ -8,6 +9,7 @@ from adam.repl_session import ReplSession
8
9
 
9
10
  # utility collection on app pods; methods are all static
10
11
  class AppPods:
12
+ @functools.lru_cache()
11
13
  def pod_names(namespace: str, env: str, app: str):
12
14
  return [pod.metadata.name for pod in AppPods.app_pods(namespace, env, app)]
13
15
 
@@ -1,33 +1,36 @@
1
- from concurrent.futures import ThreadPoolExecutor
2
1
  import sys
3
2
  from typing import TypeVar
4
3
 
4
+ from adam.config import Config
5
5
  from adam.utils_k8s.cassandra_nodes import CassandraNodes
6
6
  from adam.pod_exec_result import PodExecResult
7
- from adam.utils import log2
8
- from .statefulsets import StatefulSets
9
- from .kube_context import KubeContext
7
+ from adam.utils import log, log2
8
+ from adam.utils_k8s.pods import Pods
9
+ from adam.utils_k8s.statefulsets import StatefulSets
10
10
 
11
11
  T = TypeVar('T')
12
12
 
13
13
  # utility collection on cassandra clusters; methods are all static
14
14
  class CassandraClusters:
15
- def exec(statefulset: str, namespace: str, command: str, action: str = 'action',
16
- max_workers=0, show_out=True, on_any = False, shell = '/bin/sh', background = False) -> list[PodExecResult]:
17
- def body(executor: ThreadPoolExecutor, pod: str, namespace: str, show_out: bool):
18
- if executor:
19
- return executor.submit(CassandraNodes.exec, pod, namespace, command, False, False, shell, background)
15
+ def exec(sts: str, namespace: str, command: str, action: str = 'action',
16
+ max_workers=0, show_out=True, on_any = False, shell = '/bin/sh', background = False, log_file = None) -> list[PodExecResult]:
20
17
 
21
- return CassandraNodes.exec(pod, namespace, command, show_out=show_out, background=background)
18
+ pods = StatefulSets.pod_names(sts, namespace)
19
+ samples = 1 if on_any else sys.maxsize
20
+ msg = 'd`Running|Ran ' + action + ' command onto {size} pods'
21
+ with Pods.parallelize(pods, max_workers, samples, msg, action=action) as exec:
22
+ results: list[PodExecResult] = exec.map(lambda pod: CassandraNodes.exec(pod, namespace, command, False, False, shell, background, log_file))
23
+ for result in results:
24
+ if show_out and not Config().is_debug():
25
+ log(result.command)
26
+ if result.stdout:
27
+ log(result.stdout)
28
+ if result.stderr:
29
+ log2(result.stderr, file=sys.stderr)
22
30
 
23
- def post(result, show_out: bool):
24
- if KubeContext.show_out(show_out):
25
- print(result.command)
26
- if result.stdout:
27
- print(result.stdout)
28
- if result.stderr:
29
- log2(result.stderr, file=sys.stderr)
31
+ return results
30
32
 
31
- return result
33
+ def pod_names_by_host_id(sts: str, ns: str):
34
+ pods = StatefulSets.pods(sts, ns)
32
35
 
33
- return StatefulSets.on_cluster(statefulset, namespace, body, post=post, action=action, max_workers=max_workers, show_out=show_out, on_any=on_any, background=background)
36
+ return {CassandraNodes.get_host_id(pod.metadata.name, ns): pod.metadata.name for pod in pods}
@@ -6,8 +6,8 @@ from adam.repl_session import ReplSession
6
6
 
7
7
  # utility collection on cassandra nodes; methods are all static
8
8
  class CassandraNodes:
9
- def exec(pod_name: str, namespace: str, command: str, show_out = True, throw_err = False, shell = '/bin/sh', background = False) -> PodExecResult:
10
- r = Pods.exec(pod_name, "cassandra", namespace, command, show_out = show_out, throw_err = throw_err, shell = shell, background = background)
9
+ def exec(pod_name: str, namespace: str, command: str, show_out = True, throw_err = False, shell = '/bin/sh', background = False, log_file = None) -> PodExecResult:
10
+ r = Pods.exec(pod_name, "cassandra", namespace, command, show_out = show_out, throw_err = throw_err, shell = shell, background = background, log_file=log_file)
11
11
 
12
12
  if r and Config().get('repl.history.push-cat-remote-log-file', True):
13
13
  if r.log_file and ReplSession().prompt_session:
@@ -1,10 +1,10 @@
1
+ import functools
1
2
  import re
2
- import time
3
3
  from kubernetes import client
4
4
 
5
5
  from adam.config import Config
6
6
  from .kube_context import KubeContext
7
- from adam.utils import elapsed_time, lines_to_tabular, log2
7
+ from adam.utils import log2, log_exc
8
8
 
9
9
 
10
10
  # utility collection; methods are all static
@@ -19,14 +19,12 @@ class CustomResources:
19
19
  strip = Config().get('app.strip', '0')
20
20
 
21
21
  v1 = client.CustomObjectsApi()
22
- try:
22
+ with log_exc():
23
23
  c3cassandras = v1.list_cluster_custom_object(group=group, version=v, plural=plural)
24
24
  for c in c3cassandras.items():
25
25
  if c[0] == 'items':
26
26
  for item in c[1]:
27
27
  app_ids_by_ss[f"{item['metadata']['name']}@{item['metadata']['namespace']}"] = item['metadata']['labels'][label].strip(strip)
28
- except Exception:
29
- pass
30
28
 
31
29
  return app_ids_by_ss
32
30
 
@@ -121,11 +119,10 @@ class CustomResources:
121
119
  body = bkspecs
122
120
  pretty = 'true'
123
121
 
124
- try:
122
+ with log_exc(lambda e: "Exception when calling create_medusa_backupjob.create_namespaced_custom_object: %s\n" % e):
125
123
  api_instance.create_namespaced_custom_object(group, version, namespace, plural, body, pretty=pretty)
126
124
  log2(f"create_medusa_backupjob: created Full Backup {bkname}: {api_instance}")
127
- except Exception as e:
128
- log2("Exception when calling create_medusa_backupjob.create_namespaced_custom_object: %s\n" % e)
125
+
129
126
  return None
130
127
 
131
128
  def create_medusa_restorejob(restorejobname: str, bkname: str, dc: str, ns: str):
@@ -154,11 +151,10 @@ class CustomResources:
154
151
  body = rtspecs
155
152
  pretty = 'true'
156
153
 
157
- try:
154
+ with log_exc(lambda e: "Exception when calling create_medusa_restorejob.create_namespaced_custom_object: %s\n" % e):
158
155
  api_instance.create_namespaced_custom_object(group, version, namespace, plural, body, pretty=pretty)
159
156
  log2(f"create_medusa_restorejob: created Restore Job {restorejobname}: {api_instance}")
160
- except Exception as e:
161
- log2("Exception when calling create_medusa_restorejob.create_namespaced_custom_object: %s\n" % e)
157
+
162
158
  return None
163
159
 
164
160
  def medusa_show_backup_names(dc: str, ns: str) -> list[dict]:
@@ -171,6 +167,10 @@ class CustomResources:
171
167
 
172
168
  return None
173
169
 
170
+ def clear_caches():
171
+ CustomResources.medusa_show_backupjobs.cache_clear()
172
+
173
+ @functools.lru_cache()
174
174
  def medusa_show_backupjobs(dc: str, ns: str) -> list[dict]:
175
175
  api_instance = client.CustomObjectsApi()
176
176
  group = 'medusa.k8ssandra.io'
@@ -180,11 +180,10 @@ class CustomResources:
180
180
  pretty = 'true'
181
181
  label_selector = 'cassandra.datastax.com/datacenter=' + dc
182
182
 
183
- try:
183
+ with log_exc(lambda e: "Exception when calling medusa_show_backupjobs.list_namespaced_custom_object: %s\n" % e):
184
184
  api_response = api_instance.list_namespaced_custom_object(group, version, namespace, plural, pretty=pretty, label_selector=label_selector)
185
185
  return api_response['items']
186
- except Exception as e:
187
- log2("Exception when calling medusa_show_backupjobs.list_namespaced_custom_object: %s\n" % e)
186
+
188
187
  return None
189
188
 
190
189
  def medusa_show_restorejobs(dc: str, ns: str):
@@ -196,11 +195,11 @@ class CustomResources:
196
195
  pretty = 'true'
197
196
  label_selector = 'cassandra.datastax.com/datacenter=' + dc
198
197
  rtlist = []
199
- try:
198
+
199
+ with log_exc(lambda e: "Exception when calling medusa_show_restorejobs.list_namespaced_custom_object: %s\n" % e):
200
200
  api_response = api_instance.list_namespaced_custom_object(group, version, namespace, plural, pretty=pretty, label_selector=label_selector)
201
201
  for x in api_response['items']:
202
202
  rtlist.append(f"{x['metadata']['name']}\t{x['metadata']['creationTimestamp']}\t{x['status'].get('finishTime', '')}")
203
203
  return rtlist
204
- except Exception as e:
205
- log2("Exception when calling medusa_show_restorejobs.list_namespaced_custom_object: %s\n" % e)
204
+
206
205
  return None
@@ -1,7 +1,7 @@
1
1
  from kubernetes import client
2
2
 
3
3
  from adam.config import Config
4
- from adam.utils import log2
4
+ from adam.utils import debug, log2
5
5
 
6
6
  # utility collection on ingresses; methods are all static
7
7
  class Ingresses:
@@ -63,7 +63,7 @@ class Ingresses:
63
63
  log2(f"200 Ingress '{name}' in namespace '{namespace}' deleted successfully.")
64
64
  else:
65
65
  api.delete_namespaced_ingress(name=name, namespace=namespace)
66
- Config().debug(f"200 Ingress '{name}' in namespace '{namespace}' deleted successfully.")
66
+ debug(f"200 Ingress '{name}' in namespace '{namespace}' deleted successfully.")
67
67
  except client.ApiException as e:
68
68
  log2(f"Error deleting Ingress: {e}")
69
69
 
adam/utils_k8s/jobs.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from kubernetes import client
2
2
  from time import sleep
3
3
  from .pods import Pods
4
- from adam.utils import log2
4
+ from adam.utils import log2, log_exc
5
5
 
6
6
  # utility collection on jobs; methods are all static
7
7
  class Jobs:
@@ -20,11 +20,10 @@ class Jobs:
20
20
  metadata=client.V1ObjectMeta(name=job_name),
21
21
  spec=spec)
22
22
 
23
- try:
23
+ with log_exc(lambda e: "Exception when calling BatchV1Apii->create_namespaced_job: %s\n" % e):
24
24
  client.BatchV1Api().create_namespaced_job(body=job, namespace=namespace)
25
25
  log2(f"Job {job_name} created in {namespace}")
26
- except Exception as e:
27
- log2("Exception when calling BatchV1Apii->create_namespaced_job: %s\n" % e)
26
+
28
27
  return
29
28
 
30
29
  def get_job_pods(job_name: str, namespace: str):
@@ -32,7 +31,7 @@ class Jobs:
32
31
  return pods
33
32
 
34
33
  def delete(job_name: str, namespace: str, wait=True):
35
- try:
34
+ with log_exc(lambda e: "Exception when calling BatchV1Apii->delete_namespaced_job: %s\n" % e):
36
35
  client.BatchV1Api().delete_namespaced_job(name=job_name, namespace=namespace, propagation_policy='Background')
37
36
  if wait:
38
37
  while True:
@@ -41,14 +40,11 @@ class Jobs:
41
40
  return
42
41
  sleep(5)
43
42
  log2(f"Job {job_name} in {namespace} deleted.")
44
- except Exception as e:
45
- log2("Exception when calling BatchV1Apii->delete_namespaced_job: %s\n" % e)
43
+
46
44
  return
47
45
 
48
46
  def get_logs(job_name: str, namespace: str):
49
47
  v1 = client.CoreV1Api()
50
- try:
48
+ with log_exc(lambda e: "Exception when calling CorV1Apii->list_namespaced_pod, cannot find job pod: %s\n" % e):
51
49
  pod_name = Jobs.get_job_pods(job_name, namespace).items[0].metadata.name
52
- log2(v1.read_namespaced_pod_log(name=pod_name, namespace=namespace))
53
- except Exception as e:
54
- log2("Exception when calling CorV1Apii->list_namespaced_pod, cannot find job pod: %s\n" % e)
50
+ log2(v1.read_namespaced_pod_log(name=pod_name, namespace=namespace))
adam/utils_k8s/k8s.py ADDED
@@ -0,0 +1,87 @@
1
+ from collections.abc import Callable
2
+ import re
3
+ import portforward
4
+
5
+ from adam.commands.command import InvalidState
6
+ from adam.repl_state import ReplState
7
+ from adam.utils import log2
8
+ from adam.utils_k8s.kube_context import KubeContext
9
+
10
+ class PortForwardHandler:
11
+ connections: dict[str, int] = {}
12
+
13
+ def __init__(self, state: ReplState, local_port: int, svc_or_pod: Callable[[bool],str], target_port: int):
14
+ self.state = state
15
+ self.local_port = local_port
16
+ self.svc_or_pod = svc_or_pod
17
+ self.target_port = target_port
18
+ self.forward_connection = None
19
+ self.pod = None
20
+
21
+ def __enter__(self) -> tuple[str, str]:
22
+ state = self.state
23
+
24
+ if not self.svc_or_pod:
25
+ log2('No service or pod found.')
26
+
27
+ raise InvalidState(state)
28
+
29
+ if KubeContext.in_cluster():
30
+ svc_name = self.svc_or_pod(True)
31
+ if not svc_name:
32
+ log2('No service found.')
33
+
34
+ raise InvalidState(state)
35
+
36
+ # cs-a526330d23-cs-a526330d23-default-sts-0 ->
37
+ # curl http://cs-a526330d23-cs-a526330d23-reaper-service.stgawsscpsr.svc.cluster.local:8080
38
+ groups = re.match(r'^(.*?-.*?-.*?-.*?-).*', state.sts)
39
+ if groups:
40
+ svc = f'{groups[1]}{svc_name}.{state.namespace}.svc.cluster.local:{self.target_port}'
41
+ return (svc, svc)
42
+ else:
43
+ raise InvalidState(state)
44
+ else:
45
+ pod = self.svc_or_pod(False)
46
+ if not pod:
47
+ log2('No pod found.')
48
+
49
+ raise InvalidState(state)
50
+
51
+ self.pod = pod
52
+ self.forward_connection = portforward.forward(state.namespace, pod, self.local_port, self.target_port)
53
+ if self.inc_connection_cnt() == 1:
54
+ self.forward_connection.__enter__()
55
+
56
+ return (f'localhost:{self.local_port}', f'{pod}:{self.target_port}')
57
+
58
+ def __exit__(self, exc_type, exc_val, exc_tb):
59
+ if self.forward_connection:
60
+ if not self.dec_connection_cnt():
61
+ return self.forward_connection.__exit__(exc_type, exc_val, exc_tb)
62
+
63
+ return False
64
+
65
+ def inc_connection_cnt(self):
66
+ id = self.connection_id(self.pod)
67
+ if id not in PortForwardHandler.connections:
68
+ PortForwardHandler.connections[id] = 1
69
+ else:
70
+ PortForwardHandler.connections[id] += 1
71
+
72
+ return PortForwardHandler.connections[id]
73
+
74
+ def dec_connection_cnt(self):
75
+ id = self.connection_id(self.pod)
76
+ if id not in PortForwardHandler.connections:
77
+ PortForwardHandler.connections[id] = 0
78
+ elif PortForwardHandler.connections[id] > 0:
79
+ PortForwardHandler.connections[id] -= 1
80
+
81
+ return PortForwardHandler.connections[id]
82
+
83
+ def connection_id(self, pod: str):
84
+ return f'{self.local_port}:{pod}:{self.target_port}'
85
+
86
+ def port_forwarding(state: ReplState, local_port: int, svc_or_pod: Callable[[bool],str], target_port: int):
87
+ return PortForwardHandler(state, local_port, svc_or_pod, target_port)