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
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.
15 # Copyright 2020 Adam Stevko
19 # userland-zone - tool to manage userland template and build zone lifecycle
29 logger = logging.getLogger("userland-zone")
31 BUILD_ZONE_NAME = "bz"
32 TEMPLATE_ZONE_NAME = "-".join([BUILD_ZONE_NAME, "template"])
34 ARCHIVES_DIR = "/ws/archives"
36 DEVELOPER_PACKAGES = ["build-essential"]
41 self, name, path=None, brand="nlipkg", user="oi", user_id=1000,
44 self._path = path or self._get_zone_path()
47 self._user_id = user_id
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]
61 return self.state == "running"
63 def _zonecfg(self, args, **kwargs):
64 zonecfg_args = ["-z", self._name]
65 zonecfg_args.extend(args)
66 return execute("zonecfg", zonecfg_args, **kwargs)
68 def _zoneadm(self, args, **kwargs):
69 zoneadm_args = ["-z", self._name]
70 zoneadm_args.extend(args)
71 return execute("zoneadm", zoneadm_args, **kwargs)
73 def _pkg(self, args, **kwargs):
74 return execute("pkg", args, **kwargs)
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]
81 def create(self, extra_args=None):
83 raise ValueError("Zone path is required when creating the zone")
87 "set zonepath={path}".format(path=self._path),
88 "set zonename={name}".format(name=self._name),
89 "set brand={brand}".format(brand=self._brand),
93 zonecfg_args.extend(extra_args)
94 zonecfg_args.append("commit")
96 zonecfg_args = [";".join(zonecfg_args)]
97 return self._zonecfg(zonecfg_args)
100 return self._zonecfg(["delete", "-F"])
103 return self._zoneadm(["install", "-e"] + DEVELOPER_PACKAGES)
106 return self._zoneadm(["uninstall", "-F"])
109 return self._zoneadm(["halt"])
112 return self._zoneadm(["boot"])
114 def clone(self, source_zone):
115 return self._zoneadm(["clone", source_zone.name])
118 zone_root_path = os.path.join(self._path, "root")
119 return self._pkg(["-R", zone_root_path, "update"])
121 def configure(self, sysding_lines=None):
124 "setup_timezone UTC",
125 "setup_locale en_US.UTF-8",
126 'setup_user_account {user} {uid} {gid} "{gecos}" {home} {shell}'.format(
131 home="/export/home/{}".format(self._user),
132 shell="/usr/bin/bash",
134 "/usr/sbin/usermod -P'Primary Administrator' {user}".format(
140 sysding_config.extend(sysding_lines)
141 sysding_config.append("\n")
143 sysding_conf = os.path.join(self._path, "root", "etc", "sysding.conf")
144 with open(sysding_conf, "w") as f:
145 f.write("\n".join(sysding_config))
148 def execute(cmd, args, **kwargs):
149 ret, result = None, None
151 if "check" not in kwargs:
152 kwargs["check"] = True
155 logger.debug('Executing "%s"', " ".join([cmd] + args))
156 result = subprocess.run([cmd] + args, **kwargs)
157 except subprocess.CalledProcessError as e:
159 'Command "%s" exited with %s', " ".join([cmd] + args), e.returncode
162 if e.errno == errno.ENOENT:
163 raise ValueError("{} cannot be found".format(cmd))
165 if result and result.stdout:
166 ret = result.stdout.decode()
172 def parse_arguments():
174 if os.path.exists(path) and os.path.isdir(path):
176 raise argparse.ArgumentTypeError("{} is not a valid path".format(path))
178 parser = argparse.ArgumentParser()
179 parser.add_argument("--prefix", default=BUILD_ZONE_NAME, help="Zone name prefix")
180 subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
183 ct_parser = subparsers.add_parser(
184 "create-template", help="Create template zone for userland builds"
186 ct_parser.add_argument(
189 default=os.path.join(ZONES_ROOT, TEMPLATE_ZONE_NAME),
192 ct_parser.add_argument(
194 help="User ID to use for build user",
198 ct_parser.add_argument(
200 help="User name to use for build user",
201 default=pwd.getpwuid(os.getuid()).pw_name,
206 _ = subparsers.add_parser(
207 "update-template", help="Update template zone for userland builds"
211 _ = subparsers.add_parser(
212 "destroy-template", help="Destroy template zone for userland builds"
216 sz_parser = subparsers.add_parser(
217 "spawn-zone", help="Spawn build zone for userland builds"
219 sz_parser.add_argument(
220 "--id", required=True, help="Zone identifier that identifies build zone"
222 sz_parser.add_argument(
224 default=ARCHIVES_DIR,
226 help="Path to userland archives",
228 sz_parser.add_argument(
232 help="Path to userland code repository checkoutt",
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"
241 args = parser.parse_args()
243 if not args.subcommand:
250 def create_template(zone_path, zone_name=TEMPLATE_ZONE_NAME, user='oi', user_id=1000):
251 zone = Zone(path=zone_path, name=zone_name, user=user, user_id=user_id)
257 def destroy_zone(zone_name=TEMPLATE_ZONE_NAME):
258 zone = Zone(path=os.path.join(ZONES_ROOT, zone_name), name=zone_name)
266 def spawn_zone(id, prefix=BUILD_ZONE_NAME, archive_dir=ARCHIVES_DIR, code_dir=CODE_DIR):
267 name = "{}-{}".format(prefix, id)
268 zone = Zone(name=name, path=os.path.join(ZONES_ROOT, name))
270 template_zone = Zone(name=TEMPLATE_ZONE_NAME)
272 # To avoid zonecfg failure, we will check for existence of archive_dir and code_dir
273 if not os.path.exists(archive_dir):
275 "Userland archives dir {} has to exist in the global zone before we can clone the build zone!"
278 if not os.path.exists(code_dir):
280 "Userland archives dir {} has to exist in the global zone before we can clone the build zone!"
285 "set dir = {}".format(ARCHIVES_DIR),
286 "set special = {}".format(archive_dir),
288 "add options [rw,nodevices]",
291 "set dir = {}".format(CODE_DIR),
292 "set special = {}".format(code_dir),
294 "add options [rw,nodevices]",
297 zone.create(extra_args=extra_args)
298 zone.clone(template_zone)
302 def update_template(zone_name=TEMPLATE_ZONE_NAME):
303 zone = Zone(name=zone_name)
308 args = parse_arguments()
310 if args.subcommand == "create-template":
311 zone_name = '{}-{}'.format(args.prefix, 'template')
312 create_template(zone_path=args.zone_path, zone_name=zone_name, user=args.user, user_id=args.uid)
313 elif args.subcommand == "destroy-template":
314 zone_name = '{}-{}'.format(args.prefix, 'template')
315 destroy_zone(zone_name=zone_name)
316 elif args.subcommand == "update-template":
317 zone_name = '{}-{}'.format(args.prefix, 'template')
318 update_template(zone_name=zone_name)
319 elif args.subcommand == "spawn-zone":
320 spawn_zone(id=args.id, prefix=args.prefix, archive_dir=args.archive_dir, code_dir=args.code_dir)
321 elif args.subcommand == "destroy-zone":
322 zone_name = "{}-{}".format(args.prefix, args.id)
323 destroy_zone(zone_name=zone_name)
326 if __name__ == "__main__":