diff --git a/paunch/builder/base.py b/paunch/builder/base.py index 7990ede..53d4b51 100644 --- a/paunch/builder/base.py +++ b/paunch/builder/base.py @@ -16,6 +16,8 @@ import logging import re import tenacity +from paunch.utils import systemd + LOG = logging.getLogger(__name__) @@ -45,8 +47,15 @@ class BaseBuilder(object): for container in sorted(self.config, key=key_fltr): LOG.debug("Running container: %s" % container) - action = self.config[container].get('action', 'run') - exit_codes = self.config[container].get('exit_codes', [0]) + cconfig = self.config[container] + action = cconfig.get('action', 'run') + restart = cconfig.get('restart', 'none') + exit_codes = cconfig.get('exit_codes', [0]) + container_name = self.runner.unique_container_name(container) + systemd_managed = (restart != 'none' + and self.runner.docker_cmd == 'podman' + and action == 'run') + start_cmd = 'create' if systemd_managed else 'run' if action == 'run': if container in desired_names: @@ -55,9 +64,9 @@ class BaseBuilder(object): cmd = [ self.runner.docker_cmd, - 'run', + start_cmd, '--name', - self.runner.unique_container_name(container) + container_name ] self.label_arguments(cmd, container) self.container_run_args(cmd, container) @@ -80,6 +89,8 @@ class BaseBuilder(object): LOG.debug('Completed $ %s' % ' '.join(cmd)) LOG.info("stdout: %s" % cmd_stdout) LOG.info("stderr: %s" % cmd_stderr) + if systemd_managed: + systemd.service_create(container_name, cconfig) return stdout, stderr, deploy_status_code def delete_missing_and_updated(self): diff --git a/paunch/runner.py b/paunch/runner.py index c4595f0..8428d5b 100644 --- a/paunch/runner.py +++ b/paunch/runner.py @@ -18,6 +18,7 @@ import random import string import subprocess +from paunch.utils import systemd LOG = logging.getLogger(__name__) @@ -154,6 +155,8 @@ class BaseRunner(object): self.remove_container(container) def remove_container(self, container): + if self.docker_cmd == 'podman': + systemd.service_delete(container) cmd = [self.docker_cmd, 'rm', '-f', container] cmd_stdout, cmd_stderr, returncode = self.execute(cmd) if returncode != 0: diff --git a/paunch/tests/test_utils_systemd.py b/paunch/tests/test_utils_systemd.py new file mode 100644 index 0000000..156ab38 --- /dev/null +++ b/paunch/tests/test_utils_systemd.py @@ -0,0 +1,60 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import os +import tempfile + +from paunch.tests import base +from paunch.utils import systemd + + +class TestUtilsSystemd(base.TestCase): + + @mock.patch('subprocess.call', autospec=True) + @mock.patch('os.chmod') + def test_service_create(self, mock_chmod, mock_subprocess_call): + container = 'my_app' + cconfig = {'depends_on': ['something'], 'restart': 'unless-stopped', + 'stop_grace_period': '15'} + tempdir = tempfile.mkdtemp() + systemd.service_create(container, cconfig, tempdir) + + sysd_unit_f = tempdir + container + '.service' + unit = open(sysd_unit_f, 'rt').read() + self.assertIn('Wants=something.service', unit) + self.assertIn('Restart=always', unit) + self.assertIn('ExecStop=/usr/bin/podman stop -t 15 my_app', unit) + mock_chmod.assert_has_calls([mock.call(sysd_unit_f, 448)]) + + mock_subprocess_call.assert_has_calls([ + mock.call(['systemctl', 'enable', '--now', container]), + mock.call(['systemctl', 'daemon-reload']), + ]) + + os.rmdir(tempdir) + + @mock.patch('os.remove', autospec=True) + @mock.patch('os.path.isfile', autospec=True) + @mock.patch('subprocess.call', autospec=True) + def test_service_delete(self, mock_subprocess_call, mock_isfile, mock_rm): + mock_isfile.return_value = True + container = 'my_app' + systemd.service_delete(container) + mock_subprocess_call.assert_has_calls([ + mock.call(['systemctl', 'stop', container]), + mock.call(['systemctl', 'disable', container]), + mock.call(['systemctl', 'daemon-reload']), + ]) diff --git a/paunch/utils/__init__.py b/paunch/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/paunch/utils/systemd.py b/paunch/utils/systemd.py new file mode 100644 index 0000000..6244e8a --- /dev/null +++ b/paunch/utils/systemd.py @@ -0,0 +1,89 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os +import subprocess + +LOG = logging.getLogger(__name__) + + +def service_create(container, cconfig, sysdir='/etc/systemd/system/'): + """Create a service in systemd + + :param container: container name + :type container: String + + :param cconfig: container configuration + :type cconfig: Dictionary + + :param sysdir: systemd unit files directory + :type sysdir: string + """ + + wants = " ".join(str(x) + '.service' for x in + cconfig.get('depends_on', [])) + + restart = cconfig.get('restart', 'always') + stop_grace_period = cconfig.get('stop_grace_period', '10') + # SystemD doesn't have the equivalent of docker unless-stopped. + # Let's force 'always' so containers aren't restarted when stopped by + # systemd, but restarted when in failure. Also this code is only for + # podman now, so nothing changed for Docker deployments. + if restart == 'unless-stopped': + restart = 'always' + + sysd_unit_f = sysdir + container + '.service' + LOG.debug('Creating systemd unit file: %s' % sysd_unit_f) + s_config = { + 'name': container, + 'wants': wants, + 'restart': restart, + 'stop_grace_period': stop_grace_period, + } + with open(sysd_unit_f, 'w') as unit_file: + os.chmod(unit_file.name, 0o700) + unit_file.write("""[Unit] +Description=%(name)s container +After=paunch-container-shutdown.service +Wants=%(wants)s +[Service] +Restart=%(restart)s +ExecStart=/usr/bin/podman start -a %(name)s +ExecStop=/usr/bin/podman stop -t %(stop_grace_period)s %(name)s +KillMode=process +[Install] +WantedBy=multi-user.target""" % s_config) + subprocess.call(['systemctl', 'enable', '--now', container]) + subprocess.call(['systemctl', 'daemon-reload']) + + +def service_delete(container): + """Delete a service in systemd + + :param container: container name + :type container: String + """ + + sysd_unit_f = '/etc/systemd/system/' + container + '.service' + if os.path.isfile(sysd_unit_f): + LOG.debug('Stopping and disabling systemd service for %s' % container) + subprocess.call(['systemctl', 'stop', container]) + subprocess.call(['systemctl', 'disable', container]) + LOG.debug('Removing systemd unit file %s' % sysd_unit_f) + os.remove(sysd_unit_f) + subprocess.call(['systemctl', 'daemon-reload']) + else: + LOG.warning('No systemd unit file was found for %s' % container) diff --git a/releasenotes/notes/podman_systemd-96b2f032e7dc1173.yaml b/releasenotes/notes/podman_systemd-96b2f032e7dc1173.yaml new file mode 100644 index 0000000..e20fcb9 --- /dev/null +++ b/releasenotes/notes/podman_systemd-96b2f032e7dc1173.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + For all containers managed by podman, we'll create a systemd unit file + so the containers automatically start at boot and restart at failure. + When the container is removed, we'll disable and stop the service, then + remove the systemd unit file.