499 lines
19 KiB
Python
499 lines
19 KiB
Python
# 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 collections
|
|
import jmespath
|
|
import json
|
|
import os
|
|
import random
|
|
import string
|
|
import subprocess
|
|
import time
|
|
|
|
from paunch.builder import podman
|
|
from paunch.utils import common
|
|
from paunch.utils import systemctl
|
|
from paunch.utils import systemd
|
|
|
|
|
|
class BaseRunner(object):
|
|
def __init__(self, managed_by, cont_cmd, log=None, cont_log_path=None,
|
|
healthcheck_disabled=False):
|
|
self.managed_by = managed_by
|
|
self.cont_cmd = cont_cmd
|
|
# Leverage pre-configured logger
|
|
self.log = log or common.configure_logging(__name__)
|
|
self.cont_log_path = cont_log_path
|
|
self.healthcheck_disabled = healthcheck_disabled
|
|
if self.cont_cmd == 'docker':
|
|
self.log.warning('docker runtime is deprecated in Stein '
|
|
'and will be removed in Train.')
|
|
|
|
@staticmethod
|
|
def execute(cmd, log=None, quiet=False):
|
|
if not log:
|
|
log = common.configure_logging(__name__)
|
|
if not quiet:
|
|
log.debug('$ %s' % ' '.join(cmd))
|
|
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
cmd_stdout, cmd_stderr = subproc.communicate()
|
|
if not quiet:
|
|
log.debug(cmd_stdout)
|
|
log.debug(cmd_stderr)
|
|
return (cmd_stdout.decode('utf-8'),
|
|
cmd_stderr.decode('utf-8'),
|
|
subproc.returncode)
|
|
|
|
@staticmethod
|
|
def execute_interactive(cmd, log=None):
|
|
if not log:
|
|
log = common.configure_logging(__name__)
|
|
log.debug('$ %s' % ' '.join(cmd))
|
|
return subprocess.call(cmd)
|
|
|
|
def current_config_ids(self):
|
|
# List all config_id labels for managed containers
|
|
# FIXME(bogdando): remove once we have it fixed:
|
|
# https://github.com/containers/libpod/issues/1729
|
|
if self.cont_cmd == 'docker':
|
|
fmt = '{{.Label "config_id"}}'
|
|
else:
|
|
fmt = '{{.Labels.config_id}}'
|
|
cmd = [
|
|
self.cont_cmd, 'ps', '-a',
|
|
'--filter', 'label=managed_by=%s' % self.managed_by,
|
|
'--format', fmt
|
|
]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
results = cmd_stdout.split()
|
|
if returncode != 0 or not results or results == ['']:
|
|
# NOTE(bogdando): also look by the historically used to
|
|
# be always specified defaults, we must also identify such configs
|
|
cmd = [
|
|
self.cont_cmd, 'ps', '-a',
|
|
'--filter', 'label=managed_by=paunch',
|
|
'--format', fmt
|
|
]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
if returncode != 0:
|
|
return set()
|
|
results += cmd_stdout.split()
|
|
return set(results)
|
|
|
|
def containers_in_config(self, conf_id):
|
|
cmd = [
|
|
self.cont_cmd, 'ps', '-q', '-a',
|
|
'--filter', 'label=managed_by=%s' % self.managed_by,
|
|
'--filter', 'label=config_id=%s' % conf_id
|
|
]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
results = cmd_stdout.split()
|
|
if returncode != 0 or not results or results == ['']:
|
|
# NOTE(bogdando): also look by the historically used to
|
|
# be always specified defaults, we must also identify such configs
|
|
cmd = [
|
|
self.cont_cmd, 'ps', '-q', '-a',
|
|
'--filter', 'label=managed_by=paunch',
|
|
'--filter', 'label=config_id=%s' % conf_id
|
|
]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
if returncode != 0:
|
|
return []
|
|
results += cmd_stdout.split()
|
|
|
|
return [c for c in results]
|
|
|
|
def inspect(self, name, output_format=None, o_type='container',
|
|
quiet=False):
|
|
# In podman, if we're being asked to inspect a container image, we
|
|
# want to verify that the image exists before inspecting it.
|
|
# Context: https://github.com/containers/libpod/issues/1845
|
|
if o_type == 'image':
|
|
if not self.image_exist(name):
|
|
return
|
|
cmd = [self.cont_cmd, 'inspect', '--type', o_type]
|
|
if output_format:
|
|
cmd.append('--format')
|
|
cmd.append(output_format)
|
|
cmd.append(name)
|
|
(cmd_stdout, cmd_stderr, returncode) = self.execute(
|
|
cmd, self.log, quiet)
|
|
if returncode != 0:
|
|
return
|
|
try:
|
|
if output_format:
|
|
return cmd_stdout
|
|
else:
|
|
return json.loads(cmd_stdout)[0]
|
|
except Exception as e:
|
|
self.log.error('Problem parsing %s inspect: %s' %
|
|
(self.cont_cmd, e))
|
|
|
|
def unique_container_name(self, container):
|
|
container_name = container
|
|
if self.cont_cmd == 'docker':
|
|
while self.inspect(container_name, output_format='exists',
|
|
quiet=True):
|
|
suffix = ''.join(random.choice(
|
|
string.ascii_lowercase + string.digits) for i in range(8))
|
|
container_name = '%s-%s' % (container, suffix)
|
|
break
|
|
else:
|
|
while self.container_exist(container_name, quiet=True):
|
|
suffix = ''.join(random.choice(
|
|
string.ascii_lowercase + string.digits) for i in range(8))
|
|
container_name = '%s-%s' % (container, suffix)
|
|
break
|
|
return container_name
|
|
|
|
def discover_container_name(self, container, cid):
|
|
cmd = [
|
|
self.cont_cmd,
|
|
'ps',
|
|
'-a',
|
|
'--filter',
|
|
'label=container_name=%s' % container,
|
|
'--filter',
|
|
'label=config_id=%s' % cid,
|
|
'--format',
|
|
'{{.Names}}'
|
|
]
|
|
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd, self.log)
|
|
if returncode == 0:
|
|
names = cmd_stdout.split()
|
|
if names:
|
|
return names[0]
|
|
self.log.warning('Did not find container with "%s" - retrying without '
|
|
'config_id' % cmd)
|
|
|
|
cmd = [
|
|
self.cont_cmd,
|
|
'ps',
|
|
'-a',
|
|
'--filter',
|
|
'label=container_name=%s' % container,
|
|
'--format',
|
|
'{{.Names}}'
|
|
]
|
|
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd, self.log)
|
|
if returncode == 0:
|
|
names = cmd_stdout.split()
|
|
if names:
|
|
return names[0]
|
|
|
|
self.log.warning('Did not find container with "%s"' % cmd)
|
|
|
|
def delete_missing_configs(self, config_ids):
|
|
if not config_ids:
|
|
config_ids = []
|
|
|
|
for conf_id in self.current_config_ids():
|
|
if conf_id not in config_ids:
|
|
self.log.debug('%s no longer exists, deleting containers' %
|
|
conf_id)
|
|
self.remove_containers(conf_id)
|
|
|
|
def discover_container_config(self, configs, container, name):
|
|
'''Find the paunch and runtime configs of a container by name.'''
|
|
for conf_id in self.current_config_ids():
|
|
jquerry = ("[] | [?(Name=='%s' && "
|
|
"Config.Labels.container_name=='%s' && "
|
|
"Config.Labels.config_id=='%s')]" %
|
|
(container, name, conf_id))
|
|
runtime_conf = None
|
|
try:
|
|
runtime_conf = jmespath.search(jquerry,
|
|
configs[conf_id])[0]
|
|
result = (conf_id, runtime_conf)
|
|
except Exception:
|
|
self.log.error("Failed searching container %s "
|
|
"for config %s" % (container, conf_id))
|
|
result = (None, None)
|
|
if runtime_conf:
|
|
self.log.debug("Found container %s "
|
|
"for config %s" % (container, conf_id))
|
|
break
|
|
return result
|
|
|
|
def list_configs(self):
|
|
configs = collections.defaultdict(list)
|
|
for conf_id in self.current_config_ids():
|
|
for container in self.containers_in_config(conf_id):
|
|
configs[conf_id].append(self.inspect(container))
|
|
return configs
|
|
|
|
def container_names(self, conf_id=None):
|
|
# list every container name, and its container_name label
|
|
# FIXME(bogdando): remove once we have it fixed:
|
|
# https://github.com/containers/libpod/issues/1729
|
|
if self.cont_cmd == 'docker':
|
|
fmt = '{{.Label "container_name"}}'
|
|
else:
|
|
fmt = '{{.Labels.container_name}}'
|
|
cmd = [
|
|
self.cont_cmd, 'ps', '-a',
|
|
'--filter', 'label=managed_by=%s' % self.managed_by
|
|
]
|
|
if conf_id:
|
|
cmd.extend((
|
|
'--filter', 'label=config_id=%s' % conf_id
|
|
))
|
|
cmd.extend((
|
|
'--format', '{{.Names}} %s' % fmt
|
|
))
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
results = cmd_stdout.split("\n")
|
|
if returncode != 0 or not results or results == ['']:
|
|
# NOTE(bogdando): also look by the historically used to
|
|
# be always specified defaults, we must also identify such configs
|
|
cmd = [
|
|
self.cont_cmd, 'ps', '-a',
|
|
'--filter', 'label=managed_by=paunch'
|
|
]
|
|
if conf_id:
|
|
cmd.extend((
|
|
'--filter', 'label=config_id=%s' % conf_id
|
|
))
|
|
cmd.extend((
|
|
'--format', '{{.Names}} %s' % fmt
|
|
))
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
if returncode != 0:
|
|
return
|
|
results += cmd_stdout.split("\n")
|
|
for line in results:
|
|
if line:
|
|
yield line.split()
|
|
|
|
def remove_containers(self, conf_id):
|
|
for container in self.containers_in_config(conf_id):
|
|
self.remove_container(container)
|
|
|
|
def remove_container(self, container):
|
|
if self.cont_cmd == 'podman':
|
|
systemd.service_delete(container=container, log=self.log)
|
|
self.execute([self.cont_cmd, 'stop', container], self.log)
|
|
cmd = [self.cont_cmd, 'rm', container]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
if returncode != 0:
|
|
self.log.error('Error removing container: %s' % container)
|
|
self.log.error(cmd_stderr)
|
|
|
|
def stop_container(self, container, cont_cmd=None, quiet=False):
|
|
cont_cmd = cont_cmd or self.cont_cmd
|
|
cmd = [cont_cmd, 'stop', container]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, quiet=quiet)
|
|
if returncode != 0 and not quiet:
|
|
self.log.error('Error stopping container: %s' % container)
|
|
self.log.error(cmd_stderr)
|
|
|
|
def rename_containers(self):
|
|
current_containers = []
|
|
need_renaming = {}
|
|
for entry in self.container_names():
|
|
current_containers.append(entry[0])
|
|
|
|
# ignore if container_name label not set
|
|
if len(entry) < 2:
|
|
continue
|
|
|
|
# ignore if desired name is already actual name
|
|
if entry[0] == entry[-1]:
|
|
continue
|
|
|
|
need_renaming[entry[0]] = entry[-1]
|
|
|
|
for current, desired in sorted(need_renaming.items()):
|
|
if desired in current_containers:
|
|
self.log.info('Cannot rename "%s" since "%s" still exists' % (
|
|
current, desired))
|
|
else:
|
|
self.log.info('Renaming "%s" to "%s"' % (current, desired))
|
|
self.rename_container(current, desired)
|
|
current_containers.append(desired)
|
|
|
|
def validate_volume_source(self, volume):
|
|
"""Validate that the provided volume
|
|
|
|
This checks that the provided volume either exists on the filesystem
|
|
or is a container volume.
|
|
|
|
:param: volume: string containing either a filesystme path or container
|
|
volume name
|
|
"""
|
|
if os.path.exists(volume):
|
|
return True
|
|
|
|
if os.path.sep in volume:
|
|
# if we get here and have a path seperator, let's skip the
|
|
# container lookup because container volumes won't have / in them.
|
|
self.log.debug('Path seperator found in volume (%s), but did not '
|
|
'exist on the file system' % volume)
|
|
return False
|
|
|
|
self.log.debug('Running volume lookup for "%s"' % volume)
|
|
filter_opt = '--filter=name={}'.format(volume)
|
|
cmd = [self.cont_cmd, 'volume', 'ls', '-q', filter_opt]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd)
|
|
if returncode != 0:
|
|
self.log.error('Error during volume verification')
|
|
self.log.error(cmd_stderr)
|
|
return False
|
|
return (volume in set(cmd_stdout.split()))
|
|
|
|
|
|
class DockerRunner(BaseRunner):
|
|
|
|
def __init__(self, managed_by, cont_cmd=None, log=None):
|
|
cont_cmd = cont_cmd or 'docker'
|
|
super(DockerRunner, self).__init__(managed_by, cont_cmd, log)
|
|
|
|
def rename_container(self, container, name):
|
|
cmd = [self.cont_cmd, 'rename', container, name]
|
|
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
|
|
if returncode != 0:
|
|
self.log.error('Error renaming container: %s' % container)
|
|
self.log.error(cmd_stderr)
|
|
|
|
def image_exist(self, name, quiet=False):
|
|
self.log.warning("image_exist isn't supported "
|
|
"by %s" % self.cont_cmd)
|
|
return True
|
|
|
|
def container_exist(self, name, quiet=False):
|
|
self.log.warning("container_exist isn't supported "
|
|
"by %s" % self.cont_cmd)
|
|
return True
|
|
|
|
def container_running(self, container):
|
|
self.log.warning("container_running isn't supported "
|
|
"by %s" % self.cont_cmd)
|
|
return True
|
|
|
|
|
|
class PodmanRunner(BaseRunner):
|
|
|
|
def __init__(self, managed_by, cont_cmd=None, log=None,
|
|
cont_log_path=None, healthcheck_disabled=False):
|
|
cont_cmd = cont_cmd or 'podman'
|
|
super(PodmanRunner, self).__init__(managed_by, cont_cmd, log,
|
|
cont_log_path, healthcheck_disabled)
|
|
|
|
def rename_container(self, container, name):
|
|
# TODO(emilien) podman doesn't support rename, we'll handle it
|
|
# in paunch itself for now
|
|
configs = self.list_configs()
|
|
config_id, config = self.discover_container_config(
|
|
configs, container, name)
|
|
# Get config_data dict by the discovered conf ID,
|
|
# paunch needs it for maintaining idempotency within a conf ID
|
|
filter_names = ("[] | [?(Name!='%s' && "
|
|
"Config.Labels.config_id=='%s')]"
|
|
".Name" % (container, config_id))
|
|
filter_cdata = ("[] | [?(Name!='%s' && "
|
|
"Config.Labels.config_id=='%s')]"
|
|
".Config.Labels.config_data" % (container, config_id))
|
|
names = None
|
|
cdata = None
|
|
try:
|
|
names = jmespath.search(filter_names, configs[config_id])
|
|
cdata = jmespath.search(filter_cdata, configs[config_id])
|
|
except jmespath.exceptions.LexerError:
|
|
self.log.error("Failed to rename a container %s into %s: "
|
|
"used a bad search pattern" % (container, name))
|
|
return
|
|
|
|
if not names or not cdata:
|
|
self.log.error("Failed to rename a container %s into %s: "
|
|
"no config_data was found" % (container, name))
|
|
return
|
|
|
|
# Rename the wanted container in the config_data fetched from the
|
|
# discovered config
|
|
config_data = dict(zip(names, map(json.loads, cdata)))
|
|
config_data[name] = json.loads(
|
|
config.get('Config').get('Labels').get('config_data'))
|
|
|
|
# Re-apply a container under its amended name using the fetched configs
|
|
self.log.debug("Renaming a container known as %s into %s, "
|
|
"via re-applying its original config" %
|
|
(container, name))
|
|
self.log.debug("Removing the destination container %s" % name)
|
|
self.stop_container(name)
|
|
self.remove_container(name)
|
|
self.log.debug("Removing a container known as %s" % container)
|
|
self.stop_container(container)
|
|
self.remove_container(container)
|
|
builder = podman.PodmanBuilder(
|
|
config_id=config_id,
|
|
config=config_data,
|
|
runner=self,
|
|
labels=None,
|
|
log=self.log,
|
|
cont_log_path=self.cont_log_path,
|
|
healthcheck_disabled=self.healthcheck_disabled
|
|
)
|
|
builder.apply()
|
|
|
|
def image_exist(self, name, quiet=False):
|
|
cmd = ['podman', 'image', 'exists', name]
|
|
(_, _, returncode) = self.execute(cmd, self.log, quiet)
|
|
return returncode == 0
|
|
|
|
def container_exist(self, name, quiet=False):
|
|
cmd = ['podman', 'container', 'exists', name]
|
|
(_, _, returncode) = self.execute(cmd, self.log, quiet)
|
|
return returncode == 0
|
|
|
|
def container_running(self, container):
|
|
service_name = 'tripleo_' + container + '.service'
|
|
try:
|
|
systemctl.is_active(service_name)
|
|
self.log.debug('Unit %s is running' % service_name)
|
|
return True
|
|
except systemctl.SystemctlException:
|
|
chk_cmd = [
|
|
self.cont_cmd,
|
|
'ps',
|
|
'--filter',
|
|
'label=container_name=%s' % container,
|
|
'--format',
|
|
'{{.Names}}'
|
|
]
|
|
cmd_stdout = ''
|
|
returncode = -1
|
|
count = 1
|
|
while (not cmd_stdout or returncode != 0) and count <= 5:
|
|
self.log.warning('Attempt %i to check if %s is '
|
|
'running' % (count, container))
|
|
# at the first retry, we will force a sync with the OCI runtime
|
|
if self.cont_cmd == 'podman' and count == 2:
|
|
chk_cmd.append('--sync')
|
|
(cmd_stdout, cmd_stderr, returncode) = self.execute(chk_cmd,
|
|
self.log)
|
|
|
|
if returncode != 0:
|
|
self.log.warning('Attempt %i Error when running '
|
|
'%s:' % (count, chk_cmd))
|
|
self.log.warning(cmd_stderr)
|
|
else:
|
|
if not cmd_stdout:
|
|
self.log.warning('Attempt %i Container %s '
|
|
'is not running' % (count, container))
|
|
|
|
count += 1
|
|
time.sleep(0.2)
|
|
# return True if ps ran successfuly and returned a container name.
|
|
return (cmd_stdout and returncode == 0)
|