Skip to content

Commit

Permalink
Add GUI call functionality to scauto CLI (#147)
Browse files Browse the repository at this point in the history
* Add GUI call functionality to scauto CLI

Add CLI GUI commands to make direct calls to the GUI model library
functions necessary for some manual and remotely executed test steps.

As a part of the change to run GUI functions from the CLI, we also need
to move the initialization of the screen object in __init__ instead of
__enter__.

resolves #146

* GUI: update kb_write to handle upper case chars

kb_write sent the text directly two keyboard.write() which doesn't
handle uppercase characters.   A workaround is to use keyboard.sent()
to send shift+<char> for the uppercase characters and rely on
keyboard.write() to send the rest.

* Controller updates from gpantelakis

Updates patch that fix Controller and cli to better handle setup.

Follow up changes per PR review also added to cli_commands.py.

Add note about where NaturalOrderGroup in cli_commands came from.

* Logging update from gpantelakis

Update to fix logging to work better when multiple scauto commands may
be run.

* flake8:  install krb5-config/gcc
  • Loading branch information
spoore1 authored Jan 3, 2025
1 parent 67004e0 commit 4cd3784
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 90 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/flake8.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ jobs:
with:
python-version: 3

- name: Install krb5-config deps
run: sudo apt-get update -y
&& sudo apt-get upgrade -y
&& sudo apt-get install -y
libkrb5-dev
gcc

- name: Install dependencies
run: python3 -m pip install tox

Expand Down
217 changes: 182 additions & 35 deletions SCAutolib/cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,53 @@
from pathlib import Path
from sys import exit

from collections import OrderedDict

from SCAutolib import logger, exceptions, schema_user
from SCAutolib.controller import Controller
from SCAutolib.enums import ReturnCode


@click.group()
def check_conf_path(conf):
return click.Path(exists=True, resolve_path=True)(conf)


# In Help output, force the subcommand list to match the order
# listed in this file. Solution was found here:
# https://github.com/pallets/click/issues/513#issuecomment-301046782
class NaturalOrderGroup(click.Group):
"""
Command group trying to list subcommands in the order they were added.
Example use::
@click.group(cls=NaturalOrderGroup)
If passing dict of commands from other sources, ensure they are of type
OrderedDict and properly ordered, otherwise order of them will be random
and newly added will come to the end.
"""
def __init__(self, name=None, commands=None, **attrs):
if commands is None:
commands = OrderedDict()
elif not isinstance(commands, OrderedDict):
commands = OrderedDict(commands)
click.Group.__init__(self, name=name,
commands=commands,
**attrs)

def list_commands(self, ctx):
"""
List command names as they are in commands dict.
If the dict is OrderedDict, it will preserve the order commands
were added.
"""
return self.commands.keys()


@click.group(cls=NaturalOrderGroup)
@click.option("--conf", "-c",
default="./conf.json",
type=click.Path(exists=True, resolve_path=True),
show_default=True,
help="Path to JSON configuration file.")
@click.option('--force', "-f", is_flag=True, default=False, show_default=True,
Expand All @@ -29,35 +67,13 @@ def cli(ctx, force, verbose, conf):
logger.setLevel(verbose)
ctx.ensure_object(dict) # Create a dict to store the context
ctx.obj["FORCE"] = force # Store the force option in the context
ctx.obj["CONTROLLER"] = Controller(conf)


@click.command()
@click.option("--ca-type", "-t",
required=False,
default='all',
type=click.Choice(['all', 'local', 'ipa'], case_sensitive=False),
show_default=True,
help="Type of the CA to be configured. If not set, all CA's "
"from the config file would be configured")
@click.pass_context
def setup_ca(ctx, ca_type):
"""
Configure the CA's in the config file. If more than one CA is
specified, specified CA type would be configured.
"""
cnt = ctx.obj["CONTROLLER"]
if ca_type == 'all':
cnt.setup_local_ca(force=ctx.obj["FORCE"])
cnt.setup_ipa_client(force=ctx.obj["FORCE"])
elif ca_type == 'local':
cnt.setup_local_ca(force=ctx.obj["FORCE"])
elif ca_type == 'ipa':
cnt.setup_ipa_client(force=ctx.obj["FORCE"])
exit(ReturnCode.SUCCESS.value)
parsed_conf = None
if ctx.invoked_subcommand != "gui":
parsed_conf = check_conf_path(conf)
ctx.obj["CONTROLLER"] = Controller(parsed_conf)


@click.command()
@cli.command()
@click.option("--gdm", "-g",
required=False,
default=False,
Expand Down Expand Up @@ -85,7 +101,32 @@ def prepare(ctx, gdm, install_missing, graphical):
exit(ReturnCode.SUCCESS.value)


@click.command()
@cli.command()
@click.option("--ca-type", "-t",
required=False,
default='all',
type=click.Choice(['all', 'local', 'ipa'], case_sensitive=False),
show_default=True,
help="Type of the CA to be configured. If not set, all CA's "
"from the config file would be configured")
@click.pass_context
def setup_ca(ctx, ca_type):
"""
Configure the CA's in the config file. If more than one CA is
specified, specified CA type would be configured.
"""
cnt = ctx.obj["CONTROLLER"]
if ca_type == 'all':
cnt.setup_local_ca(force=ctx.obj["FORCE"])
cnt.setup_ipa_client(force=ctx.obj["FORCE"])
elif ca_type == 'local':
cnt.setup_local_ca(force=ctx.obj["FORCE"])
elif ca_type == 'ipa':
cnt.setup_ipa_client(force=ctx.obj["FORCE"])
exit(ReturnCode.SUCCESS.value)


@cli.command()
@click.argument("name",
required=True,
default=None)
Expand Down Expand Up @@ -154,7 +195,7 @@ def setup_user(ctx, name, card_dir, card_type, passwd, pin, user_type):
exit(ReturnCode.SUCCESS.value)


@click.command()
@cli.command()
@click.pass_context
def cleanup(ctx):
"""
Expand All @@ -165,7 +206,113 @@ def cleanup(ctx):
exit(ReturnCode.SUCCESS.value)


cli.add_command(setup_ca)
cli.add_command(prepare)
cli.add_command(setup_user)
cli.add_command(cleanup)
@cli.group(cls=NaturalOrderGroup, chain=True)
@click.option("--install-missing", "-i",
required=False,
default=False,
is_flag=True,
help="Install missing packages")
@click.pass_context
def gui(ctx, install_missing):
""" Run GUI Test commands """
pass


@gui.command()
def init():
""" Initialize GUI for testing """
return "init"


@gui.command()
@click.option("--no",
required=False,
default=False,
is_flag=True,
help="Reverse the action")
@click.argument("name")
def assert_text(name, no):
""" Check if a word is found on the screen """
if no:
return f"assert_no_text:{name}"
return f"assert_text:{name}"


@gui.command()
@click.argument("name")
def click_on(name):
""" Click on object containing word """
return f"click_on:{name}"


@gui.command()
@click.option("--no",
required=False,
default=False,
is_flag=True,
help="Reverse the action")
def check_home_screen(no):
""" Check if screen appears to be the home screen """
if no:
return "check_no_home_screen"
return "check_home_screen"


@gui.command()
@click.argument("keys")
def kb_send(keys):
""" Send key(s) to keyboard """
return f"kb_send:{keys}"


@gui.command()
@click.argument("keys")
def kb_write(keys):
""" Send string to keyboard """
return f"kb_write:{keys}"


@gui.command()
def done():
""" cleanup after testing """
return "done"


@gui.result_callback()
@click.pass_context
def run_all(ctx, actions, install_missing):
""" Run all cli actions in order """
ctx.obj["CONTROLLER"].setup_graphical(install_missing, True)

from SCAutolib.models.gui import GUI
gui = GUI(from_cli=True)
for action in actions:
if "init" in action:
gui.__enter__()
if "assert_text" in action:
assert_text = action.split(":", 1)[1]
gui.assert_text(assert_text)
if "assert_no_text" in action:
assert_text = action.split(":", 1)[1]
gui.assert_no_text(assert_text)
if "click_on" in action:
click_on = action.split(":", 1)[1]
gui.click_on(click_on)
if "check_home_screen" in action:
gui.check_home_screen()
if "check_no_home_screen" in action:
gui.check_home_screen(False)
if "kb_send" in action:
params = action.split(":", 1)[1].split()[0]
gui.kb_send(params)
if "kb_write" in action:
params = action.split(":", 1)[1].split()[0]
gui.kb_write(params)
gui.kb_send('enter')
if "done" in action:
gui.__exit__(None, None, None)
ctx.obj["CONTROLLER"].cleanup()


if __name__ == "__main__":
cli()
77 changes: 46 additions & 31 deletions SCAutolib/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Controller:
def conf_path(self):
return self._lib_conf_path

def __init__(self, config: Union[Path, str], params: {} = None):
def __init__(self, config: Union[Path, str] = None, params: {} = None):
"""
Constructor will parse and check input configuration file. If some
required fields in the configuration are missing, CLI parameters
Expand All @@ -55,15 +55,18 @@ def __init__(self, config: Union[Path, str], params: {} = None):
# Check params

# Parse config file
self._lib_conf_path = config.absolute() if isinstance(config, Path) \
else Path(config).absolute()

with self._lib_conf_path.open("r") as f:
tmp_conf = json.load(f)
if tmp_conf is None:
raise exceptions.SCAutolibException(
"Data are not loaded correctly.")
self.lib_conf = self._validate_configuration(tmp_conf, params)
self.lib_conf = None
if config:
self._lib_conf_path = config.absolute() if isinstance(config, Path) \
else Path(config).absolute()

with self._lib_conf_path.open("r") as f:
tmp_conf = json.load(f)
if tmp_conf is None:
raise exceptions.SCAutolibException(
"Data are not loaded correctly.")
self.lib_conf = self._validate_configuration(tmp_conf, params)

self.users = []
for d in (LIB_DIR, LIB_BACKUP, LIB_DUMP, LIB_DUMP_USERS, LIB_DUMP_CAS,
LIB_DUMP_CARDS, LIB_DUMP_CONFS):
Expand Down Expand Up @@ -147,12 +150,6 @@ def setup_system(self, install_missing: bool, gdm: bool, graphical: bool):

packages = ["opensc", "httpd", "sssd", "sssd-tools", "gnutls-utils",
"openssl", "nss-tools"]
if gdm:
packages.append("gdm")

if graphical:
# ffmpeg-free is in EPEL repo
packages += ["tesseract", "ffmpeg-free"]

# Prepare for virtual cards
if any(c["card_type"] == CardType.virtual
Expand Down Expand Up @@ -181,21 +178,7 @@ def setup_system(self, install_missing: bool, gdm: bool, graphical: bool):
raise exceptions.SCAutolibException(msg)

if graphical:
if not isDistro('fedora'):
run(['dnf', 'groupinstall', 'Server with GUI', '-y',
'--allowerasing'])
run(['pip', 'install', 'python-uinput'])
else:
# Fedora doesn't have server with GUI group so installed gdm
# manually and also python3-uinput should be installed from RPM
run(['dnf', 'install', 'gdm', 'python3-uinput', '-y'])
# disable subscription message
run(['systemctl', '--global', 'mask',
'org.gnome.SettingsDaemon.Subscription.target'])
# disable welcome message
self.dconf_file.create()
self.dconf_file.save()
run('dconf update')
self.setup_graphical(install_missing, gdm)

if not isDistro('fedora'):
run(['dnf', 'groupinstall', "Smart Card Support", '-y',
Expand All @@ -216,6 +199,38 @@ def setup_system(self, install_missing: bool, gdm: bool, graphical: bool):
dump_to_json(user.User(username="root",
password=self.lib_conf["root_passwd"]))

def setup_graphical(self, install_missing: bool, gdm: bool):
packages = ["gcc", "tesseract", "ffmpeg-free"]

if gdm:
packages.append("gdm")

missing = _check_packages(packages)
if install_missing and missing:
_install_packages(missing)
elif missing:
msg = "Can't continue with graphical. Some packages are missing: " \
f"{', '.join(missing)}"
logger.critical(msg)
raise exceptions.SCAutolibException(msg)

if not isDistro('fedora'):
run(['dnf', 'groupinstall', 'Server with GUI', '-y',
'--allowerasing'])
run(['pip', 'install', 'python-uinput'])
else:
# Fedora doesn't have server with GUI group so installed gdm
# manually and also python3-uinput should be installed from RPM
run(['dnf', 'install', 'gdm', 'python3-uinput', '-y'])
# disable subscription message
run(['systemctl', '--global', 'mask',
'org.gnome.SettingsDaemon.Subscription.target'])
# disable welcome message
if not self.dconf_file.exists():
self.dconf_file.create()
self.dconf_file.save()
run('dconf update')

def setup_local_ca(self, force: bool = False):
"""
Setup local CA based on configuration from the configuration file. All
Expand Down
6 changes: 6 additions & 0 deletions SCAutolib/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ def remove(self):
f"Removed file {self._conf_file}."
)

def exists(self):
"""
Checks if a file exists. Returns boolean.
"""
return self._conf_file.exists()

def set(self, key: str, value: Union[int, str, bool], section: str = None,
separator: str = "="):
"""
Expand Down
Loading

0 comments on commit 4cd3784

Please sign in to comment.