Merge "Run Ansible playbook from undercloud node"
This commit is contained in:
commit
d1fac1802c
|
@ -0,0 +1,24 @@
|
|||
# Copyright (c) 2022 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.
|
||||
from __future__ import absolute_import
|
||||
|
||||
from tobiko.shell.ansible import _playbook
|
||||
|
||||
|
||||
ansible_playbook = _playbook.ansible_playbook
|
||||
AnsiblePlaybook = _playbook.AnsiblePlaybook
|
||||
register_ansible_playbook = _playbook.register_ansible_playbook
|
||||
run_playbook = _playbook.run_playbook
|
|
@ -0,0 +1,236 @@
|
|||
# Copyright (c) 2022 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.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import typing
|
||||
|
||||
import tobiko
|
||||
from tobiko.shell import sh
|
||||
from tobiko.shell import ssh
|
||||
|
||||
|
||||
class AnsiblePlaybook(tobiko.SharedFixture):
|
||||
|
||||
def __init__(self,
|
||||
command: sh.ShellCommandType = 'ansible-playbook',
|
||||
inventory_filename: str = None,
|
||||
playbook: str = 'main',
|
||||
playbook_dirname: str = None,
|
||||
ssh_client: ssh.SSHClientType = None,
|
||||
work_dir: str = None):
|
||||
super(AnsiblePlaybook, self).__init__()
|
||||
self._command = sh.shell_command(command)
|
||||
self._inventory_filename = inventory_filename
|
||||
self._playbook = playbook
|
||||
self._playbook_dirname = playbook_dirname
|
||||
self._ssh_client = ssh_client
|
||||
self._work_dir = work_dir
|
||||
self._work_files: typing.Dict[str, str] = {}
|
||||
|
||||
def setup_fixture(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def ssh_client(self) -> ssh.SSHClientType:
|
||||
return self._ssh_client
|
||||
|
||||
_sh_connection: typing.Optional[sh.ShellConnection] = None
|
||||
|
||||
@property
|
||||
def sh_connection(self) -> sh.ShellConnection:
|
||||
if self._sh_connection is None:
|
||||
self._sh_connection = sh.shell_connection(
|
||||
ssh_client=self.ssh_client)
|
||||
return self._sh_connection
|
||||
|
||||
@property
|
||||
def work_dir(self) -> str:
|
||||
if self._work_dir is None:
|
||||
self._work_dir = self.sh_connection.make_temp_dir(auto_clean=False)
|
||||
self._work_files = {}
|
||||
return self._work_dir
|
||||
|
||||
@property
|
||||
def playbook_dirname(self) -> str:
|
||||
return tobiko.check_valid_type(self._playbook_dirname, str)
|
||||
|
||||
def _ensure_inventory_filename(self, inventory_filename: str = None) \
|
||||
-> typing.Optional[str]:
|
||||
if inventory_filename is None:
|
||||
inventory_filename = self._inventory_filename
|
||||
if inventory_filename is None:
|
||||
return None
|
||||
return self._ensure_work_file(inventory_filename, 'inventory')
|
||||
|
||||
def _ensure_playbook_filename(self,
|
||||
playbook: str = None,
|
||||
playbook_dirname: str = None,
|
||||
playbook_filename: str = None) \
|
||||
-> typing.Optional[str]:
|
||||
if playbook_filename is None:
|
||||
playbook_filename = self._get_playbook_filename(
|
||||
playbook=playbook, playbook_dirname=playbook_dirname)
|
||||
return self._ensure_work_file(playbook_filename)
|
||||
|
||||
def _get_playbook_filename(self,
|
||||
playbook: str = None,
|
||||
playbook_dirname: str = None) -> str:
|
||||
if playbook is None:
|
||||
playbook = self._playbook
|
||||
if playbook_dirname is None:
|
||||
playbook_dirname = self.playbook_dirname
|
||||
return os.path.join(playbook_dirname, playbook + '.yaml')
|
||||
|
||||
def _ensure_work_file(self, filename: str, sub_dir: str = None) -> str:
|
||||
filename = os.path.realpath(filename)
|
||||
work_filename = self._work_files.get(filename)
|
||||
if work_filename is None:
|
||||
if sub_dir is not None:
|
||||
work_filename = os.path.join(
|
||||
self.work_dir, sub_dir, os.path.basename(filename))
|
||||
else:
|
||||
work_filename = os.path.join(
|
||||
self.work_dir, os.path.basename(filename))
|
||||
if sub_dir is not None:
|
||||
self.sh_connection.make_dirs(os.path.dirname(work_filename))
|
||||
self.sh_connection.put_file(filename, work_filename)
|
||||
self._work_files[filename] = work_filename
|
||||
return work_filename
|
||||
|
||||
def cleanup_fixture(self):
|
||||
self._sh_connection = None
|
||||
self._work_files = None
|
||||
if self._work_dir is not None:
|
||||
self.sh_connection.remove_files(self._work_dir)
|
||||
self._work_dir = None
|
||||
|
||||
def _get_command(self,
|
||||
command: sh.ShellCommandType = None,
|
||||
playbook: str = None,
|
||||
playbook_dirname: str = None,
|
||||
playbook_filename: str = None,
|
||||
inventory_filename: str = None) -> \
|
||||
sh.ShellCommand:
|
||||
# ensure command
|
||||
if command is None:
|
||||
command = self._command
|
||||
assert isinstance(command, sh.ShellCommand)
|
||||
|
||||
# ensure inventory
|
||||
work_inventory_filename = self._ensure_inventory_filename(
|
||||
inventory_filename)
|
||||
if work_inventory_filename is not None:
|
||||
command += ['-i', work_inventory_filename]
|
||||
|
||||
# ensure playbook file
|
||||
command += [self._ensure_playbook_filename(
|
||||
playbook=playbook,
|
||||
playbook_dirname=playbook_dirname,
|
||||
playbook_filename=playbook_filename)]
|
||||
|
||||
return command
|
||||
|
||||
def run_playbook(self,
|
||||
command: sh.ShellCommand = None,
|
||||
playbook: str = None,
|
||||
playbook_dirname: str = None,
|
||||
playbook_filename: str = None,
|
||||
inventory_filename: str = None):
|
||||
tobiko.setup_fixture(self)
|
||||
command = self._get_command(command=command,
|
||||
playbook=playbook,
|
||||
playbook_dirname=playbook_dirname,
|
||||
playbook_filename=playbook_filename,
|
||||
inventory_filename=inventory_filename)
|
||||
return self.sh_connection.execute(command, current_dir=self.work_dir)
|
||||
|
||||
|
||||
def local_ansible_playbook() -> 'AnsiblePlaybook':
|
||||
return tobiko.get_fixture(AnsiblePlaybook)
|
||||
|
||||
|
||||
def ansible_playbook(ssh_client: ssh.SSHClientType = None,
|
||||
manager: 'AnsiblePlaybookManager' = None) -> \
|
||||
'AnsiblePlaybook':
|
||||
return ansible_playbook_manager(manager).get_ansible_playbook(
|
||||
ssh_client=ssh_client)
|
||||
|
||||
|
||||
def register_ansible_playbook(playbook: 'AnsiblePlaybook',
|
||||
manager: 'AnsiblePlaybookManager' = None) -> \
|
||||
None:
|
||||
tobiko.check_valid_type(playbook, AnsiblePlaybook)
|
||||
ansible_playbook_manager(manager).register_ansible_playbook(playbook)
|
||||
|
||||
|
||||
def ansible_playbook_manager(manager: 'AnsiblePlaybookManager' = None) -> \
|
||||
'AnsiblePlaybookManager':
|
||||
if manager is None:
|
||||
return tobiko.setup_fixture(AnsiblePlaybookManager)
|
||||
else:
|
||||
tobiko.check_valid_type(manager, AnsiblePlaybookManager)
|
||||
return manager
|
||||
|
||||
|
||||
AnsiblePlaybookKey = typing.Optional[ssh.SSHClientFixture]
|
||||
|
||||
|
||||
class AnsiblePlaybookManager(tobiko.SharedFixture):
|
||||
|
||||
def __init__(self):
|
||||
super(AnsiblePlaybookManager, self).__init__()
|
||||
self._host_playbooks: typing.Dict['AnsiblePlaybookKey',
|
||||
'AnsiblePlaybook'] = {}
|
||||
|
||||
def get_ansible_playbook(self,
|
||||
ssh_client: ssh.SSHClientType) -> \
|
||||
'AnsiblePlaybook':
|
||||
ssh_client = ssh.ssh_client_fixture(ssh_client)
|
||||
playbook = self._host_playbooks.get(ssh_client)
|
||||
if playbook is None:
|
||||
playbook = self._init_ansible_playbook(ssh_client=ssh_client)
|
||||
self._host_playbooks[ssh_client] = playbook
|
||||
return playbook
|
||||
|
||||
def register_ansible_playbook(self, playbook: 'AnsiblePlaybook'):
|
||||
ssh_client = ssh.ssh_client_fixture(playbook.ssh_client)
|
||||
self._host_playbooks[ssh_client] = playbook
|
||||
|
||||
@staticmethod
|
||||
def _init_ansible_playbook(ssh_client: ssh.SSHClientFixture = None) \
|
||||
-> 'AnsiblePlaybook':
|
||||
if ssh_client is None:
|
||||
return local_ansible_playbook()
|
||||
else:
|
||||
return tobiko.get_fixture(AnsiblePlaybook(ssh_client=ssh_client))
|
||||
|
||||
|
||||
def run_playbook(command: sh.ShellCommand = None,
|
||||
playbook: str = None,
|
||||
playbook_dirname: str = None,
|
||||
playbook_filename: str = None,
|
||||
inventory_filename: str = None,
|
||||
ssh_client: ssh.SSHClientType = None,
|
||||
manager: AnsiblePlaybookManager = None) \
|
||||
-> sh.ShellExecuteResult:
|
||||
return ansible_playbook(ssh_client=ssh_client,
|
||||
manager=manager).run_playbook(
|
||||
command=command,
|
||||
playbook=playbook,
|
||||
playbook_dirname=playbook_dirname,
|
||||
playbook_filename=playbook_filename,
|
||||
inventory_filename=inventory_filename)
|
|
@ -23,7 +23,6 @@ from tobiko.shell.sh import _execute
|
|||
from tobiko.shell.sh import _hostname
|
||||
from tobiko.shell.sh import _io
|
||||
from tobiko.shell.sh import _local
|
||||
from tobiko.shell.sh import _mktemp
|
||||
from tobiko.shell.sh import _nameservers
|
||||
from tobiko.shell.sh import _path
|
||||
from tobiko.shell.sh import _process
|
||||
|
@ -54,6 +53,8 @@ is_cirros_connection = _connection.is_cirros_connection
|
|||
is_local_connection = _connection.is_local_connection
|
||||
put_file = _connection.put_file
|
||||
get_file = _connection.get_file
|
||||
make_temp_dir = _connection.make_temp_dir
|
||||
make_dirs = _connection.make_dirs
|
||||
|
||||
ShellError = _exception.ShellError
|
||||
ShellCommandFailed = _exception.ShellCommandFailed
|
||||
|
@ -80,8 +81,6 @@ local_process = _local.local_process
|
|||
LocalShellProcessFixture = _local.LocalShellProcessFixture
|
||||
LocalExecutePathFixture = _local.LocalExecutePathFixture
|
||||
|
||||
make_temp_dir = _mktemp.make_temp_dir
|
||||
|
||||
ListNameserversFixture = _nameservers.ListNameserversFixture
|
||||
list_nameservers = _nameservers.list_nameservers
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import tobiko
|
|||
from tobiko.shell.sh import _command
|
||||
from tobiko.shell.sh import _execute
|
||||
from tobiko.shell.sh import _hostname
|
||||
from tobiko.shell.sh import _mktemp
|
||||
from tobiko.shell import ssh
|
||||
|
||||
|
||||
|
@ -72,9 +73,10 @@ def put_file(local_file: str,
|
|||
|
||||
|
||||
def make_temp_dir(ssh_client: ssh.SSHClientType = None,
|
||||
auto_clean=True) -> str:
|
||||
auto_clean=True,
|
||||
sudo: bool = None) -> str:
|
||||
return shell_connection(ssh_client=ssh_client).make_temp_dir(
|
||||
auto_clean=auto_clean)
|
||||
auto_clean=auto_clean, sudo=sudo)
|
||||
|
||||
|
||||
def remove_files(filename: str, *filenames: str,
|
||||
|
@ -83,6 +85,13 @@ def remove_files(filename: str, *filenames: str,
|
|||
filename, *filenames)
|
||||
|
||||
|
||||
def make_dirs(name: str,
|
||||
exist_ok=True,
|
||||
ssh_client: ssh.SSHClientType = None) -> str:
|
||||
return shell_connection(ssh_client=ssh_client).make_dirs(
|
||||
name=name, exist_ok=exist_ok)
|
||||
|
||||
|
||||
def local_shell_connection() -> 'LocalShellConnection':
|
||||
return tobiko.get_fixture(LocalShellConnection)
|
||||
|
||||
|
@ -194,12 +203,15 @@ class ShellConnection(tobiko.SharedFixture):
|
|||
def __str__(self) -> str:
|
||||
return f"{type(self).__name__}<{self.login}>"
|
||||
|
||||
def make_temp_dir(self, auto_clean=True) -> str:
|
||||
def make_temp_dir(self, auto_clean=True, sudo: bool = None) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_files(self, filename: str, *filenames: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def make_dirs(self, name: str, exist_ok=True):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LocalShellConnection(ShellConnection):
|
||||
|
||||
|
@ -229,13 +241,17 @@ class LocalShellConnection(ShellConnection):
|
|||
f"'{local_file}' ...")
|
||||
shutil.copyfile(remote_file, local_file)
|
||||
|
||||
def make_temp_dir(self, auto_clean=True) -> str:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
LOG.debug(f"Local temporary directory created as {self.login}: "
|
||||
f"{temp_dir}")
|
||||
if auto_clean:
|
||||
tobiko.add_cleanup(self.remove_files, temp_dir)
|
||||
return temp_dir
|
||||
def make_temp_dir(self, auto_clean=True, sudo: bool = None) -> str:
|
||||
if sudo:
|
||||
return _mktemp.make_temp_dir(ssh_client=self.ssh_client,
|
||||
sudo=True)
|
||||
else:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
LOG.debug(f"Local temporary directory created as {self.login}: "
|
||||
f"{temp_dir}")
|
||||
if auto_clean:
|
||||
tobiko.add_cleanup(self.remove_files, temp_dir)
|
||||
return temp_dir
|
||||
|
||||
def remove_files(self, filename: str, *filenames: str):
|
||||
filenames = (filename,) + filenames
|
||||
|
@ -244,6 +260,10 @@ class LocalShellConnection(ShellConnection):
|
|||
if os.path.exists(filename):
|
||||
shutil.rmtree(filename)
|
||||
|
||||
def make_dirs(self, name: str, exist_ok=True):
|
||||
os.makedirs(name=name,
|
||||
exist_ok=exist_ok)
|
||||
|
||||
|
||||
class SSHShellConnection(ShellConnection):
|
||||
|
||||
|
@ -298,8 +318,8 @@ class SSHShellConnection(ShellConnection):
|
|||
f"'{local_file}'...")
|
||||
self.sftp_client.get(remote_file, local_file)
|
||||
|
||||
def make_temp_dir(self, auto_clean=True) -> str:
|
||||
temp_dir = self.execute('mktemp -d').stdout.strip()
|
||||
def make_temp_dir(self, auto_clean=True, sudo: bool = None) -> str:
|
||||
temp_dir = self.execute('mktemp -d', sudo=sudo).stdout.strip()
|
||||
LOG.debug(f"Remote temporary directory created as {self.login}: "
|
||||
f"{temp_dir}")
|
||||
if auto_clean:
|
||||
|
@ -311,3 +331,10 @@ class SSHShellConnection(ShellConnection):
|
|||
LOG.debug(f"Remove remote files as {self.login}: {filenames}")
|
||||
command = _command.shell_command('rm -fR') + filenames
|
||||
self.execute(command)
|
||||
|
||||
def make_dirs(self, name: str, exist_ok=True):
|
||||
command = _command.shell_command('mkdir')
|
||||
if exist_ok:
|
||||
command += '-p'
|
||||
command += name
|
||||
self.execute(command)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
|
||||
- hosts: all
|
||||
tasks:
|
||||
- name: 'Ping shiftstack hosts'
|
||||
ping:
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2022 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 os
|
||||
|
||||
import testtools
|
||||
|
||||
from tobiko import tripleo
|
||||
|
||||
|
||||
PLAYBOOK_DIRNAME = os.path.join(os.path.dirname(__file__), 'playbooks')
|
||||
|
||||
|
||||
@tripleo.skip_if_missing_undercloud
|
||||
class OpenShiftTest(testtools.TestCase):
|
||||
|
||||
@tripleo.skip_if_missing_tripleo_ansible_inventory
|
||||
def test_ping_all_hosts(self):
|
||||
tripleo.run_playbook_from_undercloud(
|
||||
playbook='ping-shiftstack',
|
||||
playbook_dirname=PLAYBOOK_DIRNAME)
|
|
@ -13,19 +13,20 @@
|
|||
# under the License.
|
||||
from __future__ import absolute_import
|
||||
|
||||
from tobiko.tripleo import _ansible
|
||||
from tobiko.tripleo import _ansible as ansible
|
||||
from tobiko.tripleo import _overcloud as overcloud
|
||||
from tobiko.tripleo import _topology as topology
|
||||
from tobiko.tripleo import _undercloud as undercloud
|
||||
|
||||
|
||||
get_tripleo_ansible_inventory = _ansible.get_tripleo_ansible_inventory
|
||||
get_tripleo_ansible_inventory = ansible.get_tripleo_ansible_inventory
|
||||
get_tripleo_ansible_inventory_file = \
|
||||
_ansible.get_tripleo_ansible_inventory_file
|
||||
has_tripleo_ansible_inventory = _ansible.has_tripleo_ansible_inventory
|
||||
read_tripleo_ansible_inventory = _ansible.read_tripleo_ansible_inventory
|
||||
ansible.get_tripleo_ansible_inventory_file
|
||||
has_tripleo_ansible_inventory = ansible.has_tripleo_ansible_inventory
|
||||
read_tripleo_ansible_inventory = ansible.read_tripleo_ansible_inventory
|
||||
skip_if_missing_tripleo_ansible_inventory = \
|
||||
_ansible.skip_if_missing_tripleo_ansible_inventory
|
||||
ansible.skip_if_missing_tripleo_ansible_inventory
|
||||
run_playbook_from_undercloud = ansible.run_playbook_from_undercloud
|
||||
|
||||
find_overcloud_node = overcloud.find_overcloud_node
|
||||
list_overcloud_nodes = overcloud.list_overcloud_nodes
|
||||
|
|
|
@ -17,7 +17,9 @@ import io
|
|||
import os
|
||||
|
||||
import tobiko
|
||||
from tobiko.shell import ansible
|
||||
from tobiko.shell import sh
|
||||
from tobiko.shell import ssh
|
||||
from tobiko.tripleo import _undercloud
|
||||
from tobiko.tripleo import _config
|
||||
|
||||
|
@ -81,3 +83,37 @@ def read_tripleo_ansible_inventory():
|
|||
undercloud_rcfile=tripleo.undercloud_rcfile[0],
|
||||
overcloud_ssh_username=tripleo.overcloud_ssh_username)
|
||||
return sh.execute('/bin/bash', stdin=script, ssh_client=ssh_client).stdout
|
||||
|
||||
|
||||
class UndercloudAnsiblePlaybook(ansible.AnsiblePlaybook):
|
||||
|
||||
@property
|
||||
def ssh_client(self) -> ssh.SSHClientType:
|
||||
return _undercloud.undercloud_ssh_client()
|
||||
|
||||
def setup_fixture(self):
|
||||
self._inventory_filename = get_tripleo_ansible_inventory_file()
|
||||
super(UndercloudAnsiblePlaybook, self).setup_fixture()
|
||||
|
||||
|
||||
def undercloud_ansible_playbook() -> UndercloudAnsiblePlaybook:
|
||||
return tobiko.get_fixture(UndercloudAnsiblePlaybook)
|
||||
|
||||
|
||||
def run_playbook_from_undercloud(command: sh.ShellCommand = None,
|
||||
playbook: str = None,
|
||||
playbook_dirname: str = None,
|
||||
playbook_filename: str = None,
|
||||
inventory_filename: str = None):
|
||||
return undercloud_ansible_playbook().run_playbook(
|
||||
command=command,
|
||||
playbook=playbook,
|
||||
playbook_dirname=playbook_dirname,
|
||||
playbook_filename=playbook_filename,
|
||||
inventory_filename=inventory_filename)
|
||||
|
||||
|
||||
def setup_undercloud_ansible_plabook():
|
||||
if _undercloud.has_undercloud():
|
||||
ansible.register_ansible_playbook(
|
||||
undercloud_ansible_playbook())
|
||||
|
|
|
@ -17,6 +17,7 @@ import itertools
|
|||
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
GROUP_NAME = 'tripleo'
|
||||
OPTIONS = [
|
||||
# Undercloud options
|
||||
|
@ -74,7 +75,10 @@ def list_options():
|
|||
|
||||
def setup_tobiko_config(conf):
|
||||
# pylint: disable=unused-argument
|
||||
from tobiko.tripleo import ansible
|
||||
from tobiko.tripleo import overcloud
|
||||
from tobiko.tripleo import topology
|
||||
|
||||
ansible.setup_undercloud_ansible_plabook()
|
||||
overcloud.setup_overcloud_keystone_crederntials()
|
||||
topology.setup_tripleo_topology()
|
||||
|
|
Loading…
Reference in New Issue