mcp-souschef 2.0.1__py3-none-any.whl → 2.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.0.1.dist-info → mcp_souschef-2.2.0.dist-info}/METADATA +453 -77
- mcp_souschef-2.2.0.dist-info/RECORD +31 -0
- souschef/__init__.py +17 -0
- souschef/assessment.py +1498 -0
- souschef/cli.py +90 -0
- souschef/converters/__init__.py +23 -0
- souschef/converters/habitat.py +674 -0
- souschef/converters/playbook.py +1736 -0
- souschef/converters/resource.py +325 -0
- souschef/core/__init__.py +80 -0
- souschef/core/constants.py +145 -0
- souschef/core/errors.py +275 -0
- souschef/core/path_utils.py +58 -0
- souschef/core/ruby_utils.py +39 -0
- souschef/core/validation.py +555 -0
- souschef/deployment.py +1906 -0
- souschef/filesystem/__init__.py +5 -0
- souschef/filesystem/operations.py +67 -0
- souschef/parsers/__init__.py +36 -0
- souschef/parsers/attributes.py +257 -0
- souschef/parsers/habitat.py +317 -0
- souschef/parsers/inspec.py +809 -0
- souschef/parsers/metadata.py +211 -0
- souschef/parsers/recipe.py +200 -0
- souschef/parsers/resource.py +170 -0
- souschef/parsers/template.py +342 -0
- souschef/profiling.py +568 -0
- souschef/server.py +1854 -7481
- mcp_souschef-2.0.1.dist-info/RECORD +0 -8
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.2.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.0.1.dist-info → mcp_souschef-2.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Chef resource to Ansible task converter."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from souschef.core.constants import ACTION_TO_STATE, RESOURCE_MAPPINGS
|
|
9
|
+
|
|
10
|
+
# Type alias for parameter builder functions
|
|
11
|
+
ParamBuilder = Callable[[str, str, dict[str, Any]], dict[str, Any]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_properties(properties_str: str) -> dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Parse properties string into a dictionary.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
properties_str: String representation of properties dict.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dictionary of properties.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
if not properties_str:
|
|
26
|
+
return {}
|
|
27
|
+
try:
|
|
28
|
+
# Try ast.literal_eval first for safety
|
|
29
|
+
result = ast.literal_eval(properties_str)
|
|
30
|
+
if isinstance(result, dict):
|
|
31
|
+
return result
|
|
32
|
+
return {}
|
|
33
|
+
except (ValueError, SyntaxError):
|
|
34
|
+
# Fallback to eval if needed, but this is less safe
|
|
35
|
+
try:
|
|
36
|
+
result = eval(properties_str) # noqa: S307
|
|
37
|
+
if isinstance(result, dict):
|
|
38
|
+
return result
|
|
39
|
+
return {}
|
|
40
|
+
except Exception:
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def convert_resource_to_task(
|
|
45
|
+
resource_type: str, resource_name: str, action: str = "create", properties: str = ""
|
|
46
|
+
) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Convert a Chef resource to an Ansible task.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
resource_type: The Chef resource type (e.g., 'package', 'service').
|
|
52
|
+
resource_name: The name of the resource.
|
|
53
|
+
action: The Chef action (e.g., 'install', 'start', 'create').
|
|
54
|
+
Defaults to 'create'.
|
|
55
|
+
properties: Additional resource properties as a string representation.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
YAML representation of the equivalent Ansible task.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
task = _convert_chef_resource_to_ansible(
|
|
63
|
+
resource_type, resource_name, action, properties
|
|
64
|
+
)
|
|
65
|
+
return _format_ansible_task(task)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return f"An error occurred during conversion: {e}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_service_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Get Ansible service module parameters.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
resource_name: Service name.
|
|
76
|
+
action: Chef action.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary of Ansible service parameters.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
params: dict[str, Any] = {"name": resource_name}
|
|
83
|
+
if action in ["enable", "start"]:
|
|
84
|
+
params["enabled"] = True
|
|
85
|
+
params["state"] = "started"
|
|
86
|
+
elif action in ["disable", "stop"]:
|
|
87
|
+
params["enabled"] = False
|
|
88
|
+
params["state"] = "stopped"
|
|
89
|
+
else:
|
|
90
|
+
params["state"] = ACTION_TO_STATE.get(action, action)
|
|
91
|
+
return params
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_template_file_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
95
|
+
"""Get parameters for template resources."""
|
|
96
|
+
params = {
|
|
97
|
+
"src": resource_name,
|
|
98
|
+
"dest": resource_name.replace(".erb", ""),
|
|
99
|
+
}
|
|
100
|
+
if action == "create":
|
|
101
|
+
params["mode"] = "0644"
|
|
102
|
+
return params
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_regular_file_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
106
|
+
"""Get parameters for regular file resources."""
|
|
107
|
+
params: dict[str, Any] = {"path": resource_name}
|
|
108
|
+
if action == "create":
|
|
109
|
+
params["state"] = "file"
|
|
110
|
+
params["mode"] = "0644"
|
|
111
|
+
else:
|
|
112
|
+
params["state"] = ACTION_TO_STATE.get(action, action)
|
|
113
|
+
return params
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _get_directory_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
117
|
+
"""Get parameters for directory resources."""
|
|
118
|
+
params: dict[str, Any] = {
|
|
119
|
+
"path": resource_name,
|
|
120
|
+
"state": "directory",
|
|
121
|
+
}
|
|
122
|
+
if action == "create":
|
|
123
|
+
params["mode"] = "0755"
|
|
124
|
+
return params
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _get_file_params(
|
|
128
|
+
resource_name: str, action: str, resource_type: str
|
|
129
|
+
) -> dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Get Ansible file module parameters.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
resource_name: File/directory path.
|
|
135
|
+
action: Chef action.
|
|
136
|
+
resource_type: Type of file resource (file/directory/template).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dictionary of Ansible file parameters.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
if resource_type == "template":
|
|
143
|
+
return _get_template_file_params(resource_name, action)
|
|
144
|
+
elif resource_type == "file":
|
|
145
|
+
return _get_regular_file_params(resource_name, action)
|
|
146
|
+
elif resource_type == "directory":
|
|
147
|
+
return _get_directory_params(resource_name, action)
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_package_params(
|
|
152
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""Build parameters for package resources."""
|
|
155
|
+
return {"name": resource_name, "state": ACTION_TO_STATE.get(action, action)}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_execute_params(
|
|
159
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""Build parameters for execute/bash resources."""
|
|
162
|
+
return {"cmd": resource_name}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _get_user_group_params(
|
|
166
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Build parameters for user/group resources."""
|
|
169
|
+
return {"name": resource_name, "state": ACTION_TO_STATE.get(action, "present")}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_remote_file_params(
|
|
173
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Build parameters for remote_file resources."""
|
|
176
|
+
params = {"dest": resource_name}
|
|
177
|
+
# Map Chef properties to Ansible parameters
|
|
178
|
+
prop_mappings = {
|
|
179
|
+
"source": "url",
|
|
180
|
+
"mode": "mode",
|
|
181
|
+
"owner": "owner",
|
|
182
|
+
"group": "group",
|
|
183
|
+
"checksum": "checksum",
|
|
184
|
+
}
|
|
185
|
+
for chef_prop, ansible_param in prop_mappings.items():
|
|
186
|
+
if chef_prop in props:
|
|
187
|
+
params[ansible_param] = props[chef_prop]
|
|
188
|
+
return params
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get_default_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
192
|
+
"""Build default parameters for unknown resource types."""
|
|
193
|
+
params = {"name": resource_name}
|
|
194
|
+
if action in ACTION_TO_STATE:
|
|
195
|
+
params["state"] = ACTION_TO_STATE[action]
|
|
196
|
+
return params
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Resource type to parameter builder mappings
|
|
200
|
+
RESOURCE_PARAM_BUILDERS: dict[str, ParamBuilder | str] = {
|
|
201
|
+
"package": _get_package_params,
|
|
202
|
+
"service": "service", # Uses _get_service_params
|
|
203
|
+
"systemd_unit": "service",
|
|
204
|
+
"template": "file", # Uses _get_file_params
|
|
205
|
+
"file": "file",
|
|
206
|
+
"directory": "file",
|
|
207
|
+
"execute": _get_execute_params,
|
|
208
|
+
"bash": _get_execute_params,
|
|
209
|
+
"user": _get_user_group_params,
|
|
210
|
+
"group": _get_user_group_params,
|
|
211
|
+
"remote_file": _get_remote_file_params,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _convert_chef_resource_to_ansible(
|
|
216
|
+
resource_type: str, resource_name: str, action: str, properties: str
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Convert Chef resource to Ansible task dictionary using data-driven approach.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
resource_type: The Chef resource type.
|
|
223
|
+
resource_name: The name of the resource.
|
|
224
|
+
action: The Chef action.
|
|
225
|
+
properties: Additional properties string.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dictionary representing an Ansible task.
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
# Get Ansible module name
|
|
232
|
+
ansible_module = RESOURCE_MAPPINGS.get(resource_type, f"# Unknown: {resource_type}")
|
|
233
|
+
|
|
234
|
+
# Start building the task
|
|
235
|
+
task: dict[str, Any] = {
|
|
236
|
+
"name": f"{action.capitalize()} {resource_type} {resource_name}",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Parse properties
|
|
240
|
+
props = _parse_properties(properties)
|
|
241
|
+
|
|
242
|
+
# Build module parameters using appropriate builder
|
|
243
|
+
module_params = _build_module_params(resource_type, resource_name, action, props)
|
|
244
|
+
|
|
245
|
+
# Add special task-level flags for execute/bash resources
|
|
246
|
+
if resource_type in ["execute", "bash"]:
|
|
247
|
+
task["changed_when"] = "false"
|
|
248
|
+
|
|
249
|
+
task[ansible_module] = module_params
|
|
250
|
+
return task
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _build_module_params(
|
|
254
|
+
resource_type: str, resource_name: str, action: str, props: dict[str, Any]
|
|
255
|
+
) -> dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Build Ansible module parameters based on resource type.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
resource_type: The Chef resource type.
|
|
261
|
+
resource_name: The resource name.
|
|
262
|
+
action: The Chef action.
|
|
263
|
+
props: Parsed properties dictionary.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary of Ansible module parameters.
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
# Look up the parameter builder for this resource type
|
|
270
|
+
builder = RESOURCE_PARAM_BUILDERS.get(resource_type)
|
|
271
|
+
|
|
272
|
+
if builder is None:
|
|
273
|
+
# Unknown resource type - use default builder
|
|
274
|
+
return _get_default_params(resource_name, action)
|
|
275
|
+
|
|
276
|
+
if isinstance(builder, str):
|
|
277
|
+
# Special handler reference (service/file)
|
|
278
|
+
if builder == "service":
|
|
279
|
+
return _get_service_params(resource_name, action)
|
|
280
|
+
elif builder == "file":
|
|
281
|
+
return _get_file_params(resource_name, action, resource_type)
|
|
282
|
+
# This shouldn't happen, but handle gracefully
|
|
283
|
+
return _get_default_params(resource_name, action)
|
|
284
|
+
|
|
285
|
+
# Call the parameter builder function
|
|
286
|
+
return builder(resource_name, action, props)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _format_yaml_value(value: Any) -> str:
|
|
290
|
+
"""Format a value for YAML output."""
|
|
291
|
+
if isinstance(value, str):
|
|
292
|
+
return f'"{value}"'
|
|
293
|
+
return json.dumps(value)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _format_dict_value(key: str, value: dict[str, Any]) -> list[str]:
|
|
297
|
+
"""Format a dictionary value for YAML output."""
|
|
298
|
+
lines = [f" {key}:"]
|
|
299
|
+
for param_key, param_value in value.items():
|
|
300
|
+
lines.append(f" {param_key}: {_format_yaml_value(param_value)}")
|
|
301
|
+
return lines
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _format_ansible_task(task: dict[str, Any]) -> str:
|
|
305
|
+
"""
|
|
306
|
+
Format an Ansible task dictionary as YAML.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
task: Dictionary representing an Ansible task.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
YAML-formatted string.
|
|
313
|
+
|
|
314
|
+
"""
|
|
315
|
+
result = ["- name: " + task["name"]]
|
|
316
|
+
|
|
317
|
+
for key, value in task.items():
|
|
318
|
+
if key == "name":
|
|
319
|
+
continue
|
|
320
|
+
if isinstance(value, dict):
|
|
321
|
+
result.extend(_format_dict_value(key, value))
|
|
322
|
+
else:
|
|
323
|
+
result.append(f" {key}: {_format_yaml_value(value)}")
|
|
324
|
+
|
|
325
|
+
return "\n".join(result)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Core utilities for SousChef."""
|
|
2
|
+
|
|
3
|
+
from souschef.core.constants import (
|
|
4
|
+
ACTION_TO_STATE,
|
|
5
|
+
ANSIBLE_SERVICE_MODULE,
|
|
6
|
+
CHEF_RECIPE_PREFIX,
|
|
7
|
+
CHEF_ROLE_PREFIX,
|
|
8
|
+
ERB_PATTERNS,
|
|
9
|
+
ERROR_FILE_NOT_FOUND,
|
|
10
|
+
ERROR_IS_DIRECTORY,
|
|
11
|
+
ERROR_PERMISSION_DENIED,
|
|
12
|
+
ERROR_PREFIX,
|
|
13
|
+
INSPEC_END_INDENT,
|
|
14
|
+
INSPEC_SHOULD_EXIST,
|
|
15
|
+
JINJA2_ELIF,
|
|
16
|
+
JINJA2_ELSE,
|
|
17
|
+
JINJA2_ENDIF,
|
|
18
|
+
JINJA2_FOR,
|
|
19
|
+
JINJA2_IF_NOT,
|
|
20
|
+
JINJA2_IF_START,
|
|
21
|
+
JINJA2_NODE_ATTR_REPLACEMENT,
|
|
22
|
+
JINJA2_VAR_REPLACEMENT,
|
|
23
|
+
METADATA_FILENAME,
|
|
24
|
+
NODE_PREFIX,
|
|
25
|
+
REGEX_ERB_CONDITION,
|
|
26
|
+
REGEX_ERB_EACH,
|
|
27
|
+
REGEX_ERB_ELSE,
|
|
28
|
+
REGEX_ERB_ELSIF,
|
|
29
|
+
REGEX_ERB_END,
|
|
30
|
+
REGEX_ERB_IF_START,
|
|
31
|
+
REGEX_ERB_NODE_ATTR,
|
|
32
|
+
REGEX_ERB_OUTPUT,
|
|
33
|
+
REGEX_ERB_UNLESS,
|
|
34
|
+
REGEX_QUOTE_DO_END,
|
|
35
|
+
REGEX_RESOURCE_BRACKET,
|
|
36
|
+
REGEX_RUBY_INTERPOLATION,
|
|
37
|
+
REGEX_WHITESPACE_QUOTE,
|
|
38
|
+
REGEX_WORD_SYMBOLS,
|
|
39
|
+
RESOURCE_MAPPINGS,
|
|
40
|
+
)
|
|
41
|
+
from souschef.core.errors import (
|
|
42
|
+
ChefFileNotFoundError,
|
|
43
|
+
ConversionError,
|
|
44
|
+
InvalidCookbookError,
|
|
45
|
+
ParseError,
|
|
46
|
+
SousChefError,
|
|
47
|
+
ValidationError,
|
|
48
|
+
format_error_with_context,
|
|
49
|
+
validate_cookbook_structure,
|
|
50
|
+
validate_directory_exists,
|
|
51
|
+
validate_file_exists,
|
|
52
|
+
)
|
|
53
|
+
from souschef.core.path_utils import _normalize_path, _safe_join
|
|
54
|
+
from souschef.core.ruby_utils import _normalize_ruby_value
|
|
55
|
+
from souschef.core.validation import (
|
|
56
|
+
ValidationCategory,
|
|
57
|
+
ValidationEngine,
|
|
58
|
+
ValidationLevel,
|
|
59
|
+
ValidationResult,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"_normalize_path",
|
|
64
|
+
"_normalize_ruby_value",
|
|
65
|
+
"_safe_join",
|
|
66
|
+
"ValidationCategory",
|
|
67
|
+
"ValidationEngine",
|
|
68
|
+
"ValidationLevel",
|
|
69
|
+
"ValidationResult",
|
|
70
|
+
"SousChefError",
|
|
71
|
+
"ChefFileNotFoundError",
|
|
72
|
+
"InvalidCookbookError",
|
|
73
|
+
"ParseError",
|
|
74
|
+
"ConversionError",
|
|
75
|
+
"ValidationError",
|
|
76
|
+
"validate_file_exists",
|
|
77
|
+
"validate_directory_exists",
|
|
78
|
+
"validate_cookbook_structure",
|
|
79
|
+
"format_error_with_context",
|
|
80
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Constants used throughout SousChef."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"ANSIBLE_SERVICE_MODULE",
|
|
5
|
+
"METADATA_FILENAME",
|
|
6
|
+
"ERROR_PREFIX",
|
|
7
|
+
"NODE_PREFIX",
|
|
8
|
+
"CHEF_RECIPE_PREFIX",
|
|
9
|
+
"CHEF_ROLE_PREFIX",
|
|
10
|
+
"REGEX_WHITESPACE_QUOTE",
|
|
11
|
+
"REGEX_QUOTE_DO_END",
|
|
12
|
+
"REGEX_RESOURCE_BRACKET",
|
|
13
|
+
"REGEX_ERB_OUTPUT",
|
|
14
|
+
"REGEX_ERB_CONDITION",
|
|
15
|
+
"REGEX_ERB_NODE_ATTR",
|
|
16
|
+
"REGEX_ERB_IF_START",
|
|
17
|
+
"REGEX_ERB_UNLESS",
|
|
18
|
+
"REGEX_ERB_ELSE",
|
|
19
|
+
"REGEX_ERB_ELSIF",
|
|
20
|
+
"REGEX_ERB_END",
|
|
21
|
+
"REGEX_ERB_EACH",
|
|
22
|
+
"REGEX_WORD_SYMBOLS",
|
|
23
|
+
"REGEX_RUBY_INTERPOLATION",
|
|
24
|
+
"JINJA2_VAR_REPLACEMENT",
|
|
25
|
+
"JINJA2_NODE_ATTR_REPLACEMENT",
|
|
26
|
+
"JINJA2_IF_START",
|
|
27
|
+
"JINJA2_IF_NOT",
|
|
28
|
+
"JINJA2_ELSE",
|
|
29
|
+
"JINJA2_ELIF",
|
|
30
|
+
"JINJA2_ENDIF",
|
|
31
|
+
"JINJA2_FOR",
|
|
32
|
+
"ERB_PATTERNS",
|
|
33
|
+
"INSPEC_END_INDENT",
|
|
34
|
+
"INSPEC_SHOULD_EXIST",
|
|
35
|
+
"ERROR_FILE_NOT_FOUND",
|
|
36
|
+
"ERROR_IS_DIRECTORY",
|
|
37
|
+
"ERROR_PERMISSION_DENIED",
|
|
38
|
+
"RESOURCE_MAPPINGS",
|
|
39
|
+
"ACTION_TO_STATE",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Ansible module names
|
|
43
|
+
ANSIBLE_SERVICE_MODULE = "ansible.builtin.service"
|
|
44
|
+
|
|
45
|
+
# File names
|
|
46
|
+
METADATA_FILENAME = "metadata.rb"
|
|
47
|
+
|
|
48
|
+
# Common prefixes
|
|
49
|
+
ERROR_PREFIX = "Error:"
|
|
50
|
+
NODE_PREFIX = "node["
|
|
51
|
+
CHEF_RECIPE_PREFIX = "recipe["
|
|
52
|
+
CHEF_ROLE_PREFIX = "role["
|
|
53
|
+
|
|
54
|
+
# Regular expression patterns for Ruby/ERB parsing
|
|
55
|
+
REGEX_WHITESPACE_QUOTE = r"\s+['\"]?"
|
|
56
|
+
REGEX_QUOTE_DO_END = r"['\"]?\s+do\s*([^\n]{0,15000})\nend"
|
|
57
|
+
REGEX_RESOURCE_BRACKET = r"(\w+)\[([^\]]+)\]"
|
|
58
|
+
REGEX_ERB_OUTPUT = r"<%=\s*([^%]{1,200}?)\s*%>"
|
|
59
|
+
REGEX_ERB_CONDITION = r"[^%]{1,200}?"
|
|
60
|
+
REGEX_ERB_NODE_ATTR = rf"<%=\s*node\[(['\"])({REGEX_ERB_CONDITION})\1\]\s*%>"
|
|
61
|
+
REGEX_ERB_IF_START = rf"<%\s*if\s+({REGEX_ERB_CONDITION})\s*%>"
|
|
62
|
+
REGEX_ERB_UNLESS = rf"<%\s*unless\s+({REGEX_ERB_CONDITION})\s*%>"
|
|
63
|
+
REGEX_ERB_ELSE = r"<%\s*else\s*%>"
|
|
64
|
+
REGEX_ERB_ELSIF = rf"<%\s*elsif\s+({REGEX_ERB_CONDITION})\s*%>"
|
|
65
|
+
REGEX_ERB_END = r"<%\s*end\s*%>"
|
|
66
|
+
REGEX_ERB_EACH = rf"<%\s*({REGEX_ERB_CONDITION})\.each\s+do\s+\|(\w+)\|\s*%>"
|
|
67
|
+
REGEX_WORD_SYMBOLS = r"[\w.\[\]'\"]+"
|
|
68
|
+
REGEX_RUBY_INTERPOLATION = r"#\{([^}]+)\}"
|
|
69
|
+
|
|
70
|
+
# Jinja2 template replacements
|
|
71
|
+
JINJA2_VAR_REPLACEMENT = r"{{ \1 }}"
|
|
72
|
+
JINJA2_NODE_ATTR_REPLACEMENT = r"{{ \2 }}"
|
|
73
|
+
JINJA2_IF_START = r"{% if \1 %}"
|
|
74
|
+
JINJA2_IF_NOT = r"{% if not \1 %}"
|
|
75
|
+
JINJA2_ELSE = r"{% else %}"
|
|
76
|
+
JINJA2_ELIF = r"{% elif \1 %}"
|
|
77
|
+
JINJA2_ENDIF = r"{% endif %}"
|
|
78
|
+
JINJA2_FOR = r"{% for \2 in \1 %}"
|
|
79
|
+
|
|
80
|
+
# ERB to Jinja2 pattern mappings
|
|
81
|
+
ERB_PATTERNS = {
|
|
82
|
+
# Variable output: <%= var %> -> {{ var }}
|
|
83
|
+
"output": (REGEX_ERB_OUTPUT, JINJA2_VAR_REPLACEMENT),
|
|
84
|
+
# Variable with node prefix: <%= node['attr'] %> -> {{ attr }}
|
|
85
|
+
"node_attr": (REGEX_ERB_NODE_ATTR, JINJA2_NODE_ATTR_REPLACEMENT),
|
|
86
|
+
# If statements: <% if condition %> -> {% if condition %}
|
|
87
|
+
"if_start": (REGEX_ERB_IF_START, JINJA2_IF_START),
|
|
88
|
+
# Unless (negated if): <% unless condition %> -> {% if not condition %}
|
|
89
|
+
"unless": (REGEX_ERB_UNLESS, JINJA2_IF_NOT),
|
|
90
|
+
# Else: <% else %> -> {% else %}
|
|
91
|
+
"else": (REGEX_ERB_ELSE, JINJA2_ELSE),
|
|
92
|
+
# Elsif: <% elsif condition %> -> {% elif condition %}
|
|
93
|
+
"elsif": (REGEX_ERB_ELSIF, JINJA2_ELIF),
|
|
94
|
+
# End: <% end %> -> {% endif %}
|
|
95
|
+
"end": (REGEX_ERB_END, JINJA2_ENDIF),
|
|
96
|
+
# Each loop: <% array.each do |item| %> -> {% for item in array %}
|
|
97
|
+
# Use [^%]{1,200} to prevent matching across %> boundaries and ReDoS
|
|
98
|
+
"each": (REGEX_ERB_EACH, JINJA2_FOR),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# InSpec output formatting
|
|
102
|
+
INSPEC_END_INDENT = " end"
|
|
103
|
+
INSPEC_SHOULD_EXIST = " it { should exist }"
|
|
104
|
+
|
|
105
|
+
# Error message templates
|
|
106
|
+
ERROR_FILE_NOT_FOUND = "Error: File not found at {path}"
|
|
107
|
+
ERROR_IS_DIRECTORY = "Error: {path} is a directory, not a file"
|
|
108
|
+
ERROR_PERMISSION_DENIED = "Error: Permission denied for {path}"
|
|
109
|
+
|
|
110
|
+
# Chef resource to Ansible module mappings
|
|
111
|
+
RESOURCE_MAPPINGS = {
|
|
112
|
+
"package": "ansible.builtin.package",
|
|
113
|
+
"apt_package": "ansible.builtin.apt",
|
|
114
|
+
"yum_package": "ansible.builtin.yum",
|
|
115
|
+
"service": ANSIBLE_SERVICE_MODULE,
|
|
116
|
+
"systemd_unit": "ansible.builtin.systemd",
|
|
117
|
+
"template": "ansible.builtin.template",
|
|
118
|
+
"file": "ansible.builtin.file",
|
|
119
|
+
"directory": "ansible.builtin.file",
|
|
120
|
+
"execute": "ansible.builtin.command",
|
|
121
|
+
"bash": "ansible.builtin.shell",
|
|
122
|
+
"script": "ansible.builtin.script",
|
|
123
|
+
"user": "ansible.builtin.user",
|
|
124
|
+
"group": "ansible.builtin.group",
|
|
125
|
+
"cron": "ansible.builtin.cron",
|
|
126
|
+
"mount": "ansible.builtin.mount",
|
|
127
|
+
"git": "ansible.builtin.git",
|
|
128
|
+
"remote_file": "ansible.builtin.get_url",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Chef action to Ansible state mappings
|
|
132
|
+
ACTION_TO_STATE = {
|
|
133
|
+
"create": "present",
|
|
134
|
+
"delete": "absent",
|
|
135
|
+
"remove": "absent",
|
|
136
|
+
"install": "present",
|
|
137
|
+
"upgrade": "latest",
|
|
138
|
+
"purge": "absent",
|
|
139
|
+
"enable": "started",
|
|
140
|
+
"disable": "stopped",
|
|
141
|
+
"start": "started",
|
|
142
|
+
"stop": "stopped",
|
|
143
|
+
"restart": "restarted",
|
|
144
|
+
"reload": "reloaded",
|
|
145
|
+
}
|