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.
- osimager/__init__.py +2 -0
- osimager/cli.py +622 -0
- osimager/core.py +1863 -0
- osimager/data/ansible.cfg +12 -0
- osimager/data/ansible.json +20 -0
- osimager/data/config.yml +208 -0
- osimager/data/examples/example-location.json +40 -0
- osimager/data/examples/example-location.toml +54 -0
- osimager/data/examples/example-secrets +38 -0
- osimager/data/examples/example-vault +46 -0
- osimager/data/examples/quickstart-location.toml +21 -0
- osimager/data/files/agama/leap.json +48 -0
- osimager/data/files/agama/sles.json +49 -0
- osimager/data/files/alpine/answerfile +17 -0
- osimager/data/files/arch/user_configuration.json +43 -0
- osimager/data/files/arch/user_credentials.json +10 -0
- osimager/data/files/debian/debian-8-preseed.cfg +232 -0
- osimager/data/files/debian/debian.fix +26 -0
- osimager/data/files/debian/debian.seed +145 -0
- osimager/data/files/debian/debian_12.seed +124 -0
- osimager/data/files/debian/debian_pre10.seed +145 -0
- osimager/data/files/esxi/kickstart.cfg +9 -0
- osimager/data/files/esxi/kickstart_fb.cfg +19 -0
- osimager/data/files/esxi/kickstart_post.cfg +18 -0
- osimager/data/files/fedora/kickstart.cfg +29 -0
- osimager/data/files/fedora/kickstart_43.cfg +45 -0
- osimager/data/files/freebsd/installerconfig +55 -0
- osimager/data/files/ignition/fcos.ign +27 -0
- osimager/data/files/ignition/flatcar.ign +27 -0
- osimager/data/files/linux/banner +4 -0
- osimager/data/files/linux/findcd +17 -0
- osimager/data/files/oel/kickstart_6.cfg +25 -0
- osimager/data/files/oel/kickstart_post.cfg +43 -0
- osimager/data/files/openbsd/install.conf +19 -0
- osimager/data/files/photon/kickstart.json +35 -0
- osimager/data/files/proxmox-ve/answer.toml +14 -0
- osimager/data/files/rhel/install_vmwtools.sh +23 -0
- osimager/data/files/rhel/kickstart_10.cfg +48 -0
- osimager/data/files/rhel/kickstart_3.cfg +27 -0
- osimager/data/files/rhel/kickstart_4.cfg +28 -0
- osimager/data/files/rhel/kickstart_5.cfg +47 -0
- osimager/data/files/rhel/kickstart_6.cfg +28 -0
- osimager/data/files/rhel/kickstart_7.cfg +47 -0
- osimager/data/files/rhel/kickstart_8.cfg +49 -0
- osimager/data/files/rhel/kickstart_9.cfg +48 -0
- osimager/data/files/rhel/kickstart_end.cfg +1 -0
- osimager/data/files/rhel/kickstart_post.cfg +23 -0
- osimager/data/files/rhel/ks-part.sh +115 -0
- osimager/data/files/sles/autoinst_12.xml +202 -0
- osimager/data/files/sles/autoinst_15.xml +213 -0
- osimager/data/files/sles/autoinst_16.xml +212 -0
- osimager/data/files/ubuntu/meta-data +1 -0
- osimager/data/files/ubuntu/ubuntu.seed +154 -0
- osimager/data/files/ubuntu/user-data +39 -0
- osimager/data/files/windows/Autounattend.xml +151 -0
- osimager/data/files/xcpng/answerfile.xml +11 -0
- osimager/data/platforms/aws.json +42 -0
- osimager/data/platforms/azure.json +57 -0
- osimager/data/platforms/gcp.json +49 -0
- osimager/data/platforms/hyperv.json +28 -0
- osimager/data/platforms/none.json +14 -0
- osimager/data/platforms/proxmox.json +76 -0
- osimager/data/platforms/qemu.json +35 -0
- osimager/data/platforms/virtualbox.json +53 -0
- osimager/data/platforms/vmware.json +37 -0
- osimager/data/platforms/vsphere.json +64 -0
- osimager/data/platforms/xenserver.json +30 -0
- osimager/data/scripts/coreos_customize.py +60 -0
- osimager/data/scripts/openbsd_autoinstall.py +38 -0
- osimager/data/scripts/proxmox_post_build.py +84 -0
- osimager/data/scripts/qemu_post_build.py +84 -0
- osimager/data/specs/alma/spec.json +98 -0
- osimager/data/specs/alpine/spec.json +74 -0
- osimager/data/specs/amazon/spec.json +44 -0
- osimager/data/specs/arch/spec.json +64 -0
- osimager/data/specs/centos/spec.json +306 -0
- osimager/data/specs/debian/spec.json +511 -0
- osimager/data/specs/dragonfly/spec.json +36 -0
- osimager/data/specs/esx/spec.json +54 -0
- osimager/data/specs/esxi/config.yml +12 -0
- osimager/data/specs/esxi/spec.json +343 -0
- osimager/data/specs/esxi35/spec.json +67 -0
- osimager/data/specs/fedora/config_43.yml +22 -0
- osimager/data/specs/fedora/spec.json +278 -0
- osimager/data/specs/fedora-coreos/spec.json +50 -0
- osimager/data/specs/flatcar/spec.json +57 -0
- osimager/data/specs/freebsd/spec.json +80 -0
- osimager/data/specs/linux/spec.json +18 -0
- osimager/data/specs/mint/spec.json +61 -0
- osimager/data/specs/mxlinux/spec.json +49 -0
- osimager/data/specs/netbsd/spec.json +39 -0
- osimager/data/specs/oel/spec.json +686 -0
- osimager/data/specs/omnios/spec.json +36 -0
- osimager/data/specs/openbsd/spec.json +55 -0
- osimager/data/specs/openindiana/spec.json +36 -0
- osimager/data/specs/opensuse/spec.json +100 -0
- osimager/data/specs/openwrt/spec.json +37 -0
- osimager/data/specs/opnsense/spec.json +36 -0
- osimager/data/specs/pfsense/spec.json +37 -0
- osimager/data/specs/photon/spec.json +71 -0
- osimager/data/specs/proxmox-ve/spec.json +120 -0
- osimager/data/specs/rhel/config_5.yml +37 -0
- osimager/data/specs/rhel/config_6.yml +45 -0
- osimager/data/specs/rhel/config_7.yml +27 -0
- osimager/data/specs/rhel/config_8.yml +27 -0
- osimager/data/specs/rhel/config_9.yml +27 -0
- osimager/data/specs/rhel/config_legacy.yml +13 -0
- osimager/data/specs/rhel/config_prejson.yml +13 -0
- osimager/data/specs/rhel/configure_system +5 -0
- osimager/data/specs/rhel/spec.json +1004 -0
- osimager/data/specs/rocky/spec.json +58 -0
- osimager/data/specs/sco/spec.json +81 -0
- osimager/data/specs/sles/spec.json +190 -0
- osimager/data/specs/solaris/spec.json +36 -0
- osimager/data/specs/ssh/spec.json +10 -0
- osimager/data/specs/sysvr4/config.yml +12 -0
- osimager/data/specs/sysvr4/configure_system +5 -0
- osimager/data/specs/sysvr4/spec.json +128 -0
- osimager/data/specs/truenas/spec.json +36 -0
- osimager/data/specs/ubuntu/spec.json +238 -0
- osimager/data/specs/unixware/spec.json +81 -0
- osimager/data/specs/vyos/spec.json +36 -0
- osimager/data/specs/windows/spec.json +18 -0
- osimager/data/specs/windows-desktop/spec.json +75 -0
- osimager/data/specs/windows-server/spec.json +210 -0
- osimager/data/specs/winrm/spec.json +10 -0
- osimager/data/specs/xcpng/spec.json +76 -0
- osimager/data/tasks/AlmaLinux_post.yml +34 -0
- osimager/data/tasks/Debian_post.yml +7 -0
- osimager/data/tasks/Debian_pre.yml +21 -0
- osimager/data/tasks/Linux_post.yml +180 -0
- osimager/data/tasks/Linux_pre.yml +13 -0
- osimager/data/tasks/RedHat_post.yml +18 -0
- osimager/data/tasks/RedHat_pre.yml +250 -0
- osimager/data/tasks/Suse_post.yml +131 -0
- osimager/data/tasks/Suse_pre.yml +98 -0
- osimager/data/tasks/Windows_post.yml +145 -0
- osimager/data/tasks/Windows_pre.yml +17 -0
- osimager/data/tasks/copy_files_if.yml +14 -0
- osimager/data/tasks/gcp_linux_post.yml +60 -0
- osimager/data/tasks/gcp_post.yml +3 -0
- osimager/data/tasks/include_role_if.yml +12 -0
- osimager/data/tasks/include_tasks_if.yml +11 -0
- osimager/data/tasks/local_repo.yml +204 -0
- osimager/data/tasks/proxmox_pre.yml +16 -0
- osimager/data/tasks/rhel_6_config.yml +27 -0
- osimager/data/tasks/spec.yml +2 -0
- osimager/data/tasks/vsphere_post.yml +22 -0
- osimager/data/tasks/windows_updates.yml +163 -0
- osimager/utils.py +847 -0
- osimager-1.9.0.dist-info/METADATA +244 -0
- osimager-1.9.0.dist-info/RECORD +156 -0
- osimager-1.9.0.dist-info/WHEEL +5 -0
- osimager-1.9.0.dist-info/entry_points.txt +4 -0
- osimager-1.9.0.dist-info/licenses/LICENSE +21 -0
- osimager-1.9.0.dist-info/top_level.txt +1 -0
osimager/__init__.py
ADDED
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
|