def __init__( self, silent=False, callback=None, migrate=False, reset_cache=False, ): self.reset_cache = reset_cache if reset_cache: cache.reset() self.pool = iocage_lib.ioc_json.IOCJson( silent=silent, checking_datasets=True ).json_get_value("pool") self.callback = callback self.silent = silent self.__check_fd_mount__() self.__check_datasets__() self.pool_root_dataset = Dataset(self.pool, cache=reset_cache) self.iocage_dataset = Dataset( os.path.join(self.pool, 'iocage'), cache=reset_cache ) if migrate: self.__check_migrations__() self.__clean_files__()
def __init__(self, release, props, num, pkglist=None, plugin=False, migrate=False, config=None, silent=False, template=False, short=False, basejail=False, thickjail=False, empty=False, uuid=None, clone=False, thickconfig=False, clone_basejail=False, callback=None): cache.reset() self.pool = iocage_lib.ioc_json.IOCJson().json_get_value("pool") self.iocroot = iocage_lib.ioc_json.IOCJson( self.pool).json_get_value("iocroot") self.release = release self.props = props self.num = num self.pkglist = pkglist self.plugin = plugin self.migrate = migrate self.config = config self.template = template self.short = short self.basejail = basejail self.thickjail = thickjail self.empty = empty self.uuid = uuid self.clone = clone self.silent = silent self.callback = callback self.thickconfig = thickconfig self.log = logging.getLogger('iocage') if basejail and not clone_basejail: # We want these thick to remove any odd dependency chains later self.thickjail = True
def import_jail(self, jail, compression_algo=None, path=None): """Import from an iocage export.""" # Path can be an absolute path pointing straight to the exported jail # or it can the directory where the exported jail lives # TODO: We should introduce parsers for this image_dir = path or os.path.join(self.iocroot, 'images') if not os.path.exists(image_dir): iocage_lib.ioc_common.logit( { 'level': 'EXCEPTION', 'message': f'{image_dir} does not exist.' } ) elif os.path.isfile(image_dir): image_dir, filename = image_dir.rsplit('/', 1) else: if not compression_algo: extension_regex = r'zip|tar\.xz' else: extension_regex = r'zip' if \ compression_algo == 'zip' else r'tar.xz' regex = re.compile(rf'{jail}.*(?:{extension_regex})') matches = [ f for f in os.listdir(image_dir) if regex.match(f) ] if len(matches) > 1: msg = f"Multiple images found for {jail}:" for j in sorted(matches): msg += f"\n {j}" msg += '\nPlease explicitly select image or define ' \ 'compression algorithm to use' iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) elif len(matches) < 1: iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": f"{jail} not found!" }, _callback=self.callback, silent=self.silent) else: filename = matches[0] if filename.rsplit('.', 1)[-1] == 'zip': compression_algo = extension = 'zip' else: compression_algo = 'lzma' extension = 'tar.xz' image_target = f"{image_dir}/{filename}" uuid, date = filename[:-len(f'.{extension}')].rsplit('_', 1) if compression_algo == 'zip': reader = { 'func': zipfile.ZipFile, 'params': ['r'], 'iter': 'namelist' } else: reader = { 'func': tarfile.open, 'params': ['r:xz'], 'iter': 'getmembers' } with reader['func'](image_target, *reader['params']) as f: for member in getattr(f, reader['iter'])(): if compression_algo != 'zip': name = member.name else: name = member z_dataset_type = name.split(f'{date}_', 1)[-1] z_dataset_type = z_dataset_type.split(f'{uuid}_', 1)[-1] if z_dataset_type == date: # This is the parent dataset z_dataset_type = uuid else: z_dataset_type = \ f'{uuid}/{z_dataset_type.replace("_", "/")}'.rstrip( '/' ) iocage_lib.ioc_common.logit( { 'level': 'INFO', 'message': f'Importing dataset: {z_dataset_type}' }, self.callback, silent=self.silent ) recv = su.Popen( [ 'zfs', 'recv', '-F', os.path.join( self.pool, 'iocage/jails', z_dataset_type ) ], stdin=su.PIPE ) chunk_size = 10 * 1024 * 1024 with (f.open(name) if compression_algo == 'zip' else f.extractfile(member)) as file: data = file.read(chunk_size) while data is not None and len(data) > 0: recv.stdin.write(data) data = file.read(chunk_size) recv.communicate() # Cleanup our mess. try: target = f"{self.pool}/iocage/jails/{uuid}@ioc-export-{date}" iocage_lib.ioc_common.checkoutput( ["zfs", "destroy", "-r", target], stderr=su.STDOUT) except su.CalledProcessError as err: msg = err.output.decode('utf-8').rstrip() iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) # Templates become jails again once imported, let's make that reality. cache.reset() jail_json = iocage_lib.ioc_json.IOCJson( f'{self.iocroot}/jails/{uuid}', silent=True ) if jail_json.json_get_value('type') == 'template': jail_json.json_set_value('type=jail') jail_json.json_set_value('template=0', _import=True) msg = f"\nImported: {uuid}" iocage_lib.ioc_common.logit( { "level": "INFO", "message": msg }, self.callback, silent=self.silent)
def inherit_property(self, prop): iocage_cache.reset() inherit_property(self.resource_name, prop)
def set_property(self, prop, value): iocage_cache.reset() set_property(self.resource_name, prop, value, self.zfs_resource)
def _create_jail(self, jail_uuid, location): """ Create a snapshot of the user specified RELEASE dataset and clone a jail from that. The user can also specify properties to override the defaults. """ import iocage_lib.ioc_destroy # Circular dep start = False is_template = False source_template = None rtsold_enable = 'NO' if iocage_lib.ioc_common.match_to_dir(self.iocroot, jail_uuid): iocage_lib.ioc_common.logit({ 'level': 'EXCEPTION', 'message': f'Jail: {jail_uuid} already exists!' }) if self.migrate: config = self.config else: try: if self.clone and self.template: iocage_lib.ioc_common.logit( { 'level': 'EXCEPTION', 'message': 'You cannot clone a template, ' 'use create -t instead.' }, _callback=self.callback, silent=self.silent) elif self.template: _type = "templates" temp_path = f"{self.iocroot}/{_type}/{self.release}" template_config = iocage_lib.ioc_json.IOCJson( temp_path).json_get_value try: cloned_release = template_config('cloned_release') except KeyError: # Thick jails won't have this cloned_release = None source_template = self.release elif self.clone: _type = "jails" clone_path = f"{self.iocroot}/{_type}/{self.release}" clone_config = iocage_lib.ioc_json.IOCJson( clone_path).json_get_value try: cloned_release = clone_config('cloned_release') except KeyError: # Thick jails won't have this cloned_release = None clone_uuid = clone_config("host_hostuuid") else: _type = "releases" rel_path = f"{self.iocroot}/{_type}/{self.release}" if not self.empty: cloned_release = \ iocage_lib.ioc_common.get_jail_freebsd_version( f'{rel_path}/root', self.release ) else: cloned_release = "EMPTY" except (IOError, OSError, FileNotFoundError, UnboundLocalError): # Unintuitevly a missing template will throw a # UnboundLocalError as the missing file will kick the # migration routine for zfs props. We don't need that :) if self.template: raise RuntimeError(f"Template: {self.release} not found!") elif self.clone: if os.path.isdir(f"{self.iocroot}/templates/" f"{self.release}"): iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": "You cannot clone a template, " "use create -t instead." }, _callback=self.callback, silent=self.silent) else: # Yep, self.release is actually the source jail. iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": f"Jail: {self.release} not found!" }, _callback=self.callback, silent=self.silent) else: iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": f"RELEASE: {self.release} not found!" }, _callback=self.callback, silent=self.silent) if not self.clone: if cloned_release is None: cloned_release = self.release config = self.create_config(jail_uuid, cloned_release, source_template) else: clone_config = f"{self.iocroot}/jails/{jail_uuid}/config.json" clone_fstab = f"{self.iocroot}/jails/{jail_uuid}/fstab" clone_etc_hosts = \ f"{self.iocroot}/jails/{jail_uuid}/root/etc/hosts" jail = f"{self.pool}/iocage/jails/{jail_uuid}/root" if self.template: source = f'{self.pool}/iocage/templates/{self.release}@{jail_uuid}' snap_cmd = ['zfs', 'snapshot', '-r', source] if self.thickjail: source = f'{source.split("@")[0]}/root@{jail_uuid}' snap_cmd = ['zfs', 'snapshot', source] try: su.check_call(snap_cmd, stderr=su.PIPE) except su.CalledProcessError: raise RuntimeError(f'Template: {jail_uuid} not found!') if not self.thickjail: su.Popen([ 'zfs', 'clone', '-p', f'{source.split("@")[0]}/root' f'@{jail_uuid}', jail ], stdout=su.PIPE).communicate() else: self.create_thickjail(jail_uuid, source.split('@')[0]) del config['cloned_release'] # self.release is actually the templates name config['release'] = iocage_lib.ioc_json.IOCJson( f'{self.iocroot}/templates/{self.release}').json_get_value( 'release') try: config['cloned_release'] = iocage_lib.ioc_json.IOCJson( f'{self.iocroot}/templates/{self.release}').json_get_value( 'cloned_release') except KeyError: # Thick jails won't have this pass elif self.clone: source = f'{self.pool}/iocage/jails/{self.release}@{jail_uuid}' snap_cmd = ['zfs', 'snapshot', '-r', source] if self.thickjail: source = f'{source.split("@")[0]}/root@{jail_uuid}' snap_cmd = ['zfs', 'snapshot', source] try: su.check_call(snap_cmd, stderr=su.PIPE) except su.CalledProcessError: raise RuntimeError(f'Jail: {jail_uuid} not found!') if not self.thickjail: su.Popen(['zfs', 'clone', source, jail.replace('/root', '')], stdout=su.PIPE).communicate() su.Popen([ 'zfs', 'clone', f'{source.split("@")[0]}/root@' f'{jail_uuid}', jail ], stdout=su.PIPE).communicate() else: self.create_thickjail(jail_uuid, source.split('@')[0]) shutil.copyfile( f'{self.iocroot}/jails/{self.release}/config.json', f'{self.iocroot}/jails/{jail_uuid}/config.json') shutil.copyfile(f'{self.iocroot}/jails/{self.release}/fstab', f'{self.iocroot}/jails/{jail_uuid}/fstab') with open(clone_config, 'r') as _clone_config: config = json.load(_clone_config) # self.release is actually the clones name config['release'] = iocage_lib.ioc_json.IOCJson( f'{self.iocroot}/jails/{self.release}').json_get_value( 'release') try: config['cloned_release'] = iocage_lib.ioc_json.IOCJson( f'{self.iocroot}/jails/{self.release}').json_get_value( 'cloned_release') except KeyError: # Thick jails won't have this pass # Clones are expected to be as identical as possible. for k, v in config.items(): try: v = v.replace(clone_uuid, jail_uuid) if '_mac' in k: # They want a unique mac on start config[k] = 'none' except AttributeError: # Bool props pass config[k] = v else: if not self.empty: dataset = f'{self.pool}/iocage/releases/{self.release}/' \ f'root@{jail_uuid}' try: su.check_call(['zfs', 'snapshot', dataset], stderr=su.PIPE) except su.CalledProcessError: release = os.path.join(self.pool, 'iocage/releases', self.release) if not Dataset(release).exists: raise RuntimeError( f'RELEASE: {self.release} not found!') else: iocage_lib.ioc_common.logit( { 'level': 'EXCEPTION', 'message': f'Snapshot: {dataset} exists!\n' 'Please manually run zfs destroy' f' {dataset} if you wish to ' 'destroy it.' }, _callback=self.callback, silent=self.silent) if not self.thickjail: su.Popen(['zfs', 'clone', '-p', dataset, jail], stdout=su.PIPE).communicate() else: self.create_thickjail(jail_uuid, dataset.split('@')[0]) del config['cloned_release'] else: try: iocage_lib.ioc_common.checkoutput( ['zfs', 'create', '-p', jail], stderr=su.PIPE) except su.CalledProcessError as err: raise RuntimeError(err.output.decode('utf-8').rstrip()) cache.reset() iocjson = iocage_lib.ioc_json.IOCJson(location, silent=True) # This test is to avoid the same warnings during install_packages. if jail_uuid == "default" or jail_uuid == "help": iocage_lib.ioc_destroy.IOCDestroy().__destroy_parse_datasets__( f"{self.pool}/iocage/jails/{jail_uuid}") iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": f"You cannot name a jail {jail_uuid}, " "that is a reserved name." }, _callback=self.callback, silent=self.silent) disable_localhost = False for prop in self.props: key, _, value = prop.partition("=") is_true = iocage_lib.ioc_common.check_truthy(value) if key == "boot" and is_true and not self.empty: start = True elif self.plugin and key == "type" and value == "pluginv2": config["type"] = value elif key == 'template' and is_true: iocjson.json_write(config) # Set counts on this. location = location.replace("/jails/", "/templates/") iocjson.json_set_value("type=template") iocjson.json_set_value("template=1") Dataset( os.path.join(self.pool, 'iocage', 'templates', jail_uuid)).set_property('readonly', 'off') # If you supply pkglist and templates without setting the # config's type, you will end up with a type of jail # instead of template like we want. config["type"] = "template" start = False is_template = True elif key == 'ip6_addr': if 'accept_rtadv' in value: if not iocage_lib.ioc_common.lowercase_set( iocage_lib.ioc_common.construct_truthy('vnet') ) & iocage_lib.ioc_common.lowercase_set(self.props): iocage_lib.ioc_common.logit( { 'level': 'WARNING', 'message': 'accept_rtadv requires vnet,' ' enabling!' }, _callback=self.callback, silent=self.silent) config['vnet'] = 1 rtsold_enable = 'YES' elif (key == 'dhcp' and is_true) or (key == 'ip4_addr' and 'DHCP' in value.upper()): if not iocage_lib.ioc_common.lowercase_set( iocage_lib.ioc_common.construct_truthy('vnet') ) & iocage_lib.ioc_common.lowercase_set(self.props): iocage_lib.ioc_common.logit( { 'level': 'WARNING', 'message': 'dhcp requires vnet, enabling!' }, _callback=self.callback, silent=self.silent) config['vnet'] = 1 if not iocage_lib.ioc_common.lowercase_set( iocage_lib.ioc_common.construct_truthy('bpf') ) & iocage_lib.ioc_common.lowercase_set(self.props): iocage_lib.ioc_common.logit( { 'level': 'WARNING', 'message': 'dhcp requires bpf, enabling!' }, _callback=self.callback, silent=self.silent) config['bpf'] = 1 elif key == 'bpf' and is_true: if not iocage_lib.ioc_common.lowercase_set( iocage_lib.ioc_common.construct_truthy('vnet') ) & iocage_lib.ioc_common.lowercase_set(self.props): iocage_lib.ioc_common.logit( { 'level': 'WARNING', 'message': 'bpf requires vnet, enabling!' }, _callback=self.callback, silent=self.silent) config['vnet'] = 1 elif key == 'assign_localhost' and is_true: if iocage_lib.ioc_common.lowercase_set( iocage_lib.ioc_common.construct_truthy('vnet') ) & iocage_lib.ioc_common.lowercase_set(self.props): iocage_lib.ioc_common.logit( { 'level': 'WARNING', 'message': 'assign_localhost only applies to shared' ' IP jails, disabling!' }, _callback=self.callback, silent=self.silent) disable_localhost = True if disable_localhost: self.props = [ p for p in self.props if not p.startswith('assign_localhost') and not p.startswith('localhost_ip') ] if not self.thickconfig: try: del config['assign_localhost'] except KeyError: # They may not have specified this pass try: del config['localhost_ip'] except KeyError: # They may not have specified this pass else: config['assign_localhost'] = 0 config['localhost_ip'] = 0 try: value, config = iocjson.json_check_prop(key, value, config) config[key] = value except RuntimeError as err: iocjson.json_write(config) # Destroy counts on this. iocage_lib.ioc_destroy.IOCDestroy().destroy_jail(location) raise RuntimeError(f"***\n{err}\n***\n") except SystemExit: iocjson.json_write(config) # Destroy counts on this. iocage_lib.ioc_destroy.IOCDestroy().destroy_jail(location) exit(1) # We want these to represent reality on the FS iocjson.fix_properties(config) if not self.plugin: # TODO: Should we probably only write once and maybe at the end # of the function ? iocjson.json_write(config) # Just "touch" the fstab file, since it won't exist and write # /etc/hosts try: etc_hosts_ip_addr = config["ip4_addr"].split("|", 1)[-1].rsplit('/', 1)[0] except KeyError: # No ip4_addr specified during creation pass try: jail_uuid_short = jail_uuid.rsplit(".")[-2] jail_hostname = \ f"{jail_uuid}\t{jail_uuid_short}" except IndexError: # They supplied just a normal tag jail_uuid_short = jail_uuid jail_hostname = jail_uuid # If jail is template, the dataset would be readonly at this point if is_template: Dataset(os.path.join(self.pool, 'iocage/templates', jail_uuid)).set_property('readonly', 'off') if self.empty: open(f"{location}/fstab", "wb").close() config["release"] = "EMPTY" config["cloned_release"] = "EMPTY" iocjson.json_write(config) elif not self.clone: open(f"{location}/fstab", "wb").close() with open( f"{self.iocroot}/" f"{'releases' if not self.template else 'templates'}/" f"{self.release}/root/etc/hosts", "r") as _etc_hosts: with iocage_lib.ioc_common.open_atomic( f"{location}/root/etc/hosts", "w") as etc_hosts: # open_atomic will empty the file, we need these still. for line in _etc_hosts.readlines(): if line.startswith("127.0.0.1"): if config.get('assign_localhost' ) and not config.get('vnet'): l_ip = config.get('localhost_ip', 'none') l_ip = l_ip if l_ip != 'none' else \ iocage_lib.ioc_common.gen_unused_lo_ip() config['localhost_ip'] = l_ip iocjson.json_write(config) # If they are creating multiple jails, we want # this aliased before starting the jail su.run( ['ifconfig', 'lo0', 'alias', f'{l_ip}/32']) line = f'{l_ip}\t\tlocalhost' \ ' localhost.my.domain' \ f' {jail_uuid_short}\n' else: line = f'{line.rstrip()} {jail_uuid_short}\n' etc_hosts.write(line) else: # We want their IP to have the hostname at the end try: if config["ip4_addr"] != "none": final_line =\ f'{etc_hosts_ip_addr}\t{jail_hostname}\n' etc_hosts.write(final_line) except KeyError: # No ip4_addr specified during creation pass else: with open(clone_fstab, "r") as _clone_fstab: with iocage_lib.ioc_common.open_atomic(clone_fstab, "w") as _fstab: # open_atomic will empty the file, we need these still. for line in _clone_fstab.readlines(): _fstab.write(line.replace(clone_uuid, jail_uuid)) with open(clone_etc_hosts, "r") as _clone_etc_hosts: with iocage_lib.ioc_common.open_atomic(clone_etc_hosts, "w") as etc_hosts: # open_atomic will empty the file, we need these still. for line in _clone_etc_hosts.readlines(): etc_hosts.write(line.replace(clone_uuid, jail_uuid)) if not self.empty: self.create_rc(location, config["host_hostname"], config.get('basejail', 0)) if rtsold_enable == 'YES': iocage_lib.ioc_common.set_rcconf(location, "rtsold_enable", rtsold_enable) if self.basejail or self.plugin: basedirs = [ "bin", "boot", "lib", "libexec", "rescue", "sbin", "usr/bin", "usr/include", "usr/lib", "usr/libexec", "usr/sbin", "usr/share", "usr/libdata", "usr/lib32" ] if "-STABLE" in self.release: # HardenedBSD does not have this. basedirs.remove("usr/lib32") for bdir in basedirs: if "-RELEASE" not in self.release and "-STABLE" not in \ self.release: _type = "templates" else: _type = "releases" source = f"{self.iocroot}/{_type}/{self.release}/root/{bdir}" destination = f"{self.iocroot}/jails/{jail_uuid}/root/{bdir}" # This reduces the REFER of the basejail. # Just much faster by almost a factor of 2 than the builtins. su.Popen(["rm", "-r", "-f", destination]).communicate() os.mkdir(destination) iocage_lib.ioc_fstab.IOCFstab(jail_uuid, "add", source, destination, "nullfs", "ro", "0", "0", silent=True) config["basejail"] = 1 iocjson.json_write(config) if not self.plugin: if self.clone: msg = f"{jail_uuid} successfully cloned!" else: msg = f"{jail_uuid} successfully created!" iocage_lib.ioc_common.logit({ "level": "INFO", "message": msg }, _callback=self.callback, silent=self.silent) if self.pkglist: auto_config = config.get('dhcp') or \ config.get('ip_hostname') or \ config.get('nat') if config.get('ip4_addr', 'none') == "none" and \ config.get('ip6_addr', 'none') == "none" and \ not auto_config: iocage_lib.ioc_common.logit( { "level": "WARNING", "message": "You need an IP address for the jail to" " install packages!\n" }, _callback=self.callback, silent=self.silent) else: self.create_install_packages(jail_uuid, location) if start: iocage_lib.ioc_start.IOCStart(jail_uuid, location, silent=self.silent) if is_template: # We have to set readonly back, since we're done with our tasks Dataset(os.path.join(self.pool, 'iocage/templates', jail_uuid)).set_property('readonly', 'on') return jail_uuid
def rename(self, new_name, options=None): result = rename_dataset(self.name, new_name, options) if result: self.name = self.resource_name = new_name cache.reset() return result
def create(self, data): cache.reset() return create_dataset({'name': self.resource_name, **data})
def umount(self, force=True): cache.reset() return umount_dataset(self.resource_name, force)
def mount(self): cache.reset() return mount_dataset(self.resource_name)
def destroy(self, recursive=False, force=False): cache.reset() return destroy_zfs_resource(self.resource_name, recursive, force)