async def stop(self, force=False): """ Remove NIS_state entry entirely after stopping ypbind. This is so that the 'enable' checkbox becomes the sole source of truth regarding a service's state when it is disabled. """ state = await self.get_state() nis = await self.config() if not force: if state in ['LEAVING', 'JOINING']: raise CallError( f'Current state of NIS service is: [{state}]. Wait until operation completes.', errno.EBUSY) await self.__set_state(DSStatus['LEAVING']) await self.middleware.call('datastore.update', 'directoryservice.nis', nis['id'], {'nis_enable': False}) ypbind = await run(['/usr/sbin/service', 'ypbind', 'onestop'], check=False) if ypbind.returncode != 0: await self.__set_state(DSStatus['FAULTED']) errmsg = ypbind.stderr.decode().strip() if 'ypbind not running' not in errmsg: raise CallError( f'ypbind failed to stop: [{ypbind.stderr.decode().strip()}]' ) await self.middleware.call('cache.pop', 'NIS_State') await self.middleware.call('etc.generate', 'rc') await self.middleware.call('etc.generate', 'pam') await self.middleware.call('etc.generate', 'hostname') await self.middleware.call('etc.generate', 'nss') await self.__set_state(DSStatus['DISABLED']) self.logger.debug( f'NIS service successfully stopped. Setting state to DISABLED.') return True
def read_plugin_pkg_db(self, db, pkg): try: conn = sqlite3.connect(db) except sqlite3.Error as e: raise CallError(e) with conn: cur = conn.cursor() cur.execute( f'SELECT * FROM packages WHERE origin="{pkg}" OR name="{pkg}"' ) rows = cur.fetchall() return rows
def update_zfs_default(root): with libzfs.ZFS() as zfs: existing_pools = [p.name for p in zfs.pools] for i in ['freenas-boot', 'boot-pool']: if i in existing_pools: boot_pool = i break else: raise CallError(f'Failed to locate valid boot pool. Pools located were: {", ".join(existing_pools)}') with libzfs.ZFS() as zfs: disks = [disk.replace("/dev/", "") for disk in zfs.get(boot_pool).disks] mapping = {} for dev in filter( lambda d: not d.sys_name.startswith("sr") and d.get("DEVTYPE") in ("disk", "partition"), pyudev.Context().list_devices(subsystem="block") ): if dev.get("DEVTYPE") == "disk": mapping[dev.sys_name] = dev.get("ID_BUS") elif dev.get("ID_PART_ENTRY_UUID"): parent = dev.find_parent("block") mapping[dev.sys_name] = parent.get("ID_BUS") mapping[os.path.join("disk/by-partuuid", dev.get("ID_PART_ENTRY_UUID"))] = parent.get("ID_BUS") has_usb = False for dev in disks: if mapping.get(dev) == "usb": has_usb = True break zfs_config_path = os.path.join(root, "etc/default/zfs") with open(zfs_config_path) as f: original_config = f.read() lines = original_config.rstrip().split("\n") zfs_var_name = "ZFS_INITRD_POST_MODPROBE_SLEEP" lines = [line for line in lines if not line.startswith(f"{zfs_var_name}=")] if has_usb: lines.append(f"{zfs_var_name}=15") new_config = "\n".join(lines) + "\n" if new_config != original_config: with open(zfs_config_path, "w") as f: f.write(new_config) return True return False
async def start(self, job): """ Refuse to start service if the service is alreading in process of starting or stopping. If state is 'HEALTHY' or 'FAULTED', then stop the service first before restarting it to ensure that the service begins in a clean state. """ ldap_state = await self.middleware.call('ldap.get_state') if ldap_state in ['LEAVING', 'JOINING']: raise CallError(f'LDAP state is [{ldap_state}]. Please wait until directory service operation completes.', errno.EBUSY) job.set_progress(0, 'Preparing to configure LDAP directory service.') ldap = await self.direct_update({"enable": True}) if ldap['kerberos_realm']: job.set_progress(5, 'Starting kerberos') await self.middleware.call('kerberos.start') job.set_progress(15, 'Generating configuration files') await self.middleware.call('etc.generate', 'rc') await self.middleware.call('etc.generate', 'ldap') await self.middleware.call('etc.generate', 'pam') job.set_progress(30, 'Starting nslcd service') if not await self.nslcd_status(): await self.nslcd_cmd('start') else: await self.nslcd_cmd('restart') job.set_progress(50, 'Reconfiguring SMB service') await self.middleware.call('smb.initialize_globals') await self.synchronize() job.set_progress(60, 'Reconfiguring idmap service') await self.middleware.call('idmap.synchronize') if ldap['has_samba_schema']: job.set_progress(70, 'Storing LDAP password for SMB configuration') await self.middleware.call('smb.store_ldap_admin_password') await self._service_change('cifs', 'restart') await self.set_state(DSStatus['HEALTHY']) job.set_progress(80, 'Reloading directory service cache.') await self.middleware.call('service.start', 'dscache') ha_mode = await self.middleware.call('smb.get_smb_ha_mode') if ha_mode == 'CLUSTERED': job.set_progress(90, 'Reloading LDAP service on other cluster nodes') cl_job = await self.middleware.call('clusterjob.submit', 'ldap.cluster_reload', 'START') await cl_job.wait(raise_error=True) job.set_progress(100, 'LDAP directory service started.')
async def __ypwhich(self): """ The return code from ypwhich is not a reliable health indicator. For example, RPC failure will return 0. There are edge cases where ypwhich can hang when NIS is misconfigured. """ nis = await self.config() ypwhich = await run(['/usr/bin/ypwhich'], check=False) if ypwhich.returncode != 0: if nis['enable']: await self.__set_state(DSStatus['FAULTED']) self.logger.debug(f'NIS status check returned [{ypwhich.stderr.decode().strip()}]. Setting state to FAULTED.') return False if ypwhich.stderr: await self.__set_state(DSStatus['FAULTED']) raise CallError(f'NIS status check returned [{ypwhich.stderr.decode().strip()}]. Setting state to FAULTED.') return True
async def wrap(self, subjob): """ Wrap a job in another job, proxying progress and result/error. This is useful when we want to run a job inside a job. :param subjob: The job to wrap. """ while not subjob.time_finished: try: await subjob.wait(1) except asyncio.TimeoutError: pass self.set_progress(**subjob.progress) if subjob.exception: raise CallError(subjob.exception) return subjob.result
async def _sharesec(self, **kwargs): """ wrapper for sharesec(1). This manipulates share permissions on SMB file shares. The permissions are stored in share_info.tdb, and apply to the share as a whole. This is in contrast with filesystem permissions, which define the permissions for a file or directory, and in the latter case may also define permissions inheritance rules for newly created files in the directory. The SMB Share ACL only affects access through the SMB protocol. """ action = kwargs.get('action') share = kwargs.get('share', '') args = kwargs.get('args', '') sharesec = await run([SMBCmd.SHARESEC.value, share, action, args], check=False) if sharesec.returncode != 0: raise CallError(f'sharesec {action} failed with error: {sharesec.stderr.decode()}') return sharesec.stdout.decode()
def wait_sync(self, raise_error=False): """ Synchronous method to wait for a job in another thread. """ fut = asyncio.run_coroutine_threadsafe(self._finished.wait(), self.loop) event = threading.Event() def done(_): event.set() fut.add_done_callback(done) event.wait() if raise_error: if self.error: raise CallError(self.error) return self.result
async def groupmap_listmem(self, sid): payload = json.dumps({"alias": sid}) lm = await run([ SMBCmd.NET.value, "--json", "groupmap", "listmem", payload ], check=False) # Command will return ENOENT when fails with STATUS_NO_SUCH_ALIAS if lm.returncode == 2: return [] elif lm.returncode != 0: raise CallError(f"Failed to list membership of alias [{sid}]: " f"{lm.stderr.decode()}") output = json.loads(lm.stdout.decode()) await self.json_check_version(output['version']) return [x["sid"] for x in output['members']]
async def _service_change(self, service, verb): svc_state = (await self.middleware.call( 'service.query', [('service', '=', service)], {'get': True} ))['state'].lower() if svc_state == 'running': started = await self.middleware.call(f'service.{verb}', service, {'onetime': True}) if not started: raise CallError( f'The {service} service failed to start', CallError.ESERVICESTARTFAILURE, [service], )
async def started(self): ldap = await self.config() if not ldap['enable']: return False try: ret = await asyncio.wait_for(self.middleware.call('ldap.get_root_DSE', ldap), timeout=ldap['timeout']) except asyncio.TimeoutError: raise CallError(f'LDAP status check timed out after {ldap["timeout"]} seconds.', errno.ETIMEDOUT) if ret: await self.__set_state(DSStatus['HEALTHY']) else: await self.__set_state(DSStatus['FAULTED']) return True if ret else False
async def diff_middleware_and_registry(self, share, data): if share is None: raise CallError('Share name must be specified.') if data is None: data = await self.middleware.call('sharing.smb.query', [('name', '=', share)], {'get': True}) share_conf = await self.share_to_smbconf(data) reg_conf = await self.reg_showshare(share if not data['home'] else 'homes') s_keys = set(share_conf.keys()) r_keys = set(reg_conf.keys()) intersect = s_keys.intersection(r_keys) return { 'added': {x: share_conf[x] for x in s_keys - r_keys}, 'removed': {x: reg_conf[x] for x in r_keys - s_keys}, 'modified': {x: (share_conf[x], reg_conf[x]) for x in intersect if share_conf[x] != reg_conf[x]}, }
async def net_keytab_add_update_ads(self, service_class): if not (await self.middleware.call('nfs.config'))['v4_krb']: return False cmd = [ SMBCmd.NET.value, '--use-kerberos', 'required', '--use-krb5-ccache', krb5ccache.SYSTEM.value, 'ads', 'keytab', 'add_update_ads', service_class ] netads = await run(cmd, check=False) if netads.returncode != 0: raise CallError( 'failed to set spn entry ' f'[{service_class}]: {netads.stdout.decode().strip()}') return True
async def validate_admin_groups(self, sid): """ Check if group mapping already exists because 'net groupmap addmem' will fail if the mapping exists. Remove any entries that should not be present. Extra entries here can pose a significant security risk. The only default entry will have a RID value of "512" (Domain Admins). In LDAP environments, members of S-1-5-32-544 cannot be removed without impacting the entire LDAP environment because this alias exists on the remote LDAP server. """ sid_is_present = False if await self.middleware.call('ldap.get_state') != 'DISABLED': self.logger.debug( "As a safety precaution, extra alias entries for S-1-5-32-544" "cannot be removed while LDAP is enabled. Skipping removal.") return True listmem = await run( [SMBCmd.NET.value, 'groupmap', 'listmem', 'S-1-5-32-544'], check=False) member_list = listmem.stdout.decode() if not member_list: return True for group in member_list.splitlines(): group = group.strip() if group == sid: self.logger.debug( "SID [%s] is already a member of BUILTIN\\administrators", sid) sid_is_present = True if group.rsplit('-', 1)[-1] != "512" and group != sid: self.logger.debug(f"Removing {group} from local admins group.") rem = await run([ SMBCmd.NET.value, 'groupmap', 'delmem', 'S-1-5-32-544', group ], check=False) if rem.returncode != 0: raise CallError( f'Failed to remove sid [{sid}] from S-1-5-32-544: {rem.stderr.decode()}' ) if sid_is_present: return False else: return True
def getparm(self, parm, section): """ Get a parameter from the smb4.conf file. This is more reliable than 'testparm --parameter-name'. testparm will fail in a variety of conditions without returning the parameter's value. """ try: if section.upper() == 'GLOBAL': return param.LoadParm(SMBPath.GLOBALCONF.platform()).get( parm, section) else: return self.middleware.call_sync('sharing.smb.reg_getparm', section, parm) except Exception as e: raise CallError( f'Attempt to query smb4.conf parameter [{parm}] failed with error: {e}' )
def port_is_listening(self, host, port, timeout=1): ret = False s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if timeout: s.settimeout(timeout) try: s.connect((host, port)) ret = True except Exception as e: raise CallError(e) finally: s.close() return ret
async def groupmap_delete(self, ntgroup=None, sid=None): if not ntgroup and not sid: raise CallError("ntgroup or sid is required") if ntgroup: target = f"ntgroup={ntgroup}" elif sid: target = f"sid={sid}" gm_delete = await run( [SMBCmd.NET.value, '-d' '0', 'groupmap', 'delete', target], check=False) if gm_delete.returncode != 0: self.logger.debug( f'Failed to delete groupmap for [{target}]: ({gm_delete.stderr.decode()})' )
def export(self, identifier, starttime, endtime, aggregate=True): for rrd_file in self.get_rrd_files(identifier): cp = subprocess.run([ 'rrdtool', 'info', '--daemon', 'unix:/var/run/rrdcached.sock', rrd_file, ], capture_output=True, encoding='utf-8') if m := RE_LAST_UPDATE.search(cp.stdout): last_update = int(m.group(1)) now = time.time() if last_update > now + 1800: # Tolerance for small system time adjustments raise CallError( f"RRD file {os.path.relpath(rrd_file, self._base_path)} has update time in the future. " f"Data collection will be paused for {humanfriendly.format_timespan(last_update - now)}.", ErrnoMixin.EINVALIDRRDTIMESTAMP, )
async def retrieve(self, ds, data, options): who_str = data.get('who') who_id = data.get('id') if who_str is None and who_id is None: raise CallError("`who` or `id` entry is required to uniquely " "identify the entry to be retrieved.") tdb_name = f'{ds.lower()}_{data["idtype"].lower()}' prefix = "NAME" if who_str else "ID" tdb_key = f'{prefix}_{who_str if who_str else who_id}' try: entry = await self.middleware.call("tdb.fetch", { "name": tdb_name, "key": tdb_key }) except MatchNotFound: entry = None if not entry and options['synthesize']: """ if cache lacks entry, create one from passwd / grp info, insert into cache and return synthesized value. get_uncached_* will raise KeyError if NSS lookup fails. """ try: if data['idtype'] == 'USER': pwdobj = await self.middleware.call( 'dscache.get_uncached_user', who_str, who_id) entry = await self.middleware.call('idmap.synthetic_user', ds.lower(), pwdobj) else: grpobj = await self.middleware.call( 'dscache.get_uncached_group', who_str, who_id) entry = await self.middleware.call('idmap.synthetic_group', ds.lower(), grpobj) await self.insert(ds, data['idtype'], entry) except KeyError: entry = None elif not entry: raise KeyError(who_str if who_str else who_id) return entry
def do_update(self, jail, options): """Sets a jail property.""" plugin = options.pop("plugin") _, _, iocage = self.check_jail_existence(jail) name = options.pop("name", None) for prop, val in options.items(): p = f"{prop}={val}" try: iocage.set(p, plugin) except RuntimeError as err: raise CallError(err) if name: iocage.rename(name) return True
async def _update_service(self, old, new): await self.middleware.call( 'datastore.update', f'services.{self._config.service_model or self._config.service}', old['id'], new, {'prefix': self._config.datastore_prefix}) enabled = (await self.middleware.call( 'datastore.query', 'services.services', [('srv_service', '=', self._config.service)], {'get': True}))['srv_enable'] started = await self.middleware.call( f'service.{self._config.service_verb}', self._config.service, {'onetime': False}) if enabled and not started: raise CallError( f'The {self._config.service} service failed to start', CallError.ESERVICESTARTFAILURE)
async def process_queue(self, job): gl_enabled = (await self.middleware.call('service.query', [('service', '=', 'glusterd')], {'get': True}))['enable'] if not gl_enabled: return node = (await self.middleware.call('ctdb.general.status', {'all_nodes': False}))[0] if node['flags_str'] != 'OK': CallError( f'Cannot reload directory service. Node health: {node["flags_str"]}' ) job_list = await self.list() for idx, entry in enumerate(job_list.get(node["pnn"], [])): p = (100 / len(job_list) * idx) job.set_progress( p, f'Processing queued job for [{entry["method"]}].') if entry['status'] == CLStatus.EXPIRED.name: continue await self.update_status(entry['key'], CLStatus.RUNNING.name, entry['timeout']) try: if entry['payload']: rv = await self.middleware.call(entry['method'], entry['payload']) else: rv = await self.middleware.call(entry['method']) except Exception: self.logger.warning( "Cluster cached job for method [%s] failed.", entry['method'], exc_info=True) if entry['job']: rv = await rv.wait() await self.middleware.call('clustercache.pop', entry['key']) job.set_progress(100, 'Finished processing queue.')
async def groupmap_add(self, group, passdb_backend=None): """ Map Unix group to NT group. This is required for group members to be able to access the SMB share. Name collisions with well-known and builtin groups must be avoided. Mapping groups with the same names as users should also be avoided. """ if passdb_backend is None: passdb_backend = await self.middleware.call( 'smb.getparm', 'passdb backend', 'global') if passdb_backend != 'tdbsam': return if group in SMBBuiltin.unix_groups(): return await self.add_builtin_group(group) disallowed_list = ['USERS', 'ADMINISTRATORS', 'GUESTS'] existing_groupmap = await self.groupmap_list() if existing_groupmap.get(group): self.logger.debug( 'Setting group map for %s is not permitted. ' 'Entry already exists.', group) return False if group.upper() in disallowed_list: self.logger.debug( 'Setting group map for %s is not permitted. ' 'Entry mirrors existing builtin groupmap.', group) return False next_rid = str(await self.middleware.call("smb.get_next_rid")) gm_add = await run([ SMBCmd.NET.value, '-d', '0', 'groupmap', 'add', 'type=local', f'rid={next_rid}', f'unixgroup={group}', f'ntgroup={group}' ], check=False) if gm_add.returncode != 0: raise CallError( f'Failed to generate groupmap for [{group}]: ({gm_add.stderr.decode()})' )
async def do_update(self, id, data): """ Update AFP share `id`. """ verrors = ValidationErrors() old = await self.middleware.call( 'datastore.query', self._config.datastore, [('id', '=', id)], { 'extend': self._config.datastore_extend, 'prefix': self._config.datastore_prefix, 'get': True }) path = data.get('path') new = old.copy() new.update(data) await self.clean(new, 'sharingafp_update', verrors, id=id) await self.validate(new, 'sharingafp_update', verrors, old=old) if path: await check_path_resides_within_volume(verrors, self.middleware, 'sharingafp_create.path', path) if verrors: raise verrors if path and not os.path.exists(path): try: os.makedirs(path) except OSError as e: raise CallError(f'Failed to create {path}: {e}') await self.compress(new) await self.middleware.call('datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self.extend(new) await self._service_change('afp', 'reload') return new
async def change_trust_account_pw(self): """ Force an update of the AD machine account password. This can be used to refresh the Kerberos principals in the server's system keytab. """ await self.middleware.call("kerberos.check_ticket") workgroup = (await self.middleware.call('smb.config'))['workgroup'] cmd = [ SMBCmd.NET.value, '--use-kerberos', 'required', '--use-krb5-ccache', krb5ccache.SYSTEM.value, '-w', workgroup, 'ads', 'changetrustpw', ] netads = await run(cmd, check=False) if netads.returncode != 0: raise CallError( f"Failed to update trust password: [{netads.stderr.decode().strip()}] " f"stdout: [{netads.stdout.decode().strip()}] " )
async def add_nfs_spn(self, job, netbiosname, domain, check_health=True, update_keytab=False): if check_health: ad_state = await self.middleware.call('activedirectory.get_state') if ad_state != DSStatus.HEALTHY.name: raise CallError( "Service Principal Names that are registered in Active Directory " "may only be manipulated when the Active Directory Service is Healthy. " f"Current state is: {ad_state}") ok = await self.net_keytab_add_update_ads('nfs') if not ok: return False await self.middleware.call('kerberos.keytab.store_samba_keytab') return True
def progress_callback(content): level = content['level'] msg = content['message'].strip('\n') if job.progress['percent'] == 90: for split_msg in msg.split('\n'): fetch_output['install_notes'].append(split_msg) if level == 'EXCEPTION': fetch_output['error'] = True raise CallError(msg) job.set_progress(None, msg) if ' These pkgs will be installed:' in msg: job.set_progress(50, msg) elif 'Installing plugin packages:' in msg: job.set_progress(75, msg) elif 'Command output:' in msg: job.set_progress(90, msg)
async def do_update(self, id, data): """ Update SMB Share of `id`. """ verrors = ValidationErrors() path = data.get('path') default_perms = data.pop('default_permissions', False) old = await self.middleware.call( 'datastore.query', self._config.datastore, [('id', '=', id)], { 'extend': self._config.datastore_extend, 'prefix': self._config.datastore_prefix, 'get': True }) new = old.copy() new.update(data) new['vuid'] = await self.generate_vuid(new['timemachine'], new['vuid']) await self.clean(new, 'sharingsmb_update', verrors, id=id) await self.validate(new, 'sharingsmb_update', verrors, old=old) if verrors: raise verrors if path and not os.path.exists(path): try: os.makedirs(path) except OSError as e: raise CallError(f'Failed to create {path}: {e}') await self.compress(new) await self.middleware.call('datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self.extend(new) # same here ? await self._service_change('cifs', 'reload') await self.apply_default_perms(default_perms, path, data['home']) return new
async def validate_admin_groups(self, sid): """ Check if group mapping already exists because 'net groupmap addmem' will fail if the mapping exists. Remove any entries that should not be present. Extra entries here can pose a significant security risk. The only default entry will have a RID value of "512" (Domain Admins). In LDAP environments, members of S-1-5-32-544 cannot be removed without impacting the entire LDAP environment because this alias exists on the remote LDAP server. """ sid_is_present = False ldap = await self.middleware.call('datastore.config', 'directoryservice.ldap') if ldap['ldap_enable']: self.logger.debug("As a safety precaution, extra alias entries for S-1-5-32-544 cannot be removed while LDAP is enabled. Skipping removal.") return True proc = await Popen( ['/usr/local/bin/net', 'groupmap', 'listmem', 'S-1-5-32-544'], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) member_list = (await proc.communicate())[0].decode() if not member_list: return True for group in member_list.splitlines(): group = group.strip() if group == sid: self.logger.debug(f"SID [{sid}] is already a member of BUILTIN\\administrators") sid_is_present = True if group.rsplit('-', 1)[-1] != "512" and group != sid: self.logger.debug(f"Removing {group} from local admins group.") rem = await Popen( ['/usr/local/bin/net', 'groupmap', 'delmem', 'S-1-5-32-544', group], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) remout = await rem.communicate() if rem.returncode != 0: raise CallError(f'Failed to remove sid [{sid}] from S-1-5-32-544: {remout[1].decode()}') if sid_is_present: return False else: return True
async def do_update(self, id, data): verrors = ValidationErrors() path = data['path'] default_perms = data.pop('default_permissions', False) old = await self.middleware.call( 'datastore.query', self._config.datastore, [('id', '=', id)], { 'extend': self._config.datastore_extend, 'prefix': self._config.datastore_prefix, 'get': True }) new = old.copy() new.update(data) await self.clean(new, 'sharingcifs_update', verrors, id=id) await self.validate(new, 'sharingcifs_update', verrors, old=old) await check_path_resides_within_volume(verrors, self.middleware, "sharingcifs_update.path", path) if verrors: raise verrors if path and not os.path.exists(path): try: os.makedirs(path) except OSError as e: raise CallError(f'Failed to create {path}: {e}') await self.compress(new) await self.set_storage_tasks(new) await self.middleware.call('datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self.extend(new) await self.middleware.call('service.reload', 'cifs') await self.apply_default_perms(default_perms, path) return new