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:
parent
78dfebcbee
commit
6a6f99b724
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,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)
|
|
@ -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.
|
Loading…
Reference in New Issue