mcp-souschef 2.8.0__py3-none-any.whl → 3.2.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-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/METADATA +159 -384
- mcp_souschef-3.2.0.dist-info/RECORD +47 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +31 -7
- souschef/assessment.py +1451 -105
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +3 -92
- souschef/ci/gitlab_ci.py +2 -52
- souschef/ci/jenkins_pipeline.py +2 -59
- souschef/cli.py +149 -16
- souschef/converters/playbook.py +378 -138
- souschef/converters/resource.py +12 -11
- souschef/converters/template.py +177 -0
- souschef/core/__init__.py +6 -1
- souschef/core/metrics.py +313 -0
- souschef/core/path_utils.py +233 -19
- souschef/core/validation.py +53 -0
- souschef/deployment.py +71 -12
- souschef/generators/__init__.py +13 -0
- souschef/generators/repo.py +695 -0
- souschef/parsers/attributes.py +1 -1
- souschef/parsers/habitat.py +1 -1
- souschef/parsers/inspec.py +25 -2
- souschef/parsers/metadata.py +5 -3
- souschef/parsers/recipe.py +1 -1
- souschef/parsers/resource.py +1 -1
- souschef/parsers/template.py +1 -1
- souschef/server.py +1039 -121
- souschef/ui/app.py +486 -374
- souschef/ui/pages/ai_settings.py +74 -8
- souschef/ui/pages/cookbook_analysis.py +3216 -373
- souschef/ui/pages/validation_reports.py +274 -0
- mcp_souschef-2.8.0.dist-info/RECORD +0 -42
- souschef/converters/cookbook_specific.py.backup +0 -109
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ansible repository structure generation.
|
|
3
|
+
|
|
4
|
+
This module analyses converted Chef cookbooks and generates appropriate
|
|
5
|
+
Ansible repository structures with proper organisation, configuration files,
|
|
6
|
+
and git initialisation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Constants
|
|
15
|
+
HOSTS_FILE = "hosts.yml"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RepoType(Enum):
|
|
19
|
+
"""Types of Ansible repository structures."""
|
|
20
|
+
|
|
21
|
+
INVENTORY_FIRST = "inventory_first" # Classic inventory-first (infra management)
|
|
22
|
+
PLAYBOOKS_ROLES = "playbooks_roles" # Simpler playbooks + roles
|
|
23
|
+
COLLECTION = "collection" # Ansible Collection (reusable automation)
|
|
24
|
+
MONO_REPO = "mono_repo" # Multi-project mono-repo
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def analyse_conversion_output(
|
|
28
|
+
cookbook_path: str,
|
|
29
|
+
num_recipes: int = 0,
|
|
30
|
+
num_roles: int = 0,
|
|
31
|
+
has_multiple_apps: bool = False,
|
|
32
|
+
needs_multi_env: bool = True,
|
|
33
|
+
ai_provider: str = "",
|
|
34
|
+
api_key: str = "",
|
|
35
|
+
model: str = "",
|
|
36
|
+
) -> RepoType:
|
|
37
|
+
"""
|
|
38
|
+
Analyse conversion output and determine the best repo type.
|
|
39
|
+
|
|
40
|
+
Uses AI assessment if credentials are provided to make smarter decisions
|
|
41
|
+
based on cookbook complexity, otherwise falls back to heuristic rules.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
cookbook_path: Path to the Chef cookbook
|
|
45
|
+
num_recipes: Number of recipes converted
|
|
46
|
+
num_roles: Number of roles that would be created
|
|
47
|
+
has_multiple_apps: Whether multiple applications are being managed
|
|
48
|
+
needs_multi_env: Whether multiple environments are needed
|
|
49
|
+
ai_provider: AI provider name (anthropic, openai, watson) optional
|
|
50
|
+
api_key: API key for AI provider optional
|
|
51
|
+
model: AI model to use optional
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The recommended RepoType
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
# Try AI-enhanced analysis if credentials provided
|
|
58
|
+
if ai_provider and api_key:
|
|
59
|
+
repo_type_ai = _analyse_with_ai(
|
|
60
|
+
cookbook_path, num_recipes, num_roles, ai_provider, api_key, model
|
|
61
|
+
)
|
|
62
|
+
if repo_type_ai is not None:
|
|
63
|
+
return repo_type_ai
|
|
64
|
+
|
|
65
|
+
# Fall back to heuristic-based analysis
|
|
66
|
+
return _analyse_with_heuristics(
|
|
67
|
+
num_recipes, num_roles, has_multiple_apps, needs_multi_env
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _analyse_with_heuristics(
|
|
72
|
+
num_recipes: int,
|
|
73
|
+
num_roles: int,
|
|
74
|
+
has_multiple_apps: bool,
|
|
75
|
+
needs_multi_env: bool,
|
|
76
|
+
) -> RepoType:
|
|
77
|
+
"""Apply heuristic rules for repository type selection."""
|
|
78
|
+
# Collection layout for reusable automation (3+ roles)
|
|
79
|
+
if num_roles >= 3:
|
|
80
|
+
return RepoType.COLLECTION
|
|
81
|
+
|
|
82
|
+
# Mono-repo for multiple applications
|
|
83
|
+
if has_multiple_apps:
|
|
84
|
+
return RepoType.MONO_REPO
|
|
85
|
+
|
|
86
|
+
# Simple playbooks + roles for small projects
|
|
87
|
+
if not needs_multi_env and num_recipes <= 3:
|
|
88
|
+
return RepoType.PLAYBOOKS_ROLES
|
|
89
|
+
|
|
90
|
+
# Default: inventory-first for infrastructure management
|
|
91
|
+
return RepoType.INVENTORY_FIRST
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _analyse_with_ai(
|
|
95
|
+
cookbook_path: str,
|
|
96
|
+
num_recipes: int,
|
|
97
|
+
num_roles: int,
|
|
98
|
+
ai_provider: str,
|
|
99
|
+
api_key: str,
|
|
100
|
+
model: str,
|
|
101
|
+
) -> RepoType | None:
|
|
102
|
+
"""
|
|
103
|
+
Analyse cookbook using AI to determine optimal repository type.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
cookbook_path: Path to the Chef cookbook
|
|
107
|
+
num_recipes: Number of recipes converted
|
|
108
|
+
num_roles: Number of roles estimate
|
|
109
|
+
ai_provider: AI provider name
|
|
110
|
+
api_key: API key for AI provider
|
|
111
|
+
model: AI model to use
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Recommended RepoType if AI assessment succeeds, None otherwise
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
# Import here to avoid circular dependencies
|
|
119
|
+
from souschef.assessment import assess_single_cookbook_with_ai # noqa: F401
|
|
120
|
+
|
|
121
|
+
# Perform AI assessment
|
|
122
|
+
assessment = assess_single_cookbook_with_ai(
|
|
123
|
+
cookbook_path=cookbook_path,
|
|
124
|
+
ai_provider=ai_provider,
|
|
125
|
+
api_key=api_key,
|
|
126
|
+
model=model or "claude-3-5-sonnet-20241022",
|
|
127
|
+
temperature=0.3,
|
|
128
|
+
max_tokens=2000,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Check for errors in assessment
|
|
132
|
+
if "error" in assessment:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Extract complexity score (0-100)
|
|
136
|
+
complexity_score = assessment.get("complexity_score", 0)
|
|
137
|
+
|
|
138
|
+
# AI-informed decision making
|
|
139
|
+
# High complexity + multiple roles should be a collection
|
|
140
|
+
if complexity_score > 70 and num_roles >= 2:
|
|
141
|
+
return RepoType.COLLECTION
|
|
142
|
+
|
|
143
|
+
# Medium-high complexity with multiple roles
|
|
144
|
+
if complexity_score > 50 and num_roles >= 2:
|
|
145
|
+
return RepoType.COLLECTION
|
|
146
|
+
|
|
147
|
+
# High complexity generally benefits from inventory-first
|
|
148
|
+
if complexity_score > 70:
|
|
149
|
+
return RepoType.INVENTORY_FIRST
|
|
150
|
+
|
|
151
|
+
# Low complexity recipes with minimal roles
|
|
152
|
+
if complexity_score < 30 and num_roles <= 1 and num_recipes <= 3:
|
|
153
|
+
return RepoType.PLAYBOOKS_ROLES
|
|
154
|
+
|
|
155
|
+
# Medium complexity with simple structure
|
|
156
|
+
if complexity_score < 50 and num_roles <= 1:
|
|
157
|
+
return RepoType.PLAYBOOKS_ROLES
|
|
158
|
+
|
|
159
|
+
# Default to inventory-first for AI-assessed cookbooks
|
|
160
|
+
return RepoType.INVENTORY_FIRST
|
|
161
|
+
|
|
162
|
+
except Exception:
|
|
163
|
+
# If AI fails, return None to fall back to heuristics
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _create_ansible_cfg(repo_path: Path, repo_type: RepoType) -> None:
|
|
168
|
+
"""Create ansible.cfg with appropriate settings."""
|
|
169
|
+
inventory_path = (
|
|
170
|
+
"./inventory" if repo_type == RepoType.PLAYBOOKS_ROLES else "./inventories/prod"
|
|
171
|
+
)
|
|
172
|
+
roles_path = (
|
|
173
|
+
"./roles"
|
|
174
|
+
if repo_type != RepoType.COLLECTION
|
|
175
|
+
else "./ansible_collections/*/*/roles"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
cfg_content = f"""[defaults]
|
|
179
|
+
inventory = {inventory_path}
|
|
180
|
+
roles_path = {roles_path}
|
|
181
|
+
host_key_checking = False
|
|
182
|
+
retry_files_enabled = False
|
|
183
|
+
gathering = smart
|
|
184
|
+
fact_caching = jsonfile
|
|
185
|
+
fact_caching_connection = /tmp/ansible_facts
|
|
186
|
+
fact_caching_timeout = 3600
|
|
187
|
+
callbacks_enabled = profile_tasks, timer
|
|
188
|
+
|
|
189
|
+
[privilege_escalation]
|
|
190
|
+
become = True
|
|
191
|
+
become_method = sudo
|
|
192
|
+
become_user = root
|
|
193
|
+
become_ask_pass = False
|
|
194
|
+
|
|
195
|
+
[ssh_connection]
|
|
196
|
+
pipelining = True
|
|
197
|
+
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
|
|
198
|
+
|
|
199
|
+
[diff]
|
|
200
|
+
always = False
|
|
201
|
+
context = 3
|
|
202
|
+
"""
|
|
203
|
+
(repo_path / "ansible.cfg").write_text(cfg_content)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _create_requirements_yml(repo_path: Path) -> None:
|
|
207
|
+
"""Create requirements.yml for dependencies."""
|
|
208
|
+
requirements_content = """---
|
|
209
|
+
# Ansible Collections
|
|
210
|
+
collections:
|
|
211
|
+
- name: ansible.posix
|
|
212
|
+
version: ">=1.5.0"
|
|
213
|
+
- name: community.general
|
|
214
|
+
version: ">=8.0.0"
|
|
215
|
+
|
|
216
|
+
# Ansible Roles (if using Galaxy roles)
|
|
217
|
+
roles: []
|
|
218
|
+
"""
|
|
219
|
+
(repo_path / "requirements.yml").write_text(requirements_content)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _create_gitignore(repo_path: Path) -> None:
|
|
223
|
+
"""Create .gitignore for Ansible projects."""
|
|
224
|
+
gitignore_content = """# Ansible
|
|
225
|
+
*.retry
|
|
226
|
+
.ansible/
|
|
227
|
+
/tmp/
|
|
228
|
+
/temp/
|
|
229
|
+
|
|
230
|
+
# Vault
|
|
231
|
+
vault-password.txt
|
|
232
|
+
.vault_pass*
|
|
233
|
+
|
|
234
|
+
# Python
|
|
235
|
+
__pycache__/
|
|
236
|
+
*.py[cod]
|
|
237
|
+
*$py.class
|
|
238
|
+
*.so
|
|
239
|
+
.Python
|
|
240
|
+
venv/
|
|
241
|
+
ENV/
|
|
242
|
+
|
|
243
|
+
# IDE
|
|
244
|
+
.vscode/
|
|
245
|
+
.idea/
|
|
246
|
+
*.swp
|
|
247
|
+
*.swo
|
|
248
|
+
*~
|
|
249
|
+
|
|
250
|
+
# OS
|
|
251
|
+
.DS_Store
|
|
252
|
+
Thumbs.db
|
|
253
|
+
|
|
254
|
+
# Logs
|
|
255
|
+
*.log
|
|
256
|
+
"""
|
|
257
|
+
(repo_path / ".gitignore").write_text(gitignore_content)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _create_gitattributes(repo_path: Path) -> None:
|
|
261
|
+
"""Create .gitattributes for consistent line endings and file handling."""
|
|
262
|
+
gitattributes_content = """# Auto detect text files and perform LF normalisation
|
|
263
|
+
* text=auto eol=lf
|
|
264
|
+
|
|
265
|
+
# Explicitly declare text files
|
|
266
|
+
*.py text
|
|
267
|
+
*.yml text
|
|
268
|
+
*.yaml text
|
|
269
|
+
*.ini text
|
|
270
|
+
*.cfg text
|
|
271
|
+
*.conf text
|
|
272
|
+
*.txt text
|
|
273
|
+
*.md text
|
|
274
|
+
*.rst text
|
|
275
|
+
*.j2 text
|
|
276
|
+
|
|
277
|
+
# Declare files that will always have LF line endings on checkout
|
|
278
|
+
*.sh text eol=lf
|
|
279
|
+
|
|
280
|
+
# Denote binary files
|
|
281
|
+
*.png binary
|
|
282
|
+
*.jpg binary
|
|
283
|
+
*.jpeg binary
|
|
284
|
+
*.gif binary
|
|
285
|
+
*.ico binary
|
|
286
|
+
*.zip binary
|
|
287
|
+
*.tar binary
|
|
288
|
+
*.gz binary
|
|
289
|
+
"""
|
|
290
|
+
(repo_path / ".gitattributes").write_text(gitattributes_content)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _create_editorconfig(repo_path: Path) -> None:
|
|
294
|
+
"""Create .editorconfig for consistent coding styles."""
|
|
295
|
+
editorconfig_content = """# EditorConfig for Ansible projects
|
|
296
|
+
# https://editorconfig.org
|
|
297
|
+
|
|
298
|
+
root = true
|
|
299
|
+
|
|
300
|
+
# All files
|
|
301
|
+
[*]
|
|
302
|
+
charset = utf-8
|
|
303
|
+
end_of_line = lf
|
|
304
|
+
insert_final_newline = true
|
|
305
|
+
trim_trailing_whitespace = true
|
|
306
|
+
|
|
307
|
+
# YAML files (Ansible playbooks, vars, etc.)
|
|
308
|
+
[*.{yml,yaml}]
|
|
309
|
+
indent_style = space
|
|
310
|
+
indent_size = 2
|
|
311
|
+
|
|
312
|
+
# Python files
|
|
313
|
+
[*.py]
|
|
314
|
+
indent_style = space
|
|
315
|
+
indent_size = 4
|
|
316
|
+
max_line_length = 88
|
|
317
|
+
|
|
318
|
+
# Jinja2 templates
|
|
319
|
+
[*.j2]
|
|
320
|
+
indent_style = space
|
|
321
|
+
indent_size = 2
|
|
322
|
+
|
|
323
|
+
# Shell scripts
|
|
324
|
+
[*.sh]
|
|
325
|
+
indent_style = space
|
|
326
|
+
indent_size = 2
|
|
327
|
+
|
|
328
|
+
# Markdown
|
|
329
|
+
[*.md]
|
|
330
|
+
trim_trailing_whitespace = false
|
|
331
|
+
max_line_length = off
|
|
332
|
+
|
|
333
|
+
# Makefile
|
|
334
|
+
[Makefile]
|
|
335
|
+
indent_style = tab
|
|
336
|
+
"""
|
|
337
|
+
(repo_path / ".editorconfig").write_text(editorconfig_content)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _create_readme(repo_path: Path, repo_type: RepoType, org_name: str) -> None:
|
|
341
|
+
"""Create README.md with usage instructions."""
|
|
342
|
+
type_desc = {
|
|
343
|
+
RepoType.INVENTORY_FIRST: (
|
|
344
|
+
"inventory-first structure for infrastructure management"
|
|
345
|
+
),
|
|
346
|
+
RepoType.PLAYBOOKS_ROLES: "simple playbooks and roles structure",
|
|
347
|
+
RepoType.COLLECTION: "Ansible Collection for reusable automation",
|
|
348
|
+
RepoType.MONO_REPO: "mono-repository for multiple projects",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
readme_content = f"""# {org_name} Ansible Automation
|
|
352
|
+
|
|
353
|
+
This repository uses a {type_desc.get(repo_type, "standard")} approach.
|
|
354
|
+
|
|
355
|
+
## Structure
|
|
356
|
+
|
|
357
|
+
Generated from Chef cookbook conversion using SousChef.
|
|
358
|
+
|
|
359
|
+
## Quick Start
|
|
360
|
+
|
|
361
|
+
1. Install dependencies:
|
|
362
|
+
```bash
|
|
363
|
+
ansible-galaxy install -r requirements.yml
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
2. Configure inventory:
|
|
367
|
+
- Update inventory files with your hosts
|
|
368
|
+
- Set environment-specific variables in group_vars/host_vars
|
|
369
|
+
|
|
370
|
+
3. Run playbooks:
|
|
371
|
+
```bash
|
|
372
|
+
ansible-playbook playbooks/site.yml
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Requirements
|
|
376
|
+
|
|
377
|
+
- Ansible >= 2.15
|
|
378
|
+
- Python >= 3.9
|
|
379
|
+
|
|
380
|
+
## Security
|
|
381
|
+
|
|
382
|
+
- Never commit secrets to git
|
|
383
|
+
- Use Ansible Vault for sensitive data: `ansible-vault encrypt_string`
|
|
384
|
+
- Store vault password securely (not in this repo)
|
|
385
|
+
|
|
386
|
+
## Documentation
|
|
387
|
+
|
|
388
|
+
- [Ansible Best Practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)
|
|
389
|
+
- [Ansible Vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html)
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
Generated by [SousChef](https://github.com/kpeacocke/souschef)
|
|
393
|
+
"""
|
|
394
|
+
(repo_path / "README.md").write_text(readme_content)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _create_inventory_first_structure(repo_path: Path) -> None:
|
|
398
|
+
"""Create classic inventory-first repo structure."""
|
|
399
|
+
# Top-level directories
|
|
400
|
+
(repo_path / "inventories" / "prod" / "group_vars").mkdir(parents=True)
|
|
401
|
+
(repo_path / "inventories" / "prod" / "host_vars").mkdir(parents=True)
|
|
402
|
+
(repo_path / "inventories" / "nonprod" / "group_vars").mkdir(parents=True)
|
|
403
|
+
(repo_path / "inventories" / "nonprod" / "host_vars").mkdir(parents=True)
|
|
404
|
+
(repo_path / "playbooks").mkdir()
|
|
405
|
+
(repo_path / "roles").mkdir()
|
|
406
|
+
(repo_path / "filter_plugins").mkdir()
|
|
407
|
+
(repo_path / "library").mkdir()
|
|
408
|
+
|
|
409
|
+
# Create sample inventory files
|
|
410
|
+
prod_hosts = """---
|
|
411
|
+
all:
|
|
412
|
+
children:
|
|
413
|
+
webservers:
|
|
414
|
+
hosts:
|
|
415
|
+
web1.example.com:
|
|
416
|
+
web2.example.com:
|
|
417
|
+
databases:
|
|
418
|
+
hosts:
|
|
419
|
+
db1.example.com:
|
|
420
|
+
"""
|
|
421
|
+
(repo_path / "inventories" / "prod" / HOSTS_FILE).write_text(prod_hosts)
|
|
422
|
+
(repo_path / "inventories" / "nonprod" / HOSTS_FILE).write_text(prod_hosts)
|
|
423
|
+
|
|
424
|
+
# Create sample group_vars
|
|
425
|
+
all_vars = """---
|
|
426
|
+
# Variables for all hosts
|
|
427
|
+
ansible_user: ansible
|
|
428
|
+
ansible_python_interpreter: /usr/bin/python3
|
|
429
|
+
"""
|
|
430
|
+
(repo_path / "inventories" / "prod" / "group_vars" / "all.yml").write_text(all_vars)
|
|
431
|
+
|
|
432
|
+
# Create sample playbook
|
|
433
|
+
site_playbook = """---
|
|
434
|
+
- name: Site-wide configuration
|
|
435
|
+
hosts: all
|
|
436
|
+
gather_facts: true
|
|
437
|
+
tasks:
|
|
438
|
+
- name: Ensure system is up to date
|
|
439
|
+
ansible.builtin.package:
|
|
440
|
+
name: "*"
|
|
441
|
+
state: latest
|
|
442
|
+
when: ansible_os_family == "RedHat"
|
|
443
|
+
"""
|
|
444
|
+
(repo_path / "playbooks" / "site.yml").write_text(site_playbook)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _create_playbooks_roles_structure(repo_path: Path) -> None:
|
|
448
|
+
"""Create simple playbooks + roles structure."""
|
|
449
|
+
(repo_path / "inventory" / "group_vars").mkdir(parents=True)
|
|
450
|
+
(repo_path / "inventory" / "host_vars").mkdir(parents=True)
|
|
451
|
+
(repo_path / "playbooks").mkdir()
|
|
452
|
+
(repo_path / "roles").mkdir()
|
|
453
|
+
|
|
454
|
+
# Simple inventory
|
|
455
|
+
hosts = """---
|
|
456
|
+
all:
|
|
457
|
+
hosts:
|
|
458
|
+
localhost:
|
|
459
|
+
ansible_connection: local
|
|
460
|
+
"""
|
|
461
|
+
(repo_path / "inventory" / "hosts.yml").write_text(hosts)
|
|
462
|
+
|
|
463
|
+
# Simple playbook
|
|
464
|
+
deploy_playbook = """---
|
|
465
|
+
- name: Deploy application
|
|
466
|
+
hosts: all
|
|
467
|
+
gather_facts: true
|
|
468
|
+
roles:
|
|
469
|
+
- common
|
|
470
|
+
"""
|
|
471
|
+
(repo_path / "playbooks" / "deploy.yml").write_text(deploy_playbook)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _create_collection_structure(repo_path: Path, org_name: str) -> None:
|
|
475
|
+
"""Create Ansible Collection layout."""
|
|
476
|
+
collection_name = org_name.lower().replace("-", "_")
|
|
477
|
+
base_path = repo_path / "ansible_collections" / collection_name / "platform"
|
|
478
|
+
|
|
479
|
+
(base_path / "plugins" / "modules").mkdir(parents=True)
|
|
480
|
+
(base_path / "plugins" / "filter").mkdir(parents=True)
|
|
481
|
+
(base_path / "roles").mkdir(parents=True)
|
|
482
|
+
(base_path / "playbooks").mkdir(parents=True)
|
|
483
|
+
(base_path / "docs").mkdir(parents=True)
|
|
484
|
+
(base_path / "tests").mkdir(parents=True)
|
|
485
|
+
|
|
486
|
+
# Galaxy metadata
|
|
487
|
+
galaxy_yml = f"""---
|
|
488
|
+
namespace: {collection_name}
|
|
489
|
+
name: platform
|
|
490
|
+
version: 1.0.0
|
|
491
|
+
readme: README.md
|
|
492
|
+
authors:
|
|
493
|
+
- Your Name <you@example.com>
|
|
494
|
+
description: Platform automation collection converted from Chef
|
|
495
|
+
license:
|
|
496
|
+
- MIT
|
|
497
|
+
tags:
|
|
498
|
+
- infrastructure
|
|
499
|
+
- automation
|
|
500
|
+
dependencies: {{}}
|
|
501
|
+
repository: https://github.com/{org_name}/ansible-platform
|
|
502
|
+
"""
|
|
503
|
+
(base_path / "galaxy.yml").write_text(galaxy_yml)
|
|
504
|
+
|
|
505
|
+
# Collection README
|
|
506
|
+
collection_readme = f"""# {collection_name}.platform Collection
|
|
507
|
+
|
|
508
|
+
Ansible Collection for platform automation.
|
|
509
|
+
|
|
510
|
+
## Installation
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
ansible-galaxy collection install {collection_name}.platform
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Usage
|
|
517
|
+
|
|
518
|
+
```yaml
|
|
519
|
+
- hosts: all
|
|
520
|
+
collections:
|
|
521
|
+
- {collection_name}.platform
|
|
522
|
+
tasks:
|
|
523
|
+
- import_role:
|
|
524
|
+
name: common
|
|
525
|
+
```
|
|
526
|
+
"""
|
|
527
|
+
(base_path / "README.md").write_text(collection_readme)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _create_mono_repo_structure(repo_path: Path) -> None:
|
|
531
|
+
"""Create multi-project mono-repo structure."""
|
|
532
|
+
(repo_path / "inventories" / "prod").mkdir(parents=True)
|
|
533
|
+
(repo_path / "inventories" / "nonprod").mkdir(parents=True)
|
|
534
|
+
(repo_path / "projects" / "app1" / "playbooks").mkdir(parents=True)
|
|
535
|
+
(repo_path / "projects" / "app1" / "roles").mkdir(parents=True)
|
|
536
|
+
(repo_path / "shared_roles").mkdir()
|
|
537
|
+
(repo_path / "collections").mkdir()
|
|
538
|
+
|
|
539
|
+
# Sample project structure
|
|
540
|
+
app1_playbook = """---
|
|
541
|
+
- name: Deploy App1
|
|
542
|
+
hosts: app1_servers
|
|
543
|
+
gather_facts: true
|
|
544
|
+
roles:
|
|
545
|
+
- role: shared_roles/common
|
|
546
|
+
- role: app1_config
|
|
547
|
+
"""
|
|
548
|
+
(repo_path / "projects" / "app1" / "playbooks" / "deploy.yml").write_text(
|
|
549
|
+
app1_playbook
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _init_git_repo(repo_path: Path) -> str:
|
|
554
|
+
"""Initialise git repository with souschef user configuration."""
|
|
555
|
+
try:
|
|
556
|
+
# Initialize git
|
|
557
|
+
subprocess.run(
|
|
558
|
+
["git", "init"],
|
|
559
|
+
cwd=repo_path,
|
|
560
|
+
check=True,
|
|
561
|
+
capture_output=True,
|
|
562
|
+
text=True,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Configure git user for this repository
|
|
566
|
+
subprocess.run(
|
|
567
|
+
["git", "config", "user.name", "souschef"],
|
|
568
|
+
cwd=repo_path,
|
|
569
|
+
check=True,
|
|
570
|
+
capture_output=True,
|
|
571
|
+
text=True,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
subprocess.run(
|
|
575
|
+
["git", "config", "user.email", "souschef@ansible.local"],
|
|
576
|
+
cwd=repo_path,
|
|
577
|
+
check=True,
|
|
578
|
+
capture_output=True,
|
|
579
|
+
text=True,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Create initial commit
|
|
583
|
+
subprocess.run(
|
|
584
|
+
["git", "add", "."],
|
|
585
|
+
cwd=repo_path,
|
|
586
|
+
check=True,
|
|
587
|
+
capture_output=True,
|
|
588
|
+
text=True,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
subprocess.run(
|
|
592
|
+
["git", "commit", "-m", "Initial commit: Ansible repo structure"],
|
|
593
|
+
cwd=repo_path,
|
|
594
|
+
check=True,
|
|
595
|
+
capture_output=True,
|
|
596
|
+
text=True,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
return "Git repository initialised with initial commit"
|
|
600
|
+
except subprocess.CalledProcessError as e:
|
|
601
|
+
return f"Git initialisation failed: {e.stderr}"
|
|
602
|
+
except FileNotFoundError:
|
|
603
|
+
return "Git not found - skipped repository initialisation"
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def generate_ansible_repository(
|
|
607
|
+
output_path: str,
|
|
608
|
+
repo_type: RepoType | str,
|
|
609
|
+
org_name: str = "myorg",
|
|
610
|
+
init_git: bool = True,
|
|
611
|
+
) -> dict[str, Any]:
|
|
612
|
+
"""
|
|
613
|
+
Generate a complete Ansible repository structure.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
output_path: Path where the repository should be created
|
|
617
|
+
repo_type: Type of repository structure to generate
|
|
618
|
+
org_name: Organisation name for the repository
|
|
619
|
+
init_git: Whether to initialise a git repository
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Dictionary with generation results including:
|
|
623
|
+
- success: Whether generation succeeded
|
|
624
|
+
- repo_path: Path to the created repository
|
|
625
|
+
- repo_type: Type of repository created
|
|
626
|
+
- files_created: List of files created
|
|
627
|
+
- git_status: Git initialisation status
|
|
628
|
+
|
|
629
|
+
"""
|
|
630
|
+
# Convert string to enum if needed
|
|
631
|
+
if isinstance(repo_type, str):
|
|
632
|
+
try:
|
|
633
|
+
repo_type = RepoType(repo_type)
|
|
634
|
+
except ValueError:
|
|
635
|
+
return {
|
|
636
|
+
"success": False,
|
|
637
|
+
"error": f"Invalid repo_type: {repo_type}. "
|
|
638
|
+
f"Valid types: {[t.value for t in RepoType]}",
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
repo_path = Path(output_path)
|
|
642
|
+
|
|
643
|
+
# Check if path already exists
|
|
644
|
+
if repo_path.exists():
|
|
645
|
+
return {
|
|
646
|
+
"success": False,
|
|
647
|
+
"error": f"Path already exists: {output_path}",
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
# Create base directory
|
|
652
|
+
repo_path.mkdir(parents=True)
|
|
653
|
+
|
|
654
|
+
# Create common files
|
|
655
|
+
_create_ansible_cfg(repo_path, repo_type)
|
|
656
|
+
_create_requirements_yml(repo_path)
|
|
657
|
+
_create_gitignore(repo_path)
|
|
658
|
+
_create_gitattributes(repo_path)
|
|
659
|
+
_create_editorconfig(repo_path)
|
|
660
|
+
_create_readme(repo_path, repo_type, org_name)
|
|
661
|
+
|
|
662
|
+
# Create structure based on repo type
|
|
663
|
+
if repo_type == RepoType.INVENTORY_FIRST:
|
|
664
|
+
_create_inventory_first_structure(repo_path)
|
|
665
|
+
elif repo_type == RepoType.PLAYBOOKS_ROLES:
|
|
666
|
+
_create_playbooks_roles_structure(repo_path)
|
|
667
|
+
elif repo_type == RepoType.COLLECTION:
|
|
668
|
+
_create_collection_structure(repo_path, org_name)
|
|
669
|
+
elif repo_type == RepoType.MONO_REPO:
|
|
670
|
+
_create_mono_repo_structure(repo_path)
|
|
671
|
+
|
|
672
|
+
# Collect created files
|
|
673
|
+
files_created = [
|
|
674
|
+
str(p.relative_to(repo_path)) for p in repo_path.rglob("*") if p.is_file()
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
# Initialize git if requested
|
|
678
|
+
git_status = ""
|
|
679
|
+
if init_git:
|
|
680
|
+
git_status = _init_git_repo(repo_path)
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
"success": True,
|
|
684
|
+
"repo_path": str(repo_path),
|
|
685
|
+
"repo_type": repo_type.value,
|
|
686
|
+
"files_created": sorted(files_created),
|
|
687
|
+
"git_status": git_status,
|
|
688
|
+
"message": f"Successfully created {repo_type.value} repository structure",
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
except Exception as e:
|
|
692
|
+
return {
|
|
693
|
+
"success": False,
|
|
694
|
+
"error": f"Failed to generate repository: {e}",
|
|
695
|
+
}
|
souschef/parsers/attributes.py
CHANGED
|
@@ -41,7 +41,7 @@ def parse_attributes(path: str, resolve_precedence: bool = True) -> str:
|
|
|
41
41
|
"""
|
|
42
42
|
try:
|
|
43
43
|
file_path = _normalize_path(path)
|
|
44
|
-
content = file_path.read_text(encoding="utf-8")
|
|
44
|
+
content = file_path.read_text(encoding="utf-8") # nosonar
|
|
45
45
|
|
|
46
46
|
attributes = _extract_attributes(content)
|
|
47
47
|
|
souschef/parsers/habitat.py
CHANGED
|
@@ -37,7 +37,7 @@ def parse_habitat_plan(plan_path: str) -> str:
|
|
|
37
37
|
if normalized_path.is_dir():
|
|
38
38
|
return ERROR_IS_DIRECTORY.format(path=normalized_path)
|
|
39
39
|
|
|
40
|
-
content = normalized_path.read_text(encoding="utf-8")
|
|
40
|
+
content = normalized_path.read_text(encoding="utf-8") # nosonar
|
|
41
41
|
metadata: dict[str, Any] = {
|
|
42
42
|
"package": {},
|
|
43
43
|
"dependencies": {"build": [], "runtime": []},
|