aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sos/__init__.py8
-rw-r--r--sos/component.py20
-rw-r--r--sos/report/__init__.py497
3 files changed, 197 insertions, 328 deletions
diff --git a/sos/__init__.py b/sos/__init__.py
index 67871f13..85d59921 100644
--- a/sos/__init__.py
+++ b/sos/__init__.py
@@ -73,7 +73,7 @@ class SoS():
_com_subparser.usage = "sos %s [options]" % comp
_com_subparser.register('action', 'extend', SosListOption)
self._add_common_options(_com_subparser)
- self._components[comp].add_parser_options(_com_subparser)
+ self._components[comp].add_parser_options(parser=_com_subparser)
self.args = self.parser.parse_args()
self._init_component()
@@ -83,6 +83,9 @@ class SoS():
parser.add_argument("--config-file", type=str, action="store",
dest="config_file", default="/etc/sos.conf",
help="specify alternate configuration file")
+ parser.add_argument("--debug", action="store_true", dest="debug",
+ help="enable interactive debugging using the "
+ "python debugger")
parser.add_argument("-q", "--quiet", action="store_true",
dest="quiet", default=False,
help="only print fatal errors")
@@ -93,6 +96,9 @@ class SoS():
dest="tmp_dir",
help="specify alternate temporary directory",
default=None)
+ parser.add_argument("-t", "--threads", action="store", dest="threads",
+ default=4, type=int,
+ help="Number of threads to use")
parser.add_argument("-v", "--verbose", action="count",
dest="verbosity", default=0,
help="increase verbosity")
diff --git a/sos/component.py b/sos/component.py
index 16f77479..68c58224 100644
--- a/sos/component.py
+++ b/sos/component.py
@@ -10,6 +10,7 @@
# See the LICENSE file in the source distribution for further information.
import logging
+import os
import tempfile
import sys
import sos.policies
@@ -46,7 +47,9 @@ class SoSComponent():
_arg_defaults = {
"config_file": '/etc/sos.conf',
+ "debug": False,
"quiet": False,
+ "threads": 4,
"tmp_dir": '',
"sysroot": None,
"verbosity": 0
@@ -67,9 +70,22 @@ class SoSComponent():
# update args from component's arg_defaults defintion
self._arg_defaults.update(self.arg_defaults)
self.opts = self.load_options()
+
if self.configure_logging:
- tmpdir = self.opts.tmp_dir or tempfile.gettempdir()
- self.tmpdir = tempfile.mkdtemp(prefix="sos.", dir=tmpdir)
+ tmpdir = self.opts.tmp_dir or '/var/tmp'
+
+ if not os.path.isdir(tmpdir) \
+ or not os.access(tmpdir, os.W_OK):
+ msg = "temporary directory %s " % tmpdir
+ msg += "does not exist or is not writable\n"
+ # write directly to stderr as logging is not initialised yet
+ sys.stderr.write(msg)
+ self._exit(1)
+
+ self.sys_tmp = tmpdir
+
+
+ self.tmpdir = tempfile.mkdtemp(prefix="sos.", dir=self.sys_tmp)
self.tempfile_util = TempFileUtil(self.tmpdir)
self._setup_logging()
diff --git a/sos/report/__init__.py b/sos/report/__init__.py
index 723e2874..945180be 100644
--- a/sos/report/__init__.py
+++ b/sos/report/__init__.py
@@ -24,7 +24,7 @@ import logging
from datetime import datetime
from argparse import ArgumentParser, Action
import sos.report.plugins
-from sos.utilities import ImporterHelper, SoSTimeoutError
+from sos.utilities import ImporterHelper, SoSTimeoutError, TempFileUtil
from shutil import rmtree
import tempfile
import hashlib
@@ -33,7 +33,7 @@ import pdb
from sos import _sos as _
from sos import __version__
-from sos import _arg_defaults, SoSOptions
+from sos.component import SoSComponent
import sos.policies
from sos.archive import TarFileArchive
from sos.reporting import (Report, Section, Command, CopiedFile, CreatedFile,
@@ -73,264 +73,74 @@ def _format_since(date):
date parsing (like '2 days ago') in the future """
return datetime.strptime('{:<014s}'.format(date), '%Y%m%d%H%M%S')
-
-class TempFileUtil(object):
-
- def __init__(self, tmp_dir):
- self.tmp_dir = tmp_dir
- self.files = []
-
- def new(self):
- fd, fname = tempfile.mkstemp(dir=self.tmp_dir)
- # avoid TOCTOU race by using os.fdopen()
- fobj = os.fdopen(fd, 'w+')
- self.files.append((fname, fobj))
- return fobj
-
- def clean(self):
- for fname, f in self.files:
- try:
- f.flush()
- f.close()
- except Exception:
- pass
- try:
- os.unlink(fname)
- except Exception:
- pass
- self.files = []
-
-
-class SosListOption(Action):
-
- """Allow to specify comma delimited list of plugins"""
-
- def __call__(self, parser, namespace, values, option_string=None):
- items = [opt for opt in values.split(',')]
- if getattr(namespace, self.dest):
- items += getattr(namespace, self.dest)
- setattr(namespace, self.dest, items)
-
-
# valid modes for --chroot
chroot_modes = ["auto", "always", "never"]
-def _get_parser():
- """ Build ArgumentParser content"""
-
- usage_string = ("%(prog)s [options]\n\n"
- "Some examples:\n\n"
- "enable dlm plugin only and collect dlm lockdumps:\n"
- " # sosreport -o dlm -k dlm.lockdump\n\n"
- "disable memory and samba plugins, turn off rpm "
- "-Va collection:\n"
- " # sosreport -n memory,samba -k rpm.rpmva=off")
-
- parser = ArgumentParser(usage=usage_string)
- parser.register('action', 'extend', SosListOption)
- parser.add_argument("-a", "--alloptions", action="store_true",
- dest="alloptions", default=False,
- help="enable all options for loaded plugins")
- parser.add_argument("--all-logs", action="store_true",
- dest="all_logs", default=False,
- help="collect all available logs regardless "
- "of size")
- parser.add_argument("--since", action="store",
- dest="since", default=None,
- type=_format_since,
- help="Escapes archived files older than date. "
- "This will also affect --all-logs. "
- "Format: YYYYMMDD[HHMMSS]")
- parser.add_argument("--batch", action="store_true",
- dest="batch", default=False,
- help="batch mode - do not prompt interactively")
- parser.add_argument("--build", action="store_true",
- dest="build", default=False,
- help="preserve the temporary directory and do not "
- "package results")
- parser.add_argument("--case-id", action="store",
- dest="case_id",
- help="specify case identifier")
- parser.add_argument("-c", "--chroot", action="store", dest="chroot",
- help="chroot executed commands to SYSROOT "
- "[auto, always, never] (default=auto)",
- default=_arg_defaults["chroot"])
- parser.add_argument("--config-file", type=str, action="store",
- dest="config_file", default="/etc/sos.conf",
- help="specify alternate configuration file")
- parser.add_argument("--debug", action="store_true", dest="debug",
- help="enable interactive debugging using the "
- "python debugger")
- parser.add_argument("--desc", "--description", type=str, action="store",
- help="Description for a new preset", default="")
- parser.add_argument("--dry-run", action="store_true",
- help="Run plugins but do not collect data")
- parser.add_argument("--experimental", action="store_true",
- dest="experimental", default=False,
- help="enable experimental plugins")
- parser.add_argument("-e", "--enable-plugins", action="extend",
- dest="enableplugins", type=str,
- help="enable these plugins", default=[])
- parser.add_argument("-k", "--plugin-option", action="extend",
- dest="plugopts", type=str,
- help="plugin options in plugname.option=value "
- "format (see -l)", default=[])
- parser.add_argument("--label", "--name", action="store", dest="label",
- help="specify an additional report label")
- parser.add_argument("-l", "--list-plugins", action="store_true",
- dest="list_plugins", default=False,
- help="list plugins and available plugin options")
- parser.add_argument("--list-presets", action="store_true",
- help="display a list of available presets")
- parser.add_argument("--list-profiles", action="store_true",
- dest="list_profiles", default=False,
- help="display a list of available profiles and "
- "plugins that they include")
- parser.add_argument("--log-size", action="store", dest="log_size",
- type=int, default=_arg_defaults["log_size"],
- help="limit the size of collected logs (in MiB)")
- parser.add_argument("-n", "--skip-plugins", action="extend",
- dest="noplugins", type=str,
- help="disable these plugins", default=[])
- parser.add_argument("--no-report", action="store_true",
- dest="noreport",
- help="disable plaintext/HTML reporting", default=False)
- parser.add_argument("--no-env-vars", action="store_true", default=False,
- dest="no_env_vars",
- help="Do not collect environment variables")
- parser.add_argument("--no-postproc", default=False, dest="no_postproc",
- action="store_true",
- help="Disable all post-processing")
- parser.add_argument("--note", type=str, action="store", default="",
- help="Behaviour notes for new preset")
- parser.add_argument("-o", "--only-plugins", action="extend",
- dest="onlyplugins", type=str,
- help="enable these plugins only", default=[])
- parser.add_argument("--preset", action="store", type=str,
- help="A preset identifier", default="auto")
- parser.add_argument("--plugin-timeout", default=None,
- help="set a timeout for all plugins")
- parser.add_argument("-p", "--profile", action="extend",
- dest="profiles", type=str, default=[],
- help="enable plugins used by the given profiles")
- parser.add_argument("-q", "--quiet", action="store_true",
- dest="quiet", default=False,
- help="only print fatal errors")
- parser.add_argument("-s", "--sysroot", action="store", dest="sysroot",
- help="system root directory path (default='/')",
- default=None)
- parser.add_argument("--ticket-number", action="store",
- dest="case_id",
- help="specify ticket number")
- parser.add_argument("--tmp-dir", action="store",
- dest="tmp_dir",
- help="specify alternate temporary directory",
- default=None)
- parser.add_argument("-v", "--verbose", action="count", dest="verbosity",
- default=_arg_defaults["verbosity"],
- help="increase verbosity"),
- parser.add_argument("--verify", action="store_true",
- dest="verify", default=False,
- help="perform data verification during collection")
- parser.add_argument("-z", "--compression-type", dest="compression_type",
- default=_arg_defaults["compression_type"],
- help="compression technology to use [auto, "
- "gzip, bzip2, xz] (default=auto)")
- parser.add_argument("-t", "--threads", action="store", dest="threads",
- help="specify number of concurrent plugins to run"
- " (default=4)", default=4, type=int)
- parser.add_argument("--allow-system-changes", action="store_true",
- dest="allow_system_changes", default=False,
- help="Run commands even if they can change the "
- "system (e.g. load kernel modules)")
-
- parser.add_argument("--upload", action="store_true", default=False,
- help="Upload the archive to a policy-default location")
- parser.add_argument("--upload-url", default=None,
- help="Upload the archive to the specified server")
- parser.add_argument("--upload-directory", default=None,
- help="Specify the directory to upload the archive to")
- parser.add_argument("--upload-user", default=None,
- help="Username to authenticate to upload server with")
- parser.add_argument("--upload-pass", default=None,
- help="Password to authenticate to upload server with")
-
- # Group to make add/del preset exclusive
- preset_grp = parser.add_mutually_exclusive_group()
- preset_grp.add_argument("--add-preset", type=str, action="store",
- help="Add a new named command line preset")
- preset_grp.add_argument("--del-preset", type=str, action="store",
- help="Delete the named command line preset")
-
- # Group to make tarball encryption (via GPG/password) exclusive
- encrypt_grp = parser.add_mutually_exclusive_group()
- encrypt_grp.add_argument("--encrypt-key",
- help="Encrypt the final archive using a GPG "
- "key-pair")
- encrypt_grp.add_argument("--encrypt-pass",
- help="Encrypt the final archive using a password")
-
- return parser
-
-
-class SoSReport(object):
-
- """The main sosreport class"""
-
- def __init__(self, args):
+class SoSReport(SoSComponent):
+ """Collect files and command output to save to an archive for analysis"""
+
+
+ arg_defaults = {
+ 'alloptions': False,
+ 'all_logs': False,
+ 'batch': False,
+ 'build': False,
+ 'case_id': '',
+ 'chroot': 'auto',
+ 'desc': '',
+ 'dry_run': False,
+ 'experimental': False,
+ 'enableplugins': [],
+ 'plugopts': [],
+ 'label': '',
+ 'list_plugins': False,
+ 'list_presets': False,
+ 'list_profiles': False,
+ 'log_size': 25,
+ 'noplugins': [],
+ 'noreport': False,
+ 'no_env_vars': False,
+ 'no_postproc': False,
+ 'note': '',
+ 'onlyplugins': [],
+ 'preset': 'auto',
+ 'plugin_timeout': 300,
+ 'profiles': [],
+ 'since': None,
+ 'verify': False,
+ 'compression_type': 'auto',
+ 'allow_system_changes': False,
+ 'upload': False,
+ 'upload_url': None,
+ 'upload_directory': None,
+ 'upload_user': None,
+ 'upload_pass': None,
+ 'add_preset': '',
+ 'del_preset': '',
+ 'encrypt_key': None,
+ 'encrypt_pass': None
+ }
+
+ def __init__(self, parser, args, cmdline):
+ super(SoSReport, self).__init__(parser, args, cmdline)
self.loaded_plugins = []
self.skipped_plugins = []
self.all_options = []
self.env_vars = set()
self.archive = None
- self.tempfile_util = None
self._args = args
self.sysroot = "/"
self.sys_tmp = None
- self.exit_process = False
self.preset = None
- try:
- import signal
- signal.signal(signal.SIGTERM, self.get_exit_handler())
- except Exception:
- pass # not available in java, but we don't care
-
self.print_header()
-
- # load default options and store them in self.opts
- parser = _get_parser()
- self.opts = SoSOptions().from_args(parser.parse_args([]))
-
- # remove default options now, such that by processing cmdline options
- # we know what exact options were provided there and should not be
- # overwritten any time further
- # then merge these options on top of self.opts
- # this approach is required since:
- # - we process the more priority options first (cmdline, then config
- # file, then presets) - required to know cfgfile or preset
- # - we have to apply lower prio options only on top of non-default
- for option in parser._actions:
- if option.default != '==SUPPRESS==':
- option.default = None
- cmd_opts = SoSOptions().from_args(parser.parse_args(args))
- self.opts.merge(cmd_opts)
-
- # load options from config.file and merge them to self.opts
- self.fileopts = SoSOptions().from_file(parser, self.opts.config_file)
- self.opts.merge(self.fileopts)
self._set_debug()
- # load preset and options from it - first, identify policy for that
- try:
- self.policy = sos.policies.load(sysroot=self.opts.sysroot)
- except KeyboardInterrupt:
- self._exit(0)
self._is_root = self.policy.is_root()
# user specified command line preset
- if self.opts.preset != _arg_defaults["preset"]:
+ if self.opts.preset != self.arg_defaults["preset"]:
self.preset = self.policy.find_preset(self.opts.preset)
if not self.preset:
sys.stderr.write("Unknown preset: '%s'\n" % self.opts.preset)
@@ -343,27 +153,8 @@ class SoSReport(object):
# now merge preset options to self.opts
self.opts.merge(self.preset.opts)
- # system temporary directory to use
- tmp = os.path.abspath(self.policy.get_tmp_dir(self.opts.tmp_dir))
-
- if not os.path.isdir(tmp) \
- or not os.access(tmp, os.W_OK):
- msg = "temporary directory %s " % tmp
- msg += "does not exist or is not writable\n"
- # write directly to stderr as logging is not initialised yet
- sys.stderr.write(msg)
- self._exit(1)
-
- self.sys_tmp = tmp
-
- # our (private) temporary directory
- self.tmpdir = tempfile.mkdtemp(prefix="sos.", dir=self.sys_tmp)
- self.tempfile_util = TempFileUtil(self.tmpdir)
-
self._set_directories()
- self._setup_logging()
-
msg = "default"
host_sysroot = self.policy.host_sysroot()
# set alternate system root directory
@@ -383,6 +174,126 @@ class SoSReport(object):
self._get_hardware_devices()
+ @classmethod
+ def add_parser_options(cls, parser):
+ parser.add_argument("-a", "--alloptions", action="store_true",
+ dest="alloptions", default=False,
+ help="enable all options for loaded plugins")
+ parser.add_argument("--all-logs", action="store_true",
+ dest="all_logs", default=False,
+ help="collect all available logs regardless "
+ "of size")
+ parser.add_argument("--since", action="store",
+ dest="since", default=None,
+ type=_format_since,
+ help="Escapes archived files older than date. "
+ "This will also affect --all-logs. "
+ "Format: YYYYMMDD[HHMMSS]")
+ parser.add_argument("--batch", action="store_true",
+ dest="batch", default=False,
+ help="batch mode - do not prompt interactively")
+ parser.add_argument("--build", action="store_true",
+ dest="build", default=False,
+ help="preserve the temporary directory and do not "
+ "package results")
+ parser.add_argument("--case-id", action="store",
+ dest="case_id",
+ help="specify case identifier")
+ parser.add_argument("-c", "--chroot", action="store", dest="chroot",
+ help="chroot executed commands to SYSROOT "
+ "[auto, always, never] (default=auto)",
+ default='auto')
+ parser.add_argument("--desc", "--description", type=str,
+ action="store", default="",
+ help="Description for a new preset",)
+ parser.add_argument("--dry-run", action="store_true",
+ help="Run plugins but do not collect data")
+ parser.add_argument("--experimental", action="store_true",
+ dest="experimental", default=False,
+ help="enable experimental plugins")
+ parser.add_argument("-e", "--enable-plugins", action="extend",
+ dest="enableplugins", type=str,
+ help="enable these plugins", default=[])
+ parser.add_argument("-k", "--plugin-option", action="extend",
+ dest="plugopts", type=str,
+ help="plugin options in plugname.option=value "
+ "format (see -l)", default=[])
+ parser.add_argument("--label", "--name", action="store", dest="label",
+ help="specify an additional report label")
+ parser.add_argument("-l", "--list-plugins", action="store_true",
+ dest="list_plugins", default=False,
+ help="list plugins and available plugin options")
+ parser.add_argument("--list-presets", action="store_true",
+ help="display a list of available presets")
+ parser.add_argument("--list-profiles", action="store_true",
+ dest="list_profiles", default=False,
+ help="display a list of available profiles and "
+ "plugins that they include")
+ parser.add_argument("--log-size", action="store", dest="log_size",
+ type=int, default=25,
+ help="limit the size of collected logs (in MiB)")
+ parser.add_argument("-n", "--skip-plugins", action="extend",
+ dest="noplugins", type=str,
+ help="disable these plugins", default=[])
+ parser.add_argument("--no-report", action="store_true",
+ dest="noreport", default=False,
+ help="disable plaintext/HTML reporting")
+ parser.add_argument("--no-env-vars", action="store_true",
+ dest="no_env_vars", default=False,
+ help="Do not collect environment variables")
+ parser.add_argument("--no-postproc", default=False, dest="no_postproc",
+ action="store_true",
+ help="Disable all post-processing")
+ parser.add_argument("--note", type=str, action="store", default="",
+ help="Behaviour notes for new preset")
+ parser.add_argument("-o", "--only-plugins", action="extend",
+ dest="onlyplugins", type=str,
+ help="enable these plugins only", default=[])
+ parser.add_argument("--preset", action="store", type=str,
+ help="A preset identifier", default="auto")
+ parser.add_argument("--plugin-timeout", default=None,
+ help="set a timeout for all plugins")
+ parser.add_argument("-p", "--profile", action="extend",
+ dest="profiles", type=str, default=[],
+ help="enable plugins used by the given profiles")
+ parser.add_argument("--verify", action="store_true",
+ dest="verify", default=False,
+ help="perform data verification during collection")
+ parser.add_argument("-z", "--compression-type",
+ dest="compression_type",
+ default='auto',
+ help="compression technology to use [auto, "
+ "gzip, bzip2, xz] (default=auto)")
+ parser.add_argument("--allow-system-changes", action="store_true",
+ dest="allow_system_changes", default=False,
+ help="Run commands even if they can change the "
+ "system (e.g. load kernel modules)")
+ parser.add_argument("--upload", action="store_true", default=False,
+ help="Upload archive to a policy-default location")
+ parser.add_argument("--upload-url", default=None,
+ help="Upload the archive to the specified server")
+ parser.add_argument("--upload-directory", default=None,
+ help="Specify directory to upload the archive to")
+ parser.add_argument("--upload-user", default=None,
+ help="Username to authenticate to server with")
+ parser.add_argument("--upload-pass", default=None,
+ help="Password to authenticate to server with")
+
+ # Group to make add/del preset exclusive
+ preset_grp = parser.add_mutually_exclusive_group()
+ preset_grp.add_argument("--add-preset", type=str, action="store",
+ help="Add a new named command line preset")
+ preset_grp.add_argument("--del-preset", type=str, action="store",
+ help="Delete the named command line preset")
+
+ # Group to make tarball encryption (via GPG/password) exclusive
+ encrypt_grp = parser.add_mutually_exclusive_group()
+ encrypt_grp.add_argument("--encrypt-key",
+ help="Encrypt the archive using a GPG "
+ "key-pair")
+ encrypt_grp.add_argument("--encrypt-pass",
+ help="Encrypt the archive using a password")
+
def print_header(self):
print("\n%s\n" % _("sosreport (version %s)" % (__version__,)))
@@ -498,15 +409,6 @@ class SoSReport(object):
# ...then start the debugger in post-mortem mode.
pdb.pm()
- def _exit(self, error=0):
- raise SystemExit(error)
-
- def get_exit_handler(self):
- def exit_handler(signum, frame):
- self.exit_process = True
- self._exit()
- return exit_handler
-
def handle_exception(self, plugname=None, func=None):
if self.raise_plugins or self.exit_process:
# retrieve exception info for the current thread and stack.
@@ -519,51 +421,6 @@ class SoSReport(object):
if plugname and func:
self._log_plugin_exception(plugname, func)
- def _setup_logging(self):
- # main soslog
- self.soslog = logging.getLogger('sos')
- self.soslog.setLevel(logging.DEBUG)
- self.sos_log_file = self.get_temp_file()
- flog = logging.StreamHandler(self.sos_log_file)
- flog.setFormatter(logging.Formatter(
- '%(asctime)s %(levelname)s: %(message)s'))
- flog.setLevel(logging.INFO)
- self.soslog.addHandler(flog)
-
- if not self.opts.quiet:
- console = logging.StreamHandler(sys.stdout)
- console.setFormatter(logging.Formatter('%(message)s'))
- if self.opts.verbosity and self.opts.verbosity > 1:
- console.setLevel(logging.DEBUG)
- flog.setLevel(logging.DEBUG)
- elif self.opts.verbosity and self.opts.verbosity > 0:
- console.setLevel(logging.INFO)
- flog.setLevel(logging.DEBUG)
- else:
- console.setLevel(logging.WARNING)
- self.soslog.addHandler(console)
- # log ERROR or higher logs to stderr instead
- console_err = logging.StreamHandler(sys.stderr)
- console_err.setFormatter(logging.Formatter('%(message)s'))
- console_err.setLevel(logging.ERROR)
- self.soslog.addHandler(console_err)
-
- # ui log
- self.ui_log = logging.getLogger('sos_ui')
- self.ui_log.setLevel(logging.INFO)
- self.sos_ui_log_file = self.get_temp_file()
- ui_fhandler = logging.StreamHandler(self.sos_ui_log_file)
- ui_fhandler.setFormatter(logging.Formatter(
- '%(asctime)s %(levelname)s: %(message)s'))
-
- self.ui_log.addHandler(ui_fhandler)
-
- if not self.opts.quiet:
- ui_console = logging.StreamHandler(sys.stdout)
- ui_console.setFormatter(logging.Formatter('%(message)s'))
- ui_console.setLevel(logging.INFO)
- self.ui_log.addHandler(ui_console)
-
def _add_sos_logs(self):
# Make sure the log files are added before we remove the log
# handlers. This prevents "No handlers could be found.." messages
@@ -999,12 +856,8 @@ class SoSReport(object):
def setup(self):
# Log command line options
- msg = "[%s:%s] executing 'sosreport %s'"
- self.soslog.info(msg % (__name__, "setup", " ".join(self._args)))
-
- msg = "[%s:%s] loaded options from config file: %s'"
- self.soslog.info(msg % (__name__, "setup",
- " ".join(self.fileopts.to_args())))
+ msg = "[%s:%s] executing 'sos report %s'"
+ self.soslog.info(msg % (__name__, "setup", " ".join(self.cmdline)))
# Log active preset defaults
preset_args = self.preset.opts.to_args()
@@ -1459,10 +1312,4 @@ class SoSReport(object):
self._exit(1)
-
-def main(args):
- """The main entry point"""
- sos = SoSReport(args)
- sos.execute()
-
# vim: set et ts=4 sw=4 :