async def do_update(self, job, data): config = await self.config() new = config.copy() new.update(data) verrors = ValidationErrors() if not await self.middleware.call('zfs.pool.query', [('name', '=', data['pool'])]): verrors.add('sysdataset_update.pool', f'Pool "{data["pool"]}" not found', errno.ENOENT) if verrors: raise verrors new['syslog_usedataset'] = new['syslog'] new['rrd_usedataset'] = new['rrd'] await self.middleware.call('datastore.update', 'system.systemdataset', config['id'], new, {'prefix': 'sys_'}) if 'pool' in data and config['pool'] and data['pool'] != config['pool']: await self.migrate(config['pool'], data['pool']) if config['rrd'] != new['rrd']: # Stop collectd to flush data await self.middleware.call('service.stop', 'collectd') await self.setup() if config['syslog'] != new['syslog']: await self.middleware.call('service.restart', 'syslogd') if config['rrd'] != new['rrd']: await self.rrd_toggle() await self.middleware.call('service.restart', 'collectd') return config
async def validate_data(self, data, schema): verrors = ValidationErrors() user = data.get('user') if user: # Windows users can have spaces in their usernames # http://www.freebsd.org/cgi/query-pr.cgi?pr=164808 if ' ' in user: verrors.add( f'{schema}.user', 'Usernames cannot have spaces' ) elif not ( await self.middleware.call( 'notifier.get_user_object', user ) ): verrors.add( f'{schema}.user', 'Specified user does not exist' ) return verrors, data
async def do_create(self, data): """ Create a new group. If `gid` is not provided it is automatically filled with the next one available. `allow_duplicate_gid` allows distinct group names to share the same gid. `users` is a list of user ids (`id` attribute from `user.query`). """ verrors = ValidationErrors() await self.__common_validation(verrors, data, 'group_create') verrors.check() if not data.get('gid'): data['gid'] = await self.get_next_gid() group = data.copy() group['group'] = group.pop('name') users = group.pop('users', []) pk = await self.middleware.call('datastore.insert', 'account.bsdgroups', group, {'prefix': 'bsdgrp_'}) for user in users: await self.middleware.call('datastore.insert', 'account.bsdgroupmembership', {'bsdgrpmember_group': pk, 'bsdgrpmember_user': user}) await self.middleware.call('service.reload', 'user') await self.middleware.call('smb.groupmap_add', data['name']) return pk
async def do_update(self, id, data): """ Update kerberos keytab by id. """ old = await self._get_instance(id) new = old.copy() new.update(data) verrors = ValidationErrors() verrors.add_child('kerberos_principal_update', await self._validate(new)) if verrors: raise verrors data = await self.kerberos_keytab_compress(data) await self.middleware.call( 'datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix} ) await self.middleware.call('etc.generate', 'kerberos') return await self._get_instance(id)
def __rquery_to_start_end(self, query): unit = query.get('unit') if unit: verrors = ValidationErrors() for i in ('start', 'end'): if i in query: verrors.add( f'reporting_query.{i}', f'{i!r} should only be used if "unit" attribute is not provided.', ) verrors.check() else: if 'start' not in query: unit = 'HOURLY' else: starttime = query['start'] endtime = query.get('end') or 'now' if unit: unit = unit[0].lower() page = query['page'] starttime = f'end-{page + 1}{unit}' if not page: endtime = 'now' else: endtime = f'now-{page}{unit}' return starttime, endtime
async def do_update(self, data): old = await self.config() new = old.copy() new.update(data) verrors = ValidationErrors() if not new["v4"] and new["v4_v3owner"]: verrors.add("nfs_update.v4_v3owner", "This option requires enabling NFSv4") if new["v4_v3owner"] and new["userd_manage_gids"]: verrors.add( "nfs_update.userd_manage_gids", "This option is incompatible with NFSv3 ownership model for NFSv4") if verrors: raise verrors self.nfs_compress(new) await self._update_service(old, new) self.nfs_extend(new) return new
async def validate_data(self, data, schema): verrors = ValidationErrors() smart_tests = await self.query(filters=[('type', '=', data['type'])]) configured_disks = [d for test in smart_tests for d in test['disks']] disks_dict = {disk['identifier']: disk['name'] for disk in (await self.middleware.call('disk.query'))} disks = data.get('disks') used_disks = [] invalid_disks = [] for disk in disks: if disk in configured_disks: used_disks.append(disks_dict[disk]) if disk not in disks_dict.keys(): invalid_disks.append(disk) if used_disks: verrors.add( f'{schema}.disks', f'The following disks already have tests for this type: {", ".join(used_disks)}' ) if invalid_disks: verrors.add( f'{schema}.disks', f'The following disks are invalid: {", ".join(invalid_disks)}' ) return verrors
async def do_update(self, id, data): """ Update idmap to backend mapping by id. """ old = await self._get_instance(id) new = old.copy() new.update(data) new = await self.middleware.call('idmap.common_backend_compress', new) verrors = ValidationErrors() if new['domain'] in [dstype.DS_TYPE_LDAP.value, dstype.DS_TYPE_DEFAULT_DOMAIN.value]: if new['idmap_backend'] not in ['ldap', 'tdb']: verrors.add( 'domaintobackend_create.idmap_backend', f'idmap backend [{new["idmap_backend"]}] is not appropriate for the system domain type {dstype[new["domain"]]}' ) if verrors: raise verrors await self.middleware.call( 'datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix} ) updated_entry = await self._get_instance(id) try: await self.middleware.call('idmap.get_or_create_idmap_by_domain', updated_entry['domain']['domain_name']) except Exception as e: self.logger.debug('Failed to generate new idmap backend: %s', e) return updated_entry
async def do_create(self, data): """ Create a kerberos keytab. Uploaded keytab files will be merged with the system keytab under /etc/krb5.keytab. `file` b64encoded kerberos keytab `name` name for kerberos keytab """ verrors = ValidationErrors() verrors.add_child('kerberos_principal_create', await self._validate(data)) if verrors: raise verrors data = await self.kerberos_keytab_compress(data) data["id"] = await self.middleware.call( "datastore.insert", self._config.datastore, data, { "prefix": self._config.datastore_prefix }, ) await self.middleware.call('etc.generate', 'kerberos') return await self._get_instance(data['id'])
async def do_create(self, data): """ Create AFP share. `allow`, `deny`, `ro`, and `rw` are lists of users and groups. Groups are designated by an @ prefix. `hostsallow` and `hostsdeny` are lists of hosts and/or networks. """ verrors = ValidationErrors() path = data['path'] await self.clean(data, 'sharingafp_create', verrors) await self.validate(data, 'sharingafp_create', verrors) await check_path_resides_within_volume( verrors, self.middleware, 'sharingafp_create.path', path) verrors.check() 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(data) data['id'] = await self.middleware.call( 'datastore.insert', self._config.datastore, data, {'prefix': self._config.datastore_prefix}) await self.extend(data) await self._service_change('afp', 'reload') return data
async def do_create(self, data): """ Create an entry in the idmap backend table. `unix_primary_group` If True, the primary group membership is fetched from the LDAP attributes (gidNumber). If False, the primary group membership is calculated via the "primaryGroupID" LDAP attribute. `unix_nss_info` if True winbind will retrieve the login shell and home directory from the LDAP attributes. If False or if the AD LDAP entry lacks the SFU attributes the smb4.conf parameters `template shell` and `template homedir` are used. `schema_mode` Defines the schema that idmap_ad should use when querying Active Directory regarding user and group information. This can be either the RFC2307 schema support included in Windows 2003 R2 or the Service for Unix (SFU) schema. For SFU 3.0 or 3.5 please choose "SFU", for SFU 2.0 please choose "SFU20". The behavior of primary group membership is controlled by the unix_primary_group option. """ verrors = ValidationErrors() data = await self.middleware.call('idmap.common_backend_compress', data) verrors.add_child('idmap_ad_create', await self.middleware.call('idmap._common_validate', data)) if verrors: raise verrors data["id"] = await self.middleware.call( "datastore.insert", self._config.datastore, data, { "prefix": self._config.datastore_prefix }, ) return await self._get_instance(data['id'])
async def do_create(self, data): """ Create a new kerberos realm. This will be automatically populated during the domain join process in an Active Directory environment. Kerberos realm names are case-sensitive, but convention is to only use upper-case. Entries for kdc, admin_server, and kpasswd_server are not required. If they are unpopulated, then kerberos will use DNS srv records to discover the correct servers. The option to hard-code them is provided due to AD site discovery. Kerberos has no concept of Active Directory sites. This means that middleware performs the site discovery and sets the kerberos configuration based on the AD site. """ verrors = ValidationErrors() verrors.add_child('kerberos_realm_create', await self._validate(data)) if verrors: raise verrors data = await self.kerberos_compress(data) data["id"] = await self.middleware.call( "datastore.insert", self._config.datastore, data, { "prefix": self._config.datastore_prefix }, ) await self.middleware.call('etc.generate', 'kerberos') await self.middleware.call('service.restart', 'cron') return await self._get_instance(data['id'])
async def do_update(self, data): """ Update AFP service settings. `bindip` is a list of IPs to bind AFP to. Leave blank (empty list) to bind to all available IPs. `map_acls` defines how to map the effective permissions of authenticated users. RIGHTS - Unix-style permissions MODE - ACLs NONE - Do not map `chmod_request` defines advanced permission control that deals with ACLs. PRESERVE - Preserve ZFS ACEs for named users and groups or POSIX ACL group mask SIMPLE - Change permission as requested without any extra steps IGNORE - Permission change requests are ignored """ old = await self.config() new = old.copy() new.update(data) verrors = ValidationErrors() if new['dbpath']: await check_path_resides_within_volume( verrors, self.middleware, 'afp_update.dbpath', new['dbpath'], ) verrors.check() new = await self.compress(new) await self._update_service(old, new) return await self.config()
async def _validate(self, data): verrors = ValidationErrors() realms = await self.query() for realm in realms: if realm['realm'].upper() == data['realm'].upper(): verrors.add(f'kerberos_realm', f'kerberos realm with name {realm["realm"]} already exists.') return verrors
async def do_update(self, id, data): """ Update a kerberos realm by id. This will be automatically populated during the domain join process in an Active Directory environment. Kerberos realm names are case-sensitive, but convention is to only use upper-case. """ old = await self._get_instance(id) new = old.copy() new.update(data) verrors = ValidationErrors() verrors.add_child('kerberos_realm_update', await self._validate(new)) if verrors: raise verrors data = await self.kerberos_compress(new) await self.middleware.call( 'datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix} ) await self.middleware.call('etc.generate', 'kerberos') return await self._get_instance(id)
async def do_update(self, data): """ Update Mail Service Configuration. `fromemail` is used as a sending address which the mail server will use for sending emails. `outgoingserver` is the hostname or IP address of SMTP server used for sending an email. `security` is type of encryption desired. `smtp` is a boolean value which when set indicates that SMTP authentication has been enabled and `user`/`pass` are required attributes now. """ config = await self.config() new = config.copy() new.update(data) new['security'] = new['security'].lower() # Django Model compatibility verrors = ValidationErrors() if new['smtp'] and new['user'] == '': verrors.add( 'mail_update.user', 'This field is required when SMTP authentication is enabled', ) self.__password_verify(new['pass'], 'mail_update.pass', verrors) if verrors: raise verrors await self.middleware.call('datastore.update', 'system.email', config['id'], new, {'prefix': 'em_'}) return await self.config()
def do_create(self, data): """ Creates a ZFS dataset. """ verrors = ValidationErrors() if '/' not in data['name']: verrors.add('name', 'You need a full name, e.g. pool/newdataset') if verrors: raise verrors properties = data.get('properties') or {} sparse = properties.pop('sparse', False) params = {} for k, v in data['properties'].items(): params[k] = v try: with libzfs.ZFS() as zfs: pool = zfs.get(data['name'].split('/')[0]) pool.create(data['name'], params, fstype=getattr(libzfs.DatasetType, data['type']), sparse_vol=sparse) except libzfs.ZFSException as e: self.logger.error('Failed to create dataset', exc_info=True) raise CallError(f'Failed to create dataset: {e}')
async def common_validation(self, data, schema_name): verrors = ValidationErrors() ip = data.get('ip') if ip: await resolve_hostname(self.middleware, verrors, f'{schema_name}.ip', ip) management_ip = data.get('management_ip') if management_ip and management_ip not in (await self.get_management_ip_choices()): verrors.add( f'{schema_name}.management_ip', 'Please select a valid IP for your TrueNAS system' ) action = data.get('action') if action and action != 'UNINSTALL': if ( not (await self.middleware.call('vcenteraux.config'))['enable_https'] and (await self.middleware.call('system.general.config'))['ui_protocol'].upper() == 'HTTPS' ): verrors.add( f'{schema_name}.action', 'Please enable vCenter plugin over HTTPS' ) return verrors
async def validate_data(self, data, schema): verrors = ValidationErrors() pool_pk = data.get('pool') if pool_pk: pool_obj = await self.middleware.call( 'datastore.query', 'storage.volume', [('id', '=', pool_pk)] ) if len(pool_obj) == 0: verrors.add( f'{schema}.pool', 'The specified volume does not exist' ) elif ( 'id' not in data.keys() or ( 'id' in data.keys() and 'original_pool_id' in data.keys() and pool_pk != data['original_pool_id'] ) ): scrub_obj = await self.query(filters=[('pool', '=', pool_pk)]) if len(scrub_obj) != 0: verrors.add( f'{schema}.pool', 'A scrub with this pool already exists' ) return verrors, data
async def do_create(self, data): """ Creates a dataset/zvol. `volsize` is required for type=VOLUME and is supposed to be a multiple of the block size. """ verrors = ValidationErrors() if '/' not in data['name']: verrors.add('pool_dataset_create.name', 'You need a full name, e.g. pool/newdataset') else: await self.__common_validation(verrors, 'pool_dataset_create', data, 'CREATE') if verrors: raise verrors props = {} for i, real_name, transform in ( ('atime', None, str.lower), ('casesensitivity', None, str.lower), ('comments', 'org.freenas:description', None), ('compression', None, str.lower), ('copies', None, lambda x: str(x)), ('deduplication', 'dedup', str.lower), ('exec', None, str.lower), ('quota', None, _none), ('readonly', None, str.lower), ('recordsize', None, None), ('refquota', None, _none), ('refreservation', None, _none), ('reservation', None, _none), ('snapdir', None, str.lower), ('sparse', None, None), ('sync', None, str.lower), ('volblocksize', None, None), ('volsize', None, lambda x: str(x)), ): if i not in data: continue name = real_name or i props[name] = data[i] if not transform else transform(data[i]) await self.middleware.call('zfs.dataset.create', { 'name': data['name'], 'type': data['type'], 'properties': props, }) data['id'] = data['name'] await self.middleware.call('zfs.dataset.mount', data['name']) if data['type'] == 'FILESYSTEM': await self.middleware.call( 'notifier.change_dataset_share_type', data['name'], data.get('share_type', 'UNIX').lower() ) return await self._get_instance(data['id'])
async def add_admin_group(self, admin_group=None, check_deferred=False): """ Add a local or directory service group to BUILTIN\\Administrators (S-1-5-32-544) Members of this group have elevated privileges to the Samba server (ability to take ownership of files, override ACLs, view and modify user quotas, and administer the server via the Computer Management MMC Snap-In. Unfortuntely, group membership must be managed via "net groupmap listmem|addmem|delmem", which requires that winbind be running when the commands are executed. In this situation, net command will fail with WBC_ERR_WINBIND_NOT_AVAILABLE. If this error message is returned, then flag for a deferred command retry when service starts. @param-in (admin_group): This is the group to add to BUILTIN\\Administrators. If unset, then look up the value in the config db. @param-in (check_deferred): If this is True, then only perform the group mapping if this has been flagged as in need of deferred setup (i.e. Samba wasn't running when it was initially called). This is to avoid unecessarily calling during service start. """ verrors = ValidationErrors() if check_deferred: is_deferred = await self.middleware.call('cache.has_key', 'SMB_SET_ADMIN') if not is_deferred: self.logger.debug("No cache entry indicating delayed action to add admin_group was found.") return True else: await self.middleware.call('cache.pop', 'SMB_SET_ADMIN') if not admin_group: smb = await self.middleware.call('smb.config') admin_group = smb['admin_group'] # We must use GIDs because wbinfo --name-to-sid expects a domain prefix "FREENAS\user" group = await self.middleware.call("notifier.get_group_object", admin_group) if not group: verrors.add('smb_update.admin_group', f"Failed to validate group: {admin_group}") raise verrors sid = await self.wbinfo_gidtosid(group[2]) if sid == "WBC_ERR_WINBIND_NOT_AVAILABLE": self.logger.debug("Delaying admin group add until winbind starts") await self.middleware.call('cache.put', 'SMB_SET_ADMIN', True) return True must_add_sid = await self.validate_admin_groups(sid) if not must_add_sid: return True proc = await Popen( ['/usr/local/bin/net', 'groupmap', 'addmem', 'S-1-5-32-544', sid], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) output = await proc.communicate() if proc.returncode != 0: raise CallError(f'net groupmap addmem failed: {output[1].decode()}') self.logger.debug(f"Successfully added {admin_group} to BUILTIN\\Administrators") return True
async def do_update(self, id, data): """ Updates a dataset/zvol `id`. """ verrors = ValidationErrors() dataset = await self.middleware.call('pool.dataset.query', [('id', '=', id)]) if not dataset: verrors.add('id', f'{id} does not exist', errno.ENOENT) else: data['type'] = dataset[0]['type'] data['name'] = dataset[0]['name'] if data['type'] == 'VOLUME': data['volblocksize'] = dataset[0]['volblocksize']['value'] await self.__common_validation(verrors, 'pool_dataset_update', data, 'UPDATE') if verrors: raise verrors props = {} for i, real_name, transform, inheritable in ( ('atime', None, str.lower, True), ('comments', 'org.freenas:description', None, False), ('sync', None, str.lower, True), ('compression', None, str.lower, True), ('deduplication', 'dedup', str.lower, True), ('exec', None, str.lower, True), ('quota', None, _none, False), ('refquota', None, _none, False), ('reservation', None, _none, False), ('refreservation', None, _none, False), ('copies', None, None, False), ('snapdir', None, str.lower, True), ('readonly', None, str.lower, True), ('recordsize', None, None, True), ('volsize', None, lambda x: str(x), False), ): if i not in data: continue name = real_name or i if inheritable and data[i] == 'INHERIT': props[name] = {'source': 'INHERIT'} else: props[name] = {'value': data[i] if not transform else transform(data[i])} rv = await self.middleware.call('zfs.dataset.update', id, {'properties': props}) if data['type'] == 'FILESYSTEM' and 'share_type' in data: await self.middleware.call( 'notifier.change_dataset_share_type', id, data['share_type'].lower() ) elif data['type'] == 'VOLUME' and 'volsize' in data: if await self.middleware.call('iscsi.extent.query', [('path', '=', f'zvol/{id}')]): await self.middleware.call('service.reload', 'iscsitarget') return rv
async def do_update(self, data): old = await self.config() new = old.copy() new.update(data) verrors = ValidationErrors() if not data['v3'] and not data['community']: verrors.add('snmp_update.community', 'This field is required when SNMPv3 is disabled') if data['v3_authtype'] and not data['v3_password']: verrors.add('snmp_update.v3_password', 'This field is requires when SNMPv3 auth type is specified') if data['v3_password'] and len(data['v3_password']) < 8: verrors.add('snmp_update.v3_password', 'Password must contain at least 8 characters') if data['v3_privproto'] and not data['v3_privpassphrase']: verrors.add('snmp_update.v3_privpassphrase', 'This field is requires when SNMPv3 private protocol is specified') if verrors: raise verrors await self._update_service(old, new) return new
async def do_update(self, data): """ Update default Alert settings. """ old = await self.config() new = old.copy() new.update(data) verrors = ValidationErrors() for k, v in new["classes"].items(): if k not in AlertClass.class_by_name: verrors.add(f"alert_class_update.classes.{k}", "This alert class does not exist") if not isinstance(v, dict): verrors.add(f"alert_class_update.classes.{k}", "Not a dictionary") if "level" in v: if v["level"] not in AlertLevel.__members__: verrors.add(f"alert_class_update.classes.{k}.level", "This alert level does not exist") if "policy" in v: if v["policy"] not in POLICIES: verrors.add(f"alert_class_update.classes.{k}.policy", "This alert policy does not exist") if verrors: raise verrors await self.middleware.call("datastore.update", self._config.datastore, old["id"], new) return new
async def _validate(self, data): """ For now validation is limited to checking if we can resolve the hostnames configured for the kdc, admin_server, and kpasswd_server can be resolved by DNS, and if the realm can be resolved by DNS. """ verrors = ValidationErrors() try: base64.b64decode(data['file']) except Exception as e: verrors.add("kerberos.keytab_create", f"Keytab is a not a properly base64-encoded string: [{e}]") return verrors
def _validate(self, schema_name, data): verrors = ValidationErrors() if data["provider"] not in REMOTES: verrors.add(f"{schema_name}.provider", "Invalid provider") else: provider = REMOTES[data["provider"]] attributes_verrors = validate_attributes(provider.credentials_schema, data) verrors.add_child(f"{schema_name}.attributes", attributes_verrors) if verrors: raise verrors
async def do_update(self, id, data): if not await self.is_loaded(): raise CallError('The ipmi device could not be found') verrors = ValidationErrors() if not data.get('dhcp'): for k in ['ipaddress', 'netmask', 'gateway']: if not data.get(k): verrors.add( f'ipmi_update.{k}', 'This field is required when dhcp is not given' ) if verrors: raise verrors args = ['ipmitool', 'lan', 'set', str(id)] rv = 0 if data.get('dhcp'): rv |= (await run(*args, 'ipsrc', 'dhcp', check=False)).returncode else: rv |= (await run(*args, 'ipsrc', 'static', check=False)).returncode rv |= (await run(*args, 'ipaddr', data['ipaddress'], check=False)).returncode rv |= (await run(*args, 'netmask', data['netmask'], check=False)).returncode rv |= (await run(*args, 'defgw', 'ipaddr', data['gateway'], check=False)).returncode rv |= (await run( *args, 'vlan', 'id', str(data['vlan']) if data.get('vlan') else 'off' )).returncode rv |= (await run(*args, 'access', 'on', check=False)).returncode rv |= (await run(*args, 'auth', 'USER', 'MD2,MD5', check=False)).returncode rv |= (await run(*args, 'auth', 'OPERATOR', 'MD2,MD5', check=False)).returncode rv |= (await run(*args, 'auth', 'ADMIN', 'MD2,MD5', check=False)).returncode rv |= (await run(*args, 'auth', 'CALLBACK', 'MD2,MD5', check=False)).returncode # Setting arp have some issues in some hardwares # Do not fail if setting these couple settings do not work # See #15578 await run(*args, 'arp', 'respond', 'on', check=False) await run(*args, 'arp', 'generate', 'on', check=False) if data.get('password'): rv |= (await run( 'ipmitool', 'user', 'set', 'password', '2', data.get('password'), )).returncode rv |= (await run('ipmitool', 'user', 'enable', '2')).returncode # XXX: according to dwhite, this needs to be executed off the box via # the lanplus interface. # rv |= (await run('ipmitool', 'sol', 'set', 'enabled', 'true', '1')).returncode # ) return rv
async def common_validation(self, data, schema_name): verrors = ValidationErrors() if not data['dow']: verrors.add( f'{schema_name}.dow', 'At least one day must be chosen' ) data['ret_unit'] = data['ret_unit'].lower() data['begin'] = time(*[int(value) for value in data['begin'].split(':')]) data['end'] = time(*[int(value) for value in data['end'].split(':')]) data['byweekday'] = ','.join([str(day) for day in data.pop('dow')]) return data, verrors
async def _query_periodic_snapshot_tasks(self, ids): verrors = ValidationErrors() query_result = await self.middleware.call("pool.snapshottask.query", [["id", "in", ids]]) snapshot_tasks = [] for i, task_id in enumerate(ids): for task in query_result: if task["id"] == task_id: snapshot_tasks.append(task) break else: verrors.add(str(i), "This snapshot task does not exist") return verrors, snapshot_tasks
async def wbinfo_gidtosid(self, gid): verrors = ValidationErrors() proc = await Popen( ['/usr/local/bin/wbinfo', '--gid-to-sid', f"{gid}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) output = await proc.communicate() if proc.returncode != 0: if "WBC_ERR_WINBIND_NOT_AVAILABLE" in output[1].decode(): return "WBC_ERR_WINBIND_NOT_AVAILABLE" else: verrors.add('smb_update.admin_group', f"Failed to identify Windows SID for group: {output[1].decode()}") raise verrors return output[0].decode().strip()
async def do_update(self, job, data): """ Update System Dataset Service Configuration. `pool` is the name of a valid pool configured in the system which will be used to host the system dataset. `pool_exclude` can be specified to make sure that we don't place the system dataset on that pool if `pool` is not provided. """ config = await self.config() new = config.copy() new.update(data) verrors = ValidationErrors() if new['pool'] and new['pool'] != 'freenas-boot': pool = await self.middleware.call('pool.query', [['name', '=', new['pool']]]) if not pool: verrors.add( 'sysdataset_update.pool', f'Pool "{new["pool"]}" not found', errno.ENOENT ) elif pool[0]['encrypt'] == 2: # This will cover two cases - passphrase being set for a pool and that it might be locked as well verrors.add( 'sysdataset_update.pool', f'Pool "{new["pool"]}" has an encryption passphrase set. ' 'The system dataset cannot be placed on this pool.' ) elif not new['pool']: for pool in await self.middleware.call( 'pool.query', [ ['encrypt', '!=', 2] ] ): if data.get('pool_exclude') == pool['name']: continue new['pool'] = pool['name'] break else: # If a data pool could not be found, reset it to blank # Which will eventually mean its back to freenas-boot (temporarily) new['pool'] = '' verrors.check() new['syslog_usedataset'] = new['syslog'] update_dict = new.copy() for key in ('is_decrypted', 'basename', 'uuid_a', 'syslog', 'path', 'pool_exclude'): update_dict.pop(key, None) await self.middleware.call( 'datastore.update', 'system.systemdataset', config['id'], update_dict, {'prefix': 'sys_'} ) new = await self.config() if config['pool'] != new['pool']: await self.migrate(config['pool'], new['pool']) await self.setup(True, data.get('pool_exclude')) if config['syslog'] != new['syslog']: await self.middleware.call('service.restart', 'syslogd') if not await self.middleware.call('system.is_freenas') and await self.middleware.call('failover.licensed'): if await self.middleware.call('failover.status') == 'MASTER': try: await self.middleware.call('failover.call_remote', 'system.reboot') except Exception as e: self.logger.debug('Failed to reboot passive storage controller after system dataset change: %s', e) return await self.config()
async def _common_validate(self, idmap_backend, data): """ Common validation checks for all idmap backends. 1) Check for a high range that is lower than the low range. 2) Check for overlap with other configured idmap ranges. In some circumstances overlap is permitted: - new idmap range may overlap previously configured idmap range of same domain. - new idmap range may overlap an idmap range configured for a disabled directory service. - new idmap range for 'autorid' may overlap DS_TYPE_DEFAULT_DOMAIN - new idmap range for 'ad' may overlap other 'ad' ranges. In this situation, it is responsibility of the system administrator to avoid id collisions between the configured domains. """ verrors = ValidationErrors() if data['range_high'] < data['range_low']: verrors.add( f'idmap_range', 'Idmap high range must be greater than idmap low range') return verrors configured_domains = await self.get_configured_idmap_domains() ldap_enabled = False if await self.middleware.call( 'ldap.get_state') == 'DISABLED' else True ad_enabled = False if await self.middleware.call( 'activedirectory.get_state') == 'DISABLED' else True new_range = range(data['range_low'], data['range_high']) for i in configured_domains: # Do not generate validation error comparing to oneself. if i['domain']['id'] == data['domain']['id']: continue # Do not generate validation errors for overlapping with a disabled DS. if not ldap_enabled and i['domain'][ 'idmap_domain_name'] == 'DS_TYPE_LDAP': continue if not ad_enabled and i['domain'][ 'idmap_domain_name'] == 'DS_TYPE_ACTIVEDIRECTORY': continue # Idmap settings under Services->SMB are ignored when autorid is enabled. if idmap_backend == 'autorid' and i['domain']['id'] == 5: continue # Overlap between ranges defined for 'ad' backend are permitted. if idmap_backend == 'ad' and i['idmap_backend'] == 'ad': continue existing_range = range(i['backend_data']['range_low'], i['backend_data']['range_high']) if range(max(existing_range[0], new_range[0]), min(existing_range[-1], new_range[-1]) + 1): verrors.add( f'idmap_range', f'new idmap range conflicts with existing range for domain [{i["domain"]["idmap_domain_name"]}]' ) return verrors
async def do_update(self, id, data): """ Update SMB Share of `id`. """ verrors = ValidationErrors() path = data.get('path') 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) oldname = 'homes' if old['home'] else old['name'] newname = 'homes' if new['home'] else new['name'] 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}') if old['purpose'] != new['purpose']: await self.apply_presets(new) old_is_locked = (await self.get_instance(id))['locked'] if old['path'] != new['path']: new_is_locked = await self.middleware.call('pool.dataset.path_in_locked_datasets', new['path']) else: new_is_locked = old_is_locked await self.compress(new) await self.middleware.call( 'datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self.strip_comments(new) if not new_is_locked: """ Enabling AAPL SMB2 extensions globally affects SMB shares. If this happens, the SMB service _must_ be restarted. Skip this step if dataset underlying the new path is encrypted. """ enable_aapl = await self.check_aapl(new) else: enable_aapl = False """ OLD NEW = dataset path is encrypted ---------- - - = pre-12 behavior. Remove and replace if name changed, else update. - X = Delete share from running configuration X - = Add share to running configuration X X = no-op """ if old_is_locked and new_is_locked: """ Configuration change only impacts a locked SMB share. From standpoint of running config, this is a no-op. No need to restart or reload service. """ return await self.get_instance(id) elif not old_is_locked and not new_is_locked: """ Default behavior before changes for locked datasets. """ if newname != oldname: # This is disruptive change. Share is actually being removed and replaced. # Forcibly closes any existing SMB sessions. await self.close_share(oldname) try: await self.middleware.call('sharing.smb.reg_delshare', oldname) except Exception: self.logger.warning('Failed to remove stale share [%s]', old['name'], exc_info=True) await self.middleware.call('sharing.smb.reg_addshare', new) else: diff = await self.middleware.call( 'sharing.smb.diff_middleware_and_registry', new['name'], new ) if diff is None: await self.middleware.call('sharing.smb.reg_addshare', new) else: share_name = new['name'] if not new['home'] else 'homes' await self.middleware.call('sharing.smb.apply_conf_diff', 'REGISTRY', share_name, diff) elif old_is_locked and not new_is_locked: """ Since the old share was not in our running configuration, we need to add it. """ await self.middleware.call('sharing.smb.reg_addshare', new) elif not old_is_locked and new_is_locked: try: await self.middleware.call('sharing.smb.reg_delshare', oldname) except Exception: self.logger.warning('Failed to remove locked share [%s]', old['name'], exc_info=True) if enable_aapl: await self._service_change('cifs', 'restart') else: await self._service_change('cifs', 'reload') return await self.get_instance(id)
def __ca_sign_csr(self, data, schema_name): verrors = ValidationErrors() ca_data = self.middleware.call_sync('certificateauthority.query', ([('id', '=', data['ca_id'])])) csr_cert_data = self.middleware.call_sync( 'certificate.query', [('id', '=', data['csr_cert_id'])]) if not ca_data: verrors.add( f'{schema_name}.ca_id', f'No Certificate Authority found for id {data["ca_id"]}') else: ca_data = ca_data[0] if not ca_data.get('privatekey'): verrors.add( f'{schema_name}.ca_id', 'Please use a CA which has a private key assigned') if not csr_cert_data: verrors.add(f'{schema_name}.csr_cert_id', f'No Certificate found for id {data["csr_cert_id"]}') else: csr_cert_data = csr_cert_data[0] if not csr_cert_data.get('CSR'): verrors.add(f'{schema_name}.csr_cert_id', 'No CSR has been filed by this certificate') else: try: csr = crypto.load_certificate_request( crypto.FILETYPE_PEM, csr_cert_data['CSR']) except crypto.Error: verrors.add(f'{schema_name}.csr_cert_id', 'CSR not valid') if verrors: raise verrors cert_info = crypto.load_certificate(crypto.FILETYPE_PEM, ca_data['certificate']) PKey = load_private_key(ca_data['privatekey']) serial = self.middleware.call_sync( 'certificateauthority.get_serial_for_certificate', ca_data['id']) cert = crypto.X509() cert.set_serial_number(serial) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(86400 * 365 * 10) cert.set_issuer(cert_info.get_subject()) cert.set_subject(csr.get_subject()) cert.set_pubkey(csr.get_pubkey()) cert.sign(PKey, ca_data['digest_algorithm']) new_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode() new_csr = { 'type': CERT_TYPE_INTERNAL, 'name': data['name'], 'certificate': new_cert, 'privatekey': csr_cert_data['privatekey'], 'create_type': 'CERTIFICATE_CREATE', 'signedby': ca_data['id'] } new_csr_dict = self.middleware.call_sync('certificate.create', new_csr) return new_csr_dict
async def validate_general_settings(self, data, schema): verrors = ValidationErrors() language = data.get('language') if language: system_languages = self.language_choices() if language not in system_languages.keys(): verrors.add( f'{schema}.language', f'Specified "{language}" language not found, kindly correct it' ) # kbd map needs work timezone = data.get('timezone') if timezone: timezones = await self.timezone_choices() if timezone not in timezones: verrors.add( f'{schema}.timezone', 'Please select a correct timezone' ) ip_addresses = await self.middleware.call( 'interfaces.ip_in_use' ) ip4_addresses_list = [alias_dict['address'] for alias_dict in ip_addresses if alias_dict['type'] == 'INET'] ip6_addresses_list = [alias_dict['address'] for alias_dict in ip_addresses if alias_dict['type'] == 'INET6'] ip4_address = data.get('ui_address') if ( ip4_address and ip4_address != '0.0.0.0' and ip4_address not in ip4_addresses_list ): verrors.add( f'{schema}.ui_address', 'Selected ipv4 address is not associated with this machine' ) ip6_address = data.get('ui_v6address') if ( ip6_address and ip6_address != '::' and ip6_address not in ip6_addresses_list ): verrors.add( f'{schema}.ui_v6address', 'Selected ipv6 address is not associated with this machine' ) syslog_server = data.get('syslogserver') if syslog_server: match = re.match("^[\w\.\-]+(\:\d+)?$", syslog_server) if not match: verrors.add( f'{schema}.syslogserver', 'Invalid syslog server format' ) elif ':' in syslog_server: port = int(syslog_server.split(':')[-1]) if port < 0 or port > 65535: verrors.add( f'{schema}.syslogserver', 'Port specified should be between 0 - 65535' ) protocol = data.get('ui_protocol') if protocol: if protocol != 'HTTP': certificate_id = data.get('ui_certificate') if not certificate_id: verrors.add( f'{schema}.ui_certificate', 'Protocol has been selected as HTTPS, certificate is required' ) else: # getting fingerprint for certificate fingerprint = await self.middleware.call( 'certificate.get_fingerprint_of_cert', certificate_id ) if fingerprint: syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_USER) syslog.syslog(syslog.LOG_ERR, 'Fingerprint of the certificate used in UI : ' + fingerprint) syslog.closelog() else: # Two reasons value is None - certificate not found - error while parsing the certificate for # fingerprint verrors.add( f'{schema}.ui_certificate', 'Kindly check if the certificate has been added to the system and it is a valid certificate' ) return verrors
async def do_update(self, id, data): """ Update SMB Share of `id`. """ verrors = ValidationErrors() path = data.get('path') 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) oldname = 'homes' if old['home'] else old['name'] newname = 'homes' if new['home'] else new['name'] 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}') if old['purpose'] != new['purpose']: await self.apply_presets(new) await self.compress(new) await self.middleware.call('datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) enable_aapl = await self.check_aapl(new) if newname != oldname: # This is disruptive change. Share is actually being removed and replaced. # Forcibly closes any existing SMB sessions. await self.close_share(oldname) try: await self.middleware.call('sharing.smb.reg_delshare', oldname) except Exception: self.logger.warning('Failed to remove stale share [%s]', old['name'], exc_info=True) await self.middleware.call('sharing.smb.reg_addshare', new) else: diff = await self.middleware.call( 'sharing.smb.diff_middleware_and_registry', new['name'], new) share_name = new['name'] if not new['home'] else 'homes' await self.middleware.call('sharing.smb.apply_conf_diff', 'REGISTRY', share_name, diff) await self.extend(new) # same here ? if enable_aapl: await self._service_change('cifs', 'restart') else: await self._service_change('cifs', 'reload') return new
async def do_create(self, data): """ Create a Replication Task Create a Replication Task that will push or pull ZFS snapshots to or from remote host.. * `name` specifies a name for replication task * `direction` specifies whether task will `PUSH` or `PULL` snapshots * `transport` is a method of snapshots transfer: * `SSH` transfers snapshots via SSH connection. This method is supported everywhere but does not achieve great performance `ssh_credentials` is a required field for this transport (Keychain Credential ID of type `SSH_CREDENTIALS`) * `SSH+NETCAT` uses unencrypted connection for data transfer. This can only be used in trusted networks and requires a port (specified by range from `netcat_active_side_port_min` to `netcat_active_side_port_max`) to be open on `netcat_active_side` `ssh_credentials` is also required for control connection * `LOCAL` replicates to or from localhost * `LEGACY` uses legacy replication engine prior to FreeNAS 11.3 * `source_datasets` is a non-empty list of datasets to replicate snapshots from * `target_dataset` is a dataset to put snapshots into. It must exist on target side * `recursive` and `exclude` have the same meaning as for Periodic Snapshot Task * `properties` control whether we should send dataset properties along with snapshots * `periodic_snapshot_tasks` is a list of periodic snapshot task IDs that are sources of snapshots for this replication task. Only push replication tasks can be bound to periodic snapshot tasks. * `naming_schema` is a list of naming schemas for pull replication * `also_include_naming_schema` is a list of naming schemas for push replication * `auto` allows replication to run automatically on schedule or after bound periodic snapshot task * `schedule` is a schedule to run replication task. Only `auto` replication tasks without bound periodic snapshot tasks can have a schedule * `restrict_schedule` restricts when replication task with bound periodic snapshot tasks runs. For example, you can have periodic snapshot tasks that run every 15 minutes, but only run replication task every hour. * Enabling `only_matching_schedule` will only replicate snapshots that match `schedule` or `restrict_schedule` * `allow_from_scratch` will destroy all snapshots on target side and replicate everything from scratch if none of the snapshots on target side matches source snapshots * `hold_pending_snapshots` will prevent source snapshots from being deleted by retention of replication fails for some reason * `retention_policy` specifies how to delete old snapshots on target side: * `SOURCE` deletes snapshots that are absent on source side * `CUSTOM` deletes snapshots that are older than `lifetime_value` and `lifetime_unit` * `NONE` does not delete any snapshots * `compression` compresses SSH stream. Available only for SSH transport * `speed_limit` limits speed of SSH stream. Available only for SSH transport * `dedup`, `large_block`, `embed` and `compressed` are various ZFS stream flag documented in `man zfs send` * `retries` specifies number of retries before considering replication failed .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.create", "params": [{ "name": "Work Backup", "direction": "PUSH", "transport": "SSH", "ssh_credentials": [12], "source_datasets", ["data/work"], "target_dataset": "repl/work", "recursive": true, "periodic_snapshot_tasks": [5], "auto": true, "restrict_schedule": { "minute": "0", "hour": "*/2", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" }, "only_matching_schedule": true, "retention_policy": "CUSTOM", "lifetime_value": 1, "lifetime_unit": "WEEK", }] } """ verrors = ValidationErrors() verrors.add_child("replication_create", await self._validate(data)) if verrors: raise verrors periodic_snapshot_tasks = data["periodic_snapshot_tasks"] await self.compress(data) id = await self.middleware.call( "datastore.insert", self._config.datastore, data, {"prefix": self._config.datastore_prefix}) await self._set_periodic_snapshot_tasks(id, periodic_snapshot_tasks) await self.middleware.call("service.restart", "cron") await self.middleware.call("zettarepl.update_tasks") return await self._get_instance(id)
async def do_create(self, data): """ Create a Periodic Snapshot Task Create a Periodic Snapshot Task that will take snapshots of specified `dataset` at specified `schedule`. Recursive snapshots can be created if `recursive` flag is enabled. You can `exclude` specific child datasets or zvols from the snapshot. Snapshots will be automatically destroyed after a certain amount of time, specified by `lifetime_value` and `lifetime_unit`. If multiple periodic tasks create snapshots at the same time (for example hourly and daily at 00:00) the snapshot will be kept until the last of these tasks reaches its expiry time. Snapshots will be named according to `naming_schema` which is a `strftime`-like template for snapshot name and must contain `%Y`, `%m`, `%d`, `%H` and `%M`. .. examples(websocket):: Create a recursive Periodic Snapshot Task for dataset `data/work` excluding `data/work/temp`. Snapshots will be created on weekdays every hour from 09:00 to 18:00 and will be stored for two weeks. :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "pool.snapshottask.create", "params": [{ "dataset": "data/work", "recursive": true, "exclude": ["data/work/temp"], "lifetime_value": 2, "lifetime_unit": "WEEK", "naming_schema": "auto_%Y-%m-%d_%H-%M", "schedule": { "minute": "0", "hour": "*", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" } }] } """ verrors = ValidationErrors() verrors.add_child('periodic_snapshot_create', await self._validate(data)) if verrors: raise verrors Cron.convert_schedule_to_db_format(data, begin_end=True) data['id'] = await self.middleware.call( 'datastore.insert', self._config.datastore, data, {'prefix': self._config.datastore_prefix}) await self.middleware.call('zettarepl.update_tasks') return await self._get_instance(data['id'])
async def add_admin_group(self, admin_group=None, check_deferred=False): """ Add a local or directory service group to BUILTIN\\Administrators (S-1-5-32-544) Members of this group have elevated privileges to the Samba server (ability to take ownership of files, override ACLs, view and modify user quotas, and administer the server via the Computer Management MMC Snap-In. Unfortuntely, group membership must be managed via "net groupmap listmem|addmem|delmem", which requires that winbind be running when the commands are executed. In this situation, net command will fail with WBC_ERR_WINBIND_NOT_AVAILABLE. If this error message is returned, then flag for a deferred command retry when service starts. `admin_group` This is the group to add to BUILTIN\\Administrators. If unset, then look up the value in the config db. `check_deferred` If this is True, then only perform the group mapping if this has been flagged as in need of deferred setup (i.e. Samba wasn't running when it was initially called). This is to avoid unecessarily calling during service start. """ verrors = ValidationErrors() if check_deferred: is_deferred = await self.middleware.call('cache.has_key', 'SMB_SET_ADMIN') if not is_deferred: self.logger.debug( "No cache entry indicating delayed action to add admin_group was found." ) return True else: await self.middleware.call('cache.pop', 'SMB_SET_ADMIN') if not admin_group: smb = await self.middleware.call('smb.config') admin_group = smb['admin_group'] # We must use GIDs because wbinfo --name-to-sid expects a domain prefix "FREENAS\user" group = await self.middleware.call("dscache.get_uncached_group", admin_group) if not group: verrors.add('smb_update.admin_group', f"Failed to validate group: {admin_group}") raise verrors sid = await self.wbinfo_gidtosid(group['gr_gid']) if sid == WBCErr.WINBIND_NOT_AVAILABLE.err(): self.logger.debug("Delaying admin group add until winbind starts") await self.middleware.call('cache.put', 'SMB_SET_ADMIN', True) return True must_add_sid = await self.validate_admin_groups(sid) if not must_add_sid: return True proc = await run( ['/usr/local/bin/net', 'groupmap', 'addmem', 'S-1-5-32-544', sid], check=False) if proc.returncode != 0: raise CallError( f'net groupmap addmem failed: {proc.stderr.decode().strip()}') self.logger.debug("Successfully added [%s] to BUILTIN\\Administrators", admin_group) return True
async def do_update(self, id, data): """ Update a Periodic Snapshot Task with specific `id` See the documentation for `create` method for information on payload contents .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "pool.snapshottask.update", "params": [ 1, { "dataset": "data/work", "recursive": true, "exclude": ["data/work/temp"], "lifetime_value": 2, "lifetime_unit": "WEEK", "naming_schema": "auto_%Y-%m-%d_%H-%M", "schedule": { "minute": "0", "hour": "*", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" } } ] } """ old = await self._get_instance(id) new = old.copy() new.update(data) verrors = ValidationErrors() verrors.add_child('periodic_snapshot_update', await self._validate(new)) if not new['enabled']: for replication_task in await self.middleware.call( 'replication.query', [['enabled', '=', True]]): if any(periodic_snapshot_task['id'] == id for periodic_snapshot_task in replication_task['periodic_snapshot_tasks']): verrors.add('periodic_snapshot_update.enabled', ( f'You can\'t disable this periodic snapshot task because it is bound to enabled replication ' f'task {replication_task["id"]!r}')) break if verrors: raise verrors Cron.convert_schedule_to_db_format(new, begin_end=True) for key in ('vmware_sync', 'state'): new.pop(key, None) await self.middleware.call('datastore.update', self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self.middleware.call('zettarepl.update_tasks') return await self._get_instance(id)
async def do_update(self, pk, data): user = await self._get_instance(pk) verrors = ValidationErrors() if 'group' in data: group = await self.middleware.call('datastore.query', 'account.bsdgroups', [('id', '=', data['group'])]) if not group: verrors.add('group', f'Group {data["group"]} not found', errno.ENOENT) group = group[0] else: group = user['group'] user['group'] = group['id'] await self.__common_validation(verrors, data, pk=pk) home = data.get('home') or user['home'] # root user (uid 0) is an exception to the rule if data.get('sshpubkey' ) and not home.startswith('/mnt') and user['uid'] != 0: verrors.add('sshpubkey', 'Home directory is not writable, leave this blank"') # Do not allow attributes to be changed for builtin user if user['builtin']: for i in ('group', 'home', 'home_mode', 'uid', 'username'): if i in data: verrors.add(i, 'This attribute cannot be changed') if verrors: raise verrors # Copy the home directory if it changed if ('home' in data and data['home'] not in (user['home'], '/nonexistent') and not data["home"].startswith(f'{user["home"]}/')): home_copy = True home_old = user['home'] else: home_copy = False user.update(data) password = await self.__set_password(user) await self.__update_sshpubkey(user, group['bsdgrp_group']) home_mode = user.pop('home_mode', None) if home_mode is not None: if not user['builtin'] and os.path.exists(user['home']): try: os.chmod(user['home'], int(home_mode, 8)) except OSError: self.logger.warn('Failed to set homedir mode', exc_info=True) if home_copy: def do_home_copy(): subprocess.run( f"su - {user['username']} -c '/bin/cp -a {home_old}/* {user['home']}/'" ) asyncio.ensure_future(self.middleware.threaded(do_home_copy)) if 'groups' in user: groups = user.pop('groups') await self.__set_groups(pk, groups) await self.middleware.call('datastore.update', 'account.bsdusers', pk, user, {'prefix': 'bsdusr_'}) await self.middleware.call('service.reload', 'user') await self.__set_smbpasswd(user['username'], password) return pk
async def attach(self, job, oid, options): """ For TrueNAS Core/Enterprise platform, if the `oid` pool is passphrase GELI encrypted, `passphrase` must be specified for this operation to succeed. `target_vdev` is the GUID of the vdev where the disk needs to be attached. In case of STRIPED vdev, this is the STRIPED disk GUID which will be converted to mirror. If `target_vdev` is mirror, it will be converted into a n-way mirror. """ pool = await self.middleware.call('pool.get_instance', oid) verrors = ValidationErrors() if not pool['is_decrypted']: verrors.add('oid', 'Pool must be unlocked for this action.') verrors.check() topology = pool['topology'] topology_type = vdev = None for i in topology: for v in topology[i]: if v['guid'] == options['target_vdev']: topology_type = i vdev = v break if topology_type: break else: verrors.add('pool_attach.target_vdev', 'Unable to locate VDEV') verrors.check() if topology_type in ('cache', 'spares'): verrors.add('pool_attach.target_vdev', f'Attaching disks to {topology_type} not allowed.') elif topology_type == 'data': # We would like to make sure here that we don't have inconsistent vdev types across data if vdev['type'] not in ('DISK', 'MIRROR'): verrors.add( 'pool_attach.target_vdev', f'Attaching disk to {vdev["type"]} vdev is not allowed.') # Let's validate new disk now verrors.add_child( 'pool_attach', await self.middleware.call('disk.check_disks_availability', [options['new_disk']], options['allow_duplicate_serials']), ) verrors.check() guid = vdev['guid'] if vdev['type'] == 'DISK' else vdev['children'][0][ 'guid'] disks = { options['new_disk']: { 'create_swap': topology_type == 'data', 'vdev': [] } } await self.middleware.call('pool.format_disks', job, disks) devname = disks[options['new_disk']]['vdev'][0] extend_job = await self.middleware.call('zfs.pool.extend', pool['name'], None, [{ 'target': guid, 'type': 'DISK', 'path': devname }]) await job.wrap(extend_job) asyncio.ensure_future(self.middleware.call('disk.swaps_configure'))
async def do_create(self, data): verrors = ValidationErrors() if (not data.get('group') and not data.get('group_create')) or ( data.get('group') is not None and data.get('group_create')): verrors.add( 'group', f'Enter either a group name or create a new group to ' 'continue.', errno.EINVAL) await self.__common_validation(verrors, data) if data.get('sshpubkey') and not data['home'].startswith('/mnt'): verrors.add( 'sshpubkey', 'The home directory is not writable. Leave this field blank.') if verrors: raise verrors groups = data.pop('groups') or [] create = data.pop('group_create') if create: group = await self.middleware.call( 'group.query', [('group', '=', data['username'])]) if group: group = group[0] else: group = await self.middleware.call('group.create', {'name': data['username']}) group = (await self.middleware.call('group.query', [('id', '=', group)]))[0] data['group'] = group['id'] else: group = await self.middleware.call('group.query', [('id', '=', data['group'])]) if not group: raise CallError(f'Group {data["group"]} not found') group = group[0] # Is this a new directory or not? Let's not nuke existing directories, # e.g. /, /root, /mnt/tank/my-dataset, etc ;). new_homedir = False home_mode = data.pop('home_mode') if data['home'] != '/nonexistent': try: os.makedirs(data['home'], mode=int(home_mode, 8)) os.chown(data['home'], data['uid'], group['gid']) except FileExistsError: if not os.path.isdir(data['home']): raise CallError( 'Path for home directory already ' 'exists and is not a directory', errno.EEXIST) # If it exists, ensure the user is owner os.chown(data['home'], data['uid'], group['gid']) except OSError as oe: raise CallError('Failed to create the home directory ' f'({data["home"]}) for user: {oe}') else: new_homedir = True if os.stat(data['home']).st_dev == os.stat('/mnt').st_dev: raise CallError( f'The path for the home directory "(data["home"])" ' 'must include a volume or dataset.') if not data.get('uid'): data['uid'] = await self.get_next_uid() pk = None # Make sure pk exists to rollback in case of an error try: password = await self.__set_password(data) sshpubkey = data.pop('sshpubkey', None) # datastore does not have sshpubkey pk = await self.middleware.call('datastore.insert', 'account.bsdusers', data, {'prefix': 'bsdusr_'}) await self.__set_groups(pk, groups) except Exception: if pk is not None: await self.middleware.call('datastore.delete', 'account.bsdusers', pk) if new_homedir: # Be as atomic as possible when creating the user if # commands failed to execute cleanly. shutil.rmtree(data['home']) raise await self.middleware.call('service.reload', 'user') await self.__set_smbpasswd(data['username'], password) if os.path.exists(data['home']): for f in os.listdir(SKEL_PATH): if f.startswith('dot'): dest_file = os.path.join(data['home'], f[3:]) else: dest_file = os.path.join(data['home'], f) if not os.path.exists(dest_file): shutil.copyfile(os.path.join(SKEL_PATH, f), dest_file) os.chown(dest_file, data['uid'], group['gid']) data['sshpubkey'] = sshpubkey await self.__update_sshpubkey(data, group['group']) return pk
async def manual_test(self, disks): """ Run manual SMART tests for `disks`. `type` indicates what type of SMART test will be ran and must be specified. """ verrors = ValidationErrors() test_disks_list = [] if not disks: verrors.add('disks', 'Please specify at least one disk.') else: disks_data = await self.middleware.call('disk.query') devices = await self.middleware.call( 'device.get_storage_devices_topology') for index, disk in enumerate(disks): for d in disks_data: if disk['identifier'] == d['identifier']: current_disk = d test_disks_list.append({ 'disk': current_disk['name'], **disk }) break else: verrors.add( f'disks.{index}.identifier', f'{disk["identifier"]} is not valid. Please provide a valid disk identifier.' ) continue if current_disk['name'] is None: verrors.add( f'disks.{index}.identifier', f'Test cannot be performed for {disk["identifier"]} disk. Failed to retrieve name.' ) if current_disk['name'].startswith('nvd'): verrors.add( f'disks.{index}.identifier', f'Test cannot be performed for {disk["identifier"]} disk. NVMe devices cannot be mapped yet.' ) device = devices.get(current_disk['name']) if not device: verrors.add( f'disks.{index}.identifier', f'Test cannot be performed for {disk["identifier"]}. Unable to retrieve disk details.' ) verrors.check() return await asyncio_map(self.__manual_test, test_disks_list, 16)
async def do_update(self, id, data): """ Update a Replication Task with specific `id` See the documentation for `create` method for information on payload contents .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.update", "params": [ 7, { "name": "Work Backup", "direction": "PUSH", "transport": "SSH", "ssh_credentials": [12], "source_datasets", ["data/work"], "target_dataset": "repl/work", "recursive": true, "periodic_snapshot_tasks": [5], "auto": true, "restrict_schedule": { "minute": "0", "hour": "*/2", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" }, "only_matching_schedule": true, "retention_policy": "CUSTOM", "lifetime_value": 1, "lifetime_unit": "WEEK", } ] } """ old = await self._get_instance(id) new = old.copy() if new["ssh_credentials"]: new["ssh_credentials"] = new["ssh_credentials"]["id"] new["periodic_snapshot_tasks"] = [ task["id"] for task in new["periodic_snapshot_tasks"] ] new.update(data) verrors = ValidationErrors() verrors.add_child("replication_update", await self._validate(new, id)) if verrors: raise verrors periodic_snapshot_tasks = new["periodic_snapshot_tasks"] await self.compress(new) new.pop("state", None) new.pop("job", None) await self.middleware.call("datastore.update", self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self._set_periodic_snapshot_tasks(id, periodic_snapshot_tasks) await self.middleware.call("service.restart", "cron") await self.middleware.call("zettarepl.update_tasks") return await self._get_instance(id)
async def do_create(self, data): """ Create a SMB Share. `purpose` applies common configuration presets depending on intended purpose. `timemachine` when set, enables Time Machine backups for this share. `ro` when enabled, prohibits write access to the share. `guestok` when enabled, allows access to this share without a password. `hostsallow` is a list of hostnames / IP addresses which have access to this share. `hostsdeny` is a list of hostnames / IP addresses which are not allowed access to this share. If a handful of hostnames are to be only allowed access, `hostsdeny` can be passed "ALL" which means that it will deny access to ALL hostnames except for the ones which have been listed in `hostsallow`. `acl` enables support for storing the SMB Security Descriptor as a Filesystem ACL. `streams` enables support for storing alternate datastreams as filesystem extended attributes. `fsrvp` enables support for the filesystem remote VSS protocol. This allows clients to create ZFS snapshots through RPC. `shadowcopy` enables support for the volume shadow copy service. `auxsmbconf` is a string of additional smb4.conf parameters not covered by the system's API. """ verrors = ValidationErrors() path = data['path'] await self.clean(data, 'sharingsmb_create', verrors) await self.validate(data, 'sharingsmb_create', verrors) 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.apply_presets(data) await self.compress(data) vuid = await self.generate_vuid(data['timemachine']) data.update({'vuid': vuid}) data['id'] = await self.middleware.call( 'datastore.insert', self._config.datastore, data, {'prefix': self._config.datastore_prefix}) await self.middleware.call('sharing.smb.reg_addshare', data) await self.extend(data) # We should do this in the insert call ? enable_aapl = await self.check_aapl(data) if enable_aapl: await self._service_change('cifs', 'restart') else: await self._service_change('cifs', 'reload') return data
async def do_update(self, data): """ `hostname` list of ip addresses or hostnames of LDAP servers with which to communicate in order of preference. Failover only occurs if the current LDAP server is unresponsive. `basedn` specifies the default base DN to use when performing ldap operations. The base must be specified as a Distinguished Name in LDAP format. `binddn` specifies the default bind DN to use when performing ldap operations. The bind DN must be specified as a Distinguished Name in LDAP format. `anonbind` use anonymous authentication. `ssl` establish SSL/TLS-protected connections to the LDAP server(s). GSSAPI signing is disabled on SSL/TLS-protected connections if kerberos authentication is used. `certificate` LDAPs client certificate to be used for certificate- based authentication. `validate_certificates` specifies whether to perform checks on server certificates in a TLS session. If enabled, TLS_REQCERT demand is set. The server certificate is requested. If no certificate is provided or if a bad certificate is provided, the session is immediately terminated. If disabled, TLS_REQCERT allow is set. The server certificate is requested, but all errors are ignored. `kerberos_realm` in which the server is located. This parameter is only required for SASL GSSAPI authentication to the remote LDAP server. `kerberos_principal` kerberos principal to use for SASL GSSAPI authentication to the remote server. If `kerberos_realm` is specified without a keytab, then the `binddn` and `bindpw` are used to perform to obtain the ticket necessary for GSSAPI authentication. `timeout` specifies a timeout (in seconds) after which calls to synchronous LDAP APIs will abort if no response is received. `dns_timeout` specifies the timeout (in seconds) after which the poll(2)/select(2) following a connect(2) returns in case of no activity for openldap. For nslcd this specifies the time limit (in seconds) to use when connecting to the directory server. This directly impacts the length of time that the LDAP service tries before failing over to a secondary LDAP URI. `idmap_backend` provides a plugin interface for Winbind to use varying backends to store SID/uid/gid mapping tables. The correct setting depends on the environment in which the NAS is deployed. The default is to use idmap_ldap with the same LDAP configuration as the main LDAP service. `has_samba_schema` determines whether to configure samba to use the ldapsam passdb backend to provide SMB access to LDAP users. This feature requires the presence of Samba LDAP schema extensions on the remote LDAP server. """ verrors = ValidationErrors() must_reload = False old = await self.config() new = old.copy() new.update(data) await self.common_validate(new, old, verrors) if verrors: raise verrors if old != new: must_reload = True if new['enable']: try: await self.middleware.call('ldap.ldap_validate', new) except Exception as e: raise ValidationError('ldap_update', str(e)) await self.ldap_compress(new) await self.middleware.call('datastore.update', 'directoryservice.ldap', old['id'], new, {'prefix': 'ldap_'}) if must_reload: if new['enable']: await self.middleware.call('ldap.start') else: await self.middleware.call('ldap.stop') return await self.config()
async def _validate(self, data, id=None): verrors = ValidationErrors() await self._ensure_unique(verrors, "", "name", data["name"], id) # Direction snapshot_tasks = [] if data["direction"] == "PUSH": e, snapshot_tasks = await self._query_periodic_snapshot_tasks( data["periodic_snapshot_tasks"]) verrors.add_child("periodic_snapshot_tasks", e) if data["naming_schema"]: verrors.add("naming_schema", "This field has no sense for push replication") if data["transport"] != "LEGACY" and not snapshot_tasks and not data[ "also_include_naming_schema"]: verrors.add( "periodic_snapshot_tasks", "You must at least either bind a periodic snapshot task or provide " "\"Also Include Naming Schema\" for push replication task") if data["schedule"]: if data["periodic_snapshot_tasks"]: verrors.add( "schedule", "Push replication can't be bound to periodic snapshot task and have " "schedule at the same time") else: if data["auto"] and not data[ "periodic_snapshot_tasks"] and data[ "transport"] != "LEGACY": verrors.add( "auto", "Push replication that runs automatically must be either " "bound to periodic snapshot task or have schedule") if data["direction"] == "PULL": if data["schedule"]: pass else: if data["auto"]: verrors.add( "auto", "Pull replication that runs automatically must have schedule" ) if data["periodic_snapshot_tasks"]: verrors.add( "periodic_snapshot_tasks", "Pull replication can't be bound to periodic snapshot task" ) if not data["naming_schema"]: verrors.add("naming_schema", "Naming schema is required for pull replication") if data["also_include_naming_schema"]: verrors.add("also_include_naming_schema", "This field has no sense for pull replication") if data["hold_pending_snapshots"]: verrors.add( "hold_pending_snapshots", "Pull replication tasks can't hold pending snapshots because " "they don't do source retention") # Transport if data["transport"] == "SSH+NETCAT": if data["netcat_active_side"] is None: verrors.add( "netcat_active_side", "You must choose active side for SSH+netcat replication") if data["netcat_active_side_port_min"] is not None and data[ "netcat_active_side_port_max"] is not None: if data["netcat_active_side_port_min"] > data[ "netcat_active_side_port_max"]: verrors.add( "netcat_active_side_port_max", "Please specify value greater or equal than netcat_active_side_port_min" ) if data["compression"] is not None: verrors.add( "compression", "Compression is not supported for SSH+netcat replication") if data["speed_limit"] is not None: verrors.add( "speed_limit", "Speed limit is not supported for SSH+netcat replication") else: if data["netcat_active_side"] is not None: verrors.add( "netcat_active_side", "This field only has sense for SSH+netcat replication") for k in [ "netcat_active_side_listen_address", "netcat_active_side_port_min", "netcat_active_side_port_max", "netcat_passive_side_connect_address" ]: if data[k] is not None: verrors.add( k, "This field only has sense for SSH+netcat replication") if data["transport"] == "LOCAL": if data["ssh_credentials"] is not None: verrors.add( "ssh_credentials", "Remote credentials have no sense for local replication") if data["compression"] is not None: verrors.add("compression", "Compression has no sense for local replication") if data["speed_limit"] is not None: verrors.add("speed_limit", "Speed limit has no sense for local replication") else: if data["ssh_credentials"] is None: verrors.add( "ssh_credentials", "SSH Credentials are required for non-local replication") else: try: await self.middleware.call( "keychaincredential.get_of_type", data["ssh_credentials"], "SSH_CREDENTIALS") except CallError as e: verrors.add("ssh_credentials", str(e)) if data["transport"] == "LEGACY": for should_be_true in ["auto", "allow_from_scratch"]: if not data[should_be_true]: verrors.add( should_be_true, "Legacy replication does not support disabling this option" ) for should_be_false in [ "exclude", "periodic_snapshot_tasks", "naming_schema", "also_include_naming_schema", "only_matching_schedule", "dedup", "large_block", "embed", "compressed" ]: if data[should_be_false]: verrors.add( should_be_false, "Legacy replication does not support this option") if data["direction"] != "PUSH": verrors.add( "direction", "Only push application is allowed for Legacy transport") if len(data["source_datasets"]) != 1: verrors.add( "source_datasets", "You can only have one source dataset for legacy replication" ) if data["retention_policy"] not in ["SOURCE", "NONE"]: verrors.add( "retention_policy", "Only \"source\" and \"none\" retention policies are supported by " "legacy replication") if data["retries"] != 1: verrors.add("retries", "This value should be 1 for legacy replication") # Common for all directions and transports for i, source_dataset in enumerate(data["source_datasets"]): for snapshot_task in snapshot_tasks: if is_child(source_dataset, snapshot_task["dataset"]): if data["recursive"]: for exclude in snapshot_task["exclude"]: if exclude not in data["exclude"]: verrors.add( "exclude", f"You should exclude {exclude!r} as bound periodic snapshot " f"task dataset {snapshot_task['dataset']!r} does" ) else: if source_dataset in snapshot_task["exclude"]: verrors.add( f"source_datasets.{i}", f"Dataset {source_dataset!r} is excluded by bound " f"periodic snapshot task for dataset " f"{snapshot_task['dataset']!r}") if not data["recursive"] and data["exclude"]: verrors.add( "exclude", "Excluding child datasets is only supported for recursive replication" ) for i, v in enumerate(data["exclude"]): if not any( v.startswith(ds + "/") for ds in data["source_datasets"]): verrors.add( f"exclude.{i}", "This dataset is not a child of any of source datasets") if data["schedule"]: if not data["auto"]: verrors.add( "schedule", "You can't have schedule for replication that does not run automatically" ) else: if data["only_matching_schedule"]: verrors.add( "only_matching_schedule", "You can't have only-matching-schedule without schedule") if data["retention_policy"] == "CUSTOM": if data["lifetime_value"] is None: verrors.add( "lifetime_value", "This field is required for custom retention policy") if data["lifetime_unit"] is None: verrors.add( "lifetime_value", "This field is required for custom retention policy") else: if data["lifetime_value"] is not None: verrors.add( "lifetime_value", "This field has no sense for specified retention policy") if data["lifetime_unit"] is not None: verrors.add( "lifetime_unit", "This field has no sense for specified retention policy") if data["enabled"]: for i, snapshot_task in enumerate(snapshot_tasks): if not snapshot_task["enabled"]: verrors.add( f"periodic_snapshot_tasks.{i}", "You can't bind disabled periodic snapshot task to enabled replication task" ) return verrors
async def do_update(self, pk, data): """ Update attributes of an existing user. """ user = await self._get_instance(pk) verrors = ValidationErrors() if 'group' in data: group = await self.middleware.call('datastore.query', 'account.bsdgroups', [ ('id', '=', data['group']) ]) if not group: verrors.add('user_update.group', f'Group {data["group"]} not found', errno.ENOENT) group = group[0] else: group = user['group'] user['group'] = group['id'] await self.__common_validation(verrors, data, 'user_update', pk=pk) try: st = os.stat(user.get("home", "/nonexistent")).st_mode old_mode = f'{stat.S_IMODE(st):03o}' except FileNotFoundError: old_mode = None home = data.get('home') or user['home'] has_home = home != '/nonexistent' # root user (uid 0) is an exception to the rule if data.get('sshpubkey') and not home.startswith('/mnt') and user['uid'] != 0: verrors.add('user_update.sshpubkey', 'Home directory is not writable, leave this blank"') # Do not allow attributes to be changed for builtin user if user['builtin']: for i in ('group', 'home', 'home_mode', 'uid', 'username', 'smb'): if i in data and data[i] != user[i]: verrors.add(f'user_update.{i}', 'This attribute cannot be changed') if not user['smb'] and data.get('smb') and not data.get('password'): # Changing from non-smb user to smb user requires re-entering password. verrors.add('user_update.smb', 'Password must be changed in order to enable SMB authentication') verrors.check() must_change_pdb_entry = False for k in ('username', 'password', 'locked'): new_val = data.get(k) old_val = user.get(k) if new_val is not None and old_val != new_val: if k == 'username': try: await self.middleware.call("smb.remove_passdb_user", old_val) except Exception: self.logger.debug("Failed to remove passdb entry for user [%s]", old_val, exc_info=True) must_change_pdb_entry = True if user['smb'] is True and data.get('smb') is False: try: must_change_pdb_entry = False await self.middleware.call("smb.remove_passdb_user", user['username']) except Exception: self.logger.debug("Failed to remove passdb entry for user [%s]", user['username'], exc_info=True) if user['smb'] is False and data.get('smb') is True: must_change_pdb_entry = True # Copy the home directory if it changed if ( has_home and 'home' in data and data['home'] != user['home'] and not data['home'].startswith(f'{user["home"]}/') ): home_copy = True home_old = user['home'] else: home_copy = False # After this point user dict has values from data user.update(data) if home_copy and not os.path.isdir(user['home']): try: os.makedirs(user['home']) mode_to_set = user.get('home_mode') if not mode_to_set: mode_to_set = '700' if old_mode is None else old_mode perm_job = await self.middleware.call('filesystem.setperm', { 'path': user['home'], 'uid': user['uid'], 'gid': group['bsdgrp_gid'], 'mode': mode_to_set, 'options': {'stripacl': True}, }) await perm_job.wait() except OSError: self.logger.warn('Failed to chown homedir', exc_info=True) if not os.path.isdir(user['home']): raise CallError(f'{user["home"]} is not a directory') home_mode = user.pop('home_mode', None) if user['builtin']: home_mode = None try: update_sshpubkey_args = [ home_old if home_copy else user['home'], user, group['bsdgrp_group'], ] await self.update_sshpubkey(*update_sshpubkey_args) except PermissionError as e: self.logger.warn('Failed to update authorized keys', exc_info=True) raise CallError(f'Failed to update authorized keys: {e}') else: if user['uid'] == 0: if await self.middleware.call('failover.licensed'): try: await self.middleware.call('failover.call_remote', 'user.update_sshpubkey', update_sshpubkey_args) except Exception: self.logger.error('Failed to sync root ssh pubkey to standby node', exc_info=True) if home_copy: """ Background copy of user home directoy to new path as the user in question. """ await self.middleware.call('user.do_home_copy', home_old, user['home'], user['username'], home_mode, user['uid']) elif has_home and home_mode is not None: """ A non-recursive call to set permissions should return almost immediately. """ perm_job = await self.middleware.call('filesystem.setperm', { 'path': user['home'], 'mode': home_mode, 'options': {'stripacl': True}, }) await perm_job.wait() user.pop('sshpubkey', None) await self.__set_password(user) if 'groups' in user: groups = user.pop('groups') await self.__set_groups(pk, groups) user = await self.user_compress(user) await self.middleware.call('datastore.update', 'account.bsdusers', pk, user, {'prefix': 'bsdusr_'}) await self.middleware.call('service.reload', 'user') if user['smb'] and must_change_pdb_entry: await self.__set_smbpasswd(user['username']) return pk
async def do_update(self, id, data): """ Update `id` IPMI Configuration. `ipaddress` is a valid ip which will be used to connect to the IPMI interface. `netmask` is the subnet mask associated with `ipaddress`. `dhcp` is a boolean value which if unset means that `ipaddress`, `netmask` and `gateway` must be set. """ if not await self.is_loaded(): raise CallError('The ipmi device could not be found') verrors = ValidationErrors() if data.get('password') and len(data.get('password')) > 20: verrors.add('ipmi_update.password', 'A maximum of 20 characters are allowed') if not data.get('dhcp'): for k in ['ipaddress', 'netmask', 'gateway']: if not data.get(k): verrors.add( f'ipmi_update.{k}', 'This field is required when dhcp is not given') if verrors: raise verrors args = ['ipmitool', 'lan', 'set', str(id)] rv = 0 if data.get('dhcp'): rv |= (await run(*args, 'ipsrc', 'dhcp', check=False)).returncode else: rv |= (await run(*args, 'ipsrc', 'static', check=False)).returncode rv |= (await run(*args, 'ipaddr', data['ipaddress'], check=False)).returncode rv |= (await run(*args, 'netmask', data['netmask'], check=False)).returncode rv |= (await run(*args, 'defgw', 'ipaddr', data['gateway'], check=False)).returncode rv |= (await run( *args, 'vlan', 'id', str(data['vlan']) if data.get('vlan') else 'off')).returncode rv |= (await run(*args, 'access', 'on', check=False)).returncode rv |= (await run(*args, 'auth', 'USER', 'MD2,MD5', check=False)).returncode rv |= (await run(*args, 'auth', 'OPERATOR', 'MD2,MD5', check=False)).returncode rv |= (await run(*args, 'auth', 'ADMIN', 'MD2,MD5', check=False)).returncode rv |= (await run(*args, 'auth', 'CALLBACK', 'MD2,MD5', check=False)).returncode # Setting arp have some issues in some hardwares # Do not fail if setting these couple settings do not work # See #15578 await run(*args, 'arp', 'respond', 'on', check=False) await run(*args, 'arp', 'generate', 'on', check=False) if data.get('password'): rv |= (await run( 'ipmitool', 'user', 'set', 'password', '2', data.get('password'), )).returncode rv |= (await run('ipmitool', 'user', 'enable', '2')).returncode # XXX: according to dwhite, this needs to be executed off the box via # the lanplus interface. # rv |= (await run('ipmitool', 'sol', 'set', 'enabled', 'true', '1')).returncode # ) return rv
async def validate_rsync_task(self, data, schema): verrors = ValidationErrors() # Windows users can have spaces in their usernames # http://www.freebsd.org/cgi/query-pr.cgi?pr=164808 username = data.get('user') if ' ' in username: verrors.add(f'{schema}.user', 'User names cannot have spaces') raise verrors user = await self.middleware.call( 'notifier.get_user_object', username ) if not user: verrors.add(f'{schema}.user', f'Provided user "{username}" does not exist') raise verrors remote_host = data.get('remotehost') if not remote_host: verrors.add(f'{schema}.remotehost', 'Please specify a remote host') if data.get('extra'): data['extra'] = ' '.join(data['extra']) else: data['extra'] = '' mode = data.get('mode') if not mode: verrors.add(f'{schema}.mode', 'This field is required') remote_module = data.get('remotemodule') if mode == 'MODULE' and not remote_module: verrors.add(f'{schema}.remotemodule', 'This field is required') if mode == 'SSH': remote_port = data.get('remoteport') if not remote_port: verrors.add(f'{schema}.remoteport', 'This field is required') remote_path = data.get('remotepath') if not remote_path: verrors.add(f'{schema}.remotepath', 'This field is required') search = os.path.join(user['pw_dir'], '.ssh', 'id_[edr]*') exclude_from_search = os.path.join(user['pw_dir'], '.ssh', 'id_[edr]*pub') key_files = set(glob.glob(search)) - set(glob.glob(exclude_from_search)) if not key_files: verrors.add( f'{schema}.user', 'In order to use rsync over SSH you need a user' ' with a private key (DSA/ECDSA/RSA) set up in home dir.' ) else: for file in glob.glob(search): if '.pub' not in file: # file holds a private key and it's permissions should be 600 if os.stat(file).st_mode & 0o077 != 0: verrors.add( f'{schema}.user', f'Permissions {oct(os.stat(file).st_mode & 0o777)} for {file} are too open. Please ' f'correct them by running chmod 600 {file}' ) if( data.get('validate_rpath') and remote_path and remote_host and remote_port ): if '@' in remote_host: remote_username, remote_host = remote_host.rsplit('@', 1) else: remote_username = username try: with (await asyncio.wait_for(asyncssh.connect( remote_host, port=remote_port, username=remote_username, client_keys=key_files, known_hosts=None ), timeout=5)) as conn: await conn.run(f'test -d {shlex.quote(remote_path)}', check=True) except asyncio.TimeoutError: verrors.add( f'{schema}.remotehost', 'SSH timeout occurred. Remote path cannot be validated.' ) except OSError as e: if e.errno == 113: verrors.add( f'{schema}.remotehost', f'Connection to the remote host {remote_host} on port {remote_port} failed.' ) else: verrors.add( f'{schema}.remotehost', e.__str__() ) except asyncssh.DisconnectError as e: verrors.add( f'{schema}.remotehost', f'Disconnect Error[ error code {e.code} ] was generated when trying to ' f'communicate with remote host {remote_host} and remote user {remote_username}.' ) except asyncssh.ProcessError as e: if e.code == '1': verrors.add( f'{schema}.remotepath', 'The Remote Path you specified does not exist or is not a directory.' 'Either create one yourself on the remote machine or uncheck the ' 'validate_rpath field' ) else: verrors.add( f'{schema}.remotepath', f'Connection to Remote Host was successful but failed to verify ' f'Remote Path. {e.__str__()}' ) except asyncssh.Error as e: if e.__class__.__name__ in e.__str__(): exception_reason = e.__str__() else: exception_reason = e.__class__.__name__ + ' ' + e.__str__() verrors.add( f'{schema}.remotepath', f'Remote Path could not be validated. An exception was raised. {exception_reason}' ) elif data.get('validate_rpath'): verrors.add( f'{schema}.remotepath', 'Remote path could not be validated because of missing fields' ) data.pop('validate_rpath', None) # Keeping compatibility with legacy UI for field in ('mode', 'direction'): data[field] = data[field].lower() return verrors, data
async def do_update(self, data): """ Configure Reporting Database settings. If `cpu_in_percentage` is `true`, collectd reports CPU usage in percentage instead of "jiffies". `graphite` specifies a destination hostname or IP for collectd data sent by the Graphite plugin.. `graphite_separateinstances` corresponds to collectd SeparateInstances option. `graph_age` specifies the maximum age of stored graphs in months. `graph_points` is the number of points for each hourly, daily, weekly, etc. graph. Changing these requires destroying the current reporting database, so when these fields are changed, an additional `confirm_rrd_destroy: true` flag must be present. .. examples(websocket):: Update reporting settings :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "reporting.update", "params": [{ "cpu_in_percentage": false, "graphite": "", }] } Recreate reporting database with new settings :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "reporting.update", "params": [{ "graph_age": 12, "graph_points": 1200, "confirm_rrd_destroy": true, }] } """ confirm_rrd_destroy = data.pop('confirm_rrd_destroy', False) old = await self.config() new = copy.deepcopy(old) new.update(data) verrors = ValidationErrors() destroy_database = False for k in ['graph_age', 'graph_points']: if old[k] != new[k]: destroy_database = True if not confirm_rrd_destroy: verrors.add( f'reporting_update.{k}', 'Changing this option requires destroying the reporting database. This action ' 'must be confirmed by setting confirm_rrd_destroy flag', ) if verrors: raise verrors await self.middleware.call( 'datastore.update', self._config.datastore, old['id'], new, {'prefix': self._config.datastore_prefix} ) if destroy_database: await self.middleware.call('service.stop', 'collectd') await self.middleware.call('service.stop', 'rrdcached') await run( 'sh', '-c', f'rm {"--one-file-system -rf" if osc.IS_LINUX else "-rfx"} /var/db/collectd/rrd/*', check=False ) await self.middleware.call('reporting.setup') await self.middleware.call('service.start', 'rrdcached') await self.middleware.call('service.restart', 'collectd') return await self.config()
async def validate_data(self, data, schema): verrors = ValidationErrors() if data['pool'] and not await self.middleware.call('pool.query', [['name', '=', data['pool']]]): verrors.add(f'{schema}.pool', 'Please provide a valid pool configured in the system.') if ipaddress.ip_address(data['cluster_dns_ip']) not in ipaddress.ip_network(data['service_cidr']): verrors.add(f'{schema}.cluster_dns_ip', 'Must be in range of "service_cidr".') if data['node_ip'] not in await self.bindip_choices(): verrors.add(f'{schema}.node_ip', 'Please provide a valid IP address.') for k, _ in await self.validate_interfaces(data): verrors.add(f'{schema}.{k}', 'Please specify a valid interface.') for k in ('route_v4', 'route_v6'): gateway = data[f'{k}_gateway'] interface = data[f'{k}_interface'] if (not gateway and not interface) or (gateway and interface): continue for k2 in ('gateway', 'interface'): verrors.add(f'{schema}.{k}_{k2}', f'{k}_gateway and {k}_interface must be specified together.') verrors.check()
async def do_update(self, data): """ Update SMB Service Configuration. `netbiosname` defaults to the original hostname of the system. `workgroup` and `netbiosname` should have different values. `enable_smb1` allows legacy SMB clients to connect to the server when enabled. `localmaster` when set, determines if the system participates in a browser election. `domain_logons` is used to provide netlogin service for older Windows clients if enabled. `guest` attribute is specified to select the account to be used for guest access. It defaults to "nobody". `nullpw` when enabled allows the users to authorize access without a password. `zeroconf` should be enabled if macOS Clients will be connecting to the SMB share. `hostlookup` when enabled, allows using hostnames rather then IP addresses in "hostsallow"/"hostsdeny" fields of SMB Shares. """ old = await self.config() new = old.copy() new.update(data) verrors = ValidationErrors() if data.get('unixcharset') and data['unixcharset'] not in await self.unixcharset_choices(): verrors.add( 'smb_update.unixcharset', 'Please provide a valid value for unixcharset' ) for i in ('workgroup', 'netbiosname', 'netbiosname_b', 'netbiosalias'): if i not in data or not data[i]: continue if i == 'netbiosalias': for idx, item in enumerate(data[i]): if not await self.__validate_netbios_name(item): verrors.add(f'smb_update.{i}.{idx}', f'Invalid NetBIOS name: {item}') else: if not await self.__validate_netbios_name(data[i]): verrors.add(f'smb_update.{i}', f'Invalid NetBIOS name: {data[i]}') if new['netbiosname'] and new['netbiosname'].lower() == new['workgroup'].lower(): verrors.add('smb_update.netbiosname', 'NetBIOS and Workgroup must be unique') for i in ('filemask', 'dirmask'): if i not in data or not data[i]: continue try: if int(data[i], 8) & ~0o11777: raise ValueError('Not an octet') except (ValueError, TypeError): verrors.add(f'smb_update.{i}', 'Not a valid mask') if new['admin_group'] and new['admin_group'] != old['admin_group']: await self.add_admin_group(new['admin_group']) if verrors: raise verrors # TODO: consider using bidict for k, v in LOGLEVEL_MAP.items(): if new['loglevel'] == v: new['loglevel'] = k break await self.compress(new) await self._update_service(old, new) return await self.config()
async def validate_data(self, data, schema, old_data): verrors = ValidationErrors() if data.pop('migrate_applications', False): if data['pool'] == old_data['pool']: verrors.add( f'{schema}.migrate_applications', 'Migration of applications dataset only happens when a new pool is configured.' ) elif not data['pool']: verrors.add( f'{schema}.migrate_applications', 'Pool must be specified when migration of ix-application dataset is desired.' ) elif not old_data['pool']: verrors.add( f'{schema}.migrate_applications', 'A pool must have been configured previously for ix-application dataset migration.' ) else: if await self.middleware.call( 'zfs.dataset.query', [['id', '=', applications_ds_name(data['pool'])]]): verrors.add( f'{schema}.migrate_applications', f'Migration of {applications_ds_name(old_data["pool"])!r} to {data["pool"]!r} not ' f'possible as {applications_ds_name(data["pool"])} already exists.' ) if not await self.middleware.call( 'zfs.dataset.query', [['id', '=', applications_ds_name(old_data['pool'])]]): # Edge case but handled just to be sure verrors.add( f'{schema}.migrate_applications', f'{applications_ds_name(old_data["pool"])!r} does not exist, migration not possible.' ) network_cidrs = set([ ipaddress.ip_network( f'{ip_config["address"]}/{ip_config["netmask"]}', False) for interface in await self.middleware.call('interface.query') for ip_config in itertools.chain(interface['aliases'], interface['state']['aliases']) if ip_config['type'] != 'LINK' ]) unused_cidrs = [] if not data['cluster_cidr'] or not data['service_cidr']: unused_cidrs = await self.unused_cidrs(network_cidrs) # If index 0,1 belong to different classes, let's make sure that is not the case anymore if len(unused_cidrs) > 2 and unused_cidrs[0].split( '.')[0] != unused_cidrs[1].split('.')[0]: unused_cidrs.pop(0) if unused_cidrs and not data['cluster_cidr']: data['cluster_cidr'] = unused_cidrs.pop(0) if unused_cidrs and not data['service_cidr']: data['service_cidr'] = unused_cidrs.pop(0) if not data['cluster_dns_ip']: if data['service_cidr']: # Picking 10th ip ( which is the usual default ) from service cidr data['cluster_dns_ip'] = str( list( ipaddress.ip_network(data['service_cidr'], False).hosts())[9]) else: verrors.add(f'{schema}.cluster_dns_ip', 'Please specify cluster_dns_ip.') if data['pool'] and not await self.middleware.call( 'pool.query', [['name', '=', data['pool']]]): verrors.add( f'{schema}.pool', 'Please provide a valid pool configured in the system.') for k in ('cluster_cidr', 'service_cidr'): if not data[k]: verrors.add(f'{schema}.{k}', f'Please specify a {k.split("_")[0]} CIDR.') elif any( ipaddress.ip_network(data[k], False).overlaps(cidr) for cidr in network_cidrs): verrors.add(f'{schema}.{k}', 'Requested CIDR is already in use.') if data['cluster_cidr'] and data[ 'service_cidr'] and ipaddress.ip_network( data['cluster_cidr'], False).overlaps( ipaddress.ip_network(data['service_cidr'], False)): verrors.add(f'{schema}.cluster_cidr', 'Must not overlap with service CIDR.') if data['service_cidr'] and data[ 'cluster_dns_ip'] and ipaddress.ip_address( data['cluster_dns_ip']) not in ipaddress.ip_network( data['service_cidr']): verrors.add(f'{schema}.cluster_dns_ip', 'Must be in range of "service_cidr".') if data['node_ip'] not in await self.bindip_choices(): verrors.add(f'{schema}.node_ip', 'Please provide a valid IP address.') if not await self.middleware.call('route.configured_default_ipv4_route' ): if not data['route_v4_gateway']: verrors.add( f'{schema}.route_v4_gateway', 'Please set a default route for system or for kubernetes.') if not data['route_v4_interface']: verrors.add( f'{schema}.route_v4_interface', 'Please set a default route for system or specify default interface to be used for kubernetes.' ) for k, _ in await self.validate_interfaces(data): verrors.add(f'{schema}.{k}', 'Please specify a valid interface.') for k in ('route_v4', 'route_v6'): gateway = data[f'{k}_gateway'] interface = data[f'{k}_interface'] if (not gateway and not interface) or (gateway and interface): continue for k2 in ('gateway', 'interface'): verrors.add( f'{schema}.{k}_{k2}', f'{k}_gateway and {k}_interface must be specified together.' ) verrors.check()
async def do_create(self, data): """ Create a new user. If `uid` is not provided it is automatically filled with the next one available. `group` is required if `group_create` is false. `password` is required if `password_disabled` is false. Available choices for `shell` can be retrieved with `user.shell_choices`. `attributes` is a general-purpose object for storing arbitrary user information. """ verrors = ValidationErrors() if (not data.get('group') and not data.get('group_create')) or ( data.get('group') is not None and data.get('group_create')): verrors.add( 'user_create.group', f'Enter either a group name or create a new group to ' 'continue.', errno.EINVAL) await self.__common_validation(verrors, data, 'user_create') if data.get('sshpubkey') and not data['home'].startswith('/mnt'): verrors.add( 'user_create.sshpubkey', 'The home directory is not writable. Leave this field blank.') verrors.check() groups = data.pop('groups') create = data.pop('group_create') if create: group = await self.middleware.call( 'group.query', [('group', '=', data['username'])]) if group: group = group[0] else: group = await self.middleware.call('group.create', {'name': data['username']}) group = (await self.middleware.call('group.query', [('id', '=', group)]))[0] data['group'] = group['id'] else: group = await self.middleware.call('group.query', [('id', '=', data['group'])]) if not group: raise CallError(f'Group {data["group"]} not found') group = group[0] # Is this a new directory or not? Let's not nuke existing directories, # e.g. /, /root, /mnt/tank/my-dataset, etc ;). new_homedir = False home_mode = data.pop('home_mode') if data['home'] and data['home'] != '/nonexistent': try: try: os.makedirs(data['home'], mode=int(home_mode, 8)) new_homedir = True await self.middleware.call( 'filesystem.setperm', { 'path': data['home'], 'mode': home_mode, 'uid': data['uid'], 'gid': group['gid'], 'options': { 'stripacl': True } }) except FileExistsError: if not os.path.isdir(data['home']): raise CallError( 'Path for home directory already ' 'exists and is not a directory', errno.EEXIST) # If it exists, ensure the user is owner. await self.middleware.call( 'filesystem.chown', { 'path': data['home'], 'uid': data['uid'], 'gid': group['gid'], }) except OSError as oe: raise CallError('Failed to create the home directory ' f'({data["home"]}) for user: {oe}') except Exception: if new_homedir: shutil.rmtree(data['home']) raise if not data.get('uid'): data['uid'] = await self.get_next_uid() pk = None # Make sure pk exists to rollback in case of an error data = await self.user_compress(data) try: await self.__set_password(data) sshpubkey = data.pop('sshpubkey', None) # datastore does not have sshpubkey pk = await self.middleware.call('datastore.insert', 'account.bsdusers', data, {'prefix': 'bsdusr_'}) await self.__set_groups(pk, groups) except Exception: if pk is not None: await self.middleware.call('datastore.delete', 'account.bsdusers', pk) if new_homedir: # Be as atomic as possible when creating the user if # commands failed to execute cleanly. shutil.rmtree(data['home']) raise await self.middleware.call('service.reload', 'user') await self.__set_smbpasswd(data['username']) if os.path.exists(data['home']): for f in os.listdir(SKEL_PATH): if f.startswith('dot'): dest_file = os.path.join(data['home'], f[3:]) else: dest_file = os.path.join(data['home'], f) if not os.path.exists(dest_file): shutil.copyfile(os.path.join(SKEL_PATH, f), dest_file) await self.middleware.call( 'filesystem.chown', { 'path': dest_file, 'uid': data['uid'], 'gid': group['gid'], 'options': { 'recursive': True } }) data['sshpubkey'] = sshpubkey try: await self.__update_sshpubkey(data['home'], data, group['group']) except PermissionError as e: self.logger.warn('Failed to update authorized keys', exc_info=True) raise CallError(f'Failed to update authorized keys: {e}') return pk
async def do_update(self, pk, data): """ Update attributes of an existing user. """ user = await self._get_instance(pk) verrors = ValidationErrors() if 'group' in data: group = await self.middleware.call('datastore.query', 'account.bsdgroups', [('id', '=', data['group'])]) if not group: verrors.add('user_update.group', f'Group {data["group"]} not found', errno.ENOENT) group = group[0] else: group = user['group'] user['group'] = group['id'] await self.__common_validation(verrors, data, 'user_update', pk=pk) home = data.get('home') or user['home'] has_home = home != '/nonexistent' # root user (uid 0) is an exception to the rule if data.get('sshpubkey' ) and not home.startswith('/mnt') and user['uid'] != 0: verrors.add('user_update.sshpubkey', 'Home directory is not writable, leave this blank"') # Do not allow attributes to be changed for builtin user if user['builtin']: for i in ('group', 'home', 'home_mode', 'uid', 'username'): if i in data: verrors.add(f'user_update.{i}', 'This attribute cannot be changed') verrors.check() # Copy the home directory if it changed if (has_home and 'home' in data and data['home'] != user['home'] and not data['home'].startswith(f'{user["home"]}/')): home_copy = True home_old = user['home'] else: home_copy = False # After this point user dict has values from data user.update(data) if home_copy and not os.path.isdir(user['home']): try: os.makedirs(user['home']) await self.middleware.call( 'filesystem.chown', { 'path': user['home'], 'uid': user['uid'], 'gid': group['bsdgrp_gid'], }) except OSError: self.logger.warn('Failed to chown homedir', exc_info=True) if not os.path.isdir(user['home']): raise CallError(f'{user["home"]} is not a directory') home_mode = user.pop('home_mode', None) if user['builtin']: home_mode = None def set_home_mode(): if home_mode is not None: try: # Strip ACL before chmod. This is required when aclmode = restricted setfacl = subprocess.run( ['/bin/setfacl', '-b', user['home']], check=False) if setfacl.returncode != 0: self.logger.debug('Failed to strip ACL: %s', setfacl.stderr.decode()) os.chmod(user['home'], int(home_mode, 8)) except OSError: self.logger.warn('Failed to set homedir mode', exc_info=True) try: await self.__update_sshpubkey( home_old if home_copy else user['home'], user, group['bsdgrp_group'], ) except PermissionError as e: self.logger.warn('Failed to update authorized keys', exc_info=True) raise CallError(f'Failed to update authorized keys: {e}') if home_copy: def do_home_copy(): try: command = f"/bin/cp -a {shlex.quote(home_old) + '/'} {shlex.quote(user['home'] + '/')}" subprocess.run( ["/usr/bin/su", "-", user["username"], "-c", command], check=True) except subprocess.CalledProcessError as e: self.logger.warn(f"Failed to copy homedir: {e}") set_home_mode() asyncio.ensure_future(self.middleware.run_in_thread(do_home_copy)) elif has_home: asyncio.ensure_future(self.middleware.run_in_thread(set_home_mode)) user.pop('sshpubkey', None) await self.__set_password(user) if 'groups' in user: groups = user.pop('groups') await self.__set_groups(pk, groups) user = await self.user_compress(user) await self.middleware.call('datastore.update', 'account.bsdusers', pk, user, {'prefix': 'bsdusr_'}) await self.middleware.call('service.reload', 'user') await self.__set_smbpasswd(user['username']) return pk
async def validate_data(self, data, schema): verrors = ValidationErrors() driver = data.get('driver') if driver: if driver not in ( await self.middleware.call('ups.driver_choices')).keys(): verrors.add( f'{schema}.driver', 'Driver selected does not match local machine\'s driver list' ) identifier = data['identifier'] if identifier: if not re.search(r'^[a-z0-9\.\-_]+$', identifier, re.I): verrors.add(f'{schema}.identifier', 'Use alphanumeric characters, ".", "-" and "_"') for field in [ field for field in ['monpwd', 'monuser'] if data.get(field) ]: if re.search(r'[ #]', data[field], re.I): verrors.add(f'{schema}.{field}', 'Spaces or number signs are not allowed') mode = data.get('mode') if mode == 'MASTER': for field in filter(lambda f: not data[f], ['port', 'driver']): verrors.add(f'{schema}.{field}', 'This field is required') else: if not data.get('remotehost'): verrors.add(f'{schema}.remotehost', 'This field is required') to_emails = data.get('toemail') if to_emails: data['toemail'] = ';'.join(to_emails) else: data['toemail'] = '' data['mode'] = data['mode'].lower() data['shutdown'] = data['shutdown'].lower() return verrors, data
async def do_create(self, data): verrors = ValidationErrors() if (not data.get('group') and not data.get('group_create')) or ( data.get('group') is not None and data.get('group_create')): verrors.add('group', f'You need to either provide a group or group_create', errno.EINVAL) await self.__common_validation(verrors, data) if data.get('sshpubkey') and not data['home'].startswith('/mnt'): verrors.add('sshpubkey', 'Home directory is not writable, leave this blank"') if verrors: raise verrors groups = data.pop('groups') or [] create = data.pop('group_create') if create: group = await self.middleware.call( 'group.query', [('group', '=', data['username'])]) if group: group = group[0] else: group = await self.middleware.call('group.create', {'name': data['username']}) group = (await self.middleware.call('group.query', [('id', '=', group)]))[0] data['group'] = group['id'] else: group = await self.middleware.call('group.query', [('id', '=', data['group'])]) if not group: raise CallError(f'Group {data["group"]} not found') group = group[0] # Is this a new directory or not? Let's not nuke existing directories, # e.g. /, /root, /mnt/tank/my-dataset, etc ;). new_homedir = False home_mode = data.pop('home_mode') if data['home'] != '/nonexistent': try: os.makedirs(data['home'], mode=int(home_mode, 8)) except FileExistsError: if not os.path.isdir(data['home']): raise CallError( 'Path for home directory already ' 'exists and is not a directory', errno.EEXIST) except OSError as oe: raise CallError('Failed to create the home directory ' f'({data["home"]}) for user: {oe}') else: new_homedir = True if os.stat(data['home']).st_dev == os.stat('/mnt').st_dev: raise CallError(f'Path for the home directory (data["home"]) ' 'must be under a volume or dataset') if not data.get('uid'): data['uid'] = await self.get_next_uid() pk = None # Make sure pk exists to rollback in case of an error try: password = await self.__set_password(data) await self.__update_sshpubkey(data, group['group']) pk = await self.middleware.call('datastore.insert', 'account.bsdusers', data, {'prefix': 'bsdusr_'}) await self.__set_groups(pk, groups) except Exception: if pk is not None: await self.middleware.call('datastore.delete', 'account.bsdusers', pk) if new_homedir: # Be as atomic as possible when creating the user if # commands failed to execute cleanly. shutil.rmtree(data['home']) raise await self.middleware.call('service.reload', 'user') await self.__set_smbpasswd(data['username'], password) return pk
async def do_update(self, data): """ Update NFS Service Configuration. `servers` represents number of servers to create. When `allow_nonroot` is set, it allows non-root mount requests to be served. `bindip` is a list of IP's on which NFS will listen for requests. When it is unset/empty, NFS listens on all available addresses. `v4` when set means that we switch from NFSv3 to NFSv4. `v4_v3owner` when set means that system will use NFSv3 ownership model for NFSv4. `v4_krb` will force NFS shares to fail if the Kerberos ticket is unavailable. `v4_domain` overrides the default DNS domain name for NFSv4. `mountd_port` specifies the port mountd(8) binds to. `rpcstatd_port` specifies the port rpc.statd(8) binds to. `rpclockd_port` specifies the port rpclockd_port(8) binds to. .. examples(websocket):: Update NFS Service Configuration to listen on 192.168.0.10 and use NFSv4 :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "pool.resilver.update", "params": [{ "bindip": [ "192.168.0.10" ], "v4": true }] } """ if data.get("v4") is False: data.setdefault("v4_v3owner", False) old = await self.config() new = old.copy() new.update(data) verrors = ValidationErrors() new_v4_krb_enabled = ( new["v4_krb"] or await self.middleware.call("kerberos.keytab.query") ) if new["v4"] and new_v4_krb_enabled and not await self.middleware.call("system.is_freenas"): if await self.middleware.call("failover.licensed"): gc = await self.middleware.call("datastore.config", "network.globalconfiguration") if not gc["gc_hostname_virtual"] or not gc["gc_domain"]: verrors.add( "nfs_update.v4", "Enabling kerberos authentication on TrueNAS HA requires setting the virtual hostname and " "domain" ) if osc.IS_LINUX: if len(new['bindip']) > 1: verrors.add('nfs_update.bindip', 'Listening on more than one address is not supported') bindip_choices = await self.bindip_choices() for i, bindip in enumerate(new['bindip']): if bindip not in bindip_choices: verrors.add(f'nfs_update.bindip.{i}', 'Please provide a valid ip address') if new["v4"] and new_v4_krb_enabled and await self.middleware.call('activedirectory.get_state') != "DISABLED": """ In environments with kerberized NFSv4 enabled, we need to tell winbindd to not prefix usernames with the short form of the AD domain. Directly update the db and regenerate the smb.conf to avoid having a service disruption due to restarting the samba server. """ if await self.middleware.call('smb.get_smb_ha_mode') == 'LEGACY': raise ValidationError( 'nfs_update.v4', 'Enabling kerberos authentication on TrueNAS HA requires ' 'the system dataset to be located on a data pool.' ) ad = await self.middleware.call('activedirectory.config') await self.middleware.call( 'datastore.update', 'directoryservice.activedirectory', ad['id'], {'ad_use_default_domain': True} ) await self.middleware.call('etc.generate', 'smb') await self.middleware.call('service.reload', 'cifs') if not new["v4"] and new["v4_v3owner"]: verrors.add("nfs_update.v4_v3owner", "This option requires enabling NFSv4") if new["v4_v3owner"] and new["userd_manage_gids"]: verrors.add( "nfs_update.userd_manage_gids", "This option is incompatible with NFSv3 ownership model for NFSv4") if not new["v4"] and new["v4_domain"]: verrors.add("nfs_update.v4_domain", "This option does not apply to NFSv3") if verrors: raise verrors await self.nfs_compress(new) await self._update_service(old, new) await self.nfs_extend(new) return new