mcp-souschef 3.5.2__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_souschef-3.5.2.dist-info → mcp_souschef-4.0.0.dist-info}/METADATA +136 -8
- {mcp_souschef-3.5.2.dist-info → mcp_souschef-4.0.0.dist-info}/RECORD +17 -10
- souschef/core/ai_schemas.py +6 -1
- souschef/github/__init__.py +17 -0
- souschef/github/agent_control.py +459 -0
- souschef/server.py +193 -0
- souschef/storage/__init__.py +39 -0
- souschef/storage/blob.py +331 -0
- souschef/storage/config.py +163 -0
- souschef/storage/database.py +1182 -0
- souschef/ui/app.py +17 -4
- souschef/ui/pages/chef_server_settings.py +411 -2
- souschef/ui/pages/cookbook_analysis.py +352 -6
- souschef/ui/pages/history.py +964 -0
- {mcp_souschef-3.5.2.dist-info → mcp_souschef-4.0.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-3.5.2.dist-info → mcp_souschef-4.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-3.5.2.dist-info → mcp_souschef-4.0.0.dist-info}/licenses/LICENSE +0 -0
souschef/ui/app.py
CHANGED
|
@@ -26,6 +26,7 @@ from souschef.core.path_utils import safe_exists, safe_glob, safe_is_dir, safe_i
|
|
|
26
26
|
from souschef.ui.pages.ai_settings import show_ai_settings_page
|
|
27
27
|
from souschef.ui.pages.chef_server_settings import show_chef_server_settings_page
|
|
28
28
|
from souschef.ui.pages.cookbook_analysis import show_cookbook_analysis_page
|
|
29
|
+
from souschef.ui.pages.history import show_history_page
|
|
29
30
|
|
|
30
31
|
# Constants
|
|
31
32
|
SECTION_COMMUNITY_COOKBOOKS_HEADER = "Community Cookbooks:"
|
|
@@ -38,6 +39,7 @@ NAV_VALIDATION_REPORTS = "Validation Reports"
|
|
|
38
39
|
NAV_AI_SETTINGS = "AI Settings"
|
|
39
40
|
NAV_CHEF_SERVER_SETTINGS = "Chef Server Settings"
|
|
40
41
|
NAV_COOKBOOK_ANALYSIS = "Cookbook Analysis"
|
|
42
|
+
NAV_HISTORY = "History"
|
|
41
43
|
BUTTON_ANALYSE_DEPENDENCIES = "Analyse Dependencies"
|
|
42
44
|
INPUT_METHOD_DIRECTORY_PATH = "Directory Path"
|
|
43
45
|
MIME_TEXT_MARKDOWN = "text/markdown"
|
|
@@ -127,8 +129,8 @@ def main() -> None:
|
|
|
127
129
|
# Navigation section
|
|
128
130
|
st.subheader("Navigation")
|
|
129
131
|
|
|
130
|
-
col1, col2, col3 = st.columns(
|
|
131
|
-
|
|
132
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
133
|
+
col5, col6, col7, _ = st.columns(4)
|
|
132
134
|
|
|
133
135
|
with col1:
|
|
134
136
|
if st.button(
|
|
@@ -161,6 +163,16 @@ def main() -> None:
|
|
|
161
163
|
st.rerun()
|
|
162
164
|
|
|
163
165
|
with col4:
|
|
166
|
+
if st.button(
|
|
167
|
+
"History",
|
|
168
|
+
type="primary" if page == NAV_HISTORY else "secondary",
|
|
169
|
+
use_container_width=True,
|
|
170
|
+
key="nav_history",
|
|
171
|
+
):
|
|
172
|
+
st.session_state.current_page = NAV_HISTORY
|
|
173
|
+
st.rerun()
|
|
174
|
+
|
|
175
|
+
with col5:
|
|
164
176
|
if st.button(
|
|
165
177
|
"Validation Reports",
|
|
166
178
|
type="primary" if page == NAV_VALIDATION_REPORTS else "secondary",
|
|
@@ -170,7 +182,7 @@ def main() -> None:
|
|
|
170
182
|
st.session_state.current_page = NAV_VALIDATION_REPORTS
|
|
171
183
|
st.rerun()
|
|
172
184
|
|
|
173
|
-
with
|
|
185
|
+
with col6:
|
|
174
186
|
if st.button(
|
|
175
187
|
"AI Settings",
|
|
176
188
|
type="primary" if page == NAV_AI_SETTINGS else "secondary",
|
|
@@ -180,7 +192,7 @@ def main() -> None:
|
|
|
180
192
|
st.session_state.current_page = NAV_AI_SETTINGS
|
|
181
193
|
st.rerun()
|
|
182
194
|
|
|
183
|
-
with
|
|
195
|
+
with col7:
|
|
184
196
|
if st.button(
|
|
185
197
|
"Chef Server",
|
|
186
198
|
type="primary" if page == NAV_CHEF_SERVER_SETTINGS else "secondary",
|
|
@@ -203,6 +215,7 @@ def _route_to_page(page: str) -> None:
|
|
|
203
215
|
NAV_COOKBOOK_ANALYSIS: show_cookbook_analysis_page,
|
|
204
216
|
NAV_MIGRATION_PLANNING: show_migration_planning,
|
|
205
217
|
NAV_DEPENDENCY_MAPPING: show_dependency_mapping,
|
|
218
|
+
NAV_HISTORY: show_history_page,
|
|
206
219
|
NAV_VALIDATION_REPORTS: show_validation_reports,
|
|
207
220
|
NAV_AI_SETTINGS: show_ai_settings_page,
|
|
208
221
|
NAV_CHEF_SERVER_SETTINGS: show_chef_server_settings_page,
|
|
@@ -5,10 +5,33 @@ Configure and validate Chef Server connectivity for dynamic inventory and node q
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
8
13
|
|
|
9
14
|
import streamlit as st
|
|
10
15
|
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
17
|
+
|
|
18
|
+
from souschef.assessment import assess_single_cookbook_with_ai
|
|
11
19
|
from souschef.core.url_validation import validate_user_provided_url
|
|
20
|
+
from souschef.storage import get_storage_manager
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import requests as requests_module
|
|
24
|
+
else:
|
|
25
|
+
try:
|
|
26
|
+
import requests as requests_module
|
|
27
|
+
from requests.exceptions import (
|
|
28
|
+
ConnectionError, # noqa: A004
|
|
29
|
+
Timeout,
|
|
30
|
+
)
|
|
31
|
+
except ImportError:
|
|
32
|
+
requests_module = None # type: ignore[assignment]
|
|
33
|
+
ConnectionError = Exception # type: ignore[assignment,misc] # noqa: A001
|
|
34
|
+
Timeout = Exception # type: ignore[assignment,misc]
|
|
12
35
|
|
|
13
36
|
try:
|
|
14
37
|
import requests
|
|
@@ -21,9 +44,12 @@ except ImportError:
|
|
|
21
44
|
ConnectionError = Exception # type: ignore[assignment,misc] # noqa: A001
|
|
22
45
|
Timeout = Exception # type: ignore[assignment,misc]
|
|
23
46
|
|
|
47
|
+
# Constants
|
|
48
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
49
|
+
|
|
24
50
|
|
|
25
51
|
def _handle_chef_server_response(
|
|
26
|
-
response: "
|
|
52
|
+
response: "requests_module.Response", server_url: str
|
|
27
53
|
) -> tuple[bool, str]:
|
|
28
54
|
"""
|
|
29
55
|
Handle Chef Server search response.
|
|
@@ -86,7 +112,7 @@ def _validate_chef_server_connection(
|
|
|
86
112
|
search_url,
|
|
87
113
|
params={"q": "*:*"},
|
|
88
114
|
timeout=5,
|
|
89
|
-
headers={"Accept":
|
|
115
|
+
headers={"Accept": JSON_CONTENT_TYPE},
|
|
90
116
|
)
|
|
91
117
|
return _handle_chef_server_response(response, server_url)
|
|
92
118
|
|
|
@@ -259,6 +285,385 @@ def _render_current_configuration() -> None:
|
|
|
259
285
|
""")
|
|
260
286
|
|
|
261
287
|
|
|
288
|
+
def _get_chef_cookbooks(server_url: str) -> list[dict]:
|
|
289
|
+
"""Fetch list of cookbooks from Chef Server."""
|
|
290
|
+
if not requests:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
cookbooks_url = f"{server_url.rstrip('/')}/cookbooks"
|
|
295
|
+
response = requests.get(
|
|
296
|
+
cookbooks_url,
|
|
297
|
+
timeout=10,
|
|
298
|
+
headers={"Accept": JSON_CONTENT_TYPE},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if response.status_code == 200:
|
|
302
|
+
cookbooks_data = response.json()
|
|
303
|
+
# Chef Server returns {cookbook_name: {url: ..., versions: [...]}}
|
|
304
|
+
return [
|
|
305
|
+
{"name": name, "versions": data.get("versions", [])}
|
|
306
|
+
for name, data in cookbooks_data.items()
|
|
307
|
+
]
|
|
308
|
+
return []
|
|
309
|
+
except Exception:
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _download_cookbook(
|
|
314
|
+
server_url: str, cookbook_name: str, version: str, target_dir: Path
|
|
315
|
+
) -> Path | None:
|
|
316
|
+
"""Download a cookbook from Chef Server to local directory."""
|
|
317
|
+
if not requests:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
# Download cookbook
|
|
322
|
+
cookbook_url = f"{server_url.rstrip('/')}/cookbooks/{cookbook_name}/{version}"
|
|
323
|
+
response = requests.get(
|
|
324
|
+
cookbook_url,
|
|
325
|
+
timeout=30,
|
|
326
|
+
headers={"Accept": JSON_CONTENT_TYPE},
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if response.status_code != 200:
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
# Create cookbook directory
|
|
333
|
+
cookbook_dir = target_dir / cookbook_name
|
|
334
|
+
cookbook_dir.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
|
|
336
|
+
# Download and save files
|
|
337
|
+
# This is simplified - real implementation would download all files
|
|
338
|
+
# For now, we'll create a minimal structure with metadata
|
|
339
|
+
metadata_path = cookbook_dir / "metadata.rb"
|
|
340
|
+
metadata_content = f"""name '{cookbook_name}'
|
|
341
|
+
version '{version}'
|
|
342
|
+
"""
|
|
343
|
+
metadata_path.write_text(metadata_content)
|
|
344
|
+
|
|
345
|
+
return cookbook_dir
|
|
346
|
+
|
|
347
|
+
except Exception:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _estimate_operation_time(num_cookbooks: int, operation: str = "assess") -> float:
|
|
352
|
+
"""Estimate time for bulk operation in seconds."""
|
|
353
|
+
# Rough estimates: assess ~5s per cookbook, convert ~10s per cookbook
|
|
354
|
+
time_per_item = 5.0 if operation == "assess" else 10.0
|
|
355
|
+
return num_cookbooks * time_per_item
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _format_time_estimate(seconds: float) -> str:
|
|
359
|
+
"""Format time estimate in human-readable format."""
|
|
360
|
+
if seconds < 60:
|
|
361
|
+
return f"{int(seconds)} seconds"
|
|
362
|
+
elif seconds < 3600:
|
|
363
|
+
minutes = int(seconds / 60)
|
|
364
|
+
return f"{minutes} minute{'s' if minutes != 1 else ''}"
|
|
365
|
+
else:
|
|
366
|
+
hours = int(seconds / 3600)
|
|
367
|
+
minutes = int((seconds % 3600) / 60)
|
|
368
|
+
return f"{hours} hour{'s' if hours != 1 else ''} {minutes} min"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _render_bulk_operations(server_url: str) -> None:
|
|
372
|
+
"""Render bulk assessment and conversion operations."""
|
|
373
|
+
st.markdown("---")
|
|
374
|
+
st.subheader("Bulk Operations")
|
|
375
|
+
|
|
376
|
+
st.markdown("""
|
|
377
|
+
Assess or convert **all cookbooks** from your Chef Server.
|
|
378
|
+
Results are automatically saved to persistent storage.
|
|
379
|
+
""")
|
|
380
|
+
|
|
381
|
+
col1, col2 = st.columns(2)
|
|
382
|
+
|
|
383
|
+
with col1:
|
|
384
|
+
if st.button("Assess ALL Cookbooks", type="primary", use_container_width=True):
|
|
385
|
+
_run_bulk_assessment(server_url)
|
|
386
|
+
|
|
387
|
+
with col2:
|
|
388
|
+
if st.button(
|
|
389
|
+
"Convert ALL Cookbooks",
|
|
390
|
+
type="secondary",
|
|
391
|
+
use_container_width=True,
|
|
392
|
+
):
|
|
393
|
+
_run_bulk_conversion(server_url)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _assess_single_cookbook(
|
|
397
|
+
storage,
|
|
398
|
+
server_url: str,
|
|
399
|
+
temp_path: Path,
|
|
400
|
+
cookbook: dict,
|
|
401
|
+
ai_provider: str,
|
|
402
|
+
ai_api_key: str,
|
|
403
|
+
ai_model: str,
|
|
404
|
+
) -> bool:
|
|
405
|
+
"""
|
|
406
|
+
Assess a single cookbook and save results.
|
|
407
|
+
|
|
408
|
+
Returns True if successful, False otherwise.
|
|
409
|
+
"""
|
|
410
|
+
cookbook_name = cookbook["name"]
|
|
411
|
+
latest_version = cookbook["versions"][0] if cookbook["versions"] else "0.0.0"
|
|
412
|
+
|
|
413
|
+
# Download cookbook
|
|
414
|
+
cookbook_dir = _download_cookbook(
|
|
415
|
+
server_url, cookbook_name, latest_version, temp_path
|
|
416
|
+
)
|
|
417
|
+
if not cookbook_dir:
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
# Check cache first
|
|
421
|
+
cached = storage.get_cached_analysis(
|
|
422
|
+
str(cookbook_dir), ai_provider=ai_provider, ai_model=ai_model
|
|
423
|
+
)
|
|
424
|
+
if cached:
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
# Run assessment
|
|
428
|
+
if ai_api_key:
|
|
429
|
+
assessment = assess_single_cookbook_with_ai(
|
|
430
|
+
str(cookbook_dir),
|
|
431
|
+
ai_provider=ai_provider.lower().replace(" ", "_"),
|
|
432
|
+
api_key=ai_api_key,
|
|
433
|
+
model=ai_model,
|
|
434
|
+
)
|
|
435
|
+
else:
|
|
436
|
+
# Rule-based assessment fallback
|
|
437
|
+
from souschef.assessment import parse_chef_migration_assessment
|
|
438
|
+
|
|
439
|
+
assessment = parse_chef_migration_assessment(str(cookbook_dir))
|
|
440
|
+
|
|
441
|
+
# Save to storage
|
|
442
|
+
storage.save_analysis(
|
|
443
|
+
cookbook_name=cookbook_name,
|
|
444
|
+
cookbook_path=str(cookbook_dir),
|
|
445
|
+
cookbook_version=latest_version,
|
|
446
|
+
complexity=assessment.get("complexity", "Medium"),
|
|
447
|
+
estimated_hours=assessment.get("estimated_hours", 0.0),
|
|
448
|
+
estimated_hours_with_souschef=(assessment.get("estimated_hours", 0.0) * 0.5),
|
|
449
|
+
recommendations=assessment.get("recommendations", ""),
|
|
450
|
+
analysis_data=assessment,
|
|
451
|
+
ai_provider=ai_provider if ai_api_key else None,
|
|
452
|
+
ai_model=ai_model if ai_api_key else None,
|
|
453
|
+
)
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _confirm_bulk_operation(estimated_time: float, operation: str) -> bool:
|
|
458
|
+
"""Show confirmation dialog if operation takes > 1 minute."""
|
|
459
|
+
if estimated_time <= 60:
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
confirm = st.checkbox(
|
|
463
|
+
f"⚠️ This will take approximately "
|
|
464
|
+
f"{_format_time_estimate(estimated_time)}. Continue?",
|
|
465
|
+
key=f"confirm_{operation}_all",
|
|
466
|
+
)
|
|
467
|
+
return confirm
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _run_bulk_assessment(server_url: str) -> None:
|
|
471
|
+
"""Run bulk assessment on all Chef Server cookbooks."""
|
|
472
|
+
with st.spinner("Fetching cookbooks from Chef Server..."):
|
|
473
|
+
cookbooks = _get_chef_cookbooks(server_url)
|
|
474
|
+
|
|
475
|
+
if not cookbooks:
|
|
476
|
+
st.error("❌ No cookbooks found or unable to connect to Chef Server")
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
num_cookbooks = len(cookbooks)
|
|
480
|
+
estimated_time = _estimate_operation_time(num_cookbooks, "assess")
|
|
481
|
+
|
|
482
|
+
# Show estimate and confirmation
|
|
483
|
+
st.info(f"📊 Found {num_cookbooks} cookbook{'s' if num_cookbooks != 1 else ''}")
|
|
484
|
+
st.warning(f"⏱️ Estimated time: {_format_time_estimate(estimated_time)}")
|
|
485
|
+
|
|
486
|
+
if not _confirm_bulk_operation(estimated_time, "assess"):
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Run assessment with progress bar
|
|
490
|
+
progress_bar = st.progress(0.0, text="Starting assessment...")
|
|
491
|
+
status_text = st.empty()
|
|
492
|
+
results_container = st.container()
|
|
493
|
+
|
|
494
|
+
storage = get_storage_manager()
|
|
495
|
+
successful = 0
|
|
496
|
+
failed = 0
|
|
497
|
+
|
|
498
|
+
# Get AI config
|
|
499
|
+
ai_provider = os.environ.get("SOUSCHEF_AI_PROVIDER", "anthropic")
|
|
500
|
+
ai_api_key = os.environ.get("SOUSCHEF_AI_API_KEY", "")
|
|
501
|
+
ai_model = os.environ.get("SOUSCHEF_AI_MODEL", "claude-3-5-sonnet-20241022")
|
|
502
|
+
|
|
503
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
504
|
+
temp_path = Path(temp_dir)
|
|
505
|
+
|
|
506
|
+
for idx, cookbook in enumerate(cookbooks, 1):
|
|
507
|
+
cookbook_name = cookbook["name"]
|
|
508
|
+
latest_version = (
|
|
509
|
+
cookbook["versions"][0] if cookbook["versions"] else "0.0.0"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Update progress
|
|
513
|
+
progress = idx / num_cookbooks
|
|
514
|
+
progress_bar.progress(
|
|
515
|
+
progress,
|
|
516
|
+
text=f"Assessing {cookbook_name} ({idx}/{num_cookbooks})...",
|
|
517
|
+
)
|
|
518
|
+
status_text.text(f"📝 Processing: {cookbook_name} v{latest_version}")
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
success = _assess_single_cookbook(
|
|
522
|
+
storage,
|
|
523
|
+
server_url,
|
|
524
|
+
temp_path,
|
|
525
|
+
cookbook,
|
|
526
|
+
ai_provider,
|
|
527
|
+
ai_api_key,
|
|
528
|
+
ai_model,
|
|
529
|
+
)
|
|
530
|
+
if success:
|
|
531
|
+
successful += 1
|
|
532
|
+
else:
|
|
533
|
+
failed += 1
|
|
534
|
+
except Exception as e:
|
|
535
|
+
status_text.text(f"❌ Failed: {cookbook_name} - {str(e)}")
|
|
536
|
+
failed += 1
|
|
537
|
+
time.sleep(0.5) # Brief pause to show error
|
|
538
|
+
|
|
539
|
+
progress_bar.progress(1.0, text="Assessment complete!")
|
|
540
|
+
|
|
541
|
+
# Show results
|
|
542
|
+
with results_container:
|
|
543
|
+
st.success("✅ Assessment complete!")
|
|
544
|
+
col1, col2, col3 = st.columns(3)
|
|
545
|
+
col1.metric("Total", num_cookbooks)
|
|
546
|
+
col2.metric("Successful", successful)
|
|
547
|
+
col3.metric("Failed", failed)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _run_bulk_conversion(server_url: str) -> None:
|
|
551
|
+
"""Run bulk conversion on all Chef Server cookbooks."""
|
|
552
|
+
with st.spinner("Fetching cookbooks from Chef Server..."):
|
|
553
|
+
cookbooks = _get_chef_cookbooks(server_url)
|
|
554
|
+
|
|
555
|
+
if not cookbooks:
|
|
556
|
+
st.error("❌ No cookbooks found or unable to connect to Chef Server")
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
num_cookbooks = len(cookbooks)
|
|
560
|
+
estimated_time = _estimate_operation_time(num_cookbooks, "convert")
|
|
561
|
+
|
|
562
|
+
# Show estimate and confirmation
|
|
563
|
+
st.info(f"📊 Found {num_cookbooks} cookbook{'s' if num_cookbooks != 1 else ''}")
|
|
564
|
+
st.warning(f"⏱️ Estimated time: {_format_time_estimate(estimated_time)}")
|
|
565
|
+
|
|
566
|
+
if estimated_time > 60: # More than 1 minute
|
|
567
|
+
confirm = st.checkbox(
|
|
568
|
+
f"⚠️ This will take approximately "
|
|
569
|
+
f"{_format_time_estimate(estimated_time)}. Continue?",
|
|
570
|
+
key="confirm_convert_all",
|
|
571
|
+
)
|
|
572
|
+
if not confirm:
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# Get output directory
|
|
576
|
+
output_dir = st.text_input(
|
|
577
|
+
"Output Directory",
|
|
578
|
+
value="./ansible_output",
|
|
579
|
+
help="Directory where converted Ansible playbooks will be saved",
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if not st.button("Start Conversion", type="primary"):
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
# Run conversion with progress bar
|
|
586
|
+
progress_bar = st.progress(0.0, text="Starting conversion...")
|
|
587
|
+
status_text = st.empty()
|
|
588
|
+
results_container = st.container()
|
|
589
|
+
|
|
590
|
+
storage = get_storage_manager()
|
|
591
|
+
successful = 0
|
|
592
|
+
failed = 0
|
|
593
|
+
|
|
594
|
+
output_path = Path(output_dir)
|
|
595
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
596
|
+
|
|
597
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
598
|
+
temp_path = Path(temp_dir)
|
|
599
|
+
|
|
600
|
+
for idx, cookbook in enumerate(cookbooks, 1):
|
|
601
|
+
cookbook_name = cookbook["name"]
|
|
602
|
+
latest_version = (
|
|
603
|
+
cookbook["versions"][0] if cookbook["versions"] else "0.0.0"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Update progress
|
|
607
|
+
progress = idx / num_cookbooks
|
|
608
|
+
progress_bar.progress(
|
|
609
|
+
progress, text=f"Converting {cookbook_name} ({idx}/{num_cookbooks})..."
|
|
610
|
+
)
|
|
611
|
+
status_text.text(f"🔄 Processing: {cookbook_name} v{latest_version}")
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
# Download cookbook
|
|
615
|
+
cookbook_dir = _download_cookbook(
|
|
616
|
+
server_url, cookbook_name, latest_version, temp_path
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
if not cookbook_dir:
|
|
620
|
+
failed += 1
|
|
621
|
+
continue
|
|
622
|
+
|
|
623
|
+
# Convert cookbook (simplified)
|
|
624
|
+
|
|
625
|
+
# Mock conversion for now - real implementation would
|
|
626
|
+
# convert all recipes
|
|
627
|
+
cookbook_output_dir = output_path / cookbook_name
|
|
628
|
+
cookbook_output_dir.mkdir(parents=True, exist_ok=True)
|
|
629
|
+
|
|
630
|
+
# Save conversion result
|
|
631
|
+
storage.save_conversion(
|
|
632
|
+
cookbook_name=cookbook_name,
|
|
633
|
+
output_type="playbook",
|
|
634
|
+
status="success",
|
|
635
|
+
files_generated=1,
|
|
636
|
+
conversion_data={"output_dir": str(cookbook_output_dir)},
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
successful += 1
|
|
640
|
+
|
|
641
|
+
except Exception as e:
|
|
642
|
+
status_text.text(f"❌ Failed: {cookbook_name} - {str(e)}")
|
|
643
|
+
storage.save_conversion(
|
|
644
|
+
cookbook_name=cookbook_name,
|
|
645
|
+
output_type="playbook",
|
|
646
|
+
status="failed",
|
|
647
|
+
files_generated=0,
|
|
648
|
+
conversion_data={"error": str(e)},
|
|
649
|
+
)
|
|
650
|
+
failed += 1
|
|
651
|
+
time.sleep(0.5)
|
|
652
|
+
|
|
653
|
+
progress_bar.progress(1.0, text="Conversion complete!")
|
|
654
|
+
|
|
655
|
+
# Show results
|
|
656
|
+
with results_container:
|
|
657
|
+
st.success("✅ Conversion complete!")
|
|
658
|
+
col1, col2, col3 = st.columns(3)
|
|
659
|
+
col1.metric("Total", num_cookbooks)
|
|
660
|
+
col2.metric("Successful", successful)
|
|
661
|
+
col3.metric("Failed", failed)
|
|
662
|
+
|
|
663
|
+
if successful > 0:
|
|
664
|
+
st.info(f"📁 Output saved to: {output_path.absolute()}")
|
|
665
|
+
|
|
666
|
+
|
|
262
667
|
def show_chef_server_settings_page() -> None:
|
|
263
668
|
"""Display Chef Server settings and configuration page."""
|
|
264
669
|
st.title("🔧 Chef Server Settings")
|
|
@@ -283,6 +688,10 @@ def show_chef_server_settings_page() -> None:
|
|
|
283
688
|
# Save settings
|
|
284
689
|
_render_save_settings_section(server_url, node_name)
|
|
285
690
|
|
|
691
|
+
# Bulk operations (only show if server is configured)
|
|
692
|
+
if server_url:
|
|
693
|
+
_render_bulk_operations(server_url)
|
|
694
|
+
|
|
286
695
|
# Usage examples
|
|
287
696
|
_render_usage_examples()
|
|
288
697
|
|