commit | author | age
|
9c6c5a
|
1 |
#!/usr/bin/env python3 |
AŠ |
2 |
|
|
3 |
# |
|
4 |
# This file and its contents are supplied under the terms of the |
|
5 |
# Common Development and Distribution License ("CDDL"), version 1.0. |
|
6 |
# You may only use this file in accordance with the terms of version |
|
7 |
# 1.0 of the CDDL. |
|
8 |
# |
|
9 |
# A full copy of the text of the CDDL should have accompanied this |
|
10 |
# source. A copy of the CDDL is also available via the Internet at |
|
11 |
# http://www.illumos.org/license/CDDL. |
|
12 |
# |
|
13 |
|
|
14 |
# |
|
15 |
# Copyright 2020 Adam Stevko |
|
16 |
# |
|
17 |
|
|
18 |
# |
|
19 |
# userland-zone - tool to manage userland template and build zone lifecycle |
|
20 |
# |
|
21 |
|
|
22 |
import argparse |
|
23 |
import errno |
|
24 |
import logging |
|
25 |
import os |
bf604a
|
26 |
import pwd |
9c6c5a
|
27 |
import subprocess |
AŠ |
28 |
|
|
29 |
logger = logging.getLogger("userland-zone") |
|
30 |
|
bf604a
|
31 |
BUILD_ZONE_NAME = "bz" |
9c6c5a
|
32 |
TEMPLATE_ZONE_NAME = "-".join([BUILD_ZONE_NAME, "template"]) |
AŠ |
33 |
ZONES_ROOT = "/zones" |
|
34 |
ARCHIVES_DIR = "/ws/archives" |
|
35 |
CODE_DIR = "/ws/code" |
|
36 |
DEVELOPER_PACKAGES = ["build-essential"] |
|
37 |
|
|
38 |
|
|
39 |
class Zone: |
bf604a
|
40 |
def __init__( |
AŠ |
41 |
self, name, path=None, brand="nlipkg", user="oi", user_id=1000, |
|
42 |
): |
9c6c5a
|
43 |
self._name = name |
AŠ |
44 |
self._path = path or self._get_zone_path() |
|
45 |
self._brand = brand |
bf604a
|
46 |
self._user = user |
AŠ |
47 |
self._user_id = user_id |
9c6c5a
|
48 |
|
AŠ |
49 |
@property |
|
50 |
def name(self): |
|
51 |
return self._name |
|
52 |
|
|
53 |
@property |
|
54 |
def state(self): |
|
55 |
output = self._zoneadm(["list", "-p"], stdout=subprocess.PIPE) |
|
56 |
# -:name:installed:/zones/name:22dae542-e5ce-c86a-e5c7-e34d74b696bf:nlipkg:shared |
|
57 |
return output.split(":")[2] |
|
58 |
|
|
59 |
@property |
|
60 |
def is_running(self): |
|
61 |
return self.state == "running" |
|
62 |
|
|
63 |
def _zonecfg(self, args, **kwargs): |
|
64 |
zonecfg_args = ["-z", self._name] |
|
65 |
zonecfg_args.extend(args) |
|
66 |
return execute("zonecfg", zonecfg_args, **kwargs) |
|
67 |
|
|
68 |
def _zoneadm(self, args, **kwargs): |
|
69 |
zoneadm_args = ["-z", self._name] |
|
70 |
zoneadm_args.extend(args) |
|
71 |
return execute("zoneadm", zoneadm_args, **kwargs) |
|
72 |
|
|
73 |
def _pkg(self, args, **kwargs): |
|
74 |
return execute("pkg", args, **kwargs) |
|
75 |
|
|
76 |
def _get_zone_path(self): |
|
77 |
output = self._zoneadm(["list", "-p"], stdout=subprocess.PIPE, check=False) |
|
78 |
# -:name:installed:/zones/name:22dae542-e5ce-c86a-e5c7-e34d74b696bf:nlipkg:shared |
|
79 |
return output.split(":")[3] |
|
80 |
|
|
81 |
def create(self, extra_args=None): |
|
82 |
if not self._path: |
|
83 |
raise ValueError("Zone path is required when creating the zone") |
|
84 |
|
|
85 |
zonecfg_args = [ |
|
86 |
"create -b", |
|
87 |
"set zonepath={path}".format(path=self._path), |
|
88 |
"set zonename={name}".format(name=self._name), |
|
89 |
"set brand={brand}".format(brand=self._brand), |
|
90 |
] |
|
91 |
|
|
92 |
if extra_args: |
|
93 |
zonecfg_args.extend(extra_args) |
|
94 |
zonecfg_args.append("commit") |
|
95 |
|
|
96 |
zonecfg_args = [";".join(zonecfg_args)] |
|
97 |
return self._zonecfg(zonecfg_args) |
|
98 |
|
|
99 |
def delete(self): |
|
100 |
return self._zonecfg(["delete", "-F"]) |
|
101 |
|
|
102 |
def install(self): |
|
103 |
return self._zoneadm(["install", "-e"] + DEVELOPER_PACKAGES) |
|
104 |
|
|
105 |
def uninstall(self): |
|
106 |
return self._zoneadm(["uninstall", "-F"]) |
|
107 |
|
|
108 |
def halt(self): |
|
109 |
return self._zoneadm(["halt"]) |
|
110 |
|
|
111 |
def boot(self): |
|
112 |
return self._zoneadm(["boot"]) |
|
113 |
|
|
114 |
def clone(self, source_zone): |
|
115 |
return self._zoneadm(["clone", source_zone.name]) |
|
116 |
|
|
117 |
def update(self): |
|
118 |
zone_root_path = os.path.join(self._path, "root") |
|
119 |
return self._pkg(["-R", zone_root_path, "update"]) |
|
120 |
|
bf604a
|
121 |
def configure(self, sysding_lines=None): |
AŠ |
122 |
sysding_config = [ |
|
123 |
"#!/usr/bin/ksh", |
|
124 |
"setup_timezone UTC", |
|
125 |
"setup_locale en_US.UTF-8", |
|
126 |
'setup_user_account {user} {uid} {gid} "{gecos}" {home} {shell}'.format( |
|
127 |
user=self._user, |
|
128 |
uid=self._user_id, |
|
129 |
gid="10", |
|
130 |
gecos="Build user", |
|
131 |
home="/export/home/{}".format(self._user), |
|
132 |
shell="/usr/bin/bash", |
|
133 |
), |
|
134 |
"/usr/sbin/usermod -P'Primary Administrator' {user}".format( |
|
135 |
user=self._user |
|
136 |
), |
|
137 |
] |
|
138 |
|
|
139 |
if sysding_lines: |
|
140 |
sysding_config.extend(sysding_lines) |
|
141 |
sysding_config.append("\n") |
|
142 |
|
9c6c5a
|
143 |
sysding_conf = os.path.join(self._path, "root", "etc", "sysding.conf") |
AŠ |
144 |
with open(sysding_conf, "w") as f: |
bf604a
|
145 |
f.write("\n".join(sysding_config)) |
9c6c5a
|
146 |
|
AŠ |
147 |
|
|
148 |
def execute(cmd, args, **kwargs): |
|
149 |
ret, result = None, None |
|
150 |
|
|
151 |
if "check" not in kwargs: |
|
152 |
kwargs["check"] = True |
|
153 |
|
|
154 |
try: |
|
155 |
logger.debug('Executing "%s"', " ".join([cmd] + args)) |
|
156 |
result = subprocess.run([cmd] + args, **kwargs) |
|
157 |
except subprocess.CalledProcessError as e: |
|
158 |
logger.error( |
|
159 |
'Command "%s" exited with %s', " ".join([cmd] + args), e.returncode |
|
160 |
) |
|
161 |
except OSError as e: |
|
162 |
if e.errno == errno.ENOENT: |
|
163 |
raise ValueError("{} cannot be found".format(cmd)) |
|
164 |
|
|
165 |
if result and result.stdout: |
|
166 |
ret = result.stdout.decode() |
|
167 |
ret = ret.rstrip() |
|
168 |
|
|
169 |
return ret |
|
170 |
|
|
171 |
|
|
172 |
def parse_arguments(): |
|
173 |
def dir_path(path): |
|
174 |
if os.path.exists(path) and os.path.isdir(path): |
|
175 |
return path |
|
176 |
raise argparse.ArgumentTypeError("{} is not a valid path".format(path)) |
|
177 |
|
|
178 |
parser = argparse.ArgumentParser() |
bf604a
|
179 |
parser.add_argument("--prefix", default=BUILD_ZONE_NAME, help="Zone name prefix") |
9c6c5a
|
180 |
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") |
AŠ |
181 |
|
|
182 |
# create-template |
|
183 |
ct_parser = subparsers.add_parser( |
|
184 |
"create-template", help="Create template zone for userland builds" |
|
185 |
) |
|
186 |
ct_parser.add_argument( |
|
187 |
"-p", |
|
188 |
help="Zone path", |
|
189 |
default=os.path.join(ZONES_ROOT, TEMPLATE_ZONE_NAME), |
|
190 |
dest="zone_path", |
bf604a
|
191 |
) |
AŠ |
192 |
ct_parser.add_argument( |
|
193 |
"-u", |
|
194 |
help="User ID to use for build user", |
|
195 |
default=os.getuid(), |
|
196 |
dest="uid", |
|
197 |
) |
|
198 |
ct_parser.add_argument( |
|
199 |
"-l", |
|
200 |
help="User name to use for build user", |
|
201 |
default=pwd.getpwuid(os.getuid()).pw_name, |
|
202 |
dest="user", |
9c6c5a
|
203 |
) |
AŠ |
204 |
|
|
205 |
# update-template |
|
206 |
_ = subparsers.add_parser( |
|
207 |
"update-template", help="Update template zone for userland builds" |
|
208 |
) |
|
209 |
|
|
210 |
# destroy-template |
|
211 |
_ = subparsers.add_parser( |
|
212 |
"destroy-template", help="Destroy template zone for userland builds" |
|
213 |
) |
|
214 |
|
|
215 |
# spawn-zone |
|
216 |
sz_parser = subparsers.add_parser( |
|
217 |
"spawn-zone", help="Spawn build zone for userland builds" |
|
218 |
) |
|
219 |
sz_parser.add_argument( |
|
220 |
"--id", required=True, help="Zone identifier that identifies build zone" |
|
221 |
) |
|
222 |
sz_parser.add_argument( |
|
223 |
"--archive-dir", |
|
224 |
default=ARCHIVES_DIR, |
|
225 |
type=dir_path, |
|
226 |
help="Path to userland archives", |
|
227 |
) |
|
228 |
sz_parser.add_argument( |
|
229 |
"--code-dir", |
|
230 |
default=CODE_DIR, |
|
231 |
type=dir_path, |
|
232 |
help="Path to userland code repository checkoutt", |
|
233 |
) |
|
234 |
|
|
235 |
# destroy-zone |
|
236 |
dz_parser = subparsers.add_parser("destroy-zone", help="Destroy build zone") |
|
237 |
dz_parser.add_argument( |
|
238 |
"--id", required=True, help="Zone identifier that identifies build zone" |
|
239 |
) |
|
240 |
|
|
241 |
args = parser.parse_args() |
|
242 |
|
|
243 |
if not args.subcommand: |
|
244 |
parser.print_help() |
|
245 |
exit(1) |
|
246 |
|
|
247 |
return args |
|
248 |
|
|
249 |
|
bf604a
|
250 |
def create_template(zone_path, zone_name=TEMPLATE_ZONE_NAME, user='oi', user_id=1000): |
AŠ |
251 |
zone = Zone(path=zone_path, name=zone_name, user=user, user_id=user_id) |
9c6c5a
|
252 |
zone.create() |
AŠ |
253 |
zone.install() |
|
254 |
zone.configure() |
|
255 |
|
|
256 |
|
|
257 |
def destroy_zone(zone_name=TEMPLATE_ZONE_NAME): |
|
258 |
zone = Zone(path=os.path.join(ZONES_ROOT, zone_name), name=zone_name) |
|
259 |
|
|
260 |
if zone.is_running: |
|
261 |
zone.halt() |
|
262 |
zone.uninstall() |
|
263 |
zone.delete() |
|
264 |
|
|
265 |
|
bf604a
|
266 |
def spawn_zone(id, prefix=BUILD_ZONE_NAME, archive_dir=ARCHIVES_DIR, code_dir=CODE_DIR): |
AŠ |
267 |
name = "{}-{}".format(prefix, id) |
9c6c5a
|
268 |
zone = Zone(name=name, path=os.path.join(ZONES_ROOT, name)) |
AŠ |
269 |
|
|
270 |
template_zone = Zone(name=TEMPLATE_ZONE_NAME) |
|
271 |
|
|
272 |
# To avoid zonecfg failure, we will check for existence of archive_dir and code_dir |
|
273 |
if not os.path.exists(archive_dir): |
|
274 |
raise ValueError( |
|
275 |
"Userland archives dir {} has to exist in the global zone before we can clone the build zone!" |
|
276 |
) |
|
277 |
|
|
278 |
if not os.path.exists(code_dir): |
|
279 |
raise ValueError( |
|
280 |
"Userland archives dir {} has to exist in the global zone before we can clone the build zone!" |
|
281 |
) |
|
282 |
|
|
283 |
extra_args = [ |
|
284 |
"add fs", |
|
285 |
"set dir = {}".format(ARCHIVES_DIR), |
|
286 |
"set special = {}".format(archive_dir), |
|
287 |
"set type = lofs", |
|
288 |
"add options [rw,nodevices]", |
|
289 |
"end", |
|
290 |
"add fs", |
|
291 |
"set dir = {}".format(CODE_DIR), |
|
292 |
"set special = {}".format(code_dir), |
|
293 |
"set type = lofs", |
|
294 |
"add options [rw,nodevices]", |
|
295 |
"end", |
|
296 |
] |
|
297 |
zone.create(extra_args=extra_args) |
|
298 |
zone.clone(template_zone) |
|
299 |
zone.boot() |
|
300 |
|
|
301 |
|
bf604a
|
302 |
def update_template(zone_name=TEMPLATE_ZONE_NAME): |
AŠ |
303 |
zone = Zone(name=zone_name) |
9c6c5a
|
304 |
zone.update() |
AŠ |
305 |
|
|
306 |
|
|
307 |
def main(): |
|
308 |
args = parse_arguments() |
|
309 |
|
|
310 |
if args.subcommand == "create-template": |
bf604a
|
311 |
zone_name = '{}-{}'.format(args.prefix, 'template') |
AŠ |
312 |
create_template(zone_path=args.zone_path, zone_name=zone_name, user=args.user, user_id=args.uid) |
9c6c5a
|
313 |
elif args.subcommand == "destroy-template": |
bf604a
|
314 |
zone_name = '{}-{}'.format(args.prefix, 'template') |
AŠ |
315 |
destroy_zone(zone_name=zone_name) |
9c6c5a
|
316 |
elif args.subcommand == "update-template": |
bf604a
|
317 |
zone_name = '{}-{}'.format(args.prefix, 'template') |
AŠ |
318 |
update_template(zone_name=zone_name) |
9c6c5a
|
319 |
elif args.subcommand == "spawn-zone": |
bf604a
|
320 |
spawn_zone(id=args.id, prefix=args.prefix, archive_dir=args.archive_dir, code_dir=args.code_dir) |
9c6c5a
|
321 |
elif args.subcommand == "destroy-zone": |
bf604a
|
322 |
zone_name = "{}-{}".format(args.prefix, args.id) |
9c6c5a
|
323 |
destroy_zone(zone_name=zone_name) |
AŠ |
324 |
|
|
325 |
|
|
326 |
if __name__ == "__main__": |
|
327 |
main() |