| Viewing file:  clextselect.py (19.71 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# -*- coding: utf-8 -*-
 # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
 #
 # Licensed under CLOUD LINUX LICENSE AGREEMENT
 # http://cloudlinux.com/docs/LICENSE.TXT
 
 from __future__ import absolute_import
 from __future__ import print_function
 from __future__ import division
 import collections
 import configparser
 import os
 import operator
 
 from clcommon import clcagefs
 
 from .clselect import ClSelect
 from .clselectexcept import ClSelectExcept
 from .clselectprint import clprint
 from . import utils
 
 # dependencies modulse dict. Example { 'ext_name': 'ext1' } - ext1 depends from ext_name
 depend_modules_dict = dict()
 
 
 class ClExtSelect(ClSelect):
 CONFLICTS_PATH = (
 '/etc/cl.selector.conf.d/php.extensions.conflicts'
 if clcagefs.in_cagefs()
 else '/etc/cl.selector/php.extensions.conflicts'
 )
 SYSTEM_ALT_PATH = '/opt/alt'
 
 def __init__(self, item='php'):
 ClSelect.__init__(self, item)
 self._conflicts = []
 # Sets in _get_enabled_extensions method
 #  True  - extension list was read from native php built-ins
 #  False - extension list was read from /etc/cl.selector/defaults.cfg
 self._use_default_exts_from_native_php = False
 
 def _is_disabled_extention(self, ext_name):
 return ext_name in self._hidden_extensions
 
 def enable_extensions(self, version, ext_list):
 """
 Adds extensions to default list of extensions for a version
 """
 alternatives = self.get_all_alternatives_data()
 self._check_alternative(version, alternatives)
 defaults_contents = self._process_ini_file(
 self.DEFAULTS_PATH,
 (self._item, version),
 self._add_extensions,
 ext_list, action = 'enable_extentions')
 self._write_to_file(
 '\n'.join(defaults_contents), ClExtSelect.DEFAULTS_PATH)
 
 def replace_extensions(self, version, ext_list):
 """
 Replaces extensions to default list of extensions for a version.
 Writes/updates /etc/cl.selector/defaults.cfg file
 :param version: alt-php version to process
 :param ext_list: list extensions to set as defaults for the version
 """
 alternatives = self.get_all_alternatives_data()
 self._check_alternative(version, alternatives)
 defaults_contents = self._process_ini_file(
 self.DEFAULTS_PATH,
 (self._item, version),
 self._replace_extensions,
 ext_list)
 self._write_to_file(
 '\n'.join(defaults_contents), self.DEFAULTS_PATH)
 
 def disable_extensions(self, version, ext_list):
 """
 Removes extensions from default list of extensions for a version
 :param version: alt-php version to process
 :param ext_list: comma separated extensions list to delete
 """
 alternatives = self.get_all_alternatives_data()
 self._check_alternative(version, alternatives)
 defaults_contents = self._process_ini_file(
 self.DEFAULTS_PATH,
 (self._item, version),
 self._del_extensions,
 ext_list, action = 'disable_extentions')
 self._write_to_file('\n'.join(defaults_contents), self.DEFAULTS_PATH)
 
 def list_extensions(self, version):
 """
 Returns list of extensions marking built-ins and enabled ones
 Also replaces mysqli->nd_mysqli in defaults.cfg for new installations according to LVEMAN-1399
 :param version: php version
 :return Tuple: (extension_name, extension_state)
 extension_state:
 None        -- built-in extension
 False/True  -- disabled/enabled extension
 """
 ext_mysqli_name = 'mysqli'
 ext_nd_mysqli_name = 'nd_mysqli'
 ext_list_to_write = list()
 is_need_to_write_defaults = False
 alternatives = self.get_all_alternatives_data()
 self._check_alternative(version, alternatives)
 # Get extensions list from /etc/cl.selector/defaults.cfg for supplied verson or
 # list of built-in extesions for native if version does not present in defaults.cfg
 enabled_extensions = self._get_enabled_extensions(version)
 # Get extension list for version
 as_built_in = self._get_builtins(version)
 try:
 # Get extensions list for version - list of files
 # /opt/alt/phpXX/etc/php.d.all/*.ini without .ini extension
 # Without dependencies analysis
 as_extensions = self._load_extensions_list(version)
 except ClSelectExcept.UnableToGetExtensions:
 as_extensions = []
 # ['bz2', 'calendar'] -> ('bz2', None), ('calendar', None)
 all_extensions = list(map((lambda i: (i, None)), as_built_in))
 for ext in as_extensions:
 status = False
 if (ext in enabled_extensions) and (ext not in as_built_in):
 status = True
 ext_set = set([(ext, True), (ext, False), (ext, None)])
 if not set(all_extensions).intersection(ext_set):
 # add ext and its status to result list
 # LVEMAN-1399:
 # If defaults modules was taken from native php built-ins
 # replace mysqli to nd_mysqli (if it exist) and set status True to it
 if ext == ext_mysqli_name and self._use_default_exts_from_native_php\
 and ext_nd_mysqli_name in as_extensions:
 all_extensions.append((ext_mysqli_name, False))
 all_extensions.append((ext_nd_mysqli_name, True))
 # After module replacement we need to write new list as defaults
 is_need_to_write_defaults = True
 # add nd_mysqli to list for write
 ext_list_to_write.append(ext_nd_mysqli_name)
 else:
 all_extensions.append((ext, status))
 if status:
 # if module enabled, add it to list for write
 ext_list_to_write.append(ext)
 # all_extensions example: [('bz2', None), ('calendar', None), (u'zip', True), (u'zmq', False)]
 # If module replacement occures, write list to defaults.cfg
 if is_need_to_write_defaults:
 self.replace_extensions(version, ext_list_to_write)
 return tuple(sorted(all_extensions, key=operator.itemgetter(0)))
 
 def _get_enabled_extensions(self, version):
 """
 Returns list of enabled extensions for a version
 """
 try:
 # reads extensions list from /etc/cl.selector/defaults.cfg
 data = self._dh.get(
 "%s%s" % (self._item, version), 'modules')
 self._use_default_exts_from_native_php = False
 return list(map((lambda i: i.strip()), data.split(',')))
 except (configparser.NoSectionError, configparser.NoOptionError):
 self._use_default_exts_from_native_php = True
 return self._get_builtins('native')
 
 def _add_extensions(self, section_info, section, data, trace=True):
 """
 Adds 'modules' option to section or extends it
 @param section_info: tuple (item and version)
 @param section: list
 @param data: list
 @return: list
 """
 section_header = self._make_section_header(section_info)
 if len(section) == 0 or section_header != section[0]:
 return section
 midx = None
 modules = []
 alt_path = self._compose_alt_path(section_info[1])
 for idx in range(len(section)):
 if section[idx].startswith('modules'):
 midx = idx
 break
 if midx:
 modules_string = section[midx][section[midx].find('=')+1:].strip()
 modules.extend(
 list(map((lambda i: i.strip()), modules_string.split(','))))
 modules.extend(data)
 modules = self._check_for_conflicts(modules)
 resolved_modules = self._include_dependencies(modules, alt_path)
 modules_string = 'modules = %s' % (','.join(sorted(resolved_modules)))
 if midx:
 section[midx] = modules_string
 else:
 section.append(modules_string)
 return self._smooth_data(section)
 
 def _replace_extensions(self, section_info, section, data, trace=True):
 """
 Adds 'modules' option to section or extends it
 @param section_info: tuple (item and version).
 Example: ('php', '5.2')
 @param section: list. Modules from /etc/cl.selecto/defaults.cfg for supplied php version
 Example: ['[php5.2]',
 'modules = bcmath,dom,gd,imap,json,mcrypt,mysql,mysqli,phar,posix,sockets,uuid,wddx,xmlreader,zip',
 '', '']
 @param data: list: Modules list to set from command line
 @:param trace: ????, Currently not using, always True
 @return: list
 """
 global depend_modules_dict
 section_header = self._make_section_header(section_info)
 if len(section) == 0 or section_header != section[0]:
 return section
 midx = None
 alt_path = self._compose_alt_path(section_info[1])
 for idx in range(len(section)):
 if section[idx].startswith('modules'):
 midx = idx
 break
 modules = data[:]
 if trace:
 resolved_modules = set()
 modules = self._check_for_conflicts(modules)
 for mod in modules:
 include_dep = self._include_dependencies([mod], alt_path)
 if len(include_dep) != 1:
 # Dependencies found, add them to depend_modules_dict
 # Add deps to dict
 depend_modules_dict.update({dep_module: mod for dep_module in include_dep if dep_module != mod})
 resolved_modules.update(include_dep)
 modules_string = 'modules = %s' % (','.join(sorted(resolved_modules)))
 else:
 modules_string = 'modules = %s' % (','.join(sorted(data)))
 if midx:
 section[midx] = modules_string
 else:
 section.append(modules_string)
 # Cleanup dependency list - remove from dependencies all modules, present in command line
 modules = depend_modules_dict.copy()
 for dep_module in modules.keys():
 if dep_module in data:
 del depend_modules_dict[dep_module]
 return self._smooth_data(section)
 
 def _del_extensions(self, section_info, section, data, trace=True):
 """
 Deletes items in data list from section list
 @param section_info: tuple (item and version)
 @param section: list
 @param data: list of extension names to delete
 @return: list
 """
 section_header = self._make_section_header(section_info)
 if len(section) == 0 or section_header != section[0]:
 return section
 midx = None
 alt_path = self._compose_alt_path(section_info[1])
 for idx in range(len(section)):
 if section[idx].startswith('modules'):
 midx = idx
 break
 if not midx:
 return section
 modules_string = section[midx][section[midx].find('=')+1:].strip()
 modules = set(map((lambda i: i.strip()), modules_string.split(',')))
 resolved_modules = modules.copy()
 for item in set(data):
 if item not in modules:
 continue
 rest_of_modules = modules.difference([item])
 if self._is_dependency(item, rest_of_modules, alt_path):
 continue
 resolved_modules.discard(item)
 resolved_modules = self._include_dependencies(resolved_modules, alt_path)
 modules_string = 'modules = %s' % (','.join(sorted(resolved_modules)))
 section[midx] = modules_string
 return self._smooth_data(section)
 
 def _is_dependency(cls, ext, modules, alt_path):
 """
 Checks if module in modules dependent on ext and returns true or false
 @param ext: Module to check
 @param modules: set of names of installed modules
 @param alt_path: Path to alt-php ini dir: /opt/alt/phpXX/etc/php.d.all
 @return: bool. True if ext present in dependencies of any module in modules list
 """
 global depend_modules_dict
 for mod in modules:
 dependencies = cls._get_dependencies(mod, alt_path)
 if ext in dependencies:
 depend_modules_dict[ext] = mod
 return True
 return False
 _is_dependency = classmethod(_is_dependency)
 
 def _compose_alt_path(self, version):
 """
 Composes and returns path for alternatives
 """
 return os.path.join(
 self.SYSTEM_ALT_PATH,
 "%s%s" % (self._item, version.replace('.', '')),
 "etc",
 "%s.d.all" % (self._item,))
 
 def _include_dependencies(cls, ext_list, alt_path, data=None):
 """
 Includes dependencies into extensions list and update data dict
 if present
 @param ext_list: list
 @param alt_path: string
 @param data: dict
 @return: list
 """
 in_section = False
 result_ext_list = []
 handled = set()
 q = collections.deque(ext_list)
 
 while q:
 ext = q.popleft()
 if ext in handled:
 continue
 
 handled.add(ext)
 
 ext_path = os.path.join(alt_path, f'{ext}.ini')
 try:
 f = open(ext_path)
 file_contents = []
 pending_contents = []
 
 for line in f:
 if line.startswith('extension') or line.startswith('zend_extension'):
 ext_name = cls._single_out_extension(ext, line)
 if ext_name != ext and ext_name not in handled:
 q.appendleft(ext_name)
 continue
 
 file_contents.append(f';---{ext}---')
 in_section = True
 file_contents.extend(pending_contents)
 pending_contents = []
 
 if not (line.startswith(';') or line.startswith('\n')):
 if in_section:
 file_contents.append(line.rstrip())
 else:
 pending_contents.append(line.rstrip())
 f.close()
 
 if data is not None and ext not in data:
 data[ext] = file_contents
 
 # Adding to the beggining of the result list due to LVEMAN-504
 result_ext_list.insert(0, ext)
 except (OSError, IOError):
 continue
 
 return result_ext_list
 _include_dependencies = classmethod(_include_dependencies)
 
 def _get_dependencies(cls, ext, alt_path):
 """
 Checks if an extension has dependencies and if so returns them
 Otherwise returns None
 @param ext: string
 @return: set
 """
 dependencies = set()
 ext_path = os.path.join(alt_path, "%s.ini" % (ext,))
 try:
 f = open(ext_path)
 for line in f:
 if line.startswith('extension'):
 ext_name = cls._single_out_extension(ext, line)
 if ext_name != ext:
 dependencies.add(ext_name)
 return dependencies
 except (OSError, IOError):
 return dependencies
 _get_dependencies = classmethod(_get_dependencies)
 
 def _single_out_extension(ext, line):
 """
 Singles out and returns extension from line
 """
 quirks = {'ixed': 'sourceguardian'}
 if '/' in line:
 ext_name = line[line.rfind('/')+1:].strip()
 else:
 ext_name = line[line.find('=')+1:].strip(' "')
 if '.' in ext_name:
 ext_name = ext_name[:ext_name.find('.')]
 if '-' in ext_name:
 ext_name = ext_name[:ext_name.rfind('-')]
 if ext_name in quirks:
 ext_name = quirks[ext_name]
 elif ext in ext_name:
 ext_name = ext
 elif ('_' in ext and ''.join(map((lambda i: i.capitalize()),
 ext.split('_'))) == ext_name):
 ext_name = ext
 return ext_name
 _single_out_extension = staticmethod(_single_out_extension)
 
 def _check_for_conflicts(self, ext_list):
 """
 Removes from extensions list conflicting ones
 """
 if not self._conflicts:
 self._load_conflicting_extensions()
 clean_set = set()
 for ext in ext_list:
 if self._is_not_conflicting(ext, clean_set) and \
 not self._is_disabled_extention(ext):
 clean_set.add(ext)
 #else:
 #clprint.print_diag(
 #    'text',
 #    {'status': 'WARN',
 #     'message': '%s skipped as conflicting (%s)' % (ext, str(clean_set))})
 return clean_set
 
 def _is_not_conflicting(self, ext, clean_set):
 """
 Checks extension against conflicting sets
 """
 for conflict in self._conflicts:
 if ext in conflict:
 if len(clean_set.copy().intersection(conflict)) != 0:
 return False
 return True
 
 def _load_conflicting_extensions(self):
 """
 Loads conflicting extensions from file and saves'em as list of sets
 """
 conflicts = utils.read_file_as_string(self.CONFLICTS_PATH)
 for line in conflicts.splitlines():
 if ',' not in line:
 continue
 conflict_set = set(map((lambda i: i.strip()), line.split(',')))
 if len(conflict_set) < 2:
 continue
 self._conflicts.append(conflict_set)
 
 def _load_extensions_list(self, version):
 """
 Loads alternative extensions list for a version
 @param version: string
 """
 alt_path = self._compose_alt_path(version)
 try:
 alt_extensions = []
 for filename in os.listdir(alt_path):
 if not filename.endswith('.ini'):
 continue
 extension = filename[:filename.find('.ini')]
 if extension in self._hidden_extensions:
 continue
 alt_extensions.append(extension)
 return sorted(alt_extensions)
 except OSError:
 raise ClSelectExcept.UnableToGetExtensions(version)
 
 @staticmethod
 def _print_dependencies_info(dependens_info):
 """
 Prints info
 @param ext: string
 @param data: list
 """
 for (i, ext) in dependens_info:
 clprint.print_diag(
 'text',
 {'status': 'WARN',
 'message': '%s enabled as dependency (%s)'
 % (i, ext)})
 
 @staticmethod
 def get_dependencies_list(ext, data, ext_list):
 """
 Get array of dependenses [(ext, depending ext)]
 @param ext: string
 @param data: list
 """
 if not data:
 return []
 diff = set(data).difference([ext])
 return [(i, ext) for i in diff if i not in ext_list]
 
 @staticmethod
 def get_conflicts_info(init_list, processed_set):
 return list(set(init_list).difference(processed_set))
 
 @staticmethod
 def _print_conflicts_info(diff):
 """
 Prepares data for printing conflicts if any
 @param init_list: list
 @param processed_set: set
 """
 if diff:
 for i in diff:
 clprint.print_diag(
 'text',
 {'status': 'WARN',
 'message': '%s skipped as conflicting' % (i,)})
 
 |