holmesgpt 0.11.5__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 holmesgpt might be problematic. Click here for more details.

Files changed (183) hide show
  1. holmes/.git_archival.json +7 -0
  2. holmes/__init__.py +76 -0
  3. holmes/__init__.py.bak +76 -0
  4. holmes/clients/robusta_client.py +24 -0
  5. holmes/common/env_vars.py +47 -0
  6. holmes/config.py +526 -0
  7. holmes/core/__init__.py +0 -0
  8. holmes/core/conversations.py +578 -0
  9. holmes/core/investigation.py +152 -0
  10. holmes/core/investigation_structured_output.py +264 -0
  11. holmes/core/issue.py +54 -0
  12. holmes/core/llm.py +250 -0
  13. holmes/core/models.py +157 -0
  14. holmes/core/openai_formatting.py +51 -0
  15. holmes/core/performance_timing.py +72 -0
  16. holmes/core/prompt.py +42 -0
  17. holmes/core/resource_instruction.py +17 -0
  18. holmes/core/runbooks.py +26 -0
  19. holmes/core/safeguards.py +120 -0
  20. holmes/core/supabase_dal.py +540 -0
  21. holmes/core/tool_calling_llm.py +798 -0
  22. holmes/core/tools.py +566 -0
  23. holmes/core/tools_utils/__init__.py +0 -0
  24. holmes/core/tools_utils/tool_executor.py +65 -0
  25. holmes/core/tools_utils/toolset_utils.py +52 -0
  26. holmes/core/toolset_manager.py +418 -0
  27. holmes/interactive.py +229 -0
  28. holmes/main.py +1041 -0
  29. holmes/plugins/__init__.py +0 -0
  30. holmes/plugins/destinations/__init__.py +6 -0
  31. holmes/plugins/destinations/slack/__init__.py +2 -0
  32. holmes/plugins/destinations/slack/plugin.py +163 -0
  33. holmes/plugins/interfaces.py +32 -0
  34. holmes/plugins/prompts/__init__.py +48 -0
  35. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  36. holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
  37. holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
  38. holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
  39. holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
  41. holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
  42. holmes/plugins/prompts/generic_ask.jinja2 +36 -0
  43. holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
  44. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
  45. holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
  46. holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
  47. holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
  48. holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
  49. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
  50. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
  51. holmes/plugins/runbooks/README.md +22 -0
  52. holmes/plugins/runbooks/__init__.py +100 -0
  53. holmes/plugins/runbooks/catalog.json +14 -0
  54. holmes/plugins/runbooks/jira.yaml +12 -0
  55. holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
  56. holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
  57. holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
  58. holmes/plugins/sources/github/__init__.py +77 -0
  59. holmes/plugins/sources/jira/__init__.py +123 -0
  60. holmes/plugins/sources/opsgenie/__init__.py +93 -0
  61. holmes/plugins/sources/pagerduty/__init__.py +147 -0
  62. holmes/plugins/sources/prometheus/__init__.py +0 -0
  63. holmes/plugins/sources/prometheus/models.py +104 -0
  64. holmes/plugins/sources/prometheus/plugin.py +154 -0
  65. holmes/plugins/toolsets/__init__.py +171 -0
  66. holmes/plugins/toolsets/aks-node-health.yaml +65 -0
  67. holmes/plugins/toolsets/aks.yaml +86 -0
  68. holmes/plugins/toolsets/argocd.yaml +70 -0
  69. holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
  70. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
  71. holmes/plugins/toolsets/aws.yaml +76 -0
  72. holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
  73. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
  74. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
  75. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
  76. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
  77. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
  78. holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
  79. holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
  80. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
  81. holmes/plugins/toolsets/azure_sql/install.md +66 -0
  82. holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
  83. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
  84. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
  85. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
  86. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
  87. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
  88. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
  89. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
  90. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
  91. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
  92. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
  93. holmes/plugins/toolsets/azure_sql/utils.py +83 -0
  94. holmes/plugins/toolsets/bash/__init__.py +0 -0
  95. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
  96. holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
  97. holmes/plugins/toolsets/bash/common/bash.py +52 -0
  98. holmes/plugins/toolsets/bash/common/config.py +14 -0
  99. holmes/plugins/toolsets/bash/common/stringify.py +25 -0
  100. holmes/plugins/toolsets/bash/common/validators.py +24 -0
  101. holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
  102. holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
  103. holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
  104. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
  105. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
  106. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
  107. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
  108. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
  109. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
  110. holmes/plugins/toolsets/bash/parse_command.py +103 -0
  111. holmes/plugins/toolsets/confluence.yaml +19 -0
  112. holmes/plugins/toolsets/consts.py +5 -0
  113. holmes/plugins/toolsets/coralogix/api.py +158 -0
  114. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
  115. holmes/plugins/toolsets/coralogix/utils.py +181 -0
  116. holmes/plugins/toolsets/datadog.py +153 -0
  117. holmes/plugins/toolsets/docker.yaml +46 -0
  118. holmes/plugins/toolsets/git.py +756 -0
  119. holmes/plugins/toolsets/grafana/__init__.py +0 -0
  120. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
  121. holmes/plugins/toolsets/grafana/common.py +68 -0
  122. holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
  123. holmes/plugins/toolsets/grafana/loki_api.py +89 -0
  124. holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
  125. holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
  126. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
  127. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
  128. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
  129. holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
  130. holmes/plugins/toolsets/helm.yaml +42 -0
  131. holmes/plugins/toolsets/internet/internet.py +275 -0
  132. holmes/plugins/toolsets/internet/notion.py +137 -0
  133. holmes/plugins/toolsets/kafka.py +638 -0
  134. holmes/plugins/toolsets/kubernetes.yaml +255 -0
  135. holmes/plugins/toolsets/kubernetes_logs.py +426 -0
  136. holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
  137. holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
  138. holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
  139. holmes/plugins/toolsets/logging_utils/types.py +0 -0
  140. holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
  141. holmes/plugins/toolsets/newrelic.py +222 -0
  142. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  143. holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
  144. holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
  145. holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
  146. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
  147. holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
  148. holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
  149. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
  150. holmes/plugins/toolsets/rabbitmq/api.py +398 -0
  151. holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
  152. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
  153. holmes/plugins/toolsets/robusta/__init__.py +0 -0
  154. holmes/plugins/toolsets/robusta/robusta.py +235 -0
  155. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
  156. holmes/plugins/toolsets/runbook/__init__.py +0 -0
  157. holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
  158. holmes/plugins/toolsets/service_discovery.py +92 -0
  159. holmes/plugins/toolsets/servicenow/install.md +37 -0
  160. holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
  161. holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
  162. holmes/plugins/toolsets/slab.yaml +20 -0
  163. holmes/plugins/toolsets/utils.py +137 -0
  164. holmes/plugins/utils.py +14 -0
  165. holmes/utils/__init__.py +0 -0
  166. holmes/utils/cache.py +84 -0
  167. holmes/utils/cert_utils.py +40 -0
  168. holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
  169. holmes/utils/definitions.py +13 -0
  170. holmes/utils/env.py +53 -0
  171. holmes/utils/file_utils.py +56 -0
  172. holmes/utils/global_instructions.py +20 -0
  173. holmes/utils/holmes_status.py +22 -0
  174. holmes/utils/holmes_sync_toolsets.py +80 -0
  175. holmes/utils/markdown_utils.py +55 -0
  176. holmes/utils/pydantic_utils.py +54 -0
  177. holmes/utils/robusta.py +10 -0
  178. holmes/utils/tags.py +97 -0
  179. holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
  180. holmesgpt-0.11.5.dist-info/METADATA +400 -0
  181. holmesgpt-0.11.5.dist-info/RECORD +183 -0
  182. holmesgpt-0.11.5.dist-info/WHEEL +4 -0
  183. holmesgpt-0.11.5.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,180 @@
1
+ import logging
2
+ from typing import Dict, List, Tuple
3
+
4
+ from holmes.core.tools import StructuredToolResult, ToolParameter, ToolResultStatus
5
+ from holmes.plugins.toolsets.azure_sql.azure_base_toolset import (
6
+ BaseAzureSQLTool,
7
+ BaseAzureSQLToolset,
8
+ AzureSQLDatabaseConfig,
9
+ )
10
+ from holmes.plugins.toolsets.azure_sql.apis.azure_sql_api import AzureSQLAPIClient
11
+ from holmes.plugins.toolsets.azure_sql.utils import format_timing
12
+
13
+
14
+ class GetTopLogIOQueries(BaseAzureSQLTool):
15
+ def __init__(self, toolset: "BaseAzureSQLToolset"):
16
+ super().__init__(
17
+ name="get_top_log_io_queries",
18
+ description="Identifies queries consuming the most transaction log I/O from Query Store. Use this to find queries causing transaction log performance issues and write-heavy workload problems.",
19
+ parameters={
20
+ "top_count": ToolParameter(
21
+ description="Number of top queries to return. Use 15 for detailed analysis, 5-10 for quick overview (default: 15)",
22
+ type="integer",
23
+ required=False,
24
+ ),
25
+ "hours_back": ToolParameter(
26
+ description="Time window for analysis in hours. Use 2 for recent issues, 24+ for trend analysis (default: 2)",
27
+ type="integer",
28
+ required=False,
29
+ ),
30
+ },
31
+ toolset=toolset,
32
+ )
33
+
34
+ def _format_log_io_queries_report(
35
+ self,
36
+ queries: List[Dict],
37
+ db_config: AzureSQLDatabaseConfig,
38
+ top_count: int,
39
+ hours_back: int,
40
+ ) -> str:
41
+ """Format the log I/O queries data into a readable report."""
42
+ report_sections = []
43
+
44
+ # Header
45
+ report_sections.append("# Top Log I/O Consuming Queries Report")
46
+ report_sections.append(f"**Database:** {db_config.database_name}")
47
+ report_sections.append(f"**Server:** {db_config.server_name}")
48
+ report_sections.append(f"**Analysis Period:** Last {hours_back} hours")
49
+ report_sections.append(f"**Top Queries:** {top_count}")
50
+ report_sections.append("")
51
+
52
+ if not queries:
53
+ report_sections.append("No queries found for the specified time period.")
54
+ return "\n".join(report_sections)
55
+
56
+ # Summary
57
+ total_log_writes = sum(float(q.get("total_log_bytes_used", 0)) for q in queries)
58
+ total_executions = sum(int(q.get("execution_count", 0)) for q in queries)
59
+
60
+ report_sections.append("## Summary")
61
+ report_sections.append(f"- **Total Queries Analyzed:** {len(queries)}")
62
+ report_sections.append(
63
+ f"- **Total Log Bytes Used:** {total_log_writes:,.0f} bytes"
64
+ )
65
+ report_sections.append(f"- **Total Executions:** {total_executions:,}")
66
+ report_sections.append("")
67
+
68
+ # Query Details
69
+ report_sections.append("## Query Details")
70
+
71
+ for i, query in enumerate(queries[:top_count], 1):
72
+ avg_log_bytes = float(query.get("avg_log_bytes_used", 0))
73
+ execution_count = int(query.get("execution_count", 0))
74
+ total_log_bytes = float(query.get("total_log_bytes_used", 0))
75
+ max_log_bytes = float(query.get("max_log_bytes_used", 0))
76
+ avg_cpu = float(query.get("avg_cpu_time", 0))
77
+ avg_duration = float(query.get("avg_duration", 0))
78
+ query_text = query.get("query_sql_text", "N/A")
79
+ last_execution = query.get("last_execution_time", "N/A")
80
+
81
+ # Truncate long queries
82
+ if len(query_text) > 200:
83
+ query_text = query_text[:200] + "..."
84
+
85
+ report_sections.append(f"### Query #{i}")
86
+ report_sections.append(
87
+ f"- **Average Log Bytes Used:** {avg_log_bytes:,.0f} bytes"
88
+ )
89
+ report_sections.append(
90
+ f"- **Total Log Bytes Used:** {total_log_bytes:,.0f} bytes"
91
+ )
92
+ report_sections.append(
93
+ f"- **Max Log Bytes Used:** {max_log_bytes:,.0f} bytes"
94
+ )
95
+ report_sections.append(f"- **Execution Count:** {execution_count:,}")
96
+ report_sections.append(f"- **Average CPU Time:** {format_timing(avg_cpu)}")
97
+ report_sections.append(
98
+ f"- **Average Duration:** {format_timing(avg_duration)}"
99
+ )
100
+ report_sections.append(f"- **Last Execution:** {last_execution}")
101
+ report_sections.append("- **Query Text:**")
102
+ report_sections.append("```sql")
103
+ report_sections.append(query_text)
104
+ report_sections.append("```")
105
+ report_sections.append("")
106
+
107
+ return "\n".join(report_sections)
108
+
109
+ def _invoke(self, params: Dict) -> StructuredToolResult:
110
+ try:
111
+ top_count = params.get("top_count", 15)
112
+ hours_back = params.get("hours_back", 2)
113
+
114
+ db_config = self.toolset.database_config()
115
+ client = self.toolset.api_client()
116
+
117
+ # Get top log I/O queries
118
+ queries = client.get_top_log_io_queries(
119
+ db_config.subscription_id,
120
+ db_config.resource_group,
121
+ db_config.server_name,
122
+ db_config.database_name,
123
+ top_count,
124
+ hours_back,
125
+ )
126
+
127
+ # Format the report
128
+ report_text = self._format_log_io_queries_report(
129
+ queries, db_config, top_count, hours_back
130
+ )
131
+
132
+ return StructuredToolResult(
133
+ status=ToolResultStatus.SUCCESS,
134
+ data=report_text,
135
+ params=params,
136
+ )
137
+ except Exception as e:
138
+ error_msg = f"Failed to get top log I/O queries: {str(e)}"
139
+ logging.error(error_msg)
140
+ return StructuredToolResult(
141
+ status=ToolResultStatus.ERROR,
142
+ error=error_msg,
143
+ params=params,
144
+ )
145
+
146
+ def get_parameterized_one_liner(self, params: Dict) -> str:
147
+ db_config = self.toolset.database_config()
148
+ return f"Fetch top log I/O consuming queries for database {db_config.server_name}/{db_config.database_name}"
149
+
150
+ @staticmethod
151
+ def validate_config(
152
+ api_client: AzureSQLAPIClient, database_config: AzureSQLDatabaseConfig
153
+ ) -> Tuple[bool, str]:
154
+ errors = []
155
+
156
+ try:
157
+ # Test direct database connection for Query Store access
158
+ test_query = (
159
+ "SELECT TOP 1 query_id FROM sys.query_store_query WHERE query_id > 0"
160
+ )
161
+ api_client.execute_query(
162
+ database_config.server_name, database_config.database_name, test_query
163
+ )
164
+ except Exception as e:
165
+ error_msg = str(e)
166
+ if (
167
+ "login failed" in error_msg.lower()
168
+ or "authentication" in error_msg.lower()
169
+ ):
170
+ errors.append(f"Database authentication failed: {error_msg}")
171
+ elif "permission" in error_msg.lower() or "denied" in error_msg.lower():
172
+ errors.append(f"Query Store access denied: {error_msg}")
173
+ elif "query store" in error_msg.lower():
174
+ errors.append(f"Query Store not available or disabled: {error_msg}")
175
+ else:
176
+ errors.append(f"Database connection failed: {error_msg}")
177
+
178
+ if errors:
179
+ return False, "\n".join(errors)
180
+ return True, ""
@@ -0,0 +1,83 @@
1
+ """
2
+ Shared utility functions for Azure SQL toolset.
3
+ """
4
+
5
+
6
+ def format_timing(microseconds: float) -> str:
7
+ """Format timing values with appropriate units.
8
+
9
+ Args:
10
+ microseconds: Time value in microseconds
11
+
12
+ Returns:
13
+ Formatted string with appropriate units (s, ms, or μs)
14
+ """
15
+ if microseconds >= 1_000_000: # >= 1 second
16
+ return f"{microseconds / 1_000_000:.2f} s"
17
+ elif microseconds >= 1_000: # >= 1 millisecond
18
+ return f"{microseconds / 1_000:.2f} ms"
19
+ else: # < 1 millisecond
20
+ return f"{microseconds:.0f} μs"
21
+
22
+
23
+ def format_size_bytes(bytes_value: float) -> str:
24
+ """Format byte values with appropriate units.
25
+
26
+ Args:
27
+ bytes_value: Size value in bytes
28
+
29
+ Returns:
30
+ Formatted string with appropriate units (GB, MB, KB, or B)
31
+ """
32
+ if bytes_value >= 1_073_741_824: # >= 1 GB
33
+ return f"{bytes_value / 1_073_741_824:.2f} GB"
34
+ elif bytes_value >= 1_048_576: # >= 1 MB
35
+ return f"{bytes_value / 1_048_576:.2f} MB"
36
+ elif bytes_value >= 1_024: # >= 1 KB
37
+ return f"{bytes_value / 1_024:.2f} KB"
38
+ else: # < 1 KB
39
+ return f"{bytes_value:.0f} B"
40
+
41
+
42
+ def format_percentage(value: float, decimal_places: int = 2) -> str:
43
+ """Format percentage values.
44
+
45
+ Args:
46
+ value: Percentage value (0-100)
47
+ decimal_places: Number of decimal places to show
48
+
49
+ Returns:
50
+ Formatted percentage string
51
+ """
52
+ return f"{value:.{decimal_places}f}%"
53
+
54
+
55
+ def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
56
+ """Safely divide two numbers, handling division by zero.
57
+
58
+ Args:
59
+ numerator: The dividend
60
+ denominator: The divisor
61
+ default: Value to return if denominator is zero
62
+
63
+ Returns:
64
+ Division result or default if denominator is zero
65
+ """
66
+ if denominator == 0 or denominator is None:
67
+ return default
68
+ return numerator / denominator
69
+
70
+
71
+ def truncate_text(text: str, max_length: int = 100) -> str:
72
+ """Truncate text to a maximum length with ellipsis.
73
+
74
+ Args:
75
+ text: Text to truncate
76
+ max_length: Maximum length including ellipsis
77
+
78
+ Returns:
79
+ Truncated text with ellipsis if needed
80
+ """
81
+ if not text or len(text) <= max_length:
82
+ return text
83
+ return text[: max_length - 3] + "..."
File without changes
@@ -0,0 +1,14 @@
1
+ # Bash commands
2
+
3
+ The tool `run_bash_command` allows you to run many kubectl commands:
4
+
5
+ - `kubectl get ...`
6
+ - `kubectl describe ...`
7
+ - `kubectl events ...`
8
+ - `kubectl top ...`
9
+
10
+ It is also possible to combine `kubectl` with `grep`. For example:
11
+ - `kubectl get pods | grep holmes`
12
+
13
+ The tool `kubectl_run_image` will run an image:
14
+ - `kubectl run <name> --image=<image> --rm --attach --restart=Never --i --tty -- <command>`
@@ -0,0 +1,208 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import random
5
+ import re
6
+ import string
7
+ from typing import Dict, Any, Optional
8
+
9
+
10
+ from holmes.core.tools import (
11
+ CallablePrerequisite,
12
+ StructuredToolResult,
13
+ Tool,
14
+ ToolParameter,
15
+ ToolResultStatus,
16
+ Toolset,
17
+ ToolsetTag,
18
+ )
19
+ from holmes.plugins.toolsets.bash.common.bash import execute_bash_command
20
+ from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
21
+ from holmes.plugins.toolsets.bash.kubectl.constants import SAFE_NAMESPACE_PATTERN
22
+ from holmes.plugins.toolsets.bash.kubectl.kubectl_run import validate_image_and_commands
23
+ from holmes.plugins.toolsets.bash.parse_command import make_command_safe
24
+ from holmes.plugins.toolsets.utils import get_param_or_raise
25
+
26
+
27
+ class BaseBashExecutorToolset(Toolset):
28
+ config: Optional[BashExecutorConfig] = None
29
+
30
+ def get_example_config(self):
31
+ example_config = BashExecutorConfig()
32
+ return example_config.model_dump()
33
+
34
+
35
+ class BaseBashTool(Tool):
36
+ toolset: BaseBashExecutorToolset
37
+
38
+
39
+ class KubectlRunImageCommand(BaseBashTool):
40
+ def __init__(self, toolset: BaseBashExecutorToolset):
41
+ super().__init__(
42
+ name="kubectl_run_image",
43
+ description=(
44
+ "Executes `kubectl run <name> --image=<image> ... -- <command>` return the result"
45
+ ),
46
+ parameters={
47
+ "image": ToolParameter(
48
+ description="The image to run",
49
+ type="string",
50
+ required=True,
51
+ ),
52
+ "command": ToolParameter(
53
+ description="The command to execute on the deployed pod",
54
+ type="string",
55
+ required=True,
56
+ ),
57
+ "namespace": ToolParameter(
58
+ description="The namespace in which to deploy the temporary pod",
59
+ type="string",
60
+ required=False,
61
+ ),
62
+ "timeout": ToolParameter(
63
+ description=(
64
+ "Optional timeout in seconds for the command execution. "
65
+ "Defaults to 60s."
66
+ ),
67
+ type="integer",
68
+ required=False,
69
+ ),
70
+ },
71
+ toolset=toolset,
72
+ )
73
+
74
+ def _build_kubectl_command(self, params: dict, pod_name: str) -> str:
75
+ namespace = params.get("namespace", "default")
76
+ image = get_param_or_raise(params, "image")
77
+ command_str = get_param_or_raise(params, "command")
78
+ return f"kubectl run {pod_name} --image={image} --namespace={namespace} --rm --attach --restart=Never -i -- {command_str}"
79
+
80
+ def _invoke(self, params: Dict[str, Any]) -> StructuredToolResult:
81
+ timeout = params.get("timeout", 60)
82
+
83
+ image = get_param_or_raise(params, "image")
84
+ command_str = get_param_or_raise(params, "command")
85
+
86
+ namespace = params.get("namespace")
87
+
88
+ if namespace and not re.match(SAFE_NAMESPACE_PATTERN, namespace):
89
+ return StructuredToolResult(
90
+ status=ToolResultStatus.ERROR,
91
+ error=f"Error: The namespace is invalid. Valid namespaces must match the following regexp: {SAFE_NAMESPACE_PATTERN}",
92
+ params=params,
93
+ )
94
+
95
+ validate_image_and_commands(
96
+ image=image, container_command=command_str, config=self.toolset.config
97
+ )
98
+
99
+ pod_name = (
100
+ "holmesgpt-debug-pod-"
101
+ + "".join(random.choices(string.ascii_letters, k=8)).lower()
102
+ )
103
+ full_kubectl_command = self._build_kubectl_command(params, pod_name)
104
+ return execute_bash_command(
105
+ cmd=full_kubectl_command, timeout=timeout, params=params
106
+ )
107
+
108
+ def get_parameterized_one_liner(self, params: Dict[str, Any]) -> str:
109
+ return self._build_kubectl_command(params, "<pod_name>")
110
+
111
+
112
+ class RunBashCommand(BaseBashTool):
113
+ def __init__(self, toolset: BaseBashExecutorToolset):
114
+ super().__init__(
115
+ name="run_bash_command",
116
+ description=(
117
+ "Executes a given bash command and returns its standard output, "
118
+ "standard error, and exit code."
119
+ "The command is executed via 'bash -c \"<command>\"'."
120
+ "Only some commands are allowed."
121
+ ),
122
+ parameters={
123
+ "command": ToolParameter(
124
+ description="The bash command string to execute.",
125
+ type="string",
126
+ required=True,
127
+ ),
128
+ "timeout": ToolParameter(
129
+ description=(
130
+ "Optional timeout in seconds for the command execution. "
131
+ "Defaults to 60s."
132
+ ),
133
+ type="integer",
134
+ required=False,
135
+ ),
136
+ },
137
+ toolset=toolset,
138
+ )
139
+
140
+ def _invoke(self, params: Dict[str, Any]) -> StructuredToolResult:
141
+ command_str = params.get("command")
142
+ timeout = params.get("timeout", 60)
143
+
144
+ if not command_str:
145
+ return StructuredToolResult(
146
+ status=ToolResultStatus.ERROR,
147
+ error="The 'command' parameter is required and was not provided.",
148
+ params=params,
149
+ )
150
+
151
+ if not isinstance(command_str, str):
152
+ return StructuredToolResult(
153
+ status=ToolResultStatus.ERROR,
154
+ error=f"The 'command' parameter must be a string, got {type(command_str).__name__}.",
155
+ params=params,
156
+ )
157
+ try:
158
+ safe_command_str = make_command_safe(command_str, self.toolset.config)
159
+ return execute_bash_command(
160
+ cmd=safe_command_str, timeout=timeout, params=params
161
+ )
162
+ except (argparse.ArgumentError, ValueError) as e:
163
+ logging.info(f"Refusing LLM tool call {command_str}", exc_info=True)
164
+ return StructuredToolResult(
165
+ status=ToolResultStatus.ERROR,
166
+ error=f"Refusing to execute bash command. Only some commands are supported and this is likely because requested command is unsupported. Error: {str(e)}",
167
+ params=params,
168
+ )
169
+
170
+ def get_parameterized_one_liner(self, params: Dict[str, Any]) -> str:
171
+ command = params.get("command", "N/A")
172
+ display_command = command[:200] + "..." if len(command) > 200 else command
173
+ return display_command
174
+
175
+
176
+ class BashExecutorToolset(BaseBashExecutorToolset):
177
+ def __init__(self):
178
+ super().__init__(
179
+ name="bash",
180
+ enabled=False,
181
+ description=(
182
+ "Toolset for executing arbitrary bash commands on the system where Holmes is running. "
183
+ "WARNING: This toolset provides powerful capabilities and should be "
184
+ "enabled and used with extreme caution due to significant security risks. "
185
+ "Ensure that only trusted users have access to this tool."
186
+ ),
187
+ docs_url="", # TODO: Add relevant documentation URL
188
+ icon_url="https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Bash_Logo_Colored.svg/120px-Bash_Logo_Colored.svg.png", # Example Bash icon
189
+ prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
190
+ tools=[RunBashCommand(self), KubectlRunImageCommand(self)],
191
+ tags=[ToolsetTag.CORE],
192
+ is_default=False,
193
+ )
194
+
195
+ self._reload_llm_instructions()
196
+
197
+ def _reload_llm_instructions(self):
198
+ template_file_path = os.path.abspath(
199
+ os.path.join(os.path.dirname(__file__), "bash_instructions.jinja2")
200
+ )
201
+ self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
202
+
203
+ def prerequisites_callable(self, config: dict[str, Any]) -> tuple[bool, str]:
204
+ if config:
205
+ self.config = BashExecutorConfig(**config)
206
+ else:
207
+ self.config = BashExecutorConfig()
208
+ return True, ""
@@ -0,0 +1,52 @@
1
+ import subprocess
2
+ from holmes.core.tools import StructuredToolResult, ToolResultStatus
3
+
4
+
5
+ def execute_bash_command(cmd: str, timeout: int, params: dict) -> StructuredToolResult:
6
+ try:
7
+ process = subprocess.run(
8
+ cmd,
9
+ shell=True,
10
+ executable="/bin/bash",
11
+ stdout=subprocess.PIPE,
12
+ stderr=subprocess.STDOUT,
13
+ text=True,
14
+ timeout=timeout,
15
+ check=False,
16
+ )
17
+
18
+ stdout = process.stdout.strip() if process.stdout else ""
19
+ result_data = f"{cmd}\n" f"{stdout}"
20
+
21
+ status = ToolResultStatus.ERROR
22
+ if process.returncode == 0 and stdout:
23
+ status = ToolResultStatus.SUCCESS
24
+ elif not stdout:
25
+ status = ToolResultStatus.NO_DATA
26
+
27
+ return StructuredToolResult(
28
+ status=status,
29
+ data=result_data,
30
+ params=params,
31
+ invocation=cmd,
32
+ return_code=process.returncode,
33
+ )
34
+ except subprocess.TimeoutExpired:
35
+ return StructuredToolResult(
36
+ status=ToolResultStatus.ERROR,
37
+ error=f"Error: Command '{cmd}' timed out after {timeout} seconds.",
38
+ params=params,
39
+ )
40
+ except FileNotFoundError:
41
+ # This might occur if /bin/bash is not found, or if shell=False and command is not found
42
+ return StructuredToolResult(
43
+ status=ToolResultStatus.ERROR,
44
+ error="Error: Bash executable or command not found. Ensure bash is installed and the command is valid.",
45
+ params=params,
46
+ )
47
+ except Exception as e:
48
+ return StructuredToolResult(
49
+ status=ToolResultStatus.ERROR,
50
+ error=f"Error executing command '{cmd}': {str(e)}",
51
+ params=params,
52
+ )
@@ -0,0 +1,14 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class KubectlImageConfig(BaseModel):
5
+ image: str
6
+ allowed_commands: list[str]
7
+
8
+
9
+ class KubectlConfig(BaseModel):
10
+ allowed_images: list[KubectlImageConfig] = []
11
+
12
+
13
+ class BashExecutorConfig(BaseModel):
14
+ kubectl: KubectlConfig = KubectlConfig()
@@ -0,0 +1,25 @@
1
+ import shlex
2
+
3
+ SAFE_SHELL_CHARS = frozenset(".-_=/,:")
4
+
5
+
6
+ def escape_shell_args(args: list[str]) -> list[str]:
7
+ """
8
+ Escape shell arguments to prevent injection.
9
+ Uses shlex.quote for safe shell argument quoting.
10
+ """
11
+ escaped_args = []
12
+
13
+ for arg in args:
14
+ # If argument is safe (contains only alphanumeric, hyphens, dots, underscores, equals, slash, comma, colon)
15
+ # no escaping needed
16
+ if arg and all(c.isalnum() or c in SAFE_SHELL_CHARS for c in arg):
17
+ escaped_args.append(arg)
18
+ # If argument starts with -- or - (flag), no escaping needed
19
+ elif arg.startswith("-"):
20
+ escaped_args.append(arg)
21
+ # For everything else, use shlex.quote for proper escaping
22
+ else:
23
+ escaped_args.append(shlex.quote(arg))
24
+
25
+ return escaped_args
@@ -0,0 +1,24 @@
1
+ import argparse
2
+ import re
3
+ from typing import Union
4
+
5
+
6
+ def regex_validator(field_name: str, pattern: Union[str, re.Pattern]):
7
+ def check_regex(arg_value):
8
+ if not re.match(pattern, arg_value):
9
+ raise argparse.ArgumentTypeError(f"invalid {field_name} value")
10
+ return arg_value
11
+
12
+ return check_regex
13
+
14
+
15
+ def whitelist_validator(field_name: str, whitelisted_values: set[str]):
16
+ def validate_value(value: str) -> str:
17
+ if value not in whitelisted_values:
18
+ whitelisted_values_str = ", ".join(whitelisted_values)
19
+ raise argparse.ArgumentTypeError(
20
+ f"Invalid {field_name} format: '{value}'. Must be one of [{whitelisted_values_str}]"
21
+ )
22
+ return value
23
+
24
+ return validate_value
@@ -0,0 +1,52 @@
1
+ import argparse
2
+ import re
3
+ from typing import Any
4
+
5
+ from holmes.plugins.toolsets.bash.common.stringify import escape_shell_args
6
+
7
+ MAX_GREP_SIZE = 100
8
+ SAFE_GREP_PATTERN = re.compile(r"^[a-zA-Z0-9\-_. :*()]+$")
9
+
10
+
11
+ def create_grep_parser(main_parser: Any):
12
+ parser = main_parser.add_parser(
13
+ "grep",
14
+ help="Search text patterns in files or input",
15
+ exit_on_error=False,
16
+ )
17
+ parser.add_argument(
18
+ "keyword",
19
+ type=lambda x: validate_grep_keyword(x),
20
+ help="The pattern to search for",
21
+ )
22
+ parser.add_argument(
23
+ "-i", "--ignore-case", action="store_true", help="Ignore case distinctions"
24
+ )
25
+
26
+
27
+ def validate_grep_keyword(value: str) -> str:
28
+ """Validate the grep keyword parameter."""
29
+
30
+ if not value:
31
+ raise argparse.ArgumentTypeError("Grep keyword cannot be empty")
32
+
33
+ if not SAFE_GREP_PATTERN.match(value):
34
+ raise argparse.ArgumentTypeError(f"Unsafe grep keyword: {value}")
35
+
36
+ if len(value) > MAX_GREP_SIZE:
37
+ raise argparse.ArgumentTypeError(
38
+ f"Grep keyword too long. Max allowed size is {MAX_GREP_SIZE} but received {len(value)}"
39
+ )
40
+
41
+ return value
42
+
43
+
44
+ def stringify_grep_command(cmd: Any) -> str:
45
+ """Stringify grep command."""
46
+ parts = ["grep"]
47
+
48
+ if cmd.ignore_case:
49
+ parts.append("-i")
50
+
51
+ parts.append(cmd.keyword)
52
+ return " ".join(escape_shell_args(parts))