When generating the big ball of config JSON that helps define parameters for the smb tests in the workunit add client "node" info as well. Add a function to avoid repeating the logic of getting node info from the teuthology remote object. Signed-off-by: John Mulligan <jmulligan@redhat.com>
448 lines
14 KiB
Python
448 lines
14 KiB
Python
"""
|
|
Ceph teuthology task for managed smb features.
|
|
"""
|
|
from io import StringIO
|
|
import contextlib
|
|
import copy
|
|
import json
|
|
import logging
|
|
import shlex
|
|
import time
|
|
|
|
from teuthology.exceptions import ConfigError, CommandFailedError
|
|
from teuthology.task import ssh_keys
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def _disable_systemd_resolved(ctx, remote):
|
|
r = remote.run(args=['ss', '-lunH'], stdout=StringIO())
|
|
# this heuristic tries to detect if systemd-resolved is running
|
|
if '%lo:53' not in r.stdout.getvalue():
|
|
return
|
|
log.info('Disabling systemd-resolved on %s', remote.shortname)
|
|
# Samba AD DC container DNS support conflicts with resolved stub
|
|
# resolver when using host networking. And we want host networking
|
|
# because it is the simplest thing to set up. We therefore will turn
|
|
# off the stub resolver.
|
|
r = remote.run(
|
|
args=['sudo', 'cat', '/etc/systemd/resolved.conf'],
|
|
stdout=StringIO(),
|
|
)
|
|
resolved_conf = r.stdout.getvalue()
|
|
setattr(ctx, 'orig_resolved_conf', resolved_conf)
|
|
new_resolved_conf = (
|
|
resolved_conf + '\n# EDITED BY TEUTHOLOGY: deploy_samba_ad_dc\n'
|
|
)
|
|
if '[Resolve]' not in new_resolved_conf.splitlines():
|
|
new_resolved_conf += '[Resolve]\n'
|
|
new_resolved_conf += 'DNSStubListener=no\n'
|
|
remote.write_file(
|
|
path='/etc/systemd/resolved.conf',
|
|
data=new_resolved_conf,
|
|
sudo=True,
|
|
)
|
|
remote.run(args=['sudo', 'systemctl', 'restart', 'systemd-resolved'])
|
|
r = remote.run(args=['ss', '-lunH'], stdout=StringIO())
|
|
assert '%lo:53' not in r.stdout.getvalue()
|
|
# because docker is a big fat persistent deamon, we need to bounce it
|
|
# after resolved is restarted
|
|
remote.run(args=['sudo', 'systemctl', 'restart', 'docker'])
|
|
|
|
|
|
def _reset_systemd_resolved(ctx, remote):
|
|
orig_resolved_conf = getattr(ctx, 'orig_resolved_conf', None)
|
|
if not orig_resolved_conf:
|
|
return # no orig_resolved_conf means nothing to reset
|
|
log.info('Resetting systemd-resolved state on %s', remote.shortname)
|
|
remote.write_file(
|
|
path='/etc/systemd/resolved.conf',
|
|
data=orig_resolved_conf,
|
|
sudo=True,
|
|
)
|
|
remote.run(args=['sudo', 'systemctl', 'restart', 'systemd-resolved'])
|
|
setattr(ctx, 'orig_resolved_conf', None)
|
|
|
|
|
|
def _samba_ad_dc_conf(ctx, remote, cengine):
|
|
# this config has not been tested outside of smithi nodes. it's possible
|
|
# that this will break when used elsewhere because we have to list
|
|
# interfaces explicitly. Later I may add a feature to sambacc to exclude
|
|
# known-unwanted interfaces that having to specify known good interfaces.
|
|
cf = {
|
|
"samba-container-config": "v0",
|
|
"configs": {
|
|
"demo": {
|
|
"instance_features": ["addc"],
|
|
"domain_settings": "sink",
|
|
"instance_name": "dc1",
|
|
}
|
|
},
|
|
"domain_settings": {
|
|
"sink": {
|
|
"realm": "DOMAIN1.SINK.TEST",
|
|
"short_domain": "DOMAIN1",
|
|
"admin_password": "Passw0rd",
|
|
"interfaces": {
|
|
"exclude_pattern": "^docker[0-9]+$",
|
|
},
|
|
}
|
|
},
|
|
"domain_groups": {
|
|
"sink": [
|
|
{"name": "supervisors"},
|
|
{"name": "employees"},
|
|
{"name": "characters"},
|
|
{"name": "bulk"},
|
|
]
|
|
},
|
|
"domain_users": {
|
|
"sink": [
|
|
{
|
|
"name": "bwayne",
|
|
"password": "1115Rose.",
|
|
"given_name": "Bruce",
|
|
"surname": "Wayne",
|
|
"member_of": ["supervisors", "characters", "employees"],
|
|
},
|
|
{
|
|
"name": "ckent",
|
|
"password": "1115Rose.",
|
|
"given_name": "Clark",
|
|
"surname": "Kent",
|
|
"member_of": ["characters", "employees"],
|
|
},
|
|
{
|
|
"name": "user0",
|
|
"password": "1115Rose.",
|
|
"given_name": "George0",
|
|
"surname": "Hue-Sir",
|
|
"member_of": ["bulk"],
|
|
},
|
|
{
|
|
"name": "user1",
|
|
"password": "1115Rose.",
|
|
"given_name": "George1",
|
|
"surname": "Hue-Sir",
|
|
"member_of": ["bulk"],
|
|
},
|
|
{
|
|
"name": "user2",
|
|
"password": "1115Rose.",
|
|
"given_name": "George2",
|
|
"surname": "Hue-Sir",
|
|
"member_of": ["bulk"],
|
|
},
|
|
{
|
|
"name": "user3",
|
|
"password": "1115Rose.",
|
|
"given_name": "George3",
|
|
"surname": "Hue-Sir",
|
|
"member_of": ["bulk"],
|
|
},
|
|
]
|
|
},
|
|
}
|
|
cf_json = json.dumps(cf)
|
|
remote.run(args=['sudo', 'mkdir', '-p', '/var/tmp/samba'])
|
|
remote.write_file(
|
|
path='/var/tmp/samba/container.json', data=cf_json, sudo=True
|
|
)
|
|
return [
|
|
'--volume=/var/tmp/samba:/etc/samba-container:ro',
|
|
'-eSAMBACC_CONFIG=/etc/samba-container/container.json',
|
|
]
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def configure_samba_client_container(ctx, config):
|
|
# TODO: deduplicate logic between this task and deploy_samba_ad_dc
|
|
role = config.get('role')
|
|
samba_client_image = config.get(
|
|
'samba_client_image', 'quay.io/samba.org/samba-client:latest'
|
|
)
|
|
if not role:
|
|
raise ConfigError(
|
|
"you must specify a role to discover container engine / pull image"
|
|
)
|
|
(remote,) = ctx.cluster.only(role).remotes.keys()
|
|
cengine = 'podman'
|
|
try:
|
|
log.info("Testing if podman is available")
|
|
remote.run(args=['sudo', cengine, '--help'])
|
|
except CommandFailedError:
|
|
log.info("Failed to find podman. Using docker")
|
|
cengine = 'docker'
|
|
|
|
remote.run(args=['sudo', cengine, 'pull', samba_client_image])
|
|
samba_client_container_cmd = [
|
|
'sudo',
|
|
cengine,
|
|
'run',
|
|
'--rm',
|
|
'--net=host',
|
|
'-eKRB5_CONFIG=/dev/null',
|
|
samba_client_image,
|
|
]
|
|
|
|
setattr(ctx, 'samba_client_container_cmd', samba_client_container_cmd)
|
|
try:
|
|
yield
|
|
finally:
|
|
setattr(ctx, 'samba_client_container_cmd', None)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def deploy_samba_ad_dc(ctx, config):
|
|
role = config.get('role')
|
|
ad_dc_image = config.get(
|
|
'ad_dc_image', 'quay.io/samba.org/samba-ad-server:latest'
|
|
)
|
|
samba_client_image = config.get(
|
|
'samba_client_image', 'quay.io/samba.org/samba-client:latest'
|
|
)
|
|
test_user_pass = config.get('test_user_pass', 'DOMAIN1\\ckent%1115Rose.')
|
|
if not role:
|
|
raise ConfigError(
|
|
"you must specify a role to allocate a host for the AD DC"
|
|
)
|
|
(remote,) = ctx.cluster.only(role).remotes.keys()
|
|
ip = remote.ssh.get_transport().getpeername()[0]
|
|
cengine = 'podman'
|
|
try:
|
|
log.info("Testing if podman is available")
|
|
remote.run(args=['sudo', cengine, '--help'])
|
|
except CommandFailedError:
|
|
log.info("Failed to find podman. Using docker")
|
|
cengine = 'docker'
|
|
remote.run(args=['sudo', cengine, 'pull', ad_dc_image])
|
|
remote.run(args=['sudo', cengine, 'pull', samba_client_image])
|
|
_disable_systemd_resolved(ctx, remote)
|
|
remote.run(
|
|
args=[
|
|
'sudo',
|
|
'mkdir',
|
|
'-p',
|
|
'/var/lib/samba/container/logs',
|
|
'/var/lib/samba/container/data',
|
|
]
|
|
)
|
|
remote.run(
|
|
args=[
|
|
'sudo',
|
|
cengine,
|
|
'run',
|
|
'-d',
|
|
'--name=samba-ad',
|
|
'--network=host',
|
|
'--privileged',
|
|
]
|
|
+ _samba_ad_dc_conf(ctx, remote, cengine)
|
|
+ [ad_dc_image]
|
|
)
|
|
|
|
# test that the ad dc is running and basically works
|
|
connected = False
|
|
samba_client_container_cmd = [
|
|
'sudo',
|
|
cengine,
|
|
'run',
|
|
'--rm',
|
|
'--net=host',
|
|
f'--dns={ip}',
|
|
'-eKRB5_CONFIG=/dev/null',
|
|
samba_client_image,
|
|
]
|
|
for idx in range(10):
|
|
time.sleep((2 ** (1 + idx)) / 8)
|
|
log.info("Probing SMB status of DC %s, idx=%s", ip, idx)
|
|
cmd = samba_client_container_cmd + [
|
|
'smbclient',
|
|
'-U',
|
|
test_user_pass,
|
|
'//domain1.sink.test/sysvol',
|
|
'-c',
|
|
'ls',
|
|
]
|
|
try:
|
|
remote.run(args=cmd)
|
|
connected = True
|
|
log.info("SMB status probe succeeded")
|
|
break
|
|
except CommandFailedError:
|
|
pass
|
|
if not connected:
|
|
raise RuntimeError('failed to connect to AD DC SMB share')
|
|
|
|
setattr(ctx, 'samba_ad_dc_ip', ip)
|
|
setattr(ctx, 'samba_client_container_cmd', samba_client_container_cmd)
|
|
try:
|
|
yield
|
|
finally:
|
|
try:
|
|
remote.run(args=['sudo', cengine, 'stop', 'samba-ad'])
|
|
except CommandFailedError:
|
|
log.error("Failed to stop samba-ad container")
|
|
try:
|
|
remote.run(args=['sudo', cengine, 'rm', 'samba-ad'])
|
|
except CommandFailedError:
|
|
log.error("Failed to remove samba-ad container")
|
|
remote.run(
|
|
args=[
|
|
'sudo',
|
|
'rm',
|
|
'-rf',
|
|
'/var/lib/samba/container/logs',
|
|
'/var/lib/samba/container/data',
|
|
]
|
|
)
|
|
_reset_systemd_resolved(ctx, remote)
|
|
setattr(ctx, 'samba_ad_dc_ip', None)
|
|
setattr(ctx, 'samba_client_container_cmd', None)
|
|
|
|
|
|
def _marks(marks_value):
|
|
if not marks_value:
|
|
return ''
|
|
if isinstance(marks_value, str):
|
|
return marks_value
|
|
if isinstance(marks_value, list):
|
|
return ' or '.join(marks_value)
|
|
raise ValueError(f'unexpected type: {marks_value!r}')
|
|
|
|
|
|
def _workunit_commands(
|
|
key, values, *, default_script='smb/smb_tests.sh', default_target='tests'
|
|
):
|
|
commands = []
|
|
if isinstance(values, str):
|
|
values = values.split()
|
|
for value in values:
|
|
script = default_script
|
|
target = default_target
|
|
custom_args = []
|
|
if isinstance(value, str):
|
|
# direct marks expression
|
|
marks = _marks(value)
|
|
elif isinstance(value, list):
|
|
# just a list of marks to include
|
|
marks = _marks(value)
|
|
elif isinstance(value, dict):
|
|
# full control
|
|
opts = value
|
|
script = opts.get('script', script)
|
|
target = opts.get('target', target)
|
|
marks = _marks(opts.get('marks', []))
|
|
custom_args = [str(v) for v in (opts.get('custom_args') or [])]
|
|
|
|
cmd = [script]
|
|
if marks:
|
|
cmd.append('-m')
|
|
cmd.append(marks)
|
|
cmd += custom_args
|
|
cmd.append(target)
|
|
commands.append(shlex.join(cmd))
|
|
return commands
|
|
|
|
|
|
def workunit(ctx, config):
|
|
"""Workunit wrapper with special behaviors for smb."""
|
|
from . import workunit
|
|
|
|
_config = copy.deepcopy(config)
|
|
clients = _config.get('clients') or {}
|
|
env = _config.get('env') or {}
|
|
|
|
clients = {k: _workunit_commands(k, v) for k, v in clients.items()}
|
|
mfile = _config.get('metadata_file_path', _DEFAULT_META_FILE)
|
|
env['SMB'] = 'yes'
|
|
env['SMB_TEST_META'] = mfile
|
|
|
|
_config['clients'] = clients
|
|
_config['env'] = env
|
|
# annoyingly the stock workunit helper script command uses a tool (from the
|
|
# ceph/teuthology repo) called adjust-ulimits *and* a tool (from packages)
|
|
# called ceph-coverage. They're glued together under the
|
|
# no_coverage_and_limits option that defaults to false for the stock
|
|
# workunit task. Since, for SMB on Ceph, we are using containers and NOT
|
|
# using packages the latter tool is not available even if we invoke other
|
|
# teuthology tasks that installs adjust-ulimits. Just skip the whole thing
|
|
# for now and we can set ulimits via pytest if we really want to set
|
|
# ulimits. Allow the yaml to override our default, however unlikely.
|
|
_config['no_coverage_and_limits'] = config.get(
|
|
'no_coverage_and_limits', True
|
|
)
|
|
_ssh_keys_config = config.get('ssh_keys', {})
|
|
_config['enable_ssh_keys'] = _ssh_keys_config not in (False, None)
|
|
log.info('Passing workunit config: %r', _config)
|
|
with contextlib.ExitStack() as estack:
|
|
if _config['enable_ssh_keys']:
|
|
estack.enter_context(ssh_keys.task(ctx, _ssh_keys_config))
|
|
estack.enter_context(write_metadata_file(ctx, _config))
|
|
return workunit.task(ctx, _config)
|
|
|
|
|
|
def _node_info(ctx, role_name, **kwargs):
|
|
(remote,) = ctx.cluster.only(role_name).remotes.keys()
|
|
info = dict(remote.inventory_info)
|
|
info['_role_name'] = role_name
|
|
info['shortname'] = remote.shortname
|
|
info['ip_address'] = remote.ip_address
|
|
info.update(kwargs)
|
|
return info
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def write_metadata_file(ctx, config, *, roles=None):
|
|
obj = {
|
|
'samba_client_container_cmd': getattr(
|
|
ctx, 'samba_client_container_cmd', ''
|
|
),
|
|
'samba_ad_dc_ip': getattr(ctx, 'samba_ad_dc_ip', ''),
|
|
'smb_users': config.get('smb_users') or [],
|
|
'smb_shares': config.get('smb_shares') or [],
|
|
}
|
|
if config.get('admin_node'):
|
|
role = config.get('admin_node')
|
|
obj['admin_node'] = _node_info(ctx, role)
|
|
if config.get('smb_nodes'):
|
|
obj['smb_nodes'] = [
|
|
_node_info(ctx, node) for node in config.get('smb_nodes')
|
|
]
|
|
if config.get('clients'):
|
|
obj['client_nodes'] = [
|
|
_node_info(ctx, node, client_name=node)
|
|
for node in config.get('clients')
|
|
]
|
|
data = json.dumps(obj)
|
|
log.debug('smb metadata: %r', obj)
|
|
|
|
mfile = config.get('metadata_file_path', _DEFAULT_META_FILE)
|
|
if not roles:
|
|
roles = list(config.get('clients') or [])
|
|
remotes = []
|
|
for role in roles:
|
|
(remote,) = ctx.cluster.only(role).remotes.keys()
|
|
remotes.append(remote)
|
|
remote.write_file(
|
|
path=mfile,
|
|
data=data,
|
|
sudo=True,
|
|
mode='0644',
|
|
)
|
|
yield
|
|
for remote in remotes:
|
|
remote.run(
|
|
args=[
|
|
'sudo',
|
|
'rm',
|
|
'-rf',
|
|
'--',
|
|
mfile,
|
|
],
|
|
)
|
|
|
|
|
|
_DEFAULT_META_FILE = '/var/tmp/ceph-smb-test-meta.json'
|