Skip to content

Commit 7de1eca

Browse files
committed
First commit
0 parents  commit 7de1eca

File tree

5 files changed

+693
-0
lines changed

5 files changed

+693
-0
lines changed

LICENSE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Licensed under the Apache License, Version 2.0 (the "License");
2+
you may not use this source code except in compliance with the License.
3+
You may obtain a copy of the License at
4+
5+
https://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
macadmin-scripts
2+
3+
Some scripts that might be of use to macOS admins. Might be related to Munki;
4+
might not.

createbootvolfromautonbi.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#!/usr/bin/python
2+
# encoding: utf-8
3+
#
4+
# Copyright 2017 Greg Neagle.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
'''A tool to make bootable disk volumes from the output of autonbi. Especially
18+
useful to make bootable disks containing Imagr and the 'SIP-ignoring' kernel,
19+
which allows Imagr to run scripts that affect SIP state, set UAKEL options, and
20+
run the `startosinstall` component, all of which might otherwise require network
21+
booting from a NetInstall-style nbi.'''
22+
23+
import argparse
24+
import os
25+
import plistlib
26+
import subprocess
27+
import sys
28+
import urlparse
29+
30+
31+
# dmg helpers
32+
def mountdmg(dmgpath):
33+
"""
34+
Attempts to mount the dmg at dmgpath and returns first mountpoint
35+
"""
36+
mountpoints = []
37+
dmgname = os.path.basename(dmgpath)
38+
cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
39+
'-mountRandom', '/tmp', '-nobrowse', '-plist',
40+
'-owners', 'on']
41+
proc = subprocess.Popen(cmd, bufsize=-1,
42+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
43+
(pliststr, err) = proc.communicate()
44+
if proc.returncode:
45+
print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname)
46+
return None
47+
if pliststr:
48+
plist = plistlib.readPlistFromString(pliststr)
49+
for entity in plist['system-entities']:
50+
if 'mount-point' in entity:
51+
mountpoints.append(entity['mount-point'])
52+
53+
return mountpoints[0]
54+
55+
56+
def unmountdmg(mountpoint):
57+
"""
58+
Unmounts the dmg at mountpoint
59+
"""
60+
proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
61+
bufsize=-1, stdout=subprocess.PIPE,
62+
stderr=subprocess.PIPE)
63+
(dummy_output, err) = proc.communicate()
64+
if proc.returncode:
65+
print >> sys.stderr, 'Polite unmount failed: %s' % err
66+
print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint
67+
# try forcing the unmount
68+
retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint,
69+
'-force'])
70+
if retcode:
71+
print >> sys.stderr, 'Failed to unmount %s' % mountpoint
72+
73+
74+
def locate_basesystem_dmg(nbi):
75+
'''Finds and returns the relative path to the BaseSystem.dmg within the
76+
NetInstall.dmg'''
77+
source_boot_plist = os.path.join(nbi, 'i386/com.apple.Boot.plist')
78+
try:
79+
boot_args = plistlib.readPlist(source_boot_plist)
80+
except Exception, err:
81+
print >> sys.stderr, err
82+
sys.exit(-1)
83+
kernel_flags = boot_args.get('Kernel Flags')
84+
if not kernel_flags:
85+
print >> sys.stderr, 'i386/com.apple.Boot.plist is missing Kernel Flags'
86+
sys.exit(-1)
87+
# kernel flags should in the form 'root-dmg=file:///path'
88+
if not kernel_flags.startswith('root-dmg='):
89+
print >> sys.stderr, 'Unexpected Kernel Flags: %s' % kernel_flags
90+
sys.exit(-1)
91+
file_url = kernel_flags[9:]
92+
dmg_path = urlparse.unquote(urlparse.urlparse(file_url).path)
93+
# return path minus leading slash
94+
return dmg_path.lstrip('/')
95+
96+
97+
def copy_system_version_plist(nbi, target_volume):
98+
'''Copies System/Library/CoreServices/SystemVersion.plist from the
99+
BaseSystem.dmg to the target volume.'''
100+
netinstall_dmg = os.path.join(nbi, 'NetInstall.dmg')
101+
if not os.path.exists(netinstall_dmg):
102+
print >> sys.stderr, "Missing NetInstall.dmg from nbi folder"
103+
sys.exit(-1)
104+
print 'Mounting %s...' % netinstall_dmg
105+
netinstall_mount = mountdmg(netinstall_dmg)
106+
if not netinstall_mount:
107+
sys.exit(-1)
108+
basesystem_dmg = os.path.join(netinstall_mount, locate_basesystem_dmg(nbi))
109+
print 'Mounting %s...' % basesystem_dmg
110+
basesystem_mount = mountdmg(basesystem_dmg)
111+
if not basesystem_mount:
112+
unmountdmg(netinstall_mount)
113+
sys.exit(-1)
114+
source = os.path.join(
115+
basesystem_mount, 'System/Library/CoreServices/SystemVersion.plist')
116+
dest = os.path.join(
117+
target_volume, 'System/Library/CoreServices/SystemVersion.plist')
118+
try:
119+
subprocess.check_call(
120+
['/usr/bin/ditto', '-V', source, dest])
121+
except subprocess.CalledProcessError, err:
122+
print >> sys.stderr, err
123+
unmountdmg(basesystem_mount)
124+
unmountdmg(netinstall_mount)
125+
sys.exit(-1)
126+
127+
unmountdmg(basesystem_mount)
128+
unmountdmg(netinstall_mount)
129+
130+
131+
def copy_boot_files(nbi, target_volume):
132+
'''Copies some boot files, yo'''
133+
files_to_copy = [
134+
['NetInstall.dmg', 'NetInstall.dmg'],
135+
['i386/PlatformSupport.plist',
136+
'System/Library/CoreServices/PlatformSupport.plist'],
137+
['i386/booter', 'System/Library/CoreServices/boot.efi'],
138+
['i386/booter', 'usr/standalone/i386/boot.efi'],
139+
['i386/x86_64/kernelcache',
140+
'System/Library/PrelinkedKernels/prelinkedkernel']
141+
]
142+
for source, dest in files_to_copy:
143+
full_source = os.path.join(nbi, source)
144+
full_dest = os.path.join(target_volume, dest)
145+
try:
146+
subprocess.check_call(
147+
['/usr/bin/ditto', '-V', full_source, full_dest])
148+
except subprocess.CalledProcessError, err:
149+
print >> sys.stderr, err
150+
sys.exit(-1)
151+
152+
153+
def make_boot_plist(nbi, target_volume):
154+
'''Creates our com.apple.Boot.plist'''
155+
source_boot_plist = os.path.join(nbi, 'i386/com.apple.Boot.plist')
156+
try:
157+
boot_args = plistlib.readPlist(source_boot_plist)
158+
except Exception, err:
159+
print >> sys.stderr, err
160+
sys.exit(-1)
161+
kernel_flags = boot_args.get('Kernel Flags')
162+
if not kernel_flags:
163+
print >> sys.stderr, 'i386/com.apple.Boot.plist is missing Kernel Flags'
164+
sys.exit(-1)
165+
# prepend the container-dmg path
166+
boot_args['Kernel Flags'] = (
167+
'container-dmg=file:///NetInstall.dmg ' + kernel_flags)
168+
boot_plist = os.path.join(
169+
target_volume,
170+
'Library/Preferences/SystemConfiguration/com.apple.Boot.plist')
171+
plist_dir = os.path.dirname(boot_plist)
172+
if not os.path.exists(plist_dir):
173+
os.makedirs(plist_dir)
174+
try:
175+
plistlib.writePlist(boot_args, boot_plist)
176+
except Exception, err:
177+
print >> sys.stderr, err
178+
sys.exit(-1)
179+
180+
181+
def bless(target_volume, label=None):
182+
'''Bless the target volume'''
183+
blessfolder = os.path.join(target_volume, 'System/Library/CoreServices')
184+
if not label:
185+
label = os.path.basename(target_volume)
186+
try:
187+
subprocess.check_call(
188+
['/usr/sbin/bless', '--folder', blessfolder, '--label', label])
189+
except subprocess.CalledProcessError, err:
190+
print >> sys.stderr, err
191+
sys.exit(-1)
192+
193+
194+
def main():
195+
'''Do the thing we were made for'''
196+
parser = argparse.ArgumentParser()
197+
parser.add_argument('--nbi', required=True, metavar='path_to_nbi',
198+
help='Path to nbi folder created by autonbi.')
199+
parser.add_argument('--volume', required=True,
200+
metavar='path_to_disk_volume',
201+
help='Path to disk volume.')
202+
args = parser.parse_args()
203+
copy_system_version_plist(args.nbi, args.volume)
204+
copy_boot_files(args.nbi, args.volume)
205+
make_boot_plist(args.nbi, args.volume)
206+
bless(args.volume)
207+
208+
209+
if __name__ == '__main__':
210+
main()

0 commit comments

Comments
 (0)