예제 #1
0
파일: tftp.py 프로젝트: jiangge/freenas
class TFTPService(SystemServiceService):

    class Config:
        service = "tftp"
        datastore_prefix = "tftp_"

    @accepts(Dict(
        'tftp_update',
        Bool('newfiles'),
        Dir('directory'),
        Str('host', validators=[IpAddress()]),
        Int('port', validators=[Port()]),
        Str('options'),
        Str('umask', validators=[Match(r"^[0-7]{3}$")]),
        Str('username'),
        update=True
    ))
    async def do_update(self, data):
        old = await self.config()

        new = old.copy()
        new.update(data)

        verrors = ValidationErrors()

        if new["directory"]:
            await check_path_resides_within_volume(verrors, self.middleware, "tftp_update.directory", new["directory"])

        if verrors:
            raise verrors

        await self._update_service(old, new)

        return await self.config()
예제 #2
0
class AFPService(SystemServiceService):

    class Config:
        service = "afp"
        datastore_prefix = "afp_srv_"

    @accepts(Dict(
        'afp_update',
        Bool('guest'),
        Str('guest_user'),
        List('bindip', items=[Str('ip', validators=[IpAddress()])]),
        Int('connections_limit', validators=[Range(min=1, max=65535)]),
        Dir('dbpath'),
        Str('global_aux'),
        Str('map_acls', enum=["rights", "mode", "none"]),
        Str('chmod_request', enum=["preserve", "simple", "ignore"]),
        update=True
    ))
    async def do_update(self, data):
        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"])

        if verrors:
            raise verrors

        await self._update_service(old, new)

        return new
예제 #3
0
파일: tftp.py 프로젝트: bmhughes/freenas
class TFTPService(SystemServiceService):
    class Config:
        service = "tftp"
        datastore_prefix = "tftp_"
        cli_namespace = "service.tftp"

    ENTRY = Dict(
        'tftp_entry',
        Bool('newfiles', required=True),
        Str('directory', required=True),
        Str('host', validators=[IpAddress()], required=True),
        Int('port', validators=[Port()], required=True),
        Str('options', required=True),
        Str('umask', required=True, validators=[Match(r'^[0-7]{3}$')]),
        Str('username', required=True),
        Int('id', required=True),
    )

    @accepts(
        Patch(
            'tftp_entry',
            'tftp_update',
            ('rm', {
                'name': 'id'
            }),
            ('replace', Dir('directory')),
            ('attr', {
                'update': True
            }),
        ))
    async def do_update(self, data):
        """
        Update TFTP Service Configuration.

        `newfiles` when set enables network devices to send files to the system.

        `username` sets the user account which will be used to access `directory`. It should be ensured `username`
        has access to `directory`.
        """
        old = await self.config()

        new = old.copy()
        new.update(data)

        verrors = ValidationErrors()

        if new["directory"]:
            await check_path_resides_within_volume(verrors, self.middleware,
                                                   "tftp_update.directory",
                                                   new["directory"])

        if verrors:
            raise verrors

        await self._update_service(old, new)

        return await self.config()
예제 #4
0
 def resolve_host_name_thread(hostname):
     try:
         try:
             ip = IpAddress()
             ip(hostname)
             return hostname
         except ValueError:
             return socket.gethostbyname(hostname)
     except Exception:
         return False
예제 #5
0
 def resolve_host_name_thread(hostname):
     try:
         try:
             ip = IpAddress()
             ip(hostname)
         except ShouldBe:
             return socket.gethostbyname(hostname)
         else:
             return socket.gethostbyaddr(hostname)
     except Exception:
         return False
예제 #6
0
class AFPService(SystemServiceService):
    class Config:
        service = "afp"
        datastore_prefix = "afp_srv_"

    @accepts(
        Dict(
            'afp_update',
            Bool('guest'),
            Str('guest_user'),
            List('bindip', items=[Str('ip', validators=[IpAddress()])]),
            Int('connections_limit', validators=[Range(min=1, max=65535)]),
            Bool('homedir_enable'),
            Dir('homedir'),
            Str('homename'),
            Bool('hometimemachine'),
            Dir('dbpath'),
            Str('global_aux'),
            Str('map_acls', enum=["rights", "mode", "none"]),
            Str('chmod_request', enum=["preserve", "simple", "ignore"]),
        ), Bool('dry_run'))
    async def update(self, data, dry_run=False):
        old = await self.config()

        new = old.copy()
        new.update(data)

        verrors = ValidationErrors()

        if new["homedir_enable"] and not new["homedir"]:
            verrors.add("afp_update.homedir",
                        "This field is required for \"Home directories\".")

        if not new["homedir_enable"] and new["homedir"]:
            verrors.add("afp_update.homedir_enable",
                        "This field is required for \"Home directories\".")

        if new["homedir"]:
            await check_path_resides_within_volume(verrors, self.middleware,
                                                   "afp_update.homedir",
                                                   new["homedir"])

        if new["dbpath"]:
            await check_path_resides_within_volume(verrors, self.middleware,
                                                   "afp_update.dbpath",
                                                   new["dbpath"])

        if verrors:
            raise verrors

        if not dry_run:
            await self._update_service(old, new)

        return new
예제 #7
0
    def ping_remote(self, options):
        """
        Method that will send an ICMP echo request to "hostname"
        and will wait up to "timeout" for a reply.
        """
        ip = None
        ip_found = True
        verrors = ValidationErrors()
        try:
            ip = IpAddress()
            ip(options['hostname'])
            ip = options['hostname']
        except ValueError:
            ip_found = False
        if not ip_found:
            try:
                if options['type'] == 'ICMP':
                    ip = socket.getaddrinfo(options['hostname'], None)[0][4][0]
                elif options['type'] == 'ICMPV4':
                    ip = socket.getaddrinfo(options['hostname'], None,
                                            socket.AF_INET)[0][4][0]
                elif options['type'] == 'ICMPV6':
                    ip = socket.getaddrinfo(options['hostname'], None,
                                            socket.AF_INET6)[0][4][0]
            except socket.gaierror:
                verrors.add(
                    'options.hostname',
                    f'{options["hostname"]} cannot be resolved to an IP address.'
                )

        verrors.check()

        addr = ipaddress.ip_address(ip)
        if not addr.version == 4 and (options['type'] == 'ICMP'
                                      or options['type'] == 'ICMPV4'):
            verrors.add(
                'options.type',
                f'Requested ICMPv4 protocol, but the address provided "{addr}" is not a valid IPv4 address.'
            )
        if not addr.version == 6 and options['type'] == 'ICMPV6':
            verrors.add(
                'options.type',
                f'Requested ICMPv6 protocol, but the address provided "{addr}" is not a valid IPv6 address.'
            )
        verrors.check()

        ping_host = False
        if addr.version == 4:
            ping_host = self._ping_host(ip, options['timeout'])
        elif addr.version == 6:
            ping_host = self._ping6_host(ip, options['timeout'])

        return ping_host
예제 #8
0
 async def san_to_string(self, san_list):
     # TODO: ADD MORE TYPES WRT RFC'S
     san_string = ''
     ip_validator = IpAddress()
     for count, san in enumerate(san_list or []):
         try:
             ip_validator(san)
         except ValueError:
             san_string += f'DNS: {san}, '
         else:
             san_string += f'IP: {san}, '
     return san_string[:-2] if san_list else ''
예제 #9
0
def normalize_san(san_list: list) -> list:
    # TODO: ADD MORE TYPES WRT RFC'S
    normalized = []
    ip_validator = IpAddress()
    for count, san in enumerate(san_list or []):
        # If we already have SAN normalized, let's use the normalized version and don't
        # try to add a type ourselves
        if ':' in san:
            san_type, san = san.split(':', 1)
        else:
            try:
                ip_validator(san)
            except ValueError:
                san_type = 'DNS'
            else:
                san_type = 'IP'
        normalized.append([san_type, san])

    return normalized
예제 #10
0
    async def do_update(self, data):
        """
        `alua` is a no-op for FreeNAS.
        """
        old = await self.config()

        new = old.copy()
        new.update(data)

        verrors = ValidationErrors()

        servers = data.get('isns_servers') or []
        for server in servers:
            reg = RE_IP_PORT.search(server)
            if reg:
                ip = reg.group(1)
                if ip and ip[0] == '[' and ip[-1] == ']':
                    ip = ip[1:-1]
                try:
                    ip_validator = IpAddress()
                    ip_validator(ip)
                    continue
                except ValueError:
                    pass
            verrors.add('iscsiglobal_update.isns_servers',
                        f'Server "{server}" is not a valid IP(:PORT)? tuple.')

        if verrors:
            raise verrors

        new['isns_servers'] = '\n'.join(servers)

        await self._update_service(old, new)

        if old['alua'] != new['alua']:
            await self.middleware.call('etc.generate', 'loader')

        return await self.config()
예제 #11
0
class VCenterService(ConfigService):

    PRIVATE_GROUP_NAME = 'iXSystems'

    class Config:
        datastore = 'vcp.vcenterconfiguration'
        datastore_prefix = 'vc_'
        datastore_extend = 'vcenter.vcenter_extend'

    @private
    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_httpsredirect']):
                verrors.add(f'{schema_name}.action',
                            'Please enable vCenter plugin over HTTPS')

        return verrors

    @private
    async def vcenter_extend(self, data):
        data['password'] = await self.middleware.call('notifier.pwenc_decrypt',
                                                      data['password'])
        data['port'] = int(
            data['port']) if data['port'] else 443  # Defaulting to 443
        return data

    @accepts(
        Dict(
            'vcenter_update_dict',
            Int('port', validators=[Port()]),
            Str('action',
                enum=['INSTALL', 'REPAIR', 'UNINSTALL', 'UPGRADE'],
                required=True),
            Str('management_ip'),
            Str('ip'),  # HOST IP
            Str('password', password=True),
            Str('username'),
        ))
    async def do_update(self, data):
        old = await self.config()
        new = old.copy()
        new.update(data)

        schema_name = 'vcenter_update'
        verrors = await self.common_validation(new, schema_name)
        if verrors:
            raise verrors

        action = new.pop('action')
        system_general = await self.middleware.call('system.general.config')
        ui_protocol = 'https' if system_general['ui_httpsredirect'] else 'http'
        ui_port = system_general['ui_port'] if ui_protocol.lower(
        ) != 'https' else system_general['ui_httpsport']
        fingerprint = await self.middleware.call(
            'certificate.get_host_certificates_thumbprint',
            new['management_ip'], new['port'])
        plugin_file_name = await self.middleware.run_in_thread(
            self.get_plugin_file_name)
        # TODO: URL will change once the plugin file's location is shifted
        management_addr = f'{ui_protocol}://{new["management_ip"]}:{ui_port}/legacy/static/{plugin_file_name}'

        install_dict = {
            'port': new['port'],
            'fingerprint': fingerprint,
            'management_ip': management_addr,
            'ip': new['ip'],
            'password': new['password'],
            'username': new['username']
        }

        if action == 'INSTALL':

            if new['installed']:
                verrors.add(f'{schema_name}.action',
                            'Plugin is already installed')
            else:

                for r_key in ('management_ip', 'ip', 'password', 'port',
                              'username'):
                    if not new[r_key]:
                        verrors.add(
                            f'{schema_name}.{r_key}',
                            'This field is required to install the plugin')

                if verrors:
                    raise verrors

                try:
                    await self.middleware.run_in_thread(
                        self.__install_vcenter_plugin, install_dict)
                except ValidationError as e:
                    verrors.add_validation_error(e)
                else:
                    new['version'] = await self.middleware.run_in_thread(
                        self.get_plugin_version)
                    new['installed'] = True

        elif action == 'REPAIR':

            if not new['installed']:
                verrors.add(
                    f'{schema_name}.action',
                    'Plugin is not installed. Please install it first')
            else:

                # FROM MY UNDERSTANDING REPAIR IS CALLED WHEN THE DATABASE APPARENTLY TELLS THAT THE PLUGIN IS PRESENT
                # BUT THE SYSTEM FAILS TO RECOGNIZE THE PLUGIN EXTENSION

                try:
                    credential_dict = install_dict.copy()
                    credential_dict.pop('management_ip')
                    credential_dict.pop('fingerprint')

                    found_plugin = await self.middleware.run_in_thread(
                        self._find_plugin, credential_dict)
                    if found_plugin:
                        verrors.add(f'{schema_name}.action',
                                    'Plugin repair is not required')
                except ValidationError as e:
                    verrors.add_validation_error(e)
                else:

                    if verrors:
                        raise verrors

                    try:
                        repair_dict = install_dict.copy()
                        repair_dict['install_mode'] = 'REPAIR'
                        await self.middleware.run_in_thread(
                            self.__install_vcenter_plugin, repair_dict)
                    except ValidationError as e:
                        verrors.add_validation_error(e)

        elif action == 'UNINSTALL':

            if not new['installed']:
                verrors.add(f'{schema_name}.action',
                            'Plugin is not installed on the system')
            else:

                try:
                    uninstall_dict = install_dict.copy()
                    uninstall_dict.pop('management_ip')
                    uninstall_dict.pop('fingerprint')
                    await self.middleware.run_in_thread(
                        self.__uninstall_vcenter_plugin, uninstall_dict)
                except ValidationError as e:
                    verrors.add_validation_error(e)
                else:
                    new['installed'] = False
                    new['port'] = 443
                    for key in new:
                        # flushing existing object with empty values
                        if key not in ('installed', 'id', 'port'):
                            new[key] = ''

        else:

            if not new['installed']:
                verrors.add(f'{schema_name}.action', 'Plugin not installed')
            elif not (await self.is_update_available()):
                verrors.add(f'{schema_name}.action',
                            'No update is available for vCenter plugin')
            else:

                try:
                    await self.middleware.run_in_thread(
                        self.__upgrade_vcenter_plugin, install_dict)
                except ValidationError as e:
                    verrors.add_validation_error(e)
                else:
                    new['version'] = await self.middleware.run_in_thread(
                        self.get_plugin_version)

        if verrors:
            raise verrors

        new['password'] = await self.middleware.call('notifier.pwenc_encrypt',
                                                     new['password'])

        await self.middleware.call('datastore.update', self._config.datastore,
                                   new['id'], new,
                                   {'prefix': self._config.datastore_prefix})

        return await self.config()

    @private
    async def is_update_available(self):
        latest_version = await self.middleware.run_in_thread(
            self.get_plugin_version)
        current_version = (await self.config())['version']
        return latest_version if current_version and \
                                 parse_version(latest_version) > parse_version(current_version) else None

    @private
    async def plugin_root_path(self):
        return await self.middleware.call('notifier.gui_static_root')

    @private
    async def get_management_ip_choices(self):
        ip_list = await self.middleware.call('interfaces.ip_in_use',
                                             {'ipv4': True})

        return [ip_dict['address'] for ip_dict in ip_list]

    @private
    def get_plugin_file_name(self):
        # TODO: The path to the plugin should be moved over to middlewared from django
        root_path = self.middleware.call_sync('vcenter.plugin_root_path')
        return next(v for v in os.listdir(root_path)
                    if 'plugin' in v and '.zip' in v)

    @private
    def get_plugin_version(self):
        file_name = self.get_plugin_file_name()
        return file_name.split('_')[1]

    @private
    async def property_file_path(self):
        return os.path.join(
            (await self.middleware.call('notifier.gui_base_path')),
            'vcp/Extensionconfig.ini.dist')

    @private
    async def resource_folder_path(self):
        return os.path.join(
            (await self.middleware.call('notifier.gui_base_path')),
            'vcp/vcp_locales')

    @private
    def create_event_keyvalue_pairs(self):
        try:

            eri_list = []
            resource_folder_path = self.middleware.call_sync(
                'vcenter.resource_folder_path')
            for file in os.listdir(resource_folder_path):
                eri = vim.Extension.ResourceInfo()

                # Read locale file from vcp_locale
                eri.module = file.split("_")[0]
                with open(os.path.join(resource_folder_path, file),
                          'r') as file:
                    for line in file:
                        if len(line) > 2 and '=' in line:
                            if 'locale' in line:
                                eri.locale = line.split(
                                    '=')[1].lstrip().rstrip()
                            else:
                                prop = line.split('=')
                                key_val = vim.KeyValue()
                                key_val.key = prop[0].lstrip().rstrip()
                                key_val.value = prop[1].lstrip().rstrip()
                                eri.data.append(key_val)
                eri_list.append(eri)
            return eri_list
        except Exception as e:
            raise ValidationError('vcenter_update.create_event_keyvalue_pairs',
                                  f'Can not read locales : {e}')

    @private
    def get_extension_key(self):
        cp = configparser.ConfigParser()
        cp.read(self.middleware.call_sync('vcenter.property_file_path'))
        return cp.get('RegisterParam', 'key')

    @accepts(
        Dict(
            'install_vcenter_plugin',
            Int('port', required=True),
            Str('fingerprint', required=True),
            Str('management_ip', required=True),
            Str('install_mode',
                enum=['NEW', 'REPAIR'],
                required=False,
                default='NEW'),
            Str('ip', required=True),  # HOST IP
            Str('password', password=True,
                required=True),  # Password should be decrypted
            Str('username', required=True),
            register=True))
    def __install_vcenter_plugin(self, data):

        encrypted_password = self.middleware.call_sync(
            'notifier.pwenc_encrypt', data['password'])

        update_zipfile_dict = data.copy()
        update_zipfile_dict.pop('management_ip')
        update_zipfile_dict.pop('fingerprint')
        update_zipfile_dict['password'] = encrypted_password
        update_zipfile_dict['plugin_version_old'] = 'null'
        update_zipfile_dict['plugin_version_new'] = self.get_plugin_version()
        self.__update_plugin_zipfile(update_zipfile_dict)

        data.pop('install_mode')

        try:
            ext = self.get_extension(data['management_ip'],
                                     data['fingerprint'])

            data.pop('fingerprint')
            data.pop('management_ip')
            si = self.__check_credentials(data)

            si.RetrieveServiceContent().extensionManager.RegisterExtension(ext)

        except vim.fault.NoPermission:
            raise ValidationError(
                'vcenter_update.username',
                'VCenter user has no permission to install the plugin')

    @accepts(
        Patch('install_vcenter_plugin',
              'uninstall_vcenter_plugin', ('rm', {
                  'name': 'fingerprint'
              }), ('rm', {
                  'name': 'install_mode'
              }), ('rm', {
                  'name': 'management_ip'
              }),
              register=True))
    def __uninstall_vcenter_plugin(self, data):
        try:
            extkey = self.get_extension_key()

            si = self.__check_credentials(data)
            si.RetrieveServiceContent().extensionManager.UnregisterExtension(
                extkey)

        except vim.fault.NoPermission:
            raise ValidationError(
                'vcenter_update.username',
                'VCenter user does not have necessary permission to uninstall the plugin'
            )

    @accepts(
        Patch('install_vcenter_plugin', 'upgrade_vcenter_plugin',
              ('rm', {
                  'name': 'install_mode'
              })))
    def __upgrade_vcenter_plugin(self, data):

        update_zipfile_dict = data.copy()
        update_zipfile_dict.pop('management_ip')
        update_zipfile_dict.pop('fingerprint')
        update_zipfile_dict['install_mode'] = 'UPGRADE'
        update_zipfile_dict['password'] = self.middleware.call_sync(
            'notifier.pwenc_encrypt', data['password'])
        update_zipfile_dict['plugin_version_old'] = str(
            (self.middleware.call_sync('vcenter.config'))['version'])
        update_zipfile_dict['plugin_version_new'] = self.middleware.call_sync(
            'vcenter.get_plugin_version')
        self.__update_plugin_zipfile(update_zipfile_dict)

        try:
            ext = self.get_extension(data['management_ip'],
                                     data['fingerprint'])

            data.pop('fingerprint')
            data.pop('management_ip')
            si = self.__check_credentials(data)

            si.RetrieveServiceContent().extensionManager.UpdateExtension(ext)

        except vim.fault.NoPermission:
            raise ValidationError(
                'vcenter_update.username',
                'VCenter user has no permission to upgrade the plugin')

    @accepts(Ref('uninstall_vcenter_plugin'))
    def _find_plugin(self, data):
        try:
            si = self.__check_credentials(data)

            extkey = self.get_extension_key()
            ext = si.RetrieveServiceContent().extensionManager.FindExtension(
                extkey)

            if ext is None:
                return False
            else:
                return f'TrueNAS System : {ext.client[0].url.split("/")[2]}'
        except vim.fault.NoPermission:
            raise ValidationError(
                'vcenter_update.username',
                'VCenter user has no permission to find the plugin on this system'
            )

    @accepts(Ref('uninstall_vcenter_plugin'))
    def __check_credentials(self, data):
        try:
            si = SmartConnect("https",
                              data['ip'],
                              data['port'],
                              data['username'],
                              data['password'],
                              sslContext=get_context_object())

            if si:
                return si

        except (socket.gaierror, TimeoutError):
            raise ValidationError(
                'vcenter_update.ip',
                'Provided vCenter Hostname/IP or port are not valid')
        except vim.fault.InvalidLogin:
            raise ValidationError(
                'vcenter_update.username',
                'Provided vCenter credentials are not valid ( username or password )'
            )
        except vim.fault.NoPermission:
            raise ValidationError(
                'vcenter_update.username',
                'vCenter user does not have permission to perform this operation'
            )
        except Exception as e:

            if 'not a vim server' in str(e).lower():
                # In case an IP is provided for a server which is not a VIM server - then Exception is raised with
                # following text
                # Exception: 10.XX.XX.XX:443 is not a VIM server

                raise ValidationError(
                    'vcenter_update.ip',
                    'Provided Hostname/IP is not a VIM server')

            else:
                raise e

    @private
    def get_extension(self, vcp_url, fingerprint):
        try:
            cp = configparser.ConfigParser()
            cp.read(self.middleware.call_sync('vcenter.property_file_path'))
            version = self.middleware.call_sync('vcenter.get_plugin_version')

            description = vim.Description()
            description.label = cp.get('RegisterParam', 'label')
            description.summary = cp.get('RegisterParam', 'description')

            ext = vim.Extension()
            ext.company = cp.get('RegisterParam', 'company')
            ext.version = version
            ext.key = cp.get('RegisterParam', 'key')
            ext.description = description
            ext.lastHeartbeatTime = datetime.now()

            server_info = vim.Extension.ServerInfo()
            server_info.serverThumbprint = fingerprint
            server_info.type = vcp_url.split(':')[0].upper()  # sysgui protocol
            server_info.url = vcp_url
            server_info.description = description
            server_info.company = cp.get('RegisterParam', 'company')
            server_info.adminEmail = ['ADMIN EMAIL']
            ext.server = [server_info]

            client = vim.Extension.ClientInfo()
            client.url = vcp_url
            client.company = cp.get('RegisterParam', 'company')
            client.version = version
            client.description = description
            client.type = "vsphere-client-serenity"
            ext.client = [client]

            event_info = []
            for e in cp.get('RegisterParam', 'events').split(","):
                ext_event_type_info = vim.Extension.EventTypeInfo()
                ext_event_type_info.eventID = e
                event_info.append(ext_event_type_info)

            task_info = []
            for t in cp.get('RegisterParam', 'tasks').split(","):
                ext_type_info = vim.Extension.TaskTypeInfo()
                ext_type_info.taskID = t
                task_info.append(ext_type_info)

            # Register custom privileges required for vcp RBAC
            priv_info = []
            for priv in cp.get('RegisterParam', 'auth').split(","):
                ext_type_info = vim.Extension.PrivilegeInfo()
                ext_type_info.privID = priv
                ext_type_info.privGroupName = self.PRIVATE_GROUP_NAME
                priv_info.append(ext_type_info)

            ext.taskList = task_info
            ext.eventList = event_info
            ext.privilegeList = priv_info

            resource_list = self.create_event_keyvalue_pairs()
            ext.resourceList = resource_list

            return ext
        except configparser.NoOptionError as e:
            raise ValidationError('vcenter_update.get_extension',
                                  f'Property Missing : {e}')

    @private
    def extract_zip(self, src_path, dest_path):
        if not os.path.exists(dest_path):
            os.makedirs(dest_path)
        with zipfile.ZipFile(src_path) as zip_f:
            zip_f.extractall(dest_path)

    @private
    def zipdir(self, src_path, dest_path):

        assert os.path.isdir(src_path)
        with closing(zipfile.ZipFile(dest_path, "w")) as z:

            for root, dirs, files in os.walk(src_path):
                for fn in files:
                    absfn = os.path.join(root, fn)
                    zfn = absfn[len(src_path) + len(os.sep):]
                    z.write(absfn, zfn)

    @private
    def remove_directory(self, dest_path):
        if os.path.exists(dest_path):
            shutil.rmtree(dest_path)

    @accepts(
        Dict(
            'update_vcp_plugin_zipfile',
            Int('port', required=True),
            Str('ip', required=True, validators=[IpAddress()]),
            Str('install_mode',
                enum=['NEW', 'REPAIR', 'UPGRADE'],
                required=True),
            Str('plugin_version_old', required=True),
            Str('plugin_version_new', required=True),
            Str('password', required=True,
                password=True),  # should be encrypted
            Str('username', required=True),
            register=True))
    def __update_plugin_zipfile(self, data):
        file_name = self.middleware.call_sync('vcenter.get_plugin_file_name')
        plugin_root_path = self.middleware.call_sync(
            'vcenter.plugin_root_path')

        self.extract_zip(os.path.join(plugin_root_path, file_name),
                         os.path.join(plugin_root_path, 'plugin'))
        self.extract_zip(
            os.path.join(plugin_root_path,
                         'plugin/plugins/ixsystems-vcp-service.jar'),
            os.path.join(plugin_root_path,
                         'plugin/plugins/ixsystems-vcp-service'))

        data['fpath'] = os.path.join(
            plugin_root_path,
            'plugin/plugins/ixsystems-vcp-service/META-INF/config/install.properties'
        )

        self.__create_property_file(data)
        self.zipdir(
            os.path.join(plugin_root_path,
                         'plugin/plugins/ixsystems-vcp-service'),
            os.path.join(plugin_root_path,
                         'plugin/plugins/ixsystems-vcp-service.jar'))
        self.remove_directory(
            os.path.join(plugin_root_path,
                         'plugin/plugins/ixsystems-vcp-service'))

        shutil.make_archive(os.path.join(plugin_root_path, file_name[0:-4]),
                            'zip', os.path.join(plugin_root_path, 'plugin'))

        self.remove_directory(os.path.join(plugin_root_path, 'plugin'))

    @accepts(
        Patch(
            'update_vcp_plugin_zipfile',
            '__create_property_file',
            ('add', {
                'name': 'fpath',
                'type': 'str'
            }),
        ))
    def __create_property_file(self, data):
        # Password encrypted using notifier.pwenc_encrypt

        config = configparser.ConfigParser()
        with open(data['fpath'], 'w') as config_file:
            config.add_section('installation_parameter')
            config.set('installation_parameter', 'ip', data['ip'])
            config.set('installation_parameter', 'username', data['username'])
            config.set('installation_parameter', 'port', str(data['port']))
            config.set('installation_parameter', 'password', data['password'])
            config.set('installation_parameter', 'install_mode',
                       data['install_mode'])
            config.set('installation_parameter', 'plugin_version_old',
                       data['plugin_version_old'])
            config.set('installation_parameter', 'plugin_version_new',
                       data['plugin_version_new'])
            config.write(config_file)
예제 #12
0
class AFPService(SystemServiceService):

    class Config:
        service = 'afp'
        datastore_extend = 'afp.extend'
        datastore_prefix = 'afp_srv_'

    @private
    async def extend(self, afp):
        for i in ('map_acls', 'chmod_request'):
            afp[i] = afp[i].upper()
        return afp

    @private
    async def compress(self, afp):
        for i in ('map_acls', 'chmod_request'):
            value = afp.get(i)
            if value:
                afp[i] = value.lower()
        return afp

    @accepts(Dict(
        'afp_update',
        Bool('guest'),
        Str('guest_user'),
        List('bindip', items=[Str('ip', validators=[IpAddress()])]),
        Int('connections_limit', validators=[Range(min=1, max=65535)]),
        Dir('dbpath'),
        Str('global_aux', max_length=None),
        Str('map_acls', enum=['RIGHTS', 'MODE', 'NONE']),
        Str('chmod_request', enum=['PRESERVE', 'SIMPLE', 'IGNORE']),
        Str('loglevel', enum=[x.name for x in AFPLogLevel]),
        update=True
    ))
    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()

    @accepts()
    async def bindip_choices(self):
        """
        List of valid choices for IP addresses to which to bind the AFP service.
        """
        return {
            d['address']: d['address'] for d in await self.middleware.call('interface.ip_in_use')
        }
예제 #13
0
class CtdbPublicIpService(CRUDService):

    class Config:
        namespace = 'ctdb.public.ips'
        cli_private = True

    @filterable
    def query(self, filters, options):
        ctdb_ips = []
        if self.middleware.call_sync('service.started', 'ctdb'):
            ips = self.middleware.call_sync('ctdb.general.ips')
            ctdb_ips = list(map(lambda i: dict(i, id=i['public_ip'], enabled=(i['pnn'] >= 0)), ips))

        try:
            shared_vol = Path(CTDBConfig.CTDB_LOCAL_MOUNT.value)
            mounted = shared_vol.is_mount()
        except Exception:
            # can happen when mounted but glusterd service
            # is stopped/crashed etc
            mounted = False

        etc_ips = []
        if mounted:
            pub_ip_file = Path(CTDBConfig.GM_PUB_IP_FILE.value)
            etc_ip_file = Path(CTDBConfig.ETC_PUB_IP_FILE.value)
            if pub_ip_file.exists():
                if etc_ip_file.is_symlink() and etc_ip_file.resolve() == pub_ip_file:
                    with open(pub_ip_file) as f:
                        for i in f.read().splitlines():
                            if not i.startswith('#'):
                                enabled = True
                                public_ip = i.split('/')[0]
                            else:
                                enabled = False
                                public_ip = i.split('#')[1].split('/')[0]

                            etc_ips.append({
                                'id': public_ip,
                                'pnn': -1 if not enabled else 'N/A',
                                'enabled': enabled,
                                'public_ip': public_ip,
                                'interfaces': [{
                                    'name': i.split()[-1],
                                    'active': False,
                                    'available': False,
                                }]
                            })

        # if the public ip was gracefully disabled and ctdb daemon is running
        # then it will report the public ip address information, however,
        # if the ctdb daemon was restarted after it was disabled then it
        # won't report it at all, yet, it's still written to the config
        # file prepended with a "#". This is by design so we need to
        # make sure we normalize the output of what ctdb daemon reports
        # and what's been written to the public address config file
        normalized = []
        if not ctdb_ips:
            # means the ctdb daemon didn't return any type of
            # public address information so just return the
            # contents of etc_ips
            # NOTE: the contents of the etc file could be empty
            # (or not there) because it's a symlink pointed to
            # the cluster shared volume. In this case, there
            # isn't much we can do
            normalized = etc_ips
        else:
            if not etc_ips:
                # means the ctdb daemon is reporting public address(es)
                # however we're unable to read the config file which
                # could happen if the ctdb shared volume was umounted
                # while the ctdb daemon is running so we just return
                # what the daemon sees
                normalized = ctdb_ips
            else:
                # means the ctdb daemon is reporting public address(es)
                # and we have public addresses written to the config file
                # but it doesn't mean they necessarily match each other
                # so we need to normalize the output so the returned output
                # is always the same between the 2
                normalized.extend([i for i in ctdb_ips if i['public_ip'] not in [j.keys() for j in etc_ips]])

        return filter_list(normalized, filters, options)

    @private
    async def reload(self):
        """
        Reload the public addresses configuration file on the ctdb nodes. When it completes
        the public addresses will be reconfigured and reassigned across the cluster as
        necessary.
        """
        if await self.middleware.call('service.started', 'ctdb'):
            re = await run(['ctdb', 'reloadips'], encoding='utf8', errors='ignore', check=False)
            if re.returncode:
                # this isn't fatal it just means the newly added public ip won't show
                # up until the ctdb service has been restarted so just log a message
                self.logger.warning('Failed to reload public ip addresses %r', re.stderr)

    @accepts(Dict(
        'public_create',
        IPAddr('ip', required=True),
        Int('netmask', required=True),
        Str('interface', required=True),
    ))
    @job(lock=PUB_LOCK)
    async def do_create(self, job, data):
        """
        Add a ctdb public address to the cluster

        `ip` string representing an IP v4/v6 address
        `netmask` integer representing a cidr notated netmask (i.e. 16/24/48/64 etc)
        `interface` string representing a network interface to apply the `ip`
        """

        schema_name = 'public_create'
        verrors = ValidationErrors()

        await self.middleware.call('ctdb.ips.common_validation', data, schema_name, verrors)
        await self.middleware.call('ctdb.ips.update_file', data, schema_name)
        await self.middleware.call('ctdb.public.ips.reload')

        return await self.middleware.call('ctdb.public.ips.query', [('public_ip', '=', data['ip'])])

    @accepts(
        Str('ip', validators=[IpAddress()], required=True),
        Dict(
            'public_update',
            Bool('enable', required=True),
        )
    )
    @job(lock=PUB_LOCK)
    async def do_update(self, job, id, option):
        """
        Update Public IP address in the ctdb cluster.

        `ip` string representing the public ip address
        `enable` boolean. When True, enable the node else disable the node.
        """

        schema_name = 'public_update'
        verrors = ValidationErrors()

        data = await self.get_instance(id)
        data['enable'] = option['enable']

        await self.middleware.call('ctdb.ips.common_validation', data, schema_name, verrors)
        await self.middleware.call('ctdb.ips.update_file', data, schema_name)
        await self.middleware.call('ctdb.public.ips.reload')

        return await self.get_instance(id)
예제 #14
0
class CtdbPublicIpService(CRUDService):
    class Config:
        namespace = 'ctdb.public.ips'
        cli_private = True

    @filterable
    def query(self, filters, options):
        """
        Retrieve information about configured public IP addresses for the
        ctdb cluster. This call raise a CallError with errno set to ENXIO
        if this node is not in a state where it can provide accurate
        information about cluster. Examples problematic states are:

        - ctdb or glusterd are not running on this node

        - ctdb shared volume is not mounted
        """
        if not self.middleware.call_sync('service.started', 'ctdb'):
            raise CallError(
                "ctdb is not running. Unable to gather public address info",
                errno.ENXIO)

        ctdb_ips = self.middleware.call_sync('ctdb.general.ips')

        try:
            shared_vol = Path(CTDBConfig.CTDB_LOCAL_MOUNT.value)
            mounted = shared_vol.is_mount()
        except Exception:
            # can happen when mounted but glusterd service
            # is stopped/crashed etc
            mounted = False

        if not mounted:
            raise CallError("CTDB shared volume is in unhealthy state.",
                            errno.ENXIO)

        nodes = {}

        for entry in self.middleware.call_sync('ctdb.general.listnodes'):
            """
            Skip disabled nodes since they cannot hold public addresses.
            If a node does not have a public_addresses file, we should still
            return an entry for it (but without any configured_addresses).
            This allows us to accurately report cases where perhaps due to
            user intervention, public address file was removed but ctdb
            IPs have not been reloaded.
            """
            if not entry['enabled']:
                continue

            pnn = entry['pnn']
            nodes[pnn] = {
                'id': pnn,
                'pnn': pnn,
                'configured_ips': {},
                'active_ips': {}
            }

            with contextlib.suppress(FileNotFoundError):
                with open(f'{shared_vol}/public_addresses_{pnn}') as f:
                    for i in f.read().splitlines():
                        if not i.startswith('#'):
                            enabled = True
                            public_ip = i.split('/')[0]
                        else:
                            enabled = False
                            public_ip = i.split('#')[1].split('/')[0]

                        nodes[pnn]['configured_ips'].update({
                            public_ip: {
                                'enabled': enabled,
                                'public_ip': public_ip,
                                'interface_name': i.split()[-1]
                            }
                        })

        for entry in ctdb_ips:
            if not nodes.get(entry['pnn']):
                """
                Most likely case here is that we're transitioning IP and it's
                current pnn is -1. Generate log message for now, and we can
                determine in future whether more action is required.
                """
                self.logger.warning(
                    "%s: active ip address does not exist in public_addresses file",
                    entry['public_ip'])
                continue

            nodes[entry['pnn']]['active_ips'].update(
                {entry['public_ip']: entry['interfaces']})

        return filter_list(list(nodes.values()), filters, options)

    @private
    async def reload(self):
        """
        Reload the public addresses configuration file on the ctdb nodes. When it completes
        the public addresses will be reconfigured and reassigned across the cluster as
        necessary.
        """
        if await self.middleware.call('service.started', 'ctdb'):
            re = await run(['ctdb', 'reloadips'],
                           encoding='utf8',
                           errors='ignore',
                           check=False)
            if re.returncode:
                # this isn't fatal it just means the newly added public ip won't show
                # up until the ctdb service has been restarted so just log a message
                self.logger.warning('Failed to reload public ip addresses %r',
                                    re.stderr)

    @accepts(
        Dict(
            'public_create',
            Int('pnn', required=True),
            IPAddr('ip', required=True),
            Int('netmask', required=True),
            Str('interface', required=True),
        ))
    @job(lock=PUB_LOCK)
    async def do_create(self, job, data):
        """
        Add a ctdb public address to the cluster

        `pnn` node number of record to adjust
        `ip` string representing an IP v4/v6 address
        `netmask` integer representing a cidr notated netmask (i.e. 16/24/48/64 etc)
        `interface` string representing a network interface to apply the `ip`
        """

        schema_name = 'public_create'
        verrors = ValidationErrors()

        await self.middleware.call('ctdb.ips.common_validation', data,
                                   schema_name, verrors)
        await self.middleware.call('ctdb.ips.update_file', data, schema_name)
        await self.middleware.call('ctdb.public.ips.reload')

        return await self.middleware.call('ctdb.public.ips.query',
                                          [('id', '=', data['pnn'])])

    @accepts(Int('pnn', required=True),
             Dict(
                 'public_update',
                 Str('ip', validators=[IpAddress()], required=True),
                 Bool('enable', required=True),
             ))
    @job(lock=PUB_LOCK)
    async def do_update(self, job, id, option):
        """
        Update Public IP address in the ctdb cluster.
        `pnn` - cluster node number
        `ip` string representing the public ip address
        `enable` boolean. When True, enable the node else disable the node.
        """

        schema_name = 'public_update'
        verrors = ValidationErrors()

        data = await self.get_instance(id)
        data['enable'] = option['enable']

        await self.middleware.call('ctdb.ips.common_validation', data,
                                   schema_name, verrors)
        await self.middleware.call('ctdb.ips.update_file', data, schema_name)
        await self.middleware.call('ctdb.public.ips.reload')

        return await self.get_instance(id)
예제 #15
0
class TFTPService(SystemServiceService):
    class Config:
        service = "tftp"
        datastore_prefix = "tftp_"
        cli_namespace = "service.tftp"

    ENTRY = Dict(
        'tftp_entry',
        Bool('newfiles', required=True),
        Str('directory', required=True),
        Str('host', validators=[IpAddress()], required=True),
        Int('port', validators=[Port()], required=True),
        Str('options', required=True),
        Str('umask', required=True, validators=[Match(r'^[0-7]{3}$')]),
        Str('username', required=True),
        Int('id', required=True),
    )

    @accepts()
    @returns(Dict('tftp_host_choices', additional_attrs=True))
    async def host_choices(self):
        """
        Return host choices for TFTP service to use.
        """
        return {
            d['address']: d['address']
            for d in await self.middleware.call('interface.ip_in_use', {
                'static': True,
                'any': True
            })
        }

    @accepts(
        Patch(
            'tftp_entry',
            'tftp_update',
            ('rm', {
                'name': 'id'
            }),
            ('replace', Dir('directory')),
            ('attr', {
                'update': True
            }),
        ))
    async def do_update(self, data):
        """
        Update TFTP Service Configuration.

        `newfiles` when set enables network devices to send files to the system.

        `username` sets the user account which will be used to access `directory`. It should be ensured `username`
        has access to `directory`.
        """
        old = await self.config()

        new = old.copy()
        new.update(data)

        verrors = ValidationErrors()

        if new["directory"]:
            await check_path_resides_within_volume(verrors, self.middleware,
                                                   "tftp_update.directory",
                                                   new["directory"])

        if new['host'] not in await self.host_choices():
            verrors.add('tftp_update.host',
                        'Please provide a valid ip address')

        if verrors:
            raise verrors

        await self._update_service(old, new)

        return await self.config()
예제 #16
0
class FTPService(SystemServiceService):
    class Config:
        service = "ftp"
        datastore_prefix = "ftp_"

    @accepts(
        Dict(
            'ftp_update',
            Int('port', validators=[Range(min=1, max=65535)]),
            Int('clients', validators=[Range(min=1, max=10000)]),
            Int('ipconnections', validators=[Range(min=0, max=1000)]),
            Int('loginattempt', validators=[Range(min=0, max=1000)]),
            Int('timeout', validators=[Range(min=0, max=10000)]),
            Bool('rootlogin'),
            Bool('onlyanonymous'),
            Dir('anonpath'),
            Bool('onlylocal'),
            Str('banner'),
            Str('filemask', validators=[Match(r"^[0-7]{3}$")]),
            Str('dirmask', validators=[Match(r"^[0-7]{3}$")]),
            Bool('fxp'),
            Bool('resume'),
            Bool('defaultroot'),
            Bool('ident'),
            Bool('reversedns'),
            Str('masqaddress', validators=[Or(Exact(""), IpAddress())]),
            Int('passiveportsmin',
                validators=[Or(Exact(0), Range(min=1024, max=65535))]),
            Int('passiveportsmax',
                validators=[Or(Exact(0), Range(min=1024, max=65535))]),
            Int('localuserbw', validators=[Range(min=0)]),
            Int('localuserdlbw', validators=[Range(min=0)]),
            Int('anonuserbw', validators=[Range(min=0)]),
            Int('anonuserdlbw', validators=[Range(min=0)]),
            Bool('tls'),
            Str('tls_policy',
                enum=[
                    "on", "off", "data", "!data", "auth", "ctrl", "ctrl+data",
                    "ctrl+!data", "auth+data", "auth+!data"
                ]),
            Bool('tls_opt_allow_client_renegotiations'),
            Bool('tls_opt_allow_dot_login'),
            Bool('tls_opt_allow_per_user'),
            Bool('tls_opt_common_name_required'),
            Bool('tls_opt_enable_diags'),
            Bool('tls_opt_export_cert_data'),
            Bool('tls_opt_no_cert_request'),
            Bool('tls_opt_no_empty_fragments'),
            Bool('tls_opt_no_session_reuse_required'),
            Bool('tls_opt_stdenvvars'),
            Bool('tls_opt_dns_name_required'),
            Bool('tls_opt_ip_address_required'),
            Int('ssltls_certificate'),
            Str('options'),
        ), Bool('dry_run'))
    async def update(self, data, dry_run=False):
        old = await self.config()

        new = old.copy()
        new.update(data)

        verrors = ValidationErrors()

        if not ((new["passiveportsmin"] == 0) == (new["passiveportsmax"]
                                                  == 0)):
            verrors.add(
                "passiveportsmin",
                "passiveportsmin and passiveportsmax should be both zero or non-zero"
            )
        if not ((new["passiveportsmin"] == 0 and new["passiveportsmax"] == 0)
                or (new["passiveportsmax"] > new["passiveportsmin"])):
            verrors.add(
                "ftp_update.passiveportsmax",
                "When specified, should be greater than passiveportsmin")

        if new["onlyanonymous"] and not new["anonpath"]:
            verrors.add("ftp_update.anonpath",
                        "This field is required for anonymous login")

        if new["anonpath"]:
            await check_path_resides_within_volume(verrors, self.middleware,
                                                   "ftp_update.anonpath",
                                                   new["anonpath"])

        if new["tls"] and new["ssltls_certificate"] == 0:
            verrors.add("ftp_update.ssltls_certificate",
                        "This field is required when TLS is enabled")

        if verrors:
            raise verrors

        if not dry_run:
            await self._update_service(old, new)

            if not old['tls'] and new['tls']:
                await self.middleware.call('service._start_ssl', 'proftpd')

        return new
예제 #17
0
        schema_name = 'public_create'
        verrors = ValidationErrors()

        await self.middleware.call('ctdb.ips.common_validation', data,
                                   schema_name, verrors)
        await self.middleware.call('ctdb.ips.update_file', data, schema_name)
        await self.middleware.call('ctdb.public.ips.reload')

        return await self.middleware.call('ctdb.public.ips.query',
                                          [('id', '=', data['pnn'])])

    @accepts(Int('pnn', required=True),
             Dict(
                 'public_update',
                 Str('ip', validators=[IpAddress()], required=True),
                 Bool('enable', required=True),
             ))
    @job(lock=PUB_LOCK)
    async def do_update(self, job, id, option):
        """
        Update Public IP address in the ctdb cluster.
        `pnn` - cluster node number
        `ip` string representing the public ip address
        `enable` boolean. When True, enable the node else disable the node.
        """

        schema_name = 'public_update'
        verrors = ValidationErrors()

        data = await self.get_instance(id)
예제 #18
0
    async def validate_attrs(self, data):
        verrors = ValidationErrors()

        additional_params = data.get('additional_params')
        if additional_params:
            # Let's be very generic here and introduce very basic validation
            # Expected format is as following
            # [ipv6.icmpneighbor]
            #   history = 86400
            #   enabled = yes
            #
            # While we are here, we will also introduce basic formatting to the file to ensure
            # that we can make it as compliable as possible

            param_str = ''
            for i in additional_params.split('\n'):
                i = i.strip()
                if not i:
                    continue
                if i.startswith('#'):
                    # Let's not validate this
                    if i.replace('#', '').startswith('['):
                        param_str += f'\n\n{i}'
                    else:
                        param_str += f'\n\t{i}'

                    continue

                if i.startswith('[') and not i.endswith(']'):
                    verrors.add(
                        'netdata_update.additional_params',
                        f'Please correct format for {i}. i.e [system.intr]')
                elif not i.startswith('[') and '=' not in i:
                    verrors.add(
                        'netdata_update.additional_params',
                        f'Please correct format for {i}. i.e enabled = yes')

                if i.startswith('['):
                    param_str += f'\n\n{i}'
                else:
                    param_str += f'\n\t{i}'

            data['additional_params'] = param_str + '\n'

        bind_to_ips = data.get('bind')
        if bind_to_ips:
            valid_ips = [
                ip['address']
                for ip in await self.middleware.call('interfaces.ip_in_use')
            ]
            valid_ips.extend(['127.0.0.1', '::1', '0.0.0.0', '::'])

            for bind_ip in bind_to_ips:
                if bind_ip not in valid_ips:
                    verrors.add('netdata_update.bind',
                                f'Invalid {bind_ip} bind IP')
        else:
            verrors.add('netdata_update.bind', 'This field is required')

        update_alarms = data.pop('update_alarms', {})
        valid_alarms = self._alarms
        if update_alarms:
            for alarm in update_alarms:
                if alarm not in valid_alarms:
                    verrors.add('netdata_update.alarms',
                                f'{alarm} not a valid alarm')

            verrors.extend(
                validate_attributes([
                    Dict(key, Bool('enabled', required=True))
                    for key in update_alarms
                ], {'attributes': update_alarms}))

        # Validating streaming metrics now
        stream_mode = data.get('stream_mode')
        if stream_mode == 'SLAVE':
            for key in ('api_key', 'destination'):
                if not data.get(key):
                    verrors.add(
                        f'netdata_update.{key}',
                        f'{key} is required with stream mode as SLAVE')

            destinations = data.get('destination')
            if destinations:
                ip_addr = IpAddress()
                port = Port()
                for dest in destinations:
                    ip = dest.split(':')[0]
                    try:
                        ip_addr(ip)
                    except ValueError as e:
                        verrors.add('netdata_update.destination', str(e))
                    else:
                        if ':' in dest:
                            try:
                                port(dest.split(':')[1])
                            except ValueError as e:
                                verrors.add('netdata_update.destination',
                                            f'Not a valid port: {e}')
        elif stream_mode == 'MASTER':
            for key in ('allow_from', 'api_key'):
                if not data.get(key):
                    verrors.add(
                        f'netdata_update.{key}',
                        f'{key} is required with stream mode as MASTER')

        verrors.check()

        data['alarms'].update(update_alarms)

        return data