osimager 1.9.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.
Files changed (156) hide show
  1. osimager/__init__.py +2 -0
  2. osimager/cli.py +622 -0
  3. osimager/core.py +1863 -0
  4. osimager/data/ansible.cfg +12 -0
  5. osimager/data/ansible.json +20 -0
  6. osimager/data/config.yml +208 -0
  7. osimager/data/examples/example-location.json +40 -0
  8. osimager/data/examples/example-location.toml +54 -0
  9. osimager/data/examples/example-secrets +38 -0
  10. osimager/data/examples/example-vault +46 -0
  11. osimager/data/examples/quickstart-location.toml +21 -0
  12. osimager/data/files/agama/leap.json +48 -0
  13. osimager/data/files/agama/sles.json +49 -0
  14. osimager/data/files/alpine/answerfile +17 -0
  15. osimager/data/files/arch/user_configuration.json +43 -0
  16. osimager/data/files/arch/user_credentials.json +10 -0
  17. osimager/data/files/debian/debian-8-preseed.cfg +232 -0
  18. osimager/data/files/debian/debian.fix +26 -0
  19. osimager/data/files/debian/debian.seed +145 -0
  20. osimager/data/files/debian/debian_12.seed +124 -0
  21. osimager/data/files/debian/debian_pre10.seed +145 -0
  22. osimager/data/files/esxi/kickstart.cfg +9 -0
  23. osimager/data/files/esxi/kickstart_fb.cfg +19 -0
  24. osimager/data/files/esxi/kickstart_post.cfg +18 -0
  25. osimager/data/files/fedora/kickstart.cfg +29 -0
  26. osimager/data/files/fedora/kickstart_43.cfg +45 -0
  27. osimager/data/files/freebsd/installerconfig +55 -0
  28. osimager/data/files/ignition/fcos.ign +27 -0
  29. osimager/data/files/ignition/flatcar.ign +27 -0
  30. osimager/data/files/linux/banner +4 -0
  31. osimager/data/files/linux/findcd +17 -0
  32. osimager/data/files/oel/kickstart_6.cfg +25 -0
  33. osimager/data/files/oel/kickstart_post.cfg +43 -0
  34. osimager/data/files/openbsd/install.conf +19 -0
  35. osimager/data/files/photon/kickstart.json +35 -0
  36. osimager/data/files/proxmox-ve/answer.toml +14 -0
  37. osimager/data/files/rhel/install_vmwtools.sh +23 -0
  38. osimager/data/files/rhel/kickstart_10.cfg +48 -0
  39. osimager/data/files/rhel/kickstart_3.cfg +27 -0
  40. osimager/data/files/rhel/kickstart_4.cfg +28 -0
  41. osimager/data/files/rhel/kickstart_5.cfg +47 -0
  42. osimager/data/files/rhel/kickstart_6.cfg +28 -0
  43. osimager/data/files/rhel/kickstart_7.cfg +47 -0
  44. osimager/data/files/rhel/kickstart_8.cfg +49 -0
  45. osimager/data/files/rhel/kickstart_9.cfg +48 -0
  46. osimager/data/files/rhel/kickstart_end.cfg +1 -0
  47. osimager/data/files/rhel/kickstart_post.cfg +23 -0
  48. osimager/data/files/rhel/ks-part.sh +115 -0
  49. osimager/data/files/sles/autoinst_12.xml +202 -0
  50. osimager/data/files/sles/autoinst_15.xml +213 -0
  51. osimager/data/files/sles/autoinst_16.xml +212 -0
  52. osimager/data/files/ubuntu/meta-data +1 -0
  53. osimager/data/files/ubuntu/ubuntu.seed +154 -0
  54. osimager/data/files/ubuntu/user-data +39 -0
  55. osimager/data/files/windows/Autounattend.xml +151 -0
  56. osimager/data/files/xcpng/answerfile.xml +11 -0
  57. osimager/data/platforms/aws.json +42 -0
  58. osimager/data/platforms/azure.json +57 -0
  59. osimager/data/platforms/gcp.json +49 -0
  60. osimager/data/platforms/hyperv.json +28 -0
  61. osimager/data/platforms/none.json +14 -0
  62. osimager/data/platforms/proxmox.json +76 -0
  63. osimager/data/platforms/qemu.json +35 -0
  64. osimager/data/platforms/virtualbox.json +53 -0
  65. osimager/data/platforms/vmware.json +37 -0
  66. osimager/data/platforms/vsphere.json +64 -0
  67. osimager/data/platforms/xenserver.json +30 -0
  68. osimager/data/scripts/coreos_customize.py +60 -0
  69. osimager/data/scripts/openbsd_autoinstall.py +38 -0
  70. osimager/data/scripts/proxmox_post_build.py +84 -0
  71. osimager/data/scripts/qemu_post_build.py +84 -0
  72. osimager/data/specs/alma/spec.json +98 -0
  73. osimager/data/specs/alpine/spec.json +74 -0
  74. osimager/data/specs/amazon/spec.json +44 -0
  75. osimager/data/specs/arch/spec.json +64 -0
  76. osimager/data/specs/centos/spec.json +306 -0
  77. osimager/data/specs/debian/spec.json +511 -0
  78. osimager/data/specs/dragonfly/spec.json +36 -0
  79. osimager/data/specs/esx/spec.json +54 -0
  80. osimager/data/specs/esxi/config.yml +12 -0
  81. osimager/data/specs/esxi/spec.json +343 -0
  82. osimager/data/specs/esxi35/spec.json +67 -0
  83. osimager/data/specs/fedora/config_43.yml +22 -0
  84. osimager/data/specs/fedora/spec.json +278 -0
  85. osimager/data/specs/fedora-coreos/spec.json +50 -0
  86. osimager/data/specs/flatcar/spec.json +57 -0
  87. osimager/data/specs/freebsd/spec.json +80 -0
  88. osimager/data/specs/linux/spec.json +18 -0
  89. osimager/data/specs/mint/spec.json +61 -0
  90. osimager/data/specs/mxlinux/spec.json +49 -0
  91. osimager/data/specs/netbsd/spec.json +39 -0
  92. osimager/data/specs/oel/spec.json +686 -0
  93. osimager/data/specs/omnios/spec.json +36 -0
  94. osimager/data/specs/openbsd/spec.json +55 -0
  95. osimager/data/specs/openindiana/spec.json +36 -0
  96. osimager/data/specs/opensuse/spec.json +100 -0
  97. osimager/data/specs/openwrt/spec.json +37 -0
  98. osimager/data/specs/opnsense/spec.json +36 -0
  99. osimager/data/specs/pfsense/spec.json +37 -0
  100. osimager/data/specs/photon/spec.json +71 -0
  101. osimager/data/specs/proxmox-ve/spec.json +120 -0
  102. osimager/data/specs/rhel/config_5.yml +37 -0
  103. osimager/data/specs/rhel/config_6.yml +45 -0
  104. osimager/data/specs/rhel/config_7.yml +27 -0
  105. osimager/data/specs/rhel/config_8.yml +27 -0
  106. osimager/data/specs/rhel/config_9.yml +27 -0
  107. osimager/data/specs/rhel/config_legacy.yml +13 -0
  108. osimager/data/specs/rhel/config_prejson.yml +13 -0
  109. osimager/data/specs/rhel/configure_system +5 -0
  110. osimager/data/specs/rhel/spec.json +1004 -0
  111. osimager/data/specs/rocky/spec.json +58 -0
  112. osimager/data/specs/sco/spec.json +81 -0
  113. osimager/data/specs/sles/spec.json +190 -0
  114. osimager/data/specs/solaris/spec.json +36 -0
  115. osimager/data/specs/ssh/spec.json +10 -0
  116. osimager/data/specs/sysvr4/config.yml +12 -0
  117. osimager/data/specs/sysvr4/configure_system +5 -0
  118. osimager/data/specs/sysvr4/spec.json +128 -0
  119. osimager/data/specs/truenas/spec.json +36 -0
  120. osimager/data/specs/ubuntu/spec.json +238 -0
  121. osimager/data/specs/unixware/spec.json +81 -0
  122. osimager/data/specs/vyos/spec.json +36 -0
  123. osimager/data/specs/windows/spec.json +18 -0
  124. osimager/data/specs/windows-desktop/spec.json +75 -0
  125. osimager/data/specs/windows-server/spec.json +210 -0
  126. osimager/data/specs/winrm/spec.json +10 -0
  127. osimager/data/specs/xcpng/spec.json +76 -0
  128. osimager/data/tasks/AlmaLinux_post.yml +34 -0
  129. osimager/data/tasks/Debian_post.yml +7 -0
  130. osimager/data/tasks/Debian_pre.yml +21 -0
  131. osimager/data/tasks/Linux_post.yml +180 -0
  132. osimager/data/tasks/Linux_pre.yml +13 -0
  133. osimager/data/tasks/RedHat_post.yml +18 -0
  134. osimager/data/tasks/RedHat_pre.yml +250 -0
  135. osimager/data/tasks/Suse_post.yml +131 -0
  136. osimager/data/tasks/Suse_pre.yml +98 -0
  137. osimager/data/tasks/Windows_post.yml +145 -0
  138. osimager/data/tasks/Windows_pre.yml +17 -0
  139. osimager/data/tasks/copy_files_if.yml +14 -0
  140. osimager/data/tasks/gcp_linux_post.yml +60 -0
  141. osimager/data/tasks/gcp_post.yml +3 -0
  142. osimager/data/tasks/include_role_if.yml +12 -0
  143. osimager/data/tasks/include_tasks_if.yml +11 -0
  144. osimager/data/tasks/local_repo.yml +204 -0
  145. osimager/data/tasks/proxmox_pre.yml +16 -0
  146. osimager/data/tasks/rhel_6_config.yml +27 -0
  147. osimager/data/tasks/spec.yml +2 -0
  148. osimager/data/tasks/vsphere_post.yml +22 -0
  149. osimager/data/tasks/windows_updates.yml +163 -0
  150. osimager/utils.py +847 -0
  151. osimager-1.9.0.dist-info/METADATA +244 -0
  152. osimager-1.9.0.dist-info/RECORD +156 -0
  153. osimager-1.9.0.dist-info/WHEEL +5 -0
  154. osimager-1.9.0.dist-info/entry_points.txt +4 -0
  155. osimager-1.9.0.dist-info/licenses/LICENSE +21 -0
  156. osimager-1.9.0.dist-info/top_level.txt +1 -0
osimager/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+
2
+ from .core import OSImager
osimager/cli.py ADDED
@@ -0,0 +1,622 @@
1
+ """
2
+ OSImager CLI entry points.
3
+
4
+ Console script functions for pip-installed entry points.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import shutil
10
+ import subprocess
11
+ from typing import List, Optional
12
+
13
+ from .core import OSImager
14
+ from .core import EXIT_SUCCESS, EXIT_ERROR as EXIT_GENERAL_ERROR
15
+ from .utils import get_filename_from_url
16
+
17
+
18
+ def main_mkosimage(argv: Optional[List[str]] = None) -> int:
19
+ """Entry point for mkosimage command."""
20
+ if argv is None:
21
+ argv = sys.argv[1:]
22
+
23
+ try:
24
+ osimager = OSImager(argv=argv, which="full")
25
+
26
+ if osimager.show_config:
27
+ print(f"Config file: {os.path.join(osimager.settings['user_dir'], 'config.json')}")
28
+ print("")
29
+ for key, val in sorted(osimager.settings.items()):
30
+ if key in ('base_dir', 'user_dir'):
31
+ continue
32
+ print(f" {key:<20} = {val}")
33
+ return EXIT_SUCCESS
34
+
35
+ if osimager.check_urls:
36
+ osimager.check_all_urls()
37
+ return EXIT_SUCCESS
38
+
39
+ if osimager.local_only and not osimager.target:
40
+ osimager.avail = True
41
+
42
+ if osimager.avail:
43
+ index = osimager.get_index()
44
+ if not index:
45
+ print(f"No specs found in {osimager.system_data_dir}/specs/ (incomplete install?)")
46
+ print(f" Or create your own: {osimager.settings['user_dir']}/specs/")
47
+ return EXIT_SUCCESS
48
+
49
+ download = []
50
+ local = []
51
+ missing = []
52
+
53
+ for spec_key in sorted(index.keys()):
54
+ entry = index[spec_key]
55
+ iso_url = entry.get('iso_url', '')
56
+ iso_local = entry.get('iso_local', False)
57
+
58
+ if not iso_url:
59
+ missing.append((spec_key, '(no iso_url defined)'))
60
+ elif iso_url.startswith('file://'):
61
+ path = iso_url[7:]
62
+ if iso_local:
63
+ local.append((spec_key, path))
64
+ else:
65
+ missing.append((spec_key, path))
66
+ else:
67
+ if iso_local:
68
+ iso_name = get_filename_from_url(iso_url)
69
+ iso_path = osimager.settings.get('iso_path', '/iso')
70
+ local.append((spec_key, os.path.join(iso_path, iso_name)))
71
+ else:
72
+ download.append((spec_key, iso_url))
73
+
74
+ local_only = osimager.settings.get('local_only', False)
75
+
76
+ if local:
77
+ print(f"Local ({len(local)}):")
78
+ for spec_key, path in local:
79
+ print(f" {spec_key:<30} {path}")
80
+ print()
81
+
82
+ if not local_only and download:
83
+ print(f"Download ({len(download)}):")
84
+ for spec_key, url in download:
85
+ print(f" {spec_key:<30} {url}")
86
+ print()
87
+
88
+ if local_only:
89
+ print(f"{len(local)} local ISOs ({len(index)} total specs)")
90
+ else:
91
+ avail = len(download) + len(local)
92
+ print(f"{avail} available, {len(missing)} not available ({len(index)} total)")
93
+ return EXIT_SUCCESS
94
+
95
+ if osimager.list_platforms:
96
+ platforms = osimager.get_platforms()
97
+ print("Available platforms:")
98
+ for plat in platforms:
99
+ name = plat.get('name', '')
100
+ config = plat.get('config', {})
101
+ builder_type = config.get('type', 'unknown')
102
+ arches = plat.get('arches', [])
103
+ arches_str = ', '.join(arches) if arches else 'any'
104
+ print(f" {name:<14} {builder_type:<20} ({arches_str})")
105
+ return EXIT_SUCCESS
106
+
107
+ if osimager.init_plugins:
108
+ packer_cmd = osimager.settings.get('packer_cmd', 'packer')
109
+ if not shutil.which(packer_cmd):
110
+ print(f"error: '{packer_cmd}' not found in PATH")
111
+ print("")
112
+ print(" Packer is required. Install instructions:")
113
+ print(" https://developer.hashicorp.com/packer/install")
114
+ return EXIT_GENERAL_ERROR
115
+
116
+ # Ansible provisioner plugin — always needed
117
+ plugins = ['github.com/hashicorp/ansible']
118
+
119
+ # Collect plugins from platform files
120
+ platforms = osimager.get_platforms()
121
+ seen = set()
122
+ for plat in platforms:
123
+ plugin = plat.get('plugin')
124
+ if plugin and plugin not in seen:
125
+ plugins.append(plugin)
126
+ seen.add(plugin)
127
+
128
+ print(f"Installing {len(plugins)} Packer plugins...")
129
+ failed = []
130
+ for plugin in plugins:
131
+ print(f" {plugin}")
132
+ result = subprocess.run(
133
+ [packer_cmd, 'plugins', 'install', plugin],
134
+ capture_output=True, text=True
135
+ )
136
+ if result.returncode != 0:
137
+ print(f" FAILED: {result.stderr.strip()}")
138
+ failed.append(plugin)
139
+
140
+ if failed:
141
+ print(f"\n{len(failed)} plugin(s) failed to install:")
142
+ for p in failed:
143
+ print(f" {p}")
144
+ return EXIT_GENERAL_ERROR
145
+
146
+ print("Done.")
147
+ return EXIT_SUCCESS
148
+
149
+ if osimager.list_defs:
150
+ # Show configuration defaults (from config.json / built-in defaults)
151
+ config_defs = {
152
+ 'cpu_sockets': osimager.settings.get('cpu_sockets', 1),
153
+ 'cpu_cores': osimager.settings.get('cpu_cores', 2),
154
+ 'memory': osimager.settings.get('memory', 2048),
155
+ 'boot_disk_size': osimager.settings.get('boot_disk_size', 16384),
156
+ }
157
+
158
+ platforms = osimager.get_platforms()
159
+ # Collect all defs from platforms
160
+ all_defs = {}
161
+ for plat in platforms:
162
+ name = plat.get('name', '')
163
+ plat_defs = plat.get('defs', {})
164
+ for key, val in plat_defs.items():
165
+ if key not in all_defs:
166
+ all_defs[key] = {'value': val, 'source': name}
167
+
168
+ # Add computed defs
169
+ computed = {
170
+ 'name': 'hostname or spec name',
171
+ 'fqdn': 'name + domain',
172
+ 'ip': 'CLI arg or DNS lookup',
173
+ 'platform': 'from target',
174
+ 'location': 'from target',
175
+ 'dist': 'from spec provides',
176
+ 'version': 'from spec provides',
177
+ 'major': 'major version number',
178
+ 'minor': 'minor version number',
179
+ 'arch': 'from spec provides',
180
+ 'subnet': 'from CIDR',
181
+ 'prefix': 'from CIDR',
182
+ 'netmask': 'from CIDR prefix',
183
+ 'gateway': 'from location or CIDR',
184
+ 'domain': 'from location',
185
+ 'dns1': 'from location dns.servers',
186
+ 'dns_search': 'from location dns.search',
187
+ 'ntp1': 'from location ntp.servers',
188
+ 'iso_url': 'from spec',
189
+ 'iso_path': 'from settings (default: /iso)',
190
+ 'vms_path': 'from location',
191
+ }
192
+
193
+ print("Configuration defaults (~/.config/osimager/config.json):")
194
+ for key, val in sorted(config_defs.items()):
195
+ print(f" {key:<20} = {val}")
196
+
197
+ print("\nPlatform defs:")
198
+ for key, info in sorted(all_defs.items()):
199
+ val_str = str(info['value'])
200
+ if len(val_str) > 50:
201
+ val_str = val_str[:47] + '...'
202
+ print(f" {key:<20} = {val_str:<50} ({info['source']})")
203
+
204
+ print("\nComputed defs:")
205
+ for key, desc in sorted(computed.items()):
206
+ if key not in all_defs:
207
+ print(f" {key:<20} {desc}")
208
+
209
+ print("\nUse -D KEY=VALUE to override any def at build time.")
210
+ print("Use -x PLATFORM/LOCATION/SPEC to see all resolved defs for a specific build.")
211
+ return EXIT_SUCCESS
212
+
213
+ if osimager.list:
214
+ index = osimager.get_index()
215
+ if index:
216
+ print("Available specs:")
217
+ for spec_key in sorted(index.keys()):
218
+ entry = index[spec_key]
219
+ iso_flag = " (*)" if entry.get('iso_local', False) else ""
220
+ print(f" {spec_key}{iso_flag}")
221
+ else:
222
+ print(f"No specs found in {osimager.system_data_dir}/specs/ (incomplete install?)")
223
+ print(f" Or create your own: {osimager.settings['user_dir']}/specs/")
224
+ return EXIT_SUCCESS
225
+
226
+ if not osimager.target:
227
+ print("Usage: mkosimage [OPTIONS] PLATFORM/LOCATION/SPEC [NAME] [IP]")
228
+ print("")
229
+
230
+ # Check what the user is missing
231
+ user_dir = osimager.settings['user_dir']
232
+ locations = osimager.get_locations()
233
+ cred_source = osimager.settings.get('credential_source', 'vault')
234
+
235
+ examples_dir = osimager.resolve_data_path('examples') or os.path.join(osimager.settings['user_dir'], 'examples')
236
+
237
+ docs_url = "https://sshoecraft.github.io/osimager"
238
+ need_docs = False
239
+
240
+ if not locations:
241
+ print("No locations configured.")
242
+ print(f" Create a location file in {os.path.join(user_dir, 'locations/')}")
243
+ print(f" Supports .toml and .json formats (e.g. lab.toml or lab.json)")
244
+ print("")
245
+ print(" Quick start (VirtualBox):")
246
+ print(f" cp {os.path.join(examples_dir, 'quickstart-location.toml')} \\")
247
+ print(f" {os.path.join(user_dir, 'locations', 'local.toml')}")
248
+ print("")
249
+ need_docs = True
250
+
251
+ if cred_source == "vault":
252
+ vault_addr = osimager.settings.get('vault_addr', '')
253
+ vault_token = osimager.settings.get('vault_token', '')
254
+ if not vault_addr or not vault_token:
255
+ print("No credentials configured.")
256
+ print(" Option 1 - Local secrets file (simplest):")
257
+ print(" mkosimage --set credential_source=config")
258
+ print(f" cp {os.path.join(examples_dir, 'example-secrets')} \\")
259
+ print(f" {os.path.join(user_dir, 'secrets')}")
260
+ print(" Then edit ~/.config/osimager/secrets with your passwords.")
261
+ print("")
262
+ print(" Option 2 - HashiCorp Vault:")
263
+ print(" mkosimage --set vault_addr=http://your-vault:8200")
264
+ print(" mkosimage --set vault_token=your-token")
265
+ print("")
266
+ need_docs = True
267
+ elif cred_source == "config":
268
+ secrets_path = os.path.join(user_dir, 'secrets')
269
+ if not os.path.exists(secrets_path):
270
+ print("No secrets file found.")
271
+ print(f" cp {os.path.join(examples_dir, 'example-secrets')} \\")
272
+ print(f" {secrets_path}")
273
+ print(" Then edit the file with your passwords.")
274
+ print("")
275
+ need_docs = True
276
+
277
+ if need_docs:
278
+ print(f" Full documentation: {docs_url}")
279
+ print("")
280
+
281
+ if locations:
282
+ platforms = osimager.get_platforms()
283
+ if platforms:
284
+ print("Available platforms/locations:")
285
+ for plat in platforms:
286
+ plat_name = plat.get('name', '')
287
+ plat_locations = [l.get('name', '') for l in locations if plat_name in l.get('platforms', [])]
288
+ if plat_locations:
289
+ print(f" {plat_name}: {', '.join(plat_locations)}")
290
+ print("")
291
+ print("Use --list to see available specs.")
292
+ else:
293
+ print(f"No platforms found in {osimager.system_data_dir}/platforms/ (incomplete install?)")
294
+ print(f" Or create your own: {osimager.settings['user_dir']}/platforms/")
295
+
296
+ print("Use --help for all options.")
297
+ return EXIT_SUCCESS
298
+
299
+ build_config = osimager.make_build(
300
+ osimager.target,
301
+ name=osimager.name,
302
+ ip=osimager.ip or ""
303
+ )
304
+
305
+ if osimager.dump_defs or osimager.dump_build:
306
+ return EXIT_SUCCESS
307
+
308
+ osimager.run_packer()
309
+ return osimager.exit_code if hasattr(osimager, 'exit_code') and osimager.exit_code else EXIT_SUCCESS
310
+
311
+ except SystemExit as e:
312
+ return e.code if e.code is not None else EXIT_GENERAL_ERROR
313
+ except KeyboardInterrupt:
314
+ print("\nOperation cancelled by user.")
315
+ return EXIT_GENERAL_ERROR
316
+ except Exception as e:
317
+ print(f"Error: {e}")
318
+ return EXIT_GENERAL_ERROR
319
+
320
+
321
+ def main_rfosimage(argv: Optional[List[str]] = None) -> int:
322
+ """Entry point for rfosimage command."""
323
+ if argv is None:
324
+ argv = sys.argv[1:]
325
+
326
+ try:
327
+ extra_args = {
328
+ "plan": {"flags": [], "kwargs": {"help": "platform/location/spec", "dest": "plan"}},
329
+ "name": {"flags": [], "kwargs": {"help": "hostname", "dest": "name", "nargs": "?", "default": None}},
330
+ "ip": {"flags": [], "kwargs": {"help": "ip", "dest": "ip", "nargs": "?", "default": None}},
331
+ }
332
+ img = OSImager(argv, "full", extra_args)
333
+ plan = img.args.plan
334
+ name = img.args.name
335
+ ip = img.args.ip
336
+
337
+ non_flag_args = [arg for arg in argv if not arg.startswith('-')]
338
+ if len(non_flag_args) >= 2 and not name:
339
+ name = non_flag_args[1]
340
+ if len(non_flag_args) >= 3 and not ip:
341
+ ip = non_flag_args[2]
342
+
343
+ build = img.make_build(plan, name, ip)
344
+ if not build:
345
+ return EXIT_GENERAL_ERROR
346
+ defs = img.defs
347
+
348
+ if 'files' in img.spec:
349
+ del img.spec['files']
350
+
351
+ builders = build['builders']
352
+ config = builders[0]
353
+ comm = config.get('communicator', None)
354
+ if not comm:
355
+ print("error: build does not have a communicator defined")
356
+ return EXIT_GENERAL_ERROR
357
+
358
+ new_config = {
359
+ "name": defs.get("spec_name", "name"),
360
+ "type": "null"
361
+ }
362
+ for key in config:
363
+ if key.startswith(comm):
364
+ new_config[key] = config[key]
365
+
366
+ del builders[0]
367
+ builders.append(new_config)
368
+ build['builders'] = builders
369
+
370
+ img.run_packer()
371
+ return img.exit_code if hasattr(img, 'exit_code') and img.exit_code else EXIT_SUCCESS
372
+
373
+ except SystemExit as e:
374
+ return e.code if e.code is not None else EXIT_GENERAL_ERROR
375
+ except KeyboardInterrupt:
376
+ print("\nOperation cancelled by user.")
377
+ return EXIT_GENERAL_ERROR
378
+ except Exception as e:
379
+ print(f"Error: {e}")
380
+ return EXIT_GENERAL_ERROR
381
+
382
+
383
+ def _get_python_version(python_path):
384
+ """Get the major.minor version tuple from a Python interpreter."""
385
+ rc = subprocess.run([python_path, "--version"], capture_output=True, text=True)
386
+ ver_str = rc.stdout.strip() or rc.stderr.strip()
387
+ try:
388
+ return tuple(int(x) for x in ver_str.split()[1].split(".")[:2])
389
+ except (IndexError, ValueError):
390
+ return None
391
+
392
+
393
+ def _find_python(min_version, max_version=None):
394
+ """Find a Python interpreter >= min_version and <= max_version.
395
+ Versions are strings like '3.10' or '2.7'. max_version=None means no upper limit.
396
+ Returns the path to the interpreter, or None."""
397
+ min_parts = tuple(int(x) for x in min_version.split("."))
398
+ max_parts = tuple(int(x) for x in max_version.split(".")) if max_version else None
399
+ is_py2 = min_parts[0] == 2
400
+
401
+ if is_py2:
402
+ candidates = ["python2.7", "python2.6", "python2"]
403
+ else:
404
+ # Try versioned interpreters from high to low, then generic
405
+ candidates = [f"python3.{m}" for m in range(20, min_parts[1] - 1, -1)]
406
+ candidates.append("python3")
407
+
408
+ for cmd in candidates:
409
+ path = shutil.which(cmd)
410
+ if not path:
411
+ continue
412
+ ver = _get_python_version(path)
413
+ if not ver:
414
+ continue
415
+ if ver < min_parts:
416
+ continue
417
+ if max_parts and ver > max_parts:
418
+ continue
419
+ return path
420
+
421
+ return None
422
+
423
+
424
+ def _create_venv(venv_path, python_path):
425
+ """Create a virtualenv using the appropriate method for the Python version."""
426
+ ver = _get_python_version(python_path)
427
+ is_python2 = ver and ver[0] == 2
428
+
429
+ if is_python2:
430
+ # Check if virtualenv is available under this Python
431
+ rc = subprocess.run([python_path, "-m", "virtualenv", "--version"], capture_output=True)
432
+ if rc.returncode != 0:
433
+ print(f"Installing virtualenv for {python_path}...")
434
+ rc = subprocess.run([python_path, "-m", "pip", "install", "virtualenv"], capture_output=True)
435
+ if rc.returncode != 0:
436
+ print(f"error: virtualenv is required for {python_path} and could not be installed")
437
+ sys.exit(1)
438
+ rc = subprocess.run([python_path, "-m", "virtualenv", venv_path])
439
+ else:
440
+ rc = subprocess.run([python_path, "-m", "venv", venv_path])
441
+
442
+ return rc.returncode == 0
443
+
444
+
445
+ def _load_ansible_versions(osimager):
446
+ """Load ansible version definitions from ansible.json via two-layer resolution.
447
+ Returns dict keyed by version string."""
448
+ import json
449
+
450
+ # Try user dir first, then system data dir
451
+ for base in [osimager.settings['user_dir'], osimager.system_data_dir]:
452
+ if not base:
453
+ continue
454
+ path = os.path.join(base, "ansible.json")
455
+ if os.path.isfile(path):
456
+ with open(path, 'r') as f:
457
+ data = json.load(f)
458
+ return {entry["version"]: entry for entry in data.get("versions", [])}
459
+
460
+ return {}
461
+
462
+
463
+ def _scan_specs_for_venvs(osimager):
464
+ """Scan all specs for unique ansible_version values.
465
+ Returns a set of version strings."""
466
+ import json
467
+ required = set()
468
+ specs = osimager.resolve_data_files("specs", "*.json")
469
+ for spec_file in specs:
470
+ try:
471
+ with open(spec_file, 'r') as f:
472
+ data = json.load(f)
473
+ except Exception:
474
+ continue
475
+
476
+ def collect(entry):
477
+ av = entry.get("ansible_version")
478
+ if av:
479
+ required.add(av)
480
+
481
+ collect(data)
482
+ for entry in data.get("version_specific", []):
483
+ collect(entry)
484
+ for arch_entry in entry.get("arch_specific", []):
485
+ collect(arch_entry)
486
+ for entry in data.get("arch_specific", []):
487
+ collect(entry)
488
+
489
+ return required
490
+
491
+
492
+ def main_mkvenv(argv: Optional[List[str]] = None) -> int:
493
+ """Entry point for mkvenv command."""
494
+ if argv is None:
495
+ argv = sys.argv[1:]
496
+
497
+ try:
498
+ extra_args = {
499
+ "--all": {"flags": ["--all"], "kwargs": {"default": False, "action": "store_true", "help": "Create all missing venvs", "dest": "all"}},
500
+ "ansible_version": {"flags": ["ansible_version"], "kwargs": {"nargs": "?", "default": None, "help": "Ansible version to create venv for (e.g. 2.16)"}},
501
+ }
502
+ osimager = OSImager(argv=argv, which="venv", extra_args=extra_args)
503
+ args = osimager.args
504
+ venv_dir = osimager.settings['venv_dir']
505
+
506
+ # Load ansible version definitions from ansible.json
507
+ ansible_defs = _load_ansible_versions(osimager)
508
+ if not ansible_defs:
509
+ print("error: ansible.json not found")
510
+ print(f" Expected {osimager.system_data_dir}/ansible.json or ~/.config/osimager/ansible.json")
511
+ return EXIT_GENERAL_ERROR
512
+
513
+ # Scan all specs for required ansible versions
514
+ required = _scan_specs_for_venvs(osimager)
515
+
516
+ version = args.ansible_version
517
+ create_all = args.all
518
+
519
+ # No args: list status
520
+ if not version and not create_all:
521
+ print(f"Venv directory: {venv_dir}")
522
+ print()
523
+ if not required:
524
+ print("No venvs required by any specs.")
525
+ return EXIT_SUCCESS
526
+ print("Required ansible venvs:")
527
+ for ver in sorted(required):
528
+ venv_path = os.path.join(venv_dir, ver)
529
+ exists = os.path.isfile(os.path.join(venv_path, "bin", "activate"))
530
+ adef = ansible_defs.get(ver)
531
+ if exists:
532
+ status = "installed"
533
+ elif not adef:
534
+ status = f"missing (ansible {ver} not defined in ansible.json)"
535
+ else:
536
+ min_py = adef.get("python_min")
537
+ max_py = adef.get("python_max")
538
+ pkg = adef.get("pkg", "ansible-core")
539
+ python_path = _find_python(min_py, max_py)
540
+ if python_path:
541
+ status = f"missing ({pkg}, python: {python_path})"
542
+ else:
543
+ req = f">= {min_py}"
544
+ if max_py:
545
+ req += f" and <= {max_py}"
546
+ status = f"missing ({pkg}, requires python {req} - NOT FOUND)"
547
+ print(f" {ver:10s} {status}")
548
+ return EXIT_SUCCESS
549
+
550
+ # Build list of versions to create
551
+ if create_all:
552
+ versions = sorted(required)
553
+ else:
554
+ versions = [version]
555
+
556
+ # Create each venv
557
+ for ver in versions:
558
+ venv_path = os.path.join(venv_dir, ver)
559
+ activator = os.path.join(venv_path, "bin", "activate")
560
+
561
+ if os.path.isfile(activator):
562
+ print(f"Venv {ver} already exists at {venv_path}")
563
+ continue
564
+
565
+ adef = ansible_defs.get(ver)
566
+ if not adef:
567
+ print(f"error: ansible {ver} not defined in ansible.json")
568
+ print(f" Known versions: {', '.join(sorted(ansible_defs.keys()))}")
569
+ return EXIT_GENERAL_ERROR
570
+
571
+ min_py = adef.get("python_min")
572
+ max_py = adef.get("python_max")
573
+ pkg = adef.get("pkg", "ansible-core")
574
+
575
+ python_path = _find_python(min_py, max_py)
576
+ if not python_path:
577
+ req = f">= {min_py}"
578
+ if max_py:
579
+ req += f" and <= {max_py}"
580
+ print(f"error: no Python {req} found for ansible {ver}")
581
+ print(f" Install a compatible Python version and ensure it is in your PATH")
582
+ return EXIT_GENERAL_ERROR
583
+
584
+ pkg_spec = f"{pkg}=={ver}.*"
585
+ print(f"Creating venv for {pkg} {ver} (using {python_path})...")
586
+ os.makedirs(venv_dir, exist_ok=True)
587
+
588
+ if not _create_venv(venv_path, python_path):
589
+ print(f"error: failed to create venv at {venv_path}")
590
+ return EXIT_GENERAL_ERROR
591
+
592
+ pip = os.path.join(venv_path, "bin", "pip")
593
+
594
+ # Install pre-requirements from ansible.json before the ansible package
595
+ requirements = adef.get("requirements", [])
596
+ for req in requirements:
597
+ print(f"Installing prerequisite: {req}...")
598
+ rc = subprocess.run([pip, "install", req])
599
+ if rc.returncode != 0:
600
+ print(f"error: failed to install {req} into {venv_path}")
601
+ shutil.rmtree(venv_path, ignore_errors=True)
602
+ return EXIT_GENERAL_ERROR
603
+
604
+ print(f"Installing {pkg_spec}...")
605
+ rc = subprocess.run([pip, "install", pkg_spec])
606
+ if rc.returncode != 0:
607
+ print(f"error: failed to install {pkg_spec} into {venv_path}")
608
+ shutil.rmtree(venv_path, ignore_errors=True)
609
+ return EXIT_GENERAL_ERROR
610
+
611
+ print(f"Venv {ver} created at {venv_path}")
612
+
613
+ return EXIT_SUCCESS
614
+
615
+ except SystemExit as e:
616
+ return e.code if e.code is not None else EXIT_GENERAL_ERROR
617
+ except KeyboardInterrupt:
618
+ print("\nOperation cancelled by user.")
619
+ return EXIT_GENERAL_ERROR
620
+ except Exception as e:
621
+ print(f"Error: {e}")
622
+ return EXIT_GENERAL_ERROR