podman: create/delete systemd unit files when restart policy is used

This patch will create a basic systemd unit file that start/stop/status
any container that we manage with restart policy in paunch config.
It's only created when podman is the container runtime and when the
container is configured with a restart policy.

The systemd unit file will be removed when paunch removes the container.

KillMode=process is used so in the case of Neutron, we don't kill
children containers.

Note: if the policy is set to unless-stopped, we'll force the always
policy because unless-stopped doesn't exist in systemd.

Change-Id: I676e5fff3daecadba45efddff11f7afc602a50ef
This commit is contained in:
Emilien Macchi 2018-09-07 15:08:33 -04:00
parent 78dfebcbee
commit 6a6f99b724
6 changed files with 174 additions and 4 deletions

View File

@ -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):

View File

@ -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:

View File

@ -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']),
])

0
paunch/utils/__init__.py Normal file
View File

89
paunch/utils/systemd.py Normal file
View File

@ -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)

View File

@ -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.