Commit 140225ea authored by Jérome Perrin's avatar Jérome Perrin

Buildout debug mode for slapos node software / instance

Expose buildout debugging capabilities in `slapos node software` and `slapos node instance` with a `--buildout-debug` command line option.

From buildout's command line reference:

> `-D`
>  Debug errors. If an error occurs, then the post-mortem debugger will be started. This is especially useful 
>  for debugging recipe problems.

This is to make it easier to debug instance profiles written as jinja templates.

This fix from buildout nexedi/slapos.buildout!17 is recommended, otherwise slapos node does not realize there was an error in buildout if that error is debugged.

/reviewed-on nexedi/slapos.core!51
parents b10d32db 5e2d7e8f
...@@ -131,6 +131,9 @@ class SoftwareCommand(SlapgridCommand): ...@@ -131,6 +131,9 @@ class SoftwareCommand(SlapgridCommand):
def get_parser(self, prog_name): def get_parser(self, prog_name):
ap = super(SoftwareCommand, self).get_parser(prog_name) ap = super(SoftwareCommand, self).get_parser(prog_name)
ap.add_argument('--buildout-debug',
action='store_true',
help='Run buildout in debug mode (with -D command line switch)')
only = ap.add_mutually_exclusive_group() only = ap.add_mutually_exclusive_group()
only.add_argument('--all', action='store_true', only.add_argument('--all', action='store_true',
help='Process all Software Releases, even if already installed.') help='Process all Software Releases, even if already installed.')
...@@ -151,6 +154,9 @@ class InstanceCommand(SlapgridCommand): ...@@ -151,6 +154,9 @@ class InstanceCommand(SlapgridCommand):
def get_parser(self, prog_name): def get_parser(self, prog_name):
ap = super(InstanceCommand, self).get_parser(prog_name) ap = super(InstanceCommand, self).get_parser(prog_name)
ap.add_argument('--buildout-debug',
action='store_true',
help='Run buildout in debug mode (with -D command line switch)')
only = ap.add_mutually_exclusive_group() only = ap.add_mutually_exclusive_group()
only.add_argument('--all', action='store_true', only.add_argument('--all', action='store_true',
help='Process all Computer Partitions.') help='Process all Computer Partitions.')
......
...@@ -109,7 +109,8 @@ class Software(object): ...@@ -109,7 +109,8 @@ class Software(object):
download_binary_dir_url=None, upload_binary_dir_url=None, download_binary_dir_url=None, upload_binary_dir_url=None,
download_from_binary_cache_url_blacklist=None, download_from_binary_cache_url_blacklist=None,
upload_to_binary_cache_url_blacklist=None, upload_to_binary_cache_url_blacklist=None,
software_min_free_space=None): software_min_free_space=None,
buildout_debug=False,):
"""Initialisation of class parameters """Initialisation of class parameters
""" """
...@@ -125,6 +126,7 @@ class Software(object): ...@@ -125,6 +126,7 @@ class Software(object):
self.software_path = os.path.join(self.software_root, self.software_path = os.path.join(self.software_root,
self.software_url_hash) self.software_url_hash)
self.buildout = buildout self.buildout = buildout
self.buildout_debug = buildout_debug
self.logger = logger self.logger = logger
self.signature_private_key_file = signature_private_key_file self.signature_private_key_file = signature_private_key_file
self.signature_certificate_list = signature_certificate_list self.signature_certificate_list = signature_certificate_list
...@@ -268,7 +270,8 @@ class Software(object): ...@@ -268,7 +270,8 @@ class Software(object):
utils.launchBuildout(path=self.software_path, utils.launchBuildout(path=self.software_path,
buildout_binary=os.path.join(self.software_path, 'bin', 'buildout'), buildout_binary=os.path.join(self.software_path, 'bin', 'buildout'),
logger=self.logger, logger=self.logger,
additional_buildout_parameter_list=additional_parameters) additional_buildout_parameter_list=additional_parameters,
debug=self.buildout_debug)
finally: finally:
shutil.rmtree(extends_cache) shutil.rmtree(extends_cache)
...@@ -348,9 +351,11 @@ class Partition(object): ...@@ -348,9 +351,11 @@ class Partition(object):
instance_min_free_space=None, instance_min_free_space=None,
instance_storage_home='', instance_storage_home='',
ipv4_global_network='', ipv4_global_network='',
buildout_debug=False,
): ):
"""Initialisation of class parameters""" """Initialisation of class parameters"""
self.buildout = buildout self.buildout = buildout
self.buildout_debug = buildout_debug
self.logger = logger self.logger = logger
self.software_path = software_path self.software_path = software_path
self.instance_path = instance_path self.instance_path = instance_path
...@@ -619,7 +624,8 @@ class Partition(object): ...@@ -619,7 +624,8 @@ class Partition(object):
# Launches buildout # Launches buildout
utils.launchBuildout(path=self.instance_path, utils.launchBuildout(path=self.instance_path,
buildout_binary=buildout_binary, buildout_binary=buildout_binary,
logger=self.logger) logger=self.logger,
debug=self.buildout_debug)
self.generateSupervisorConfigurationFile() self.generateSupervisorConfigurationFile()
self.createRetentionLockDelay() self.createRetentionLockDelay()
......
...@@ -242,6 +242,7 @@ def create_slapgrid_object(options, logger): ...@@ -242,6 +242,7 @@ def create_slapgrid_object(options, logger):
master_url=op['master_url'], master_url=op['master_url'],
computer_id=op['computer_id'], computer_id=op['computer_id'],
buildout=op.get('buildout'), buildout=op.get('buildout'),
buildout_debug=op.get('buildout_debug'),
logger=logger, logger=logger,
maximum_periodicity = op.get('maximum_periodicity', 86400), maximum_periodicity = op.get('maximum_periodicity', 86400),
key_file=op.get('key_file'), key_file=op.get('key_file'),
...@@ -336,6 +337,7 @@ class Slapgrid(object): ...@@ -336,6 +337,7 @@ class Slapgrid(object):
ipv4_global_network=None, ipv4_global_network=None,
firewall_conf={}, firewall_conf={},
config=None, config=None,
buildout_debug=False,
): ):
"""Makes easy initialisation of class parameters""" """Makes easy initialisation of class parameters"""
# Parses arguments # Parses arguments
...@@ -375,6 +377,7 @@ class Slapgrid(object): ...@@ -375,6 +377,7 @@ class Slapgrid(object):
self.computer = self.slap.registerComputer(self.computer_id) self.computer = self.slap.registerComputer(self.computer_id)
# Defines all needed paths # Defines all needed paths
self.buildout = buildout self.buildout = buildout
self.buildout_debug = buildout_debug
self.promise_timeout = promise_timeout self.promise_timeout = promise_timeout
self.develop = develop self.develop = develop
if software_release_filter_list is not None: if software_release_filter_list is not None:
...@@ -537,6 +540,7 @@ stderr_logfile_backups=1 ...@@ -537,6 +540,7 @@ stderr_logfile_backups=1
software = Software(url=software_release_uri, software = Software(url=software_release_uri,
software_root=self.software_root, software_root=self.software_root,
buildout=self.buildout, buildout=self.buildout,
buildout_debug=self.buildout_debug,
logger=self.logger, logger=self.logger,
signature_private_key_file=self.signature_private_key_file, signature_private_key_file=self.signature_private_key_file,
signature_certificate_list=self.signature_certificate_list, signature_certificate_list=self.signature_certificate_list,
...@@ -1009,6 +1013,7 @@ stderr_logfile_backups=1 ...@@ -1009,6 +1013,7 @@ stderr_logfile_backups=1
software_release_url=software_url, software_release_url=software_url,
certificate_repository_path=self.certificate_repository_path, certificate_repository_path=self.certificate_repository_path,
buildout=self.buildout, buildout=self.buildout,
buildout_debug=self.buildout_debug,
logger=self.logger, logger=self.logger,
retention_delay=getattr(computer_partition, '_filter_dict', {}).get('retention_delay', '0'), retention_delay=getattr(computer_partition, '_filter_dict', {}).get('retention_delay', '0'),
instance_min_free_space=self.instance_min_free_space, instance_min_free_space=self.instance_min_free_space,
...@@ -1595,6 +1600,7 @@ stderr_logfile_backups=1 ...@@ -1595,6 +1600,7 @@ stderr_logfile_backups=1
software_release_url=software_url, software_release_url=software_url,
certificate_repository_path=self.certificate_repository_path, certificate_repository_path=self.certificate_repository_path,
buildout=self.buildout, buildout=self.buildout,
buildout_debug=self.buildout_debug,
logger=self.logger, logger=self.logger,
instance_storage_home=self.instance_storage_home, instance_storage_home=self.instance_storage_home,
ipv4_global_network=self.ipv4_global_network, ipv4_global_network=self.ipv4_global_network,
......
...@@ -91,12 +91,23 @@ LOCALE_ENVIRONMENT_REMOVE_LIST = [ ...@@ -91,12 +91,23 @@ LOCALE_ENVIRONMENT_REMOVE_LIST = [
class SlapPopen(subprocess.Popen): class SlapPopen(subprocess.Popen):
""" """
Almost normal subprocess with greedish features and logging. Almost normal subprocess with greedish features and logging.
Each line is logged "live", and self.output is a string containing the whole Each line is logged "live", and self.output is a string containing the whole
log. log, unless kwargs['debug'] is True, in which case the process outputs
normally on stdout and stderr.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
logger = kwargs.pop('logger') logger = kwargs.pop('logger')
kwargs.update(stdin=subprocess.PIPE)
debug = kwargs.pop('debug', False)
if debug:
kwargs.pop('stdout', None)
kwargs.pop('stderr', None)
else:
kwargs.setdefault('stdout', subprocess.PIPE)
kwargs.setdefault('stderr', subprocess.STDOUT)
kwargs.update(stdin=subprocess.PIPE)
if sys.platform == 'cygwin' and kwargs.get('env') == {}: if sys.platform == 'cygwin' and kwargs.get('env') == {}:
kwargs['env'] = None kwargs['env'] = None
...@@ -104,6 +115,10 @@ class SlapPopen(subprocess.Popen): ...@@ -104,6 +115,10 @@ class SlapPopen(subprocess.Popen):
kwargs.setdefault('close_fds', True) kwargs.setdefault('close_fds', True)
subprocess.Popen.__init__(self, *args, **kwargs) subprocess.Popen.__init__(self, *args, **kwargs)
if debug:
self.wait()
self.output = '(output not captured in debug mode)'
return
self.stdin.flush() self.stdin.flush()
self.stdin.close() self.stdin.close()
self.stdin = None self.stdin = None
...@@ -273,8 +288,6 @@ def bootstrapBuildout(path, logger, buildout=None, ...@@ -273,8 +288,6 @@ def bootstrapBuildout(path, logger, buildout=None,
process_handler = SlapPopen(invocation_list, process_handler = SlapPopen(invocation_list,
preexec_fn=lambda: dropPrivileges(uid, gid, logger=logger), preexec_fn=lambda: dropPrivileges(uid, gid, logger=logger),
cwd=path, cwd=path,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
logger=logger) logger=logger)
if process_handler.returncode is None or process_handler.returncode != 0: if process_handler.returncode is None or process_handler.returncode != 0:
message = 'Failed to run buildout profile in directory %r' % path message = 'Failed to run buildout profile in directory %r' % path
...@@ -289,7 +302,8 @@ def bootstrapBuildout(path, logger, buildout=None, ...@@ -289,7 +302,8 @@ def bootstrapBuildout(path, logger, buildout=None,
def launchBuildout(path, buildout_binary, logger, def launchBuildout(path, buildout_binary, logger,
additional_buildout_parameter_list=None): additional_buildout_parameter_list=None,
debug=False):
""" Launches buildout.""" """ Launches buildout."""
if additional_buildout_parameter_list is None: if additional_buildout_parameter_list is None:
additional_buildout_parameter_list = [] additional_buildout_parameter_list = []
...@@ -304,6 +318,10 @@ def launchBuildout(path, buildout_binary, logger, ...@@ -304,6 +318,10 @@ def launchBuildout(path, buildout_binary, logger,
line = line[2:] line = line[2:]
# Prepares parameters for buildout # Prepares parameters for buildout
invocation_list = line.split() + [buildout_binary] invocation_list = line.split() + [buildout_binary]
if debug:
invocation_list.append('-D')
# Run buildout without reading user defaults # Run buildout without reading user defaults
invocation_list.append('-U') invocation_list.append('-U')
invocation_list.extend(additional_buildout_parameter_list) invocation_list.extend(additional_buildout_parameter_list)
...@@ -317,8 +335,7 @@ def launchBuildout(path, buildout_binary, logger, ...@@ -317,8 +335,7 @@ def launchBuildout(path, buildout_binary, logger,
cwd=path, cwd=path,
env=getCleanEnvironment(logger=logger, env=getCleanEnvironment(logger=logger,
home_path=path), home_path=path),
stdout=subprocess.PIPE, debug=debug,
stderr=subprocess.STDOUT,
logger=logger) logger=logger)
if process_handler.returncode is None or process_handler.returncode != 0: if process_handler.returncode is None or process_handler.returncode != 0:
message = 'Failed to run buildout profile in directory %r' % path message = 'Failed to run buildout profile in directory %r' % path
......
...@@ -33,6 +33,7 @@ import StringIO ...@@ -33,6 +33,7 @@ import StringIO
import sys import sys
from mock import patch, create_autospec from mock import patch, create_autospec
import mock
import slapos.cli.console import slapos.cli.console
import slapos.cli.entry import slapos.cli.entry
...@@ -81,6 +82,66 @@ product2 url2""" ...@@ -81,6 +82,66 @@ product2 url2"""
{} {}
) )
class TestCliNode(CliMixin):
def test_node_software(self):
"""slapos node software command
"""
app = slapos.cli.entry.SlapOSApp()
software_release = mock.MagicMock()
software_release.getState = mock.Mock(return_value='available')
software_release.getURI = mock.Mock(return_value='http://example.org/software.cfg')
software_release.building = mock.Mock()
computer = mock.MagicMock()
computer.getSoftwareReleaseList = mock.Mock(return_value=[software_release])
software = mock.MagicMock()
from slapos.grid.slapgrid import Slapgrid
from slapos.slap.slap import slap
with patch('slapos.cli.slapgrid.check_root_user', return_value=True) as checked_root_user, \
patch('slapos.cli.slapgrid.setRunning') as write_pid_file, \
patch.object(Slapgrid, 'checkEnvironmentAndCreateStructure') as checkEnvironmentAndCreateStructure, \
patch.object(slap, 'registerComputer', return_value=computer) as registerComputer, \
patch('slapos.grid.slapgrid.Software', return_value=software) as Software, \
patch('slapos.grid.slapgrid.open') as _open:
app.run(('node', 'software'))
checked_root_user.assert_called_once()
write_pid_file.assert_called_once_with(
logger=mock.ANY,
pidfile='/opt/slapos/slapgrid-sr.pid')
checkEnvironmentAndCreateStructure.assert_called_once()
registerComputer.assert_called_once()
software_constructor_call, = Software.call_args_list
self.assertEqual('http://example.org/software.cfg', software_constructor_call[1]['url'])
# by default software are not built in debug mode
self.assertFalse(software_constructor_call[1]['buildout_debug'])
software.install.assert_called_once()
def test_node_instance(self):
"""slapos node instance command
"""
app = slapos.cli.entry.SlapOSApp()
from slapos.grid.slapgrid import Slapgrid
with patch('slapos.cli.slapgrid.check_root_user', return_value=True) as checked_root_user, \
patch('slapos.cli.slapgrid.setRunning') as write_pid_file, \
patch.object(Slapgrid, 'processComputerPartitionList') as processComputerPartitionList:
app.run(('node', 'instance'))
checked_root_user.assert_called_once()
write_pid_file.assert_called_once_with(
logger=mock.ANY,
pidfile='/opt/slapos/slapgrid-cp.pid')
processComputerPartitionList.assert_called_once()
class TestCliList(CliMixin): class TestCliList(CliMixin):
def test_list(self): def test_list(self):
""" """
...@@ -185,7 +246,7 @@ master_url=null ...@@ -185,7 +246,7 @@ master_url=null
sys.stdout = app_stdout = StringIO.StringIO() sys.stdout = app_stdout = StringIO.StringIO()
app.run(('console', '--cfg', self.config_file.name)) app.run(('console', '--cfg', self.config_file.name))
finally: finally:
sys.sdin = saved_stdin sys.stdin = saved_stdin
sys.stdout = saved_stdout sys.stdout = saved_stdout
self.mock_request.assert_called_once_with('software_release', 'instance') self.mock_request.assert_called_once_with('software_release', 'instance')
......
##############################################################################
#
# Copyright (c) 2018 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import os
import sys
import tempfile
import unittest
import logging
import mock
import slapos.grid.utils
class SlapPopenTestCase(unittest.TestCase):
def setUp(self):
self.script = tempfile.NamedTemporaryFile(delete=False)
# make executable
os.chmod(self.script.name, 0o777)
def tearDown(self):
os.unlink(self.script.name)
def test_exec(self):
"""Test command execution with SlapPopen.
"""
self.script.write('#!/bin/sh\necho "hello"\nexit 123')
self.script.close()
logger = mock.MagicMock()
program = slapos.grid.utils.SlapPopen(
self.script.name,
logger=logger)
# error code and output are returned
self.assertEqual(123, program.returncode)
self.assertEqual('hello\n', program.output)
# output is also logged "live"
logger.info.assert_called_with('hello')
def test_debug(self):
"""Test debug=True, which keeps interactive.
"""
self.script.write('#!/bin/sh\necho "exit code?"\nread rc\nexit $rc')
self.script.close()
# keep a reference to stdin and stdout to restore them later
stdin_backup = os.dup(sys.stdin.fileno())
stdout_backup = os.dup(sys.stdout.fileno())
# replace stdin with a pipe that will write 123
child_stdin_r, child_stdin_w = os.pipe()
os.write(child_stdin_w, "123")
os.close(child_stdin_w)
os.dup2(child_stdin_r, sys.stdin.fileno())
# and stdout with the pipe to capture output
child_stdout_r, child_stdout_w = os.pipe()
os.dup2(child_stdout_w, sys.stdout.fileno())
try:
program = slapos.grid.utils.SlapPopen(
self.script.name,
debug=True,
logger=logging.getLogger())
# program output
self.assertEqual('exit code?\n', os.read(child_stdout_r, 1024))
self.assertEqual(123, program.returncode)
self.assertEqual('(output not captured in debug mode)', program.output)
finally:
# restore stdin & stderr
os.dup2(stdin_backup, sys.stdin.fileno())
os.dup2(stdout_backup, sys.stdout.fileno())
# close all fds open for the test
for fd in (child_stdin_r, child_stdout_r, child_stdout_w, stdin_backup, stdout_backup):
os.close(fd)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment