mcp-souschef 3.2.0__py3-none-any.whl → 3.5.2__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.
@@ -0,0 +1,300 @@
1
+ """
2
+ Chef Server Settings Page for SousChef UI.
3
+
4
+ Configure and validate Chef Server connectivity for dynamic inventory and node queries.
5
+ """
6
+
7
+ import os
8
+
9
+ import streamlit as st
10
+
11
+ from souschef.core.url_validation import validate_user_provided_url
12
+
13
+ try:
14
+ import requests
15
+ from requests.exceptions import (
16
+ ConnectionError, # noqa: A004
17
+ Timeout,
18
+ )
19
+ except ImportError:
20
+ requests = None # type: ignore[assignment]
21
+ ConnectionError = Exception # type: ignore[assignment,misc] # noqa: A001
22
+ Timeout = Exception # type: ignore[assignment,misc]
23
+
24
+
25
+ def _handle_chef_server_response(
26
+ response: "requests.Response", server_url: str
27
+ ) -> tuple[bool, str]:
28
+ """
29
+ Handle Chef Server search response.
30
+
31
+ Args:
32
+ response: HTTP response from Chef Server
33
+ server_url: The Chef Server URL that was queried
34
+
35
+ Returns:
36
+ Tuple of (success: bool, message: str)
37
+
38
+ """
39
+ if response.status_code == 200:
40
+ return True, f"✅ Successfully connected to Chef Server at {server_url}"
41
+ if response.status_code == 401:
42
+ return (
43
+ False,
44
+ "❌ Authentication failed - check your Chef Server credentials",
45
+ )
46
+ if response.status_code == 404:
47
+ return False, "❌ Chef Server search endpoint not found"
48
+ return (
49
+ False,
50
+ f"❌ Connection failed with status code {response.status_code}",
51
+ )
52
+
53
+
54
+ def _validate_chef_server_connection(
55
+ server_url: str, node_name: str
56
+ ) -> tuple[bool, str]:
57
+ """
58
+ Validate Chef Server connection by testing the search endpoint.
59
+
60
+ Args:
61
+ server_url: Base URL of the Chef Server
62
+ node_name: Chef node name for authentication
63
+
64
+ Returns:
65
+ Tuple of (success: bool, message: str)
66
+
67
+ """
68
+ if not requests:
69
+ return False, "requests library not installed"
70
+
71
+ if not server_url:
72
+ return False, "Server URL is required"
73
+
74
+ try:
75
+ server_url = validate_user_provided_url(server_url)
76
+ except ValueError as exc:
77
+ return False, f"Invalid server URL: {exc}"
78
+
79
+ if not node_name:
80
+ return False, "Node name is required for authentication"
81
+
82
+ # Test the search endpoint
83
+ try:
84
+ search_url = f"{server_url.rstrip('/')}/search/node"
85
+ response = requests.get(
86
+ search_url,
87
+ params={"q": "*:*"},
88
+ timeout=5,
89
+ headers={"Accept": "application/json"},
90
+ )
91
+ return _handle_chef_server_response(response, server_url)
92
+
93
+ except Timeout:
94
+ return False, f"❌ Connection timeout - could not reach {server_url}"
95
+ except ConnectionError:
96
+ return False, f"❌ Connection error - Chef Server not reachable at {server_url}"
97
+ except Exception as e:
98
+ return False, f"❌ Unexpected error: {e}"
99
+
100
+
101
+ def _render_chef_server_configuration() -> tuple[str, str]:
102
+ """
103
+ Render Chef Server configuration UI and return config values.
104
+
105
+ Returns:
106
+ Tuple of (server_url, node_name)
107
+
108
+ """
109
+ st.subheader("Chef Server Configuration")
110
+
111
+ st.markdown("""
112
+ Configure your Chef Server connection for dynamic inventory generation
113
+ and node queries. This allows SousChef to retrieve live node data from
114
+ your Chef infrastructure.
115
+ """)
116
+
117
+ col1, col2 = st.columns(2)
118
+
119
+ with col1:
120
+ server_url = st.text_input(
121
+ "Chef Server URL",
122
+ help="Full URL of your Chef Server (e.g., https://chef.example.com)",
123
+ key="chef_server_url_input",
124
+ placeholder="https://chef.example.com",
125
+ value=os.environ.get("CHEF_SERVER_URL", ""),
126
+ )
127
+
128
+ with col2:
129
+ node_name = st.text_input(
130
+ "Chef Node Name",
131
+ help="Node name for authentication with Chef Server",
132
+ key="chef_node_name_input",
133
+ placeholder="my-node",
134
+ value=os.environ.get("CHEF_NODE_NAME", ""),
135
+ )
136
+
137
+ return server_url, node_name
138
+
139
+
140
+ def _render_test_connection_button(server_url: str, node_name: str) -> None:
141
+ """
142
+ Render the test connection button and display results.
143
+
144
+ Args:
145
+ server_url: Chef Server URL to test
146
+ node_name: Chef node name for authentication
147
+
148
+ """
149
+ st.markdown("---")
150
+ st.subheader("Test Connection")
151
+
152
+ col1, col2 = st.columns([1, 3])
153
+
154
+ with col1:
155
+ test_button = st.button(
156
+ "Test Chef Server Connection",
157
+ type="primary",
158
+ help="Verify connectivity to Chef Server",
159
+ )
160
+
161
+ if test_button:
162
+ with col2, st.spinner("Testing Chef Server connection..."):
163
+ success, message = _validate_chef_server_connection(server_url, node_name)
164
+
165
+ if success:
166
+ st.success(message)
167
+ else:
168
+ st.error(message)
169
+
170
+
171
+ def _render_usage_examples() -> None:
172
+ """Render usage examples for Chef Server integration."""
173
+ st.markdown("---")
174
+ st.subheader("Usage Examples")
175
+
176
+ with st.expander("Dynamic Inventory from Chef Searches"):
177
+ st.markdown("""
178
+ Once configured, SousChef can query your Chef Server to generate dynamic
179
+ Ansible inventories based on Chef node searches:
180
+
181
+ ```python
182
+ # Example Chef search query
183
+ search_query = "role:webserver AND chef_environment:production"
184
+
185
+ # SousChef will convert this to an Ansible dynamic inventory
186
+ # that queries your Chef Server in real-time
187
+ ```
188
+
189
+ Benefits:
190
+ - Real-time node discovery
191
+ - No manual inventory maintenance
192
+ - Leverage existing Chef infrastructure
193
+ - Seamless migration path
194
+ """)
195
+
196
+ with st.expander("Environment Variables"):
197
+ st.markdown("""
198
+ You can also configure Chef Server settings via environment variables:
199
+
200
+ ```bash
201
+ export CHEF_SERVER_URL="https://chef.example.com"
202
+ export CHEF_NODE_NAME="my-node"
203
+ ```
204
+
205
+ These will be automatically detected by SousChef.
206
+ """)
207
+
208
+
209
+ def _render_save_settings_section(server_url: str, node_name: str) -> None:
210
+ """
211
+ Render the save settings section.
212
+
213
+ Args:
214
+ server_url: Chef Server URL to save
215
+ node_name: Chef node name to save
216
+
217
+ """
218
+ st.markdown("---")
219
+ st.subheader("Save Settings")
220
+
221
+ col1, col2 = st.columns([1, 3])
222
+
223
+ with col1:
224
+ save_button = st.button(
225
+ "Save Configuration",
226
+ type="primary",
227
+ help="Save Chef Server settings to session",
228
+ )
229
+
230
+ if save_button:
231
+ with col2:
232
+ # Save to session state
233
+ st.session_state.chef_server_url = server_url
234
+ st.session_state.chef_node_name = node_name
235
+
236
+ st.success("""
237
+ ✅ Chef Server configuration saved to session!
238
+
239
+ **Note:** For persistent configuration across sessions,
240
+ set environment variables:
241
+ - `CHEF_SERVER_URL`
242
+ - `CHEF_NODE_NAME`
243
+ """)
244
+
245
+
246
+ def _render_current_configuration() -> None:
247
+ """Display current Chef Server configuration from environment or session."""
248
+ current_url = os.environ.get("CHEF_SERVER_URL") or st.session_state.get(
249
+ "chef_server_url", "Not configured"
250
+ )
251
+ current_node = os.environ.get("CHEF_NODE_NAME") or st.session_state.get(
252
+ "chef_node_name", "Not configured"
253
+ )
254
+
255
+ st.info(f"""
256
+ **Current Configuration:**
257
+ - Server URL: `{current_url}`
258
+ - Node Name: `{current_node}`
259
+ """)
260
+
261
+
262
+ def show_chef_server_settings_page() -> None:
263
+ """Display Chef Server settings and configuration page."""
264
+ st.title("🔧 Chef Server Settings")
265
+
266
+ st.markdown("""
267
+ Configure your Chef Server connection to enable dynamic inventory generation
268
+ and live node queries. This allows SousChef to integrate with your existing
269
+ Chef infrastructure during the migration process.
270
+ """)
271
+
272
+ # Display current configuration
273
+ _render_current_configuration()
274
+
275
+ st.markdown("---")
276
+
277
+ # Configuration inputs
278
+ server_url, node_name = _render_chef_server_configuration()
279
+
280
+ # Test connection
281
+ _render_test_connection_button(server_url, node_name)
282
+
283
+ # Save settings
284
+ _render_save_settings_section(server_url, node_name)
285
+
286
+ # Usage examples
287
+ _render_usage_examples()
288
+
289
+ # Additional information
290
+ st.markdown("---")
291
+ st.markdown("""
292
+ ### Security Note
293
+
294
+ Chef Server authentication typically requires:
295
+ - Client key file for API authentication
296
+ - Proper permissions on the Chef Server
297
+
298
+ For production use, ensure your Chef Server credentials are properly secured
299
+ and not committed to version control.
300
+ """)
@@ -522,6 +522,9 @@ def _extract_tar_securely(
522
522
  # Use 'data' filter to prevent extraction of special files and symlinks
523
523
  open_kwargs["filter"] = "data"
524
524
 
525
+ # Resource consumption controls (S5042): Pre-scan validates all members for
526
+ # size limits (MAX_ARCHIVE_SIZE, MAX_FILE_SIZE), file count (MAX_FILES),
527
+ # depth (MAX_DEPTH), and blocks malicious files before extraction.
525
528
  with tarfile.open(**open_kwargs) as tar_ref:
526
529
  members = tar_ref.getmembers()
527
530
  # Pre-validate all members before allowing extraction
@@ -665,6 +668,11 @@ def create_results_archive(results: list, cookbook_path: str) -> bytes:
665
668
  # Add individual cookbook reports
666
669
  for result in results:
667
670
  if result["status"] == ANALYSIS_STATUS_ANALYSED:
671
+ manual_hours = result["estimated_hours"]
672
+ souschef_hours = result.get(
673
+ "estimated_hours_with_souschef", manual_hours * 0.5
674
+ )
675
+ time_saved = manual_hours - souschef_hours
668
676
  report_content = f"""# Cookbook Analysis Report: {result["name"]}
669
677
 
670
678
  ## Metadata
@@ -672,7 +680,14 @@ def create_results_archive(results: list, cookbook_path: str) -> bytes:
672
680
  - **Maintainer**: {result["maintainer"]}
673
681
  - **Dependencies**: {result["dependencies"]}
674
682
  - **Complexity**: {result["complexity"]}
675
- - **Estimated Hours**: {result["estimated_hours"]:.1f}
683
+
684
+ ## Effort Estimates
685
+ ### Manual Migration (Without SousChef):
686
+ - **Estimated Hours**: {manual_hours:.1f}
687
+
688
+ ### AI-Assisted (With SousChef):
689
+ - **Estimated Hours**: {souschef_hours:.1f}
690
+ - **Time Saved**: {time_saved:.1f} hours (50% faster)
676
691
 
677
692
  ## Recommendations
678
693
  {result["recommendations"]}
@@ -686,16 +701,27 @@ def create_results_archive(results: list, cookbook_path: str) -> bytes:
686
701
  successful = len(
687
702
  [r for r in results if r["status"] == ANALYSIS_STATUS_ANALYSED]
688
703
  )
689
- total_hours = sum(r.get("estimated_hours", 0) for r in results)
704
+ total_hours_manual = sum(r.get("estimated_hours", 0) for r in results)
705
+ total_hours_souschef = sum(
706
+ r.get("estimated_hours_with_souschef", r.get("estimated_hours", 0) * 0.5)
707
+ for r in results
708
+ )
709
+ time_saved_total = total_hours_manual - total_hours_souschef
690
710
 
691
711
  summary_content = f"""# SousChef Cookbook Analysis Summary
692
712
 
693
713
  ## Overview
694
714
  - **Cookbooks Analysed**: {len(results)}
695
-
696
715
  - **Successfully Analysed**: {successful}
697
716
 
698
- - **Total Estimated Hours**: {total_hours:.1f}
717
+ ## Effort Estimates
718
+ ### Manual Migration (Without AI):
719
+ - **Total Estimated Hours**: {total_hours_manual:.1f}
720
+
721
+ ### AI-Assisted (With SousChef):
722
+ - **Total Estimated Hours**: {total_hours_souschef:.1f}
723
+ - **Time Saved**: {time_saved_total:.1f} hours (50% faster)
724
+
699
725
  - **Source**: {cookbook_path} # deepcode ignore PT: used for display only
700
726
 
701
727
  ## Results Summary
@@ -704,10 +730,15 @@ def create_results_archive(results: list, cookbook_path: str) -> bytes:
704
730
  status_icon = (
705
731
  "PASS" if result["status"] == ANALYSIS_STATUS_ANALYSED else "FAIL"
706
732
  )
733
+ manual_hours = result.get("estimated_hours", 0)
734
+ souschef_hours = result.get(
735
+ "estimated_hours_with_souschef", manual_hours * 0.5
736
+ )
707
737
  summary_content += f"- {status_icon} {result['name']}: {result['status']}"
708
738
  if result["status"] == ANALYSIS_STATUS_ANALYSED:
709
739
  summary_content += (
710
- f" ({result['estimated_hours']:.1f} hours, "
740
+ f" (Manual: {manual_hours:.1f}h, "
741
+ f"With SousChef: {souschef_hours:.1f}h, "
711
742
  f"{result['complexity']} complexity)"
712
743
  )
713
744
  summary_content += "\n"
@@ -1415,6 +1446,9 @@ def _build_assessment_result(
1415
1446
  "dependencies": int(cookbook_assessment.get("dependencies", 0) or 0),
1416
1447
  "complexity": cookbook_assessment.get("migration_priority", "Unknown").title(),
1417
1448
  "estimated_hours": effort_metrics.estimated_hours,
1449
+ "estimated_hours_with_souschef": effort_metrics.estimated_hours_with_souschef,
1450
+ "time_saved_hours": effort_metrics.time_saved * 8,
1451
+ "efficiency_gain_percent": effort_metrics.efficiency_gain_percent,
1418
1452
  "recommendations": recommendations,
1419
1453
  "status": ANALYSIS_STATUS_ANALYSED,
1420
1454
  }
@@ -3404,8 +3438,18 @@ def _display_analysis_summary(results, total_cookbooks):
3404
3438
  st.metric("Successfully Analysed", f"{successful}/{total_cookbooks}")
3405
3439
 
3406
3440
  with col2:
3407
- total_hours = sum(r.get("estimated_hours", 0) for r in results)
3408
- st.metric("Total Estimated Hours", f"{total_hours:.1f}")
3441
+ total_hours_manual = sum(r.get("estimated_hours", 0) for r in results)
3442
+ total_hours_souschef = sum(
3443
+ r.get("estimated_hours_with_souschef", r.get("estimated_hours", 0) * 0.5)
3444
+ for r in results
3445
+ )
3446
+ time_saved = total_hours_manual - total_hours_souschef
3447
+ st.metric(
3448
+ "Manual Effort (hrs)",
3449
+ f"{total_hours_manual:.1f}",
3450
+ delta=f"With AI: {total_hours_souschef:.1f}h (save {time_saved:.1f}h)",
3451
+ delta_color="inverse",
3452
+ )
3409
3453
 
3410
3454
  with col3:
3411
3455
  complexities = [r.get("complexity", "Unknown") for r in results]
@@ -3491,7 +3535,7 @@ def _display_single_cookbook_details(result):
3491
3535
  st.metric("Dependencies", result.get("dependencies", 0))
3492
3536
 
3493
3537
  # Complexity and effort
3494
- col1, col2 = st.columns(2)
3538
+ col1, col2, col3 = st.columns(3)
3495
3539
  with col1:
3496
3540
  complexity = result.get("complexity", "Unknown")
3497
3541
  if complexity == "High":
@@ -3501,8 +3545,20 @@ def _display_single_cookbook_details(result):
3501
3545
  else:
3502
3546
  st.metric("Complexity", complexity, delta="Low")
3503
3547
  with col2:
3504
- hours = result.get("estimated_hours", 0)
3505
- st.metric("Estimated Hours", f"{hours:.1f}")
3548
+ hours_manual = result.get("estimated_hours", 0)
3549
+ st.metric("Manual Effort (hrs)", f"{hours_manual:.1f}")
3550
+ with col3:
3551
+ hours_souschef = result.get(
3552
+ "estimated_hours_with_souschef", hours_manual * 0.5
3553
+ )
3554
+ time_saved = hours_manual - hours_souschef
3555
+ savings_pct = int((time_saved / hours_manual) * 100)
3556
+ st.metric(
3557
+ "With SousChef (hrs)",
3558
+ f"{hours_souschef:.1f}",
3559
+ delta=f"Save {time_saved:.1f}h ({savings_pct}%)",
3560
+ delta_color="inverse",
3561
+ )
3506
3562
 
3507
3563
  # Path
3508
3564
  st.write(f"**Cookbook Path:** {result['path']}")