Mini Shell
# -*- 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.clcagefs import in_cagefs
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 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,)})
Zerion Mini Shell 1.0