From 1f58eb5b7a0f8981930119c03909dd5a9ffbe957 Mon Sep 17 00:00:00 2001 From: Gonzalo Rafuls Date: Thu, 5 Oct 2023 12:27:21 +0200 Subject: [PATCH] tests: added cli test plus additional fixes to BPs coveragerc added omit for BF refactored setup and teardown for reduced boilerplate added cli actions for core services https://github.com/redhat-performance/quads/issues/450 Change-Id: I0ba862227c6277771b0175be0d8ce72e5896c9ce --- .coveragerc | 6 +- .gitignore | 2 + quads/cli/cli.py | 768 +++++++----------- quads/cli/parser.py | 63 ++ quads/quads_api.py | 66 +- quads/server/blueprints/assignments.py | 29 +- quads/server/blueprints/available.py | 6 +- quads/server/blueprints/clouds.py | 13 +- quads/server/blueprints/hosts.py | 4 + quads/server/blueprints/interfaces.py | 41 +- quads/server/blueprints/schedules.py | 13 +- quads/server/dao/assignment.py | 67 +- quads/server/dao/cloud.py | 2 +- quads/server/dao/disk.py | 9 + quads/server/dao/interface.py | 9 + quads/server/dao/memory.py | 1 + quads/server/dao/schedule.py | 35 +- quads/server/models.py | 28 +- quads/tools/create_input_assignments.py | 2 +- quads/tools/external/foreman.py | 2 + quads/tools/external/ssh_helper.py | 6 +- quads/tools/foreman_heal.py | 122 ++- quads/tools/make_instackenv_json.py | 30 +- quads/tools/move_and_rebuild.py | 2 +- quads/tools/notify.py | 128 +-- quads/tools/regenerate_vlans_wiki.py | 16 +- quads/tools/regenerate_wiki.py | 10 +- quads/tools/reports.py | 21 +- quads/tools/simple_table_generator.py | 14 +- quads/tools/validate_env.py | 205 +++-- tests/api/test_clouds.py | 14 +- tests/api/test_interfaces.py | 28 +- tests/api/test_schedules.py | 118 +-- tests/cli/config.py | 80 +- tests/cli/conftest.py | 134 +++ .../cli/fixtures/badhost_metadata_import.yaml | 27 + tests/cli/fixtures/metadata | 48 ++ tests/cli/fixtures/metadata_import.yaml | 27 + tests/cli/test_base.py | 17 +- tests/cli/test_broken.py | 120 ++- tests/cli/test_cloud.py | 305 +++++-- tests/cli/test_disk.py | 62 +- tests/cli/test_host.py | 228 ++++-- tests/cli/test_interface.py | 265 ++++-- tests/cli/test_memory.py | 56 +- tests/cli/test_metadata.py | 118 +++ tests/cli/test_notify.py | 49 ++ tests/cli/test_quads.py | 113 ++- tests/cli/test_regen.py | 47 ++ tests/cli/test_report.py | 59 ++ tests/cli/test_retired.py | 76 +- tests/cli/test_schedule.py | 405 +++++++-- tests/cli/test_summary.py | 78 +- tests/cli/test_validate_env.py | 83 ++ tests/requirements.txt | 7 +- tests/tools/artifacts/cloud99_instackenv.json | 16 - .../tools/artifacts/cloud99_ocpinventory.json | 16 - tests/tools/conftest.py | 140 ++++ tests/tools/fixtures/cloud99_env.json | 18 +- tests/tools/fixtures/markdown.md | 0 tests/tools/test_base.py | 17 +- tests/tools/test_foreman.py | 104 ++- tests/tools/test_foreman_heal.py | 15 + tests/tools/test_instackenv.py | 83 +- tests/tools/test_postman.py | 4 +- tests/tools/test_ssh_helper.py | 109 ++- tests/tools/test_table.py | 27 + tests/tools/test_wiki.py | 41 + tox.ini | 2 +- 69 files changed, 3080 insertions(+), 1796 deletions(-) create mode 100644 tests/cli/conftest.py create mode 100644 tests/cli/fixtures/badhost_metadata_import.yaml create mode 100644 tests/cli/fixtures/metadata create mode 100644 tests/cli/fixtures/metadata_import.yaml create mode 100644 tests/cli/test_metadata.py create mode 100644 tests/cli/test_notify.py create mode 100644 tests/cli/test_regen.py create mode 100644 tests/cli/test_report.py create mode 100644 tests/cli/test_validate_env.py delete mode 100644 tests/tools/artifacts/cloud99_instackenv.json delete mode 100644 tests/tools/artifacts/cloud99_ocpinventory.json create mode 100644 tests/tools/conftest.py create mode 100644 tests/tools/fixtures/markdown.md create mode 100644 tests/tools/test_foreman_heal.py create mode 100644 tests/tools/test_table.py create mode 100644 tests/tools/test_wiki.py diff --git a/.coveragerc b/.coveragerc index 59166afed..2331164b7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,9 @@ source = quads/cli/cli.py [run] -omit = tests/* +omit = + tests/* + quads/tools/external/badfish.py [report] # Regexes for lines to exclude from consideration @@ -24,5 +26,7 @@ exclude_lines = # Don't complain about abstract methods, they aren't run: @(abc\.)?abstractmethod + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) ignore_errors = True diff --git a/.gitignore b/.gitignore index 8af8293cc..c593e0ced 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,5 @@ data_db/ wiki_db/ wordpress_data/ codecov +tests/cli/artifacts/* +tests/tools/artifacts/* diff --git a/quads/cli/cli.py b/quads/cli/cli.py index 823317cf0..3c2ae2d8b 100644 --- a/quads/cli/cli.py +++ b/quads/cli/cli.py @@ -10,7 +10,6 @@ from tempfile import NamedTemporaryFile from typing import Tuple, Optional -import requests import yaml from jinja2 import Template from requests import ConnectionError @@ -18,10 +17,16 @@ from quads.exceptions import CliException, BaseQuadsException from quads.helpers import first_day_month, last_day_month from quads.quads_api import QuadsApi as Quads, APIServerException, APIBadRequest -from quads.server.models import Assignment, Host +from quads.server.models import Assignment from quads.tools import reports from quads.tools.external.jira import Jira, JiraException from quads.tools.move_and_rebuild import move_and_rebuild, switch_config +from quads.tools.make_instackenv_json import main as regen_instack +from quads.tools.simple_table_web import main as regen_heatmap +from quads.tools.regenerate_wiki import main as regen_wiki +from quads.tools.foreman_heal import main as foreman_heal +from quads.tools.notify import main as notify +from quads.tools.validate_env import main as validate_env default_move_command = "/opt/quads/quads/tools/move_and_rebuild.py" @@ -45,13 +50,6 @@ def run(self, action: str, cli_args: dict) -> Optional[int]: self.cli_args = cli_args self.logger.debug(self.cli_args) - if self.cli_args.get("datearg"): - assert "date" not in self.cli_args, "cli arg date already exists?" - - self.cli_args["date"] = datetime.strptime( - self.cli_args.get("datearg"), "%Y-%m-%d %H:%M" - ).isoformat() - if action: # action method should always be available # unless not implemented on this class yet @@ -78,10 +76,10 @@ def run(self, action: str, cli_args: dict) -> Optional[int]: if cloud.name == "cloud01": available = [] for host in hosts: - _date = ":".join(_date.isoformat().split(":")[:-1]) + start_date = ":".join(_date.isoformat().split(":")[:-1]) payload = { - "start": _date, - "end": _date, + "start": start_date, + "end": start_date, } try: if self.quads.is_available(host.name, payload): @@ -93,7 +91,8 @@ def run(self, action: str, cli_args: dict) -> Optional[int]: for host in available: self.logger.info(f" - {host.name}") else: - payload = {"cloud": cloud, "date": _date} + date_str = ":".join(_date.isoformat().split(":")[:-1]) + payload = {"cloud": cloud, "date": date_str} try: schedules = self.quads.get_current_schedules(payload) except (APIServerException, APIBadRequest) as ex: @@ -112,8 +111,8 @@ def clear_field(self, host, key): "memory": self.quads.remove_memory, "processors": self.quads.remove_processor, } - field = host.get(key) - if not field: + field = getattr(host, key) + if field is None: raise CliException("{key} is not a Host property") for obj in field: @@ -168,9 +167,7 @@ def _filter_kwargs(self, filter_args): else: if keys[0].strip().lower() == "model": if str(value).upper() not in conf["models"].split(","): - self.logger.warning( - f"Accepted model names are: {conf['models']}" - ) + self.logger.warning(f"Accepted model names are: {conf['models']}") raise CliException("Model type not recognized.") if type(value) == str: @@ -181,13 +178,9 @@ def _filter_kwargs(self, filter_args): if not op_found: self.logger.warning(f"Condition: {condition}") self.logger.warning(f"Accepted operators: {', '.join(ops.keys())}") - raise CliException( - "A filter was defined but not parsed correctly. Check filter operator." - ) + raise CliException("A filter was defined but not parsed correctly. Check filter operator.") if not kwargs: - raise CliException( - "A filter was defined but not parsed correctly. Check filter syntax." - ) + raise CliException("A filter was defined but not parsed correctly. Check filter syntax.") return kwargs def _output_json_result(self, request, data): @@ -196,9 +189,7 @@ def _output_json_result(self, request, data): self.logger.info("Successfully removed") else: js = request.json() - self.logger.debug( - "%s %s: %s" % (request.status_code, request.reason, data) - ) + self.logger.debug("%s %s: %s" % (request.status_code, request.reason, data)) if request.request.method == "POST" and request.status_code == 200: self.logger.info("Successful request") if js.get("result"): @@ -263,9 +254,7 @@ def action_ccuser(self): def action_interface(self): hostname = self.cli_args.get("host") if not hostname: - raise CliException( - "Missing option. --host option is required for --ls-interface." - ) + raise CliException("Missing option. --host option is required for --ls-interface.") try: self.quads.get_host(hostname) @@ -291,9 +280,7 @@ def action_interface(self): def action_memory(self): hostname = self.cli_args.get("host") if hostname is None: - raise CliException( - "Missing option. --host option is required for --ls-memory." - ) + raise CliException("Missing option. --host option is required for --ls-memory.") try: host = self.quads.get_host(hostname) @@ -310,9 +297,7 @@ def action_memory(self): def action_disks(self): hostname = self.cli_args.get("host") if hostname is None: - raise CliException( - "Missing option. --host option is required for --ls-disks." - ) + raise CliException("Missing option. --host option is required for --ls-disks.") try: host = self.quads.get_host(hostname) @@ -331,9 +316,7 @@ def action_disks(self): def action_processors(self): hostname = self.cli_args.get("host") if not hostname: - raise CliException( - "Missing option. --host option is required for --ls-processors." - ) + raise CliException("Missing option. --host option is required for --ls-processors.") try: host = self.quads.get_host(hostname) @@ -357,7 +340,7 @@ def action_ls_vlan(self): except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) if not _vlans: - raise CliException("No VLANs defined") + raise CliException("No VLANs defined.") for vlan in _vlans: payload = {"vlan.vlan_id": vlan.vlan_id} try: @@ -392,8 +375,6 @@ def action_schedule(self): self.logger.info("Current cloud: %s" % _host.default_cloud.name) else: self.logger.info("Current cloud: %s" % _host.default_cloud.name) - if "date" in _kwargs: - _kwargs.pop("date") try: _host_schedules = self.quads.get_schedules(_kwargs) except (APIServerException, APIBadRequest) as ex: @@ -403,9 +384,7 @@ def action_schedule(self): _cloud_name = schedule.assignment.cloud.name start = ":".join(schedule.start.isoformat().split(":")[:-1]) end = ":".join(schedule.end.isoformat().split(":")[:-1]) - self.logger.info( - f"{schedule.id}| start={start}, end={end}, cloud={_cloud_name}" - ) + self.logger.info(f"{schedule.id}| start={start}, end={end}, cloud={_cloud_name}") else: try: _clouds = self.quads.get_clouds() @@ -414,19 +393,21 @@ def action_schedule(self): for cloud in _clouds: self.logger.info("%s:" % cloud.name) _kwargs["cloud"] = cloud.name - if cloud.name == "cloud01": - if _kwargs.get("date"): + if cloud.name == conf.get("spare_pool_name"): + if self.cli_args.get("datearg"): + _date = datetime.strptime(self.cli_args.get("datearg"), "%Y-%m-%d %H:%M") + _date_iso = ":".join(_date.isoformat().split(":")[:-1]) data = { - "start": _kwargs["date"], - "end": _kwargs["date"], + "start": _date_iso, + "end": _date_iso, } try: - available_hosts = self.quads.filter_available(**data) + available_hosts = self.quads.filter_available(data) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) for host in available_hosts: - self.logger.info(host) + self.logger.info(host.name) else: payload = {"cloud": cloud.name} try: @@ -483,9 +464,7 @@ def action_free_cloud(self): continue else: cloud_reservation_lock = int(conf["cloud_reservation_lock"]) - last_redefined = datetime.strptime( - str(cloud.last_redefined), "%a, %d %b %Y %H:%M:%S %Z" - ) + last_redefined = datetime.strptime(str(cloud.last_redefined), "%a, %d %b %Y %H:%M:%S %Z") lock_release = last_redefined + timedelta(hours=cloud_reservation_lock) cloud_string = f"{cloud.name}" if lock_release > datetime.now(): @@ -499,53 +478,57 @@ def action_free_cloud(self): self.logger.info(cloud_string) def action_available(self): - kwargs = {} - if self.cli_args.get("filter"): - filter_args = self._filter_kwargs(self.cli_args.get("filter")) + host_kwargs = {} + _filter = self.cli_args.get("filter") + _schedstart = self.cli_args.get("schedstart") + _schedend = self.cli_args.get("schedend") + _start = _end = "T".join(":".join(datetime.now().isoformat().split(":")[:-1]).split()) + + if _filter: + filter_args = self._filter_kwargs(_filter) + host_kwargs.update(filter_args) kwargs.update(filter_args) - if self.cli_args.get("schedstart"): - kwargs["start"] = datetime.strptime( - self.cli_args.get("schedstart"), "%Y-%m-%d %H:%M" - ) + if _schedstart: + _start_date = datetime.strptime(_schedstart, "%Y-%m-%d %H:%M") + kwargs["start"] = _start_date + _start = _start_date.isoformat()[:-3] - if self.cli_args.get("schedend"): - kwargs["end"] = datetime.strptime( - self.cli_args.get("schedend"), "%Y-%m-%d %H:%M" - ) + if _schedend: + _end_date = datetime.strptime(_schedend, "%Y-%m-%d %H:%M") + kwargs["end"] = _end_date + _end = _end_date.isoformat()[:-3] available = [] current = [] try: - all_hosts = self.quads.filter_hosts(kwargs) + all_hosts = self.quads.filter_hosts(host_kwargs) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - omit_cloud = "" - if self.cli_args.get("omitcloud"): + omit_cloud_arg = self.cli_args.get("omitcloud") + if omit_cloud_arg: try: - omit_cloud = self.quads.get_cloud(self.cli_args.get("omitcloud")) + omit_cloud = self.quads.get_cloud(omit_cloud_arg) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if not omit_cloud: - raise CliException("Omit cloud not found") for host in all_hosts: data = {"start": _start, "end": _end} # TODO: check return on this below try: - if self.quads.is_available(host["name"], data): - current_schedule = self.quads.get_current_schedules({"host": host}) + if self.quads.is_available(host.name, data): + current_schedule = self.quads.get_current_schedules({"host": host.name}) if current_schedule: if ( host.default_cloud.name == conf["spare_pool_name"] - and current_schedule[0].cloud != omit_cloud + and current_schedule[0].assignment.cloud.name != omit_cloud_arg ): - current.append(host["name"]) + current.append(host.name) else: if host.default_cloud.name == conf["spare_pool_name"]: - available.append(host["name"]) + available.append(host.name) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) @@ -606,14 +589,12 @@ def action_extend(self): end_date = None if not weeks and not date_arg: - raise CliException( - "Missing option. Need --weeks or --date when using --extend" - ) + msg = "Missing option. Need --weeks or --date when using --extend" + raise CliException(msg) if not cloud_name and not host_name: - raise CliException( - "Missing option. At least one of either --host or --cloud is required." - ) + msg = "Missing option. At least one of either --host or --cloud is required." + raise CliException(msg) if weeks: try: @@ -627,34 +608,25 @@ def action_extend(self): try: dispatched_obj = dispatch[dispatch_key](data_dispatch[dispatch_key]) - if not dispatched_obj: - raise CliException(f"{dispatch_key.capitalize()} not found") schedules = self.quads.get_current_schedules(data_dispatch) if not schedules: - self.logger.warning( - f"The selected {dispatch_key} does not have any active schedules" - ) + self.logger.warning(f"The selected {dispatch_key} does not have any active schedules") future_schedules = self.quads.get_future_schedules(data_dispatch) if not future_schedules: return if not self._confirmation_dialog( - "Would you like to extend a future allocation of " - f"{data_dispatch[dispatch_key]}? (y/N): " + "Would you like to extend a future allocation of " f"{data_dispatch[dispatch_key]}? (y/N): " ): return schedules = future_schedules - except (APIServerException, APIBadRequest, CliException) as ex: + except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) non_extendable = [] for schedule in schedules: - end_date = ( - schedule.end + timedelta(weeks=weeks) - if weeks - else datetime.strptime(date_arg, "%Y-%m-%d %H:%M") - ) + end_date = schedule.end + timedelta(weeks=weeks) if weeks else datetime.strptime(date_arg, "%Y-%m-%d %H:%M") data = { "start": ":".join(schedule.end.isoformat().split(":")[:-1]), "end": ":".join(end_date.isoformat().split(":")[:-1]), @@ -723,14 +695,10 @@ def action_shrink(self): end_date = None if not weeks and not now and not date_arg: - raise CliException( - "Missing option. Need --weeks, --date or --now when using --shrink" - ) + raise CliException("Missing option. Need --weeks, --date or --now when using --shrink") if not cloud_name and not host_name: - raise CliException( - "Missing option. At least one of either --host or --cloud is required" - ) + raise CliException("Missing option. At least one of either --host or --cloud is required") if weeks: try: @@ -757,16 +725,13 @@ def action_shrink(self): schedules = self.quads.get_current_schedules(data_dispatch) if not schedules: - self.logger.error( - f"The selected {dispatch_key} does not have any active schedules" - ) + self.logger.error(f"The selected {dispatch_key} does not have any active schedules") future_schedules = self.quads.get_future_schedules(data_dispatch) if not future_schedules: return if not self._confirmation_dialog( - "Would you like to shrink a future allocation of" - f" {data_dispatch[dispatch_key]}? (y/N): " + "Would you like to shrink a future allocation of" f" {data_dispatch[dispatch_key]}? (y/N): " ): return schedules = future_schedules @@ -776,11 +741,7 @@ def action_shrink(self): non_shrinkable = [] for schedule in schedules: end_date = schedule.end - timedelta(weeks=weeks) if weeks else _date - if ( - end_date < schedule.start - or end_date > schedule.end - or (not now and end_date < threshold) - ): + if end_date < schedule.start or end_date > schedule.end or (not now and end_date < threshold): non_shrinkable.append(schedule.host) if non_shrinkable: @@ -793,20 +754,16 @@ def action_shrink(self): return if not check: - confirm_msg = ( - f"for {weeks} week[s]? (y/N): " - if weeks - else f"to {str(_date)[:16]}? (y/N): " - ) + confirm_msg = f"for {weeks} week[s]? (y/N): " if weeks else f"to {str(_date)[:16]}? (y/N): " if not self._confirmation_dialog( - f"Are you sure you want to shrink {data_dispatch[dispatch_key]} " - + confirm_msg + f"Are you sure you want to shrink {data_dispatch[dispatch_key]} " + confirm_msg ): return for schedule in schedules: end_date = schedule.end - timedelta(weeks=weeks) if weeks else _date - self.quads.update_schedule(schedule.id, {"end": end_date}) + end = ":".join(end_date.isoformat().split(":")[:-1]) + self.quads.update_schedule(schedule.id, {"end": end}) if weeks: self.logger.info( @@ -830,12 +787,11 @@ def action_shrink(self): f"{dispatch_key.capitalize()} {data_dispatch[dispatch_key]} can be shrunk to {str(end_date)[:16]}" ) else: - self.logger.info( - f"{dispatch_key.capitalize()} {data_dispatch[dispatch_key]} can be terminated now" - ) + self.logger.info(f"{dispatch_key.capitalize()} {data_dispatch[dispatch_key]} can be terminated now") def action_cloudresource(self): assignment = None + cloud = None data = { "cloud": self.cli_args.get("cloud"), "description": self.cli_args.get("description"), @@ -851,28 +807,24 @@ def action_cloudresource(self): data["vlan"] = int(self.cli_args.get("vlan")) except (TypeError, ValueError) as ཀʖ̯ཀ: self.logger.debug(ཀʖ̯ཀ, exc_info=ཀʖ̯ཀ) - self.logger.error("Could not parse vlan id. Only integers accepted.") - return 1 + raise CliException("Could not parse vlan id. Only integers accepted.") cloud_reservation_lock = int(conf["cloud_reservation_lock"]) try: cloud = self.quads.get_cloud(self.cli_args.get("cloud")) except (APIServerException, APIBadRequest) as ex: - raise CliException(str(ex)) + self.logger.debug(ex, exc_info=ex) if cloud: try: - assignment = self.quads.get_active_cloud_assignment( - self.cli_args.get("cloud") - ) + assignment = self.quads.get_active_cloud_assignment(self.cli_args.get("cloud")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) if assignment: - lock_release = assignment.last_redefined + timedelta( - hours=cloud_reservation_lock - ) - cloud_string = f"{assignment.cloud.name}" + last_redefined = datetime.strptime(str(cloud.last_redefined), "%a, %d %b %Y %H:%M:%S GMT") + lock_release = last_redefined + timedelta(hours=cloud_reservation_lock) + cloud_string = f"{cloud.name}" if lock_release > datetime.now(): time_left = lock_release - datetime.now() hours = time_left.total_seconds() // 3600 @@ -910,9 +862,7 @@ def action_cloudresource(self): self.logger.info("Assignment updated.") except ConnectionError: - raise CliException( - "Could not connect to the quads-server, verify service is up and running." - ) + raise CliException("Could not connect to the quads-server, verify service is up and running.") def action_modcloud(self): data = { @@ -926,9 +876,10 @@ def action_modcloud(self): clean_data = {k: v for k, v in data.items() if v and k != "cloud"} if self.cli_args.get("vlan"): try: - clean_data["vlan"] = int(self.cli_args.get("vlan")) - except (TypeError, ValueError): - clean_data["vlan"] = None + data["vlan"] = int(self.cli_args.get("vlan")) + except (TypeError, ValueError) as ཀʖ̯ཀ: + self.logger.debug(ཀʖ̯ཀ, exc_info=ཀʖ̯ཀ) + raise CliException("Could not parse vlan id. Only integers accepted.") if "wipe" in self.cli_args: clean_data["wipe"] = self.cli_args.get("wipe") @@ -936,22 +887,22 @@ def action_modcloud(self): clean_data["qinq"] = self.cli_args.get("qinq") try: - response = self.quads.get_active_cloud_assignment( - self.cli_args.get("cloud") - ) + assignment = self.quads.get_active_cloud_assignment(self.cli_args.get("cloud")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - assignment = response.json() + + if not assignment: + raise CliException(f"No active cloud assignment for {self.cli_args.get('cloud')}") if self.cli_args.get("cloudticket"): payload = {"ticket": self.cli_args.get("cloudticket")} try: - self.quads.update_assignment(assignment.get("id"), payload) + self.quads.update_assignment(assignment[0].id, payload) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) try: - self.quads.update_assignment(assignment.get("id"), clean_data) + self.quads.update_assignment(assignment[0].id, clean_data) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) @@ -963,10 +914,9 @@ def action_rmcloud(self): raise CliException("Missing parameter --cloud") try: - response = self.quads.get_active_cloud_assignment(cloud) + assignment = self.quads.get_active_cloud_assignment(cloud) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - assignment = response.json() if assignment: raise CliException(f"There is an active cloud assignment for {cloud}") @@ -997,13 +947,10 @@ def action_hostresource(self): "force": self.cli_args.get("force"), } try: - _response = self.quads.create_host(data) + _host = self.quads.create_host(data) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if isinstance(_response, Host): - self.logger.info(f'{self.cli_args.get("hostresource")}') - else: - self.logger.error("Something went wrong.") + self.logger.info(f"{_host.name}") def action_define_host_metadata(self): if not self.cli_args.get("metadata"): @@ -1017,9 +964,7 @@ def action_define_host_metadata(self): hosts_metadata = yaml.safe_load(md) except IOError as ಠ_ಠ: self.logger.debug(ಠ_ಠ, exc_info=ಠ_ಠ) - raise CliException( - f"There was something wrong reading from {self.cli_args['metadata']}" - ) + raise CliException(f"There was something wrong reading from {self.cli_args['metadata']}") for host_md in hosts_metadata: ready_defined = [] @@ -1027,11 +972,6 @@ def action_define_host_metadata(self): host = self.quads.get_host(host_md.get("name")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if not host: - self.logger.warning( - f"Host {host_md.get('name')} not found. Check hostname or if name is defined on the yaml. IGNORING." - ) - continue data = {} dispatch_create = { @@ -1041,7 +981,7 @@ def action_define_host_metadata(self): "processors": self.quads.create_processor, } for key, value in host_md.items(): - if key != "name" and host[key]: + if key != "name" and key != "default_cloud" and getattr(host, key) is not None: ready_defined.append(key) if not self.cli_args.get("force"): continue @@ -1052,28 +992,26 @@ def action_define_host_metadata(self): if dispatch_func: try: - dispatch_func(obj) + dispatch_func(host.name, obj) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) else: - raise CliException( - f"Invalid key '{key}' on metadata for {host.name}" - ) + raise CliException(f"Invalid key '{key}' on metadata for {host.name}") + else: + data[key] = value elif key == "default_cloud": try: cloud = self.quads.get_cloud(value) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - data[key] = cloud - else: - data = {"name": host.name, key: value} + data[key] = cloud.name if ready_defined: action = "SKIPPING" if not self.cli_args.get("force") else "RECREATING" self.logger.warning(f"{host.name} [{action}]: {ready_defined}") if data and len(data.keys()) > 1: - host.update(**data) + self.quads.update_host(host.name, data) if not self.cli_args.get("force"): self.logger.warning("For overwriting existing values use the --force.") @@ -1154,11 +1092,8 @@ def action_host_metadata_export(self): self.logger.info(f"Metadata successfully exported to {temp.name}.") except Exception as ಠ益ಠ: self.logger.debug(ಠ益ಠ, exc_info=ಠ益ಠ) - raise BaseQuadsException( - "There was something wrong writing to file." - ) from ಠ益ಠ + raise BaseQuadsException("There was something wrong writing to file.") from ಠ益ಠ - self.logger.info("Metadata successfully exported.") return 0 def action_add_schedule(self): @@ -1168,46 +1103,29 @@ def action_add_schedule(self): or self.cli_args.get("schedcloud") is None ): raise CliException( - "\n".join( - ( - "Missing option. All of these options are required for --add-schedule:", - "\t--schedule-start", - "\t--schedule-end", - "\t--schedule-cloud", - ) - ) + "Missing option. All of these options are required for --add-schedule:\n" + "\t--schedule-start\n" + "\t--schedule-end\n" + "\t--schedule-cloud" ) if not self.cli_args.get("host") and not self.cli_args.get("host_list"): raise CliException("Missing option. --host or --host-list required.") - omitted_cloud_id = None if self.cli_args.get("omitcloud"): try: - _clouds = self.quads.get_clouds() + omit_cloud = self.quads.get_cloud(self.cli_args.get("omitcloud")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - clouds = json.loads(_clouds) - omitted_cloud = [ - c for c in clouds if c.get("name") == self.cli_args.get("omitcloud") - ] - if len(omitted_cloud) == 0: - self.logger.warning( - f"No cloud named {self.cli_args['omitcloud']} found." - ) - omitted_cloud_id = omitted_cloud[0].get("_id").get("$oid") if self.cli_args.get("host"): - if self.cli_args.get("omitcloud") and omitted_cloud_id: + if self.cli_args.get("omitcloud"): try: - host_obj = self.quads.get_host(self.cli_args.get("host")) + host = self.quads.get_host(self.cli_args.get("host")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - host_json = json.loads(host_obj) - if host_json.get("cloud").get("$oid") == omitted_cloud_id: - self.logger.info( - "Host is in part of the cloud specified with --omit-cloud. Nothing has been done." - ) + if host.cloud.name == self.cli_args.get("omitcloud"): + self.logger.info("Host is in part of the cloud specified with --omit-cloud. Nothing has been done.") else: data = { "cloud": self.cli_args.get("schedcloud"), @@ -1216,21 +1134,10 @@ def action_add_schedule(self): "end": self.cli_args.get("schedend"), } try: - try: - response = self.quads.insert_schedule(data) - except (APIServerException, APIBadRequest) as ex: - raise CliException(str(ex)) - if response.status_code == 200: - self.logger.info("Schedule created") - else: - data = response.json() - self.logger.error( - f"Status code:{data.get('status_code')}, Error: {data.get('message')}" - ) - except ConnectionError: - raise CliException( - "Could not connect to the quads-server, verify service is up and running." - ) + self.quads.insert_schedule(data) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) + self.logger.info("Schedule created") elif self.cli_args.get("host_list"): try: @@ -1241,17 +1148,11 @@ def action_add_schedule(self): host_list = host_list_stream.split() non_available = [] - _sched_start = datetime.strptime( - self.cli_args.get("schedstart"), "%Y-%m-%d %H:%M" - ) - _sched_end = datetime.strptime( - self.cli_args.get("schedend"), "%Y-%m-%d %H:%M" - ) + _sched_start = datetime.strptime(self.cli_args.get("schedstart"), "%Y-%m-%d %H:%M") + _sched_end = datetime.strptime(self.cli_args.get("schedend"), "%Y-%m-%d %H:%M") - if self.cli_args.get("omitcloud") and omitted_cloud_id: - self.logger.info( - f"INFO - All hosts from {self.cli_args['omitcloud']} will be omitted." - ) + if self.cli_args.get("omitcloud"): + self.logger.info(f"INFO - All hosts from {self.cli_args['omitcloud']} will be omitted.") omitted = [] for host in host_list: @@ -1259,8 +1160,7 @@ def action_add_schedule(self): host_obj = self.quads.get_host(host) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - host_json = json.loads(host_obj) - if host_json.get("cloud").get("$oid") == omitted_cloud_id: + if host_obj.cloud.name == self.cli_args.get("omitcloud"): omitted.append(host) for host in omitted: host_list.remove(host) @@ -1282,9 +1182,7 @@ def action_add_schedule(self): raise CliException(str(ex)) if non_available: - self.logger.error( - "The following hosts are either broken or unavailable:" - ) + self.logger.error("The following hosts are either broken or unavailable:") for host in non_available: self.logger.error(host) @@ -1299,19 +1197,12 @@ def action_add_schedule(self): } try: try: - response = self.quads.insert_schedule(data) + self.quads.insert_schedule(data) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if response.status_code == 200: - self.logger.info("Schedule created") - else: - self.logger.error( - "There was something wrong creating the schedule entry" - ) + self.logger.info(f"Schedule created for {host}") except ConnectionError: - raise CliException( - "Could not connect to the quads-server, verify service is up and running." - ) + raise CliException("Could not connect to the quads-server, verify service is up and running.") template_file = "jira_ticket_assignment" with open(os.path.join(conf.TEMPLATES_PATH, template_file)) as _file: @@ -1352,9 +1243,7 @@ def action_add_schedule(self): t_name = transition.get("name") if t_name and t_name.lower() == "scheduled": transition_id = transition.get("id") - transition_result = loop.run_until_complete( - jira.post_transition(_cloud.ticket, transition_id) - ) + transition_result = loop.run_until_complete(jira.post_transition(_cloud.ticket, transition_id)) break if not transition_result: @@ -1375,13 +1264,9 @@ def action_rmschedule(self): def action_modschedule(self): if not self.cli_args.get("schedstart") and not self.cli_args.get("schedend"): raise CliException( - "\n".join( - ( - "Missing option. At least one these options are required for --mod-schedule:", - "\t--schedule-start", - "\t--schedule-end", - ) - ) + "Missing option. At least one these options are required for --mod-schedule:\n" + "\t--schedule-start\n" + "\t--schedule-end" ) mapping = { @@ -1394,11 +1279,7 @@ def action_modschedule(self): value = self.cli_args.get(v) if value: if k in ["start", "end"]: - value = ":".join( - datetime.strptime(value, "%Y-%m-%d %H:%M") - .isoformat() - .split(":")[:-1] - ) + value = ":".join(datetime.strptime(value, "%Y-%m-%d %H:%M").isoformat().split(":")[:-1]) data[k] = value try: self.quads.update_schedule(self.cli_args.get("schedid"), data) @@ -1420,24 +1301,14 @@ def action_addinterface(self): _ifmaintenance = self.cli_args.get("ifmaintenance", False) _force = self.cli_args.get("force", None) _host = self.cli_args.get("host", None) - if ( - _ifmac is None - or _ifname is None - or _ifip is None - or _ifport is None - or _ifport is None - ): + if _ifmac is None or _ifname is None or _ifip is None or _ifport is None or _ifport is None: raise CliException( - "\n".join( - ( - "Missing option. All these options are required for --add-interface:", - "\t--host", - "\t--interface-name", - "\t--interface-mac", - "\t--interface-ip", - "\t--interface-port", - ) - ) + "Missing option. All these options are required for --add-interface:\n" + "\t--host\n" + "\t--interface-name\n" + "\t--interface-mac\n" + "\t--interface-ip\n" + "\t--interface-port" ) default_interface = conf.get("default_pxe_interface") @@ -1468,20 +1339,23 @@ def action_addinterface(self): def action_rminterface(self): if not self.cli_args.get("host") or not self.cli_args.get("ifname"): - self.logger.error( - "Missing option. --host and --interface-name options are required for --rm-interface" - ) - exit(1) - # TODO: handle non existing resource - data = {"host": self.cli_args.get("host"), "name": self.cli_args.get("ifname")} + raise CliException("Missing option. --host and --interface-name options are required for --rm-interface") + + data = { + "hostname": self.cli_args.get("host"), + "if_name": self.cli_args.get("ifname"), + } + try: + host = self.quads.get_host(self.cli_args.get("host")) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) + try: response = self.quads.remove_interface(**data) self.logger.info(response) - except ConnectionError: - self.logger.error( - "Could not connect to the quads-server, verify service is up and running." - ) - return 1 + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) + return 0 def action_modinterface(self): @@ -1508,13 +1382,12 @@ def action_modinterface(self): _host = self.cli_args.get("host", None) # TODO: fix all if _host is None or _ifname is None: - raise CliException( - "Missing option. --host and --interface-name options are required for --mod-interface:" - ) + raise CliException("Missing option. --host and --interface-name options are required for --mod-interface:") - host = self.quads.get_host(_host) - if not host: - raise CliException("Host not found") + try: + host = self.quads.get_host(_host) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) mod_interface = None for interface in host.interfaces: @@ -1527,7 +1400,6 @@ def action_modinterface(self): if ( _ifbiosid is None - and _ifname is None and _ifmac is None and _ifip is None and _ifport is None @@ -1537,25 +1409,19 @@ def action_modinterface(self): and not hasattr(self.cli_args, "ifmaintenance") ): raise CliException( - "\n".join( - ( - "Missing options. At least one of these options are required for --mod-interface:", - "\t--interface-name", - "\t--interface-bios-id", - "\t--interface-mac", - "\t--interface-ip", - "\t--interface-port", - "\t--interface-speed", - "\t--interface-vendor", - "\t--pxe-boot", - "\t--maintenance", - ) - ) + "Missing options. At least one of these options are required for --mod-interface:\n" + "\t--interface-name\n" + "\t--interface-bios-id\n" + "\t--interface-mac\n" + "\t--interface-ip\n" + "\t--interface-port\n" + "\t--interface-speed\n" + "\t--interface-vendor\n" + "\t--pxe-boot\n" + "\t--maintenance" ) - data = { - "id": mod_interface.id, - } + data = {"id": mod_interface.id} for arg, key in fields_map.items(): if arg in self.cli_args and self.cli_args is not None: @@ -1565,32 +1431,30 @@ def action_modinterface(self): self.quads.update_interface(self.cli_args.get("host"), data) self.logger.info("Interface successfully updated") except (APIServerException, APIBadRequest) as ex: - self.logger.debug(str(ex)) self.logger.error("Failed to update interface") - return 1 + raise CliException(str(ex)) return 0 def action_movehosts(self): if self.cli_args.get("datearg") and not self.cli_args.get("dryrun"): - raise CliException( - "--move-hosts and --date are mutually exclusive unless using --dry-run." - ) + raise CliException("--move-hosts and --date are mutually exclusive unless using --dry-run.") - url = os.path.join(conf.API_URL, "moves") date = "" if self.cli_args.get("datearg"): - date = datetime.strptime( - self.cli_args.get("datearg"), "%Y-%m-%d %H:%M" - ).isoformat() - _response = requests.get(os.path.join(url, date)) - if _response.status_code == 200: - data = _response.json() - if not data: - self.logger.info("Nothing to do.") - return 0 + date = datetime.strptime(self.cli_args.get("datearg"), "%Y-%m-%d %H:%M").isoformat() + try: + moves = self.quads.get_moves(date) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) + + if not moves: + self.logger.info("Nothing to do.") + return 0 + + if moves: _clouds = defaultdict(list) - for result in data: + for result in moves: _clouds[result["new"]].append(result) # TODO: @@ -1612,15 +1476,10 @@ def action_movehosts(self): raise CliException(str(ex)) target_assignment = None if data: - target_assignment = Assignment().from_dict( - data=json.loads(data.text) - ) + target_assignment = Assignment().from_dict(data=data[0]) wipe = target_assignment.wipe if target_assignment else False - self.logger.info( - "Moving %s from %s to %s, wipe = %s" - % (host, current, new, wipe) - ) + self.logger.info(f"Moving {host} from {current} to {new}, wipe = {wipe}") if not self.cli_args.get("dryrun"): try: self.quads.update_host( @@ -1634,42 +1493,23 @@ def action_movehosts(self): raise CliException(str(ex)) if new != "cloud01": try: - has_active_schedule = self.quads.get_current_schedules( - {"cloud": f"{cloud.name}"} - ) + has_active_schedule = self.quads.get_current_schedules({"cloud": f"{cloud.name}"}) if has_active_schedule and wipe: - assignment = self.quads.get_active_cloud_assignment( - cloud.name - ) - assignment = Assignment().from_dict( - data=json.loads(assignment.text) - ) - self.quads.update_assignment( - assignment.id, {"validated": False} - ) + assignment = self.quads.get_active_cloud_assignment(cloud.name) + self.quads.update_assignment(assignment[0].id, {"validated": False}) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) try: if self.cli_args.get("movecommand") == default_move_command: - fn = functools.partial( - move_and_rebuild, host, new, semaphore, cloud.wipe - ) + fn = functools.partial(move_and_rebuild, host, new, semaphore, cloud.wipe) tasks.append(fn) omits = conf.get("omit_network_move") omit = False if omits: omits = omits.split(",") - omit = [ - omit - for omit in omits - if omit in host or omit == new - ] + omit = [omit for omit in omits if omit in host or omit == new] if not omit: - switch_tasks.append( - functools.partial( - switch_config, host, current, new - ) - ) + switch_tasks.append(functools.partial(switch_config, host, current, new)) else: if wipe: subprocess.check_call( @@ -1692,42 +1532,29 @@ def action_movehosts(self): ) except Exception as ex: self.logger.debug(ex) - self.logger.exception( - "Move command failed for host: %s" % host - ) + self.logger.exception("Move command failed for host: %s" % host) provisioned = False if not self.cli_args.get("dryrun"): try: _old_cloud_obj = self.quads.get_cloud(results[0]["current"]) - old_cloud_schedule = self.quads.get_current_schedules( - {"cloud": _old_cloud_obj.name} - ) + old_cloud_schedule = self.quads.get_current_schedules({"cloud": _old_cloud_obj.name}) if not old_cloud_schedule and _old_cloud_obj.name != "cloud01": - assignment = self.quads.get_active_cloud_assignment( - _old_cloud_obj.name - ) - assignment = Assignment().from_dict( - data=json.loads(assignment.text) - ) + assignment = self.quads.get_active_cloud_assignment(_old_cloud_obj.name) payload = {"active": False} - self.quads.update_assignment(assignment.id, payload) + self.quads.update_assignment(assignment[0].id, payload) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) done = None loop = asyncio.get_event_loop() loop.set_exception_handler( - lambda _loop, ctx: self.logger.error( - f"Caught exception: {ctx['message']}" - ) + lambda _loop, ctx: self.logger.error(f"Caught exception: {ctx['message']}") ) try: - done = loop.run_until_complete( - asyncio.gather(*[task(loop) for task in tasks]) - ) + done = loop.run_until_complete(asyncio.gather(*[task(loop) for task in tasks])) except ( asyncio.CancelledError, SystemExit, @@ -1743,9 +1570,7 @@ def action_movehosts(self): raise CliException(str(ex)) if not host_obj.switch_config_applied: - self.logger.info( - f"Running switch config for {task.args[0]}" - ) + self.logger.info(f"Running switch config for {task.args[0]}") try: result = task() @@ -1757,15 +1582,11 @@ def action_movehosts(self): if result: try: - self.quads.update_host( - task.args[0], {"switch_config_applied": True} - ) + self.quads.update_host(task.args[0], {"switch_config_applied": True}) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) else: - self.logger.exception( - "There was something wrong configuring the switch." - ) + self.logger.exception("There was something wrong configuring the switch.") if done: for future in done: @@ -1777,12 +1598,8 @@ def action_movehosts(self): if provisioned: try: _new_cloud_obj = self.quads.get_cloud(_cloud) - resp_assignment = self.quads.get_active_cloud_assignment( - _new_cloud_obj.name - ) - assignment = Assignment().from_dict( - data=json.loads(resp_assignment.text) - ) + resp_assignment = self.quads.get_active_cloud_assignment(_new_cloud_obj.name) + assignment = Assignment().from_dict(data=json.loads(resp_assignment.text)) validate = assignment.wipe self.quads.update_assignment( assignment.id, @@ -1801,21 +1618,15 @@ def action_mark_broken(self): host = self.quads.get_host(self.cli_args.get("host")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if host: - if host.broken: - self.logger.warning( - f"Host {self.cli_args['host']} has already been marked broken" - ) - else: - try: - self.quads.update_host(self.cli_args.get("host"), {"broken": True}) - except (APIServerException, APIBadRequest) as ex: - raise CliException(str(ex)) - self.logger.info( - f"Host {self.cli_args['host']} is now marked as broken" - ) + + if host.broken: + self.logger.warning(f"Host {self.cli_args['host']} has already been marked broken") else: - raise CliException(f"Host {self.cli_args['host']} not found") + try: + self.quads.update_host(self.cli_args.get("host"), {"broken": True}) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) + self.logger.info(f"Host {self.cli_args['host']} is now marked as broken") def action_mark_repaired(self): if not self.cli_args.get("host"): @@ -1825,13 +1636,9 @@ def action_mark_repaired(self): host = self.quads.get_host(self.cli_args.get("host")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if not host: - raise CliException("Host not found") if not host.broken: - self.logger.warning( - f"Host {self.cli_args['host']} has already been marked repaired" - ) + self.logger.warning(f"Host {self.cli_args['host']} has already been marked repaired") else: try: self.quads.update_host(self.cli_args.get("host"), {"broken": False}) @@ -1847,13 +1654,9 @@ def action_retire(self): host = self.quads.get_host(self.cli_args.get("host")) except (APIServerException, APIBadRequest) as ex: raise CliException(str(ex)) - if not host: - raise CliException(f"Host {self.cli_args['host']} not found") if host.retired: - self.logger.warning( - f"Host {self.cli_args['host']} has already been marked as retired" - ) + self.logger.warning(f"Host {self.cli_args['host']} has already been marked as retired") else: try: self.quads.update_host(self.cli_args.get("host"), {"retired": True}) @@ -1871,54 +1674,57 @@ def action_unretire(self): raise CliException(str(ex)) if not host.retired: - self.logger.warning( - f"Host {self.cli_args['host']} has already been marked unretired" - ) + self.logger.warning(f"Host {self.cli_args['host']} has already been marked unretired") else: - self.quads.update_host(self.cli_args.get("host"), {"retired": False}) + try: + self.quads.update_host(self.cli_args.get("host"), {"retired": False}) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) self.logger.info(f"Host {self.cli_args['host']} is now marked as unretired") def action_host(self): - host = self.quads.get_host(self.cli_args.get("host")) - if not host: - raise CliException(f"Unknown host: {self.cli_args['host']}") + try: + host = self.quads.get_host(self.cli_args.get("host")) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) _kwargs = {"host": host} if self.cli_args.get("datearg"): - datetime_obj = datetime.strptime( - self.cli_args.get("datearg"), "%Y-%m-%d %H:%M" - ) - _kwargs["date"] = datetime_obj.isoformat() + datetime_obj = datetime.strptime(self.cli_args.get("datearg"), "%Y-%m-%d %H:%M") + datearg_iso = datetime_obj.isoformat() + date_str = ":".join(datearg_iso.split(":")[:-1]) + _kwargs["date"] = date_str else: datetime_obj = datetime.now() - schedules = self.quads.get_current_schedules(**_kwargs) + schedules = self.quads.get_current_schedules(_kwargs) if schedules: for schedule in schedules: if schedule.end != datetime_obj: - self.logger.info(schedule.cloud.name) + self.logger.info(schedule.assignment.cloud.name) else: self.logger.info(host.default_cloud.name) def action_cloudonly(self): - _cloud = self.quads.get_cloud(self.cli_args.get("cloud")) - if not _cloud: - raise CliException("Cloud is not defined.") + try: + _cloud = self.quads.get_cloud(self.cli_args.get("cloud")) + except (APIServerException, APIBadRequest) as ex: + raise CliException(str(ex)) _kwargs = {"cloud": _cloud} if self.cli_args.get("datearg"): - _kwargs["date"] = datetime.strptime( - self.cli_args.get("datearg"), "%Y-%m-%d %H:%M" - ).isoformat() - schedules = self.quads.get_current_schedules(**_kwargs) + _kwargs["date"] = datetime.strptime(self.cli_args.get("datearg"), "%Y-%m-%d %H:%M").isoformat() + schedules = self.quads.get_current_schedules(_kwargs) if schedules: - _kwargs = {"retired": False} + host_kwargs = {"retired": False} if self.cli_args.get("filter"): filter_args = self._filter_kwargs(self.cli_args.get("filter")) - _kwargs.update(filter_args) - _hosts = self.quads.get_hosts() - for schedule in sorted(schedules, key=lambda k: k["host"]["name"]): + host_kwargs.update(filter_args) + _hosts = self.quads.filter_hosts(host_kwargs) + else: + _hosts = self.quads.get_hosts() + for schedule in sorted(schedules, key=lambda k: k.host.name): # TODO: check data properties - if schedule.host.name in _hosts: + if schedule.host.name in [host.name for host in _hosts]: self.logger.info(schedule.host.name) else: if _kwargs.get("date") and self.cli_args.get("cloudonly") == "cloud01": @@ -1931,59 +1737,89 @@ def action_cloudonly(self): available_hosts = self.quads.filter_available(data) except (APIServerException, APIBadRequest) as ex: self.logger.debug(str(ex)) - raise CliException( - "Could not connect to the quads-server, verify service is up and running." - ) + raise CliException("Could not connect to the quads-server, verify service is up and running.") - _kwargs = {} + host_kwargs = {} if self.cli_args.get("filter"): filter_args = self._filter_kwargs(self.cli_args.get("filter")) - _kwargs.update(filter_args) - _hosts = [] - _hosts = self.quads.get_hosts() + host_kwargs.update(filter_args) + _host = self.quads.filter_hosts(host_kwargs) + else: + _hosts = self.quads.get_hosts() for host in sorted(_hosts, key=lambda k: k.name): _hosts.append(host.name) for host in sorted(available_hosts): if host in _hosts: self.logger.info(host) else: - _kwargs = {"cloud": _cloud} + host_kwargs = {"cloud": _cloud} if self.cli_args.get("filter"): filter_args = self._filter_kwargs(self.cli_args.get("filter")) - _kwargs.update(filter_args) - _hosts = self.quads.get_hosts() + host_kwargs.update(filter_args) + _host = self.quads.filter_hosts(host_kwargs) + else: + _hosts = self.quads.get_hosts() for host in sorted(_hosts, key=lambda k: k.name): self.logger.info(host.name) def action_summary(self): _kwargs = {} if self.cli_args.get("datearg"): - _kwargs["date"] = datetime.strptime( - self.cli_args.get("datearg"), "%Y-%m-%d %H:%M" - ).isoformat() + datearg_obj = datetime.strptime(self.cli_args.get("datearg"), "%Y-%m-%d %H:%M") + datearg_iso = datearg_obj.isoformat() + date_str = ":".join(datearg_iso.split(":")[:-1]) + _kwargs["date"] = date_str try: - summary = self.quads.get_summary(**_kwargs) + summary = self.quads.get_summary(_kwargs) except (APIServerException, APIBadRequest) as ex: - self.logger.debug(str(ex)) - raise CliException( - "Could not connect to the quads-server, verify service is up and running." - ) + raise CliException(str(ex)) summary_json = summary.json() for cloud in summary_json: + cloud_name = cloud.get("name") + cloud_count = cloud.get("count") + cloud_description = cloud.get("description") + cloud_owner = cloud.get("owner") + cloud_ticket = cloud.get("ticket") if self.cli_args.get("all") or cloud["count"] > 0: if self.cli_args.get("detail"): self.logger.info( - "%s (%s): %s (%s) - %s" - % ( - cloud["name"], - cloud["owner"], - cloud["count"], - cloud["description"], - cloud["ticket"], - ) + f"{cloud_name} ({cloud_owner}): {cloud_count} ({cloud_description}) - {cloud_ticket}" ) else: - self.logger.info( - "%s: %s (%s)" - % (cloud["name"], cloud["count"], cloud["description"]) - ) + self.logger.info(f"{cloud_name}: {cloud_count} ({cloud_description})") + + def action_regen_instack(self): + regen_instack() + if conf["openstack_management"]: + self.logger.info("Regenerated 'instackenv' for OpenStack Management.") + if conf["openshift_management"]: + self.logger.info("Regenerated 'ocpinventory' for OpenShift Management.") + + def action_regen_heatmap(self): + regen_heatmap() + self.logger.info("Regenerated web table heatmap.") + + def action_regen_wiki(self): + regen_wiki(self.logger) + self.logger.info("Regenerated wiki.") + + def action_foreman_rbac(self): + foreman_heal(self.logger) + self.logger.info("Foreman RBAC repaired.") + + def action_notify(self): + notify(self.logger) + self.logger.info("Notifications sent out.") + + def action_validate_env(self): + _args = { + "cloud": self.cli_args.get("cloud"), + "skip_system": self.cli_args.get("skip_system"), + "skip_network": self.cli_args.get("skip_system"), + "skip_hosts": self.cli_args.get("skip_system"), + } + _loop = asyncio.get_event_loop() + asyncio.set_event_loop(_loop) + + validate_env(_args, _loop, self.logger) + self.logger.info("Quads assignments validation executed.") diff --git a/quads/cli/parser.py b/quads/cli/parser.py index ed627ebe6..faea0e832 100644 --- a/quads/cli/parser.py +++ b/quads/cli/parser.py @@ -636,6 +636,69 @@ default=None, help="Filter search by host metadata", ) +action_group.add_argument( + "--regen-instack", + dest="action", + action="store_const", + const="regen_instack", + help="Regenerate instack JSON", +) +action_group.add_argument( + "--regen-heatmap", + dest="action", + action="store_const", + const="regen_heatmap", + help="Regenerate web table heatmap", +) +action_group.add_argument( + "--regen-wiki", + dest="action", + action="store_const", + const="regen_wiki", + help="Regenerate wiki", +) +action_group.add_argument( + "--foreman-rbac", + dest="action", + action="store_const", + const="foreman_rbac", + help="Regenerate foreman RBAC", +) +action_group.add_argument( + "--notify", + dest="action", + action="store_const", + const="notify", + help="Send notifications for cloud assignments", +) +action_group.add_argument( + "--validate-env", + dest="action", + action="store_const", + const="validate_env", + help="Validate Quads assignments", +) +parser.add_argument( + "--skip-system", + dest="skip_system", + action="store_true", + default=False, + help="Skip system tests, when validating Quads assignments", +) +parser.add_argument( + "--skip-network", + dest="skip_network ", + action="store_true", + default=False, + help="Skip network tests, when validating Quads assignments", +) +parser.add_argument( + "--skip-hosts", + dest="skip_hosts", + action='append', + nargs='*', + help="Skip specific hosts, when validating Quads assignments", +) if __name__ == "__main__": diff --git a/quads/quads_api.py b/quads/quads_api.py index 604d35ca9..d64a3aca6 100644 --- a/quads/quads_api.py +++ b/quads/quads_api.py @@ -12,11 +12,6 @@ from quads.server.models import Host, Cloud, Schedule, Interface, Vlan, Assignment -class MessengerDTO: - def __init__(self, response): - self.__dict__ = response.json() - - class APIServerException(Exception): pass @@ -42,15 +37,11 @@ def __init__(self, config: Config): self.config = config self.base_url = config.API_URL self.session = requests.Session() - self.auth = HTTPBasicAuth( - self.config.get("quads_api_username"), self.config.get("quads_api_password") - ) + self.auth = HTTPBasicAuth(self.config.get("quads_api_username"), self.config.get("quads_api_password")) # Base functions def get(self, endpoint: str) -> Response: - _response = self.session.get( - os.path.join(self.base_url, endpoint), verify=False, auth=self.auth - ) + _response = self.session.get(os.path.join(self.base_url, endpoint), verify=False, auth=self.auth) if _response.status_code == 500: raise APIServerException("Check the flask server logs") if _response.status_code == 400: @@ -90,9 +81,7 @@ def patch(self, endpoint, data) -> Response: return _response def delete(self, endpoint) -> Response: - _response = self.session.delete( - os.path.join(self.base_url, endpoint), verify=False, auth=self.auth - ) + _response = self.session.delete(os.path.join(self.base_url, endpoint), verify=False, auth=self.auth) if _response.status_code == 500: raise APIServerException("Check the flask server logs") if _response.status_code == 400: @@ -133,8 +122,8 @@ def filter_assignments(self, data) -> List[Assignment]: response = self.get(f"assignments?{url_params}") assignments = [] for ass in response.json(): - host_obj = Assignment().from_dict(data=ass) - assignments.append(host_obj) + ass_obj = Assignment().from_dict(data=ass) + assignments.append(ass_obj) return assignments def get_host(self, hostname) -> Optional[Host]: @@ -169,7 +158,7 @@ def get_clouds(self) -> List[Cloud]: clouds = [] for cloud in response.json(): clouds.append(Cloud(**cloud)) - return clouds + return [cloud for cloud in sorted(clouds, key=lambda x: x.name)] def filter_clouds(self, data) -> List[Cloud]: response = self.get("clouds", **data) @@ -246,12 +235,14 @@ def get_available(self) -> List[Host]: return hosts # Available - def get_moves(self) -> List: - response = self.get("moves") - hosts = [] - for host in response.json(): - hosts.append(Host(**host)) - return hosts + def get_moves(self, date=None) -> List: + url = "moves" + if date: + url_params = url_parse.urlencode({"date": date}) + url = f"moves?{url_params}" + response = self.get(url) + data = response.json() + return data def filter_available(self, data) -> List[Host]: response = self.get(f"available?{urlencode(data)}") @@ -268,8 +259,15 @@ def insert_assignment(self, data) -> Response: def update_assignment(self, assignment_id, data) -> Response: return self.patch(os.path.join("assignments", str(assignment_id)), data) - def get_active_cloud_assignment(self, cloud_name) -> Response: - return self.get(os.path.join("assignments/active", cloud_name)) + def get_active_cloud_assignment(self, cloud_name) -> List[Assignment]: + response = self.get(os.path.join("assignments/active", cloud_name)) + data = response.json() + assignments = [] + if data: + ass_object = Assignment().from_dict(data) + assignments.append(ass_object) + + return assignments def get_active_assignments(self) -> List[Assignment]: response = self.get("assignments/active") @@ -301,8 +299,8 @@ def get_interfaces(self) -> Response: def update_interface(self, hostname, data) -> Response: return self.patch(os.path.join("interfaces", hostname), data) - def remove_interface(self, interface_id) -> Response: - return self.delete(os.path.join("interfaces", interface_id)) + def remove_interface(self, hostname, if_name) -> Response: + return self.delete(os.path.join("interfaces", hostname, if_name)) def create_interface(self, hostname, data) -> Response: return self.post(os.path.join("interfaces", hostname), data) @@ -341,15 +339,21 @@ def get_vlans(self) -> List[Vlan]: def get_vlan(self, vlan_id) -> Response: return self.get(os.path.join("vlans", str(vlan_id))) - def update_vlan(self, vlan_id, data) -> Response: + def update_vlan(self, vlan_id, data: dict) -> Response: return self.patch(os.path.join("vlans", str(vlan_id)), data) # Processor - def create_vlan(self, data) -> Response: + def create_vlan(self, data: dict) -> Response: return self.post("vlans", data) - def get_summary(self) -> Response: - return self.get("clouds/summary") + def get_summary(self, data: dict) -> Response: + url_params = url_parse.urlencode(data) + endpoint = os.path.join("clouds", "summary") + url = f"{endpoint}" + if data: + url = f"{endpoint}?{url_params}" + response = self.get(url) + return response def get_version(self) -> Response: return self.get("version") diff --git a/quads/server/blueprints/assignments.py b/quads/server/blueprints/assignments.py index 0c0f73e8e..069252aea 100644 --- a/quads/server/blueprints/assignments.py +++ b/quads/server/blueprints/assignments.py @@ -5,7 +5,6 @@ from quads.server.dao.baseDao import EntryNotFound, InvalidArgument, BaseDao from quads.server.dao.cloud import CloudDao from quads.server.dao.vlan import VlanDao -from quads.server.models import Assignment, Notification, db assignment_bp = Blueprint("assignments", __name__) @@ -49,7 +48,7 @@ def get_assignment(assignment_id: str) -> Response: :param assignment_id: Get the assignment from the database :return: The assignment as a json object """ - _assignment = AssignmentDao.get_assignment(assignment_id) + _assignment = AssignmentDao.get_assignment(int(assignment_id)) if not _assignment: response = { "status_code": 400, @@ -117,7 +116,6 @@ def create_assignment() -> Response: """ data = request.get_json() - notification = Notification() _cloud = None _vlan = None cloud_name = data.get("cloud") @@ -175,19 +173,18 @@ def create_assignment() -> Response: } return make_response(jsonify(response), 400) - _assignment_obj = Assignment( - description=description, - owner=owner, - ticket=ticket, - qinq=qinq, - wipe=wipe, - ccuser=cc_user, - vlan=_vlan, - cloud=_cloud, - notification=notification, - ) - db.session.add(_assignment_obj) - BaseDao.safe_commit() + kwargs = { + "description": description, + "owner": owner, + "ticket": ticket, + "qinq": qinq, + "wipe": wipe, + "ccuser": cc_user, + "cloud": cloud_name, + } + if _vlan: + kwargs["vlan_id"] = int(vlan) + _assignment_obj = AssignmentDao.create_assignment(**kwargs) return jsonify(_assignment_obj.as_dict()) diff --git a/quads/server/blueprints/available.py b/quads/server/blueprints/available.py index f8ad16e19..c8bb55f45 100644 --- a/quads/server/blueprints/available.py +++ b/quads/server/blueprints/available.py @@ -43,9 +43,7 @@ def get_available() -> Response: if ScheduleDao.is_host_available(host.name, _start, _end): if _cloud: _sched_cloud = ScheduleDao.get_current_schedule(host=host) - _sched_cloud = ( - _sched_cloud[0].assignment.cloud.name if _sched_cloud else None - ) + _sched_cloud = _sched_cloud[0].assignment.cloud.name if _sched_cloud else None if _cloud != _sched_cloud: continue available.append(host.name) @@ -69,7 +67,7 @@ def is_available(hostname) -> Response: _params = request.args.to_dict() _start = _end = datetime.now() if _params.get("start"): - _start = datetime.strptime(_params.get("start"), "%Y-%m-%dT%H:%M") + _start = datetime.strptime(_params.get("start"), "%Y-%m-%dT%H:%M") + timedelta(minutes=1) if _params.get("end"): _end = datetime.strptime(_params.get("end"), "%Y-%m-%dT%H:%M") if _start > _end: diff --git a/quads/server/blueprints/clouds.py b/quads/server/blueprints/clouds.py index 9806859dd..906b79b03 100644 --- a/quads/server/blueprints/clouds.py +++ b/quads/server/blueprints/clouds.py @@ -23,7 +23,14 @@ def get_cloud(cloud: str) -> Response: :return: A response object that contains the json representation of the cloud """ _cloud = CloudDao.get_cloud(cloud) - return jsonify(_cloud.as_dict() if _cloud else {}) + if not _cloud: + response = { + "status_code": 400, + "error": "Bad Request", + "message": f"Cloud not found: {cloud}", + } + return make_response(jsonify(response), 400) + return jsonify(_cloud.as_dict()) @cloud_bp.route("/") @@ -136,9 +143,7 @@ def get_summary() -> Response: count = len(hosts) else: date = ( - datetime.strptime(_date, "%Y-%m-%dT%H:%M:%S") - if _date - else datetime.now() + datetime.strptime(_date, "%Y-%m-%dT%H:%M") if _date else datetime.now() ) schedules = ScheduleDao.get_current_schedule(cloud=_cloud, date=date) count = len(schedules) diff --git a/quads/server/blueprints/hosts.py b/quads/server/blueprints/hosts.py index c20669865..800ae1c82 100644 --- a/quads/server/blueprints/hosts.py +++ b/quads/server/blueprints/hosts.py @@ -47,6 +47,7 @@ def update_host(hostname: str) -> Response: cloud_name = data.get("cloud") default_cloud = data.get("default_cloud") host_type = data.get("host_type") + model = data.get("model") broken = data.get("broken") retired = data.get("retired") @@ -87,6 +88,9 @@ def update_host(hostname: str) -> Response: if host_type: _host.host_type = host_type + if model: + _host.model = model.upper() + if isinstance(broken, bool): _host.broken = broken diff --git a/quads/server/blueprints/interfaces.py b/quads/server/blueprints/interfaces.py index a5d0e4641..fbcc12d95 100644 --- a/quads/server/blueprints/interfaces.py +++ b/quads/server/blueprints/interfaces.py @@ -157,9 +157,9 @@ def update_interface(hostname: str) -> Response: return jsonify(interface_obj.as_dict()) -@interface_bp.route("/", methods=["DELETE"]) +@interface_bp.route("//", methods=["DELETE"]) @check_access("admin") -def delete_interface(hostname: str) -> Response: +def delete_interface(hostname: str, if_name: str) -> Response: _host = HostDao.get_host(hostname) if not _host: response = { @@ -169,30 +169,19 @@ def delete_interface(hostname: str) -> Response: } return make_response(jsonify(response), 400) - data = request.get_json() - - interface_id = data.get("id") - if not interface_id: - response = { - "status_code": 400, - "error": "Bad Request", - "message": "Missing argument: id", - } - return make_response(jsonify(response), 400) - - _interface_obj = InterfaceDao.get_interface(interface_id) - if not _interface_obj: - response = { - "status_code": 400, - "error": "Bad Request", - "message": f"Interface not found: {interface_id}", - } - return make_response(jsonify(response), 400) + for interface in _host.interfaces: + if interface.name == if_name: + db.session.delete(interface) + BaseDao.safe_commit() + response = { + "status_code": 200, + "message": "Interface deleted", + } + return jsonify(response) - db.session.delete(_interface_obj) - BaseDao.safe_commit() response = { - "status_code": 200, - "message": "Interface deleted", + "status_code": 400, + "error": "Bad Request", + "message": f"Interface not found: {if_name}", } - return jsonify(response) + return make_response(jsonify(response), 400) diff --git a/quads/server/blueprints/schedules.py b/quads/server/blueprints/schedules.py index 006d12201..22443dbd4 100644 --- a/quads/server/blueprints/schedules.py +++ b/quads/server/blueprints/schedules.py @@ -49,9 +49,16 @@ def get_current_schedule() -> Response: date = data.get("date") hostname = data.get("host") cloud_name = data.get("cloud") - host = HostDao.get_host(hostname) - cloud = CloudDao.get_cloud(cloud_name) - _schedules = ScheduleDao.get_current_schedule(date, host, cloud) + _kwargs = {} + if date: + _kwargs["date"] = datetime.strptime(date, "%Y-%m-%dT%H:%M") + if hostname: + host = HostDao.get_host(hostname) + _kwargs["host"] = host + if cloud_name: + cloud = CloudDao.get_cloud(cloud_name) + _kwargs["cloud"] = cloud + _schedules = ScheduleDao.get_current_schedule(**_kwargs) return jsonify([_schedule.as_dict() for _schedule in _schedules]) diff --git a/quads/server/dao/assignment.py b/quads/server/dao/assignment.py index 185e59129..d682c4416 100644 --- a/quads/server/dao/assignment.py +++ b/quads/server/dao/assignment.py @@ -23,24 +23,27 @@ def create_assignment( qinq: int, wipe: bool, ccuser: List[str], - vlan_id: int, cloud: str, + vlan_id: int = None, ) -> Assignment: - vlan = VlanDao.get_vlan(vlan_id) cloud = CloudDao.get_cloud(cloud) notification = Notification() + kwargs = { + "description": description, + "owner": owner, + "ticket": ticket, + "qinq": qinq, + "wipe": wipe, + "ccuser": ccuser, + "cloud": cloud, + "notification": notification, + } + if vlan_id: + vlan = VlanDao.get_vlan(vlan_id) + if vlan: + kwargs["vlan"] = vlan try: - _assignment_obj = Assignment( - description=description, - owner=owner, - ticket=ticket, - qinq=qinq, - wipe=wipe, - ccuser=ccuser, - vlan=vlan, - cloud=cloud, - notification=notification, - ) + _assignment_obj = Assignment(**kwargs) except Exception as ex: print(ex) db.session.add(_assignment_obj) @@ -97,15 +100,9 @@ def udpate_assignment(cls, assignment_id: int, **kwargs) -> Assignment: @staticmethod def get_assignment(assignment_id: int) -> Assignment: - assignment = ( - db.session.query(Assignment).filter(Assignment.id == assignment_id).first() - ) + assignment = db.session.query(Assignment).filter(Assignment.id == assignment_id).first() if assignment and not assignment.notification: - assignment.notification = ( - db.session.query(Notification) - .filter(Assignment.id == assignment_id) - .first() - ) + assignment.notification = db.session.query(Notification).filter(Assignment.id == assignment_id).first() return assignment @classmethod @@ -123,11 +120,7 @@ def get_assignments() -> List[Assignment]: if assignment: for a in assignment: if not a.notification: - a.notification = ( - db.session.query(Notification) - .filter(Assignment.id == a.id) - .first() - ) + a.notification = db.session.query(Notification).filter(Assignment.id == a.id).first() return assignment @staticmethod @@ -158,7 +151,7 @@ def filter_assignments(data: dict) -> List[Type[Assignment]]: and type(field) != Relationship and type(field.columns[0].type) == Boolean ): - value = value.lower() in ["true", "y", 1, "yes"] + value = str(value).lower() in ["true", "y", 1, "yes"] else: if first_field in ["cloud"]: cloud = CloudDao.get_cloud(value) @@ -173,41 +166,31 @@ def filter_assignments(data: dict) -> List[Type[Assignment]]: ) ) if filter_tuples: - _hosts = AssignmentDao.create_query_select( - Assignment, filters=filter_tuples - ) + _hosts = AssignmentDao.create_query_select(Assignment, filters=filter_tuples) else: _hosts = AssignmentDao.get_assignments() return _hosts @staticmethod def get_all_cloud_assignments(cloud: Cloud) -> List[Assignment]: - assignments = ( - db.session.query(Assignment).filter(Assignment.cloud == cloud).all() - ) + assignments = db.session.query(Assignment).filter(Assignment.cloud == cloud).all() return assignments @staticmethod def get_active_cloud_assignment(cloud: Cloud) -> Assignment: assignment = ( - db.session.query(Assignment) - .filter(and_(Assignment.cloud == cloud, Assignment.active == True)) - .first() + db.session.query(Assignment).filter(and_(Assignment.cloud == cloud, Assignment.active == True)).first() ) return assignment @staticmethod def get_active_assignments() -> List[Assignment]: - assignments = ( - db.session.query(Assignment).filter(Assignment.active == True).all() - ) + assignments = db.session.query(Assignment).filter(Assignment.active == True).all() return assignments @classmethod def delete_assignment(cls, assignment_id: int): - _assignment_obj = ( - db.session.query(Assignment).filter(Assignment.id == assignment_id).first() - ) + _assignment_obj = db.session.query(Assignment).filter(Assignment.id == assignment_id).first() if not _assignment_obj: raise EntryNotFound(f"Could not find assignment with id: {assignment_id}") diff --git a/quads/server/dao/cloud.py b/quads/server/dao/cloud.py index 2299e261a..a14d76315 100644 --- a/quads/server/dao/cloud.py +++ b/quads/server/dao/cloud.py @@ -39,7 +39,7 @@ def get_cloud(name) -> Optional[Cloud]: return cloud @staticmethod - def get_clouds() -> List[Type[Cloud]]: + def get_clouds() -> List[Cloud]: clouds = db.session.query(Cloud).all() return clouds diff --git a/quads/server/dao/disk.py b/quads/server/dao/disk.py index 2c441c6a9..c8a234a8e 100644 --- a/quads/server/dao/disk.py +++ b/quads/server/dao/disk.py @@ -36,3 +36,12 @@ def get_disks() -> List[Disk]: def get_disk(disk_id: int) -> Disk: disk = db.session.query(Disk).filter(Disk.id == disk_id).first() return disk + + @classmethod + def delete_disk(cls, disk_id) -> None: + _disk_obj = cls.get_disk(disk_id) + if not _disk_obj: + raise EntryNotFound + db.session.delete(_disk_obj) + cls.safe_commit() + return diff --git a/quads/server/dao/interface.py b/quads/server/dao/interface.py index 8e5fe3e54..e8d81b43a 100644 --- a/quads/server/dao/interface.py +++ b/quads/server/dao/interface.py @@ -50,3 +50,12 @@ def get_interface(interface_id: int) -> Interface: db.session.query(Interface).filter(Interface.id == interface_id).first() ) return interface + + @classmethod + def delete_interface(cls, interface_id: int) -> None: + _interface = InterfaceDao.get_interface(interface_id) + if not _interface: + raise EntryNotFound + db.session.delete(_interface) + cls.safe_commit() + return diff --git a/quads/server/dao/memory.py b/quads/server/dao/memory.py index 240cad324..d0e0979f4 100644 --- a/quads/server/dao/memory.py +++ b/quads/server/dao/memory.py @@ -26,6 +26,7 @@ def delete_memory(cls, memory_id: int) -> None: if not _memory: raise EntryNotFound db.session.delete(_memory) + cls.safe_commit() return @staticmethod diff --git a/quads/server/dao/schedule.py b/quads/server/dao/schedule.py index bf23f6810..13d4da8a0 100644 --- a/quads/server/dao/schedule.py +++ b/quads/server/dao/schedule.py @@ -11,9 +11,7 @@ class ScheduleDao(BaseDao): @classmethod - def create_schedule( - cls, start: datetime, end: datetime, assignment: Assignment, host: Host - ) -> Schedule: + def create_schedule(cls, start: datetime, end: datetime, assignment: Assignment, host: Host) -> Schedule: _schedule_obj = Schedule(start=start, end=end, assignment=assignment, host=host) db.session.add(_schedule_obj) cls.safe_commit() @@ -99,11 +97,25 @@ def filter_schedules( ) -> List[Type[Schedule]]: query = db.session.query(Schedule) if start: - if not isinstance(start, datetime): + if isinstance(start, str): + try: + start_date = datetime.strptime(start, "%Y-%m-%dT%H:%M") + start = start_date + except ValueError: + raise InvalidArgument( + "start argument must be a datetime object or a correct datetime format string" + ) + elif not isinstance(start, datetime): raise InvalidArgument("start argument must be a datetime object") query = query.filter(Schedule.start >= start) if end: - if not isinstance(end, datetime): + if isinstance(end, str): + try: + end_date = datetime.strptime(end, "%Y-%m-%dT%H:%M") + end = end_date + except ValueError: + raise InvalidArgument("end argument must be a datetime object or a correct datetime format string") + elif not isinstance(end, datetime): raise InvalidArgument("end argument must be a datetime object") query = query.filter(Schedule.end <= end) if host: @@ -119,22 +131,17 @@ def filter_schedules( return filter_schedules @staticmethod - def get_current_schedule( - date: datetime = None, host: Host = None, cloud: Cloud = None - ) -> List[Type[Schedule]]: + def get_current_schedule(date: datetime = None, host: Host = None, cloud: Cloud = None) -> List[Type[Schedule]]: query = db.session.query(Schedule) + if cloud: + query = query.join(Assignment).filter(Assignment.cloud == cloud) if not date: date = datetime.now() query = query.filter(and_(Schedule.start <= date, Schedule.end >= date)) if host: query = query.filter(Schedule.host == host) - # TODO: check assignment cloud schedule relationship - if cloud: - result = query.all() - return [ - schedule for schedule in result if schedule.assignment.cloud == cloud - ] + current_schedule = query.all() return current_schedule diff --git a/quads/server/models.py b/quads/server/models.py index b2293a5eb..84c68208c 100644 --- a/quads/server/models.py +++ b/quads/server/models.py @@ -138,7 +138,10 @@ def from_dict(self, data): obj_attrs = inspect(self).mapper.attrs for i, attr in enumerate(obj_attrs): if attr.key in ["cloud", "default_cloud"]: - value = data.get(attr.key) + if type(data) == dict: + value = data.get(attr.key) + else: + value = getattr(data, attr.key) if value: cloud = Cloud().from_dict(value) setattr(self, attr.key, cloud) @@ -150,19 +153,28 @@ def from_dict(self, data): setattr(self, attr.key, host) continue if attr.key == "vlan": - value = data.get(attr.key) + if type(data) == dict: + value = data.get(attr.key) + else: + value = getattr(data, attr.key) if value: vlan = Vlan().from_dict(value) setattr(self, attr.key, vlan) continue if attr.key == "assignment": - value = data.get(attr.key) + if type(data) == dict: + value = data.get(attr.key) + else: + value = getattr(data, attr.key) if value: assignment = Assignment().from_dict(value) setattr(self, attr.key, assignment) continue if attr.key == "notification": - value = data.get(attr.key) + if type(data) == dict: + value = data.get(attr.key) + else: + value = getattr(data, attr.key) if value: notification = Notification().from_dict(value) setattr(self, attr.key, notification) @@ -203,10 +215,14 @@ def from_dict(self, data): processor_list.append(processor_obj) setattr(self, attr.key, processor_list) continue - value = data.get(attr.key) + if type(data) == dict: + value = data.get(attr.key) + else: + value = getattr(data, attr.key) if value is not None: if type(attr.columns[0].type) == DateTime: - value = datetime.strptime(value, "%a, %d %b %Y %H:%M:%S GMT") + if type(value) != datetime: + value = datetime.strptime(value, "%a, %d %b %Y %H:%M:%S GMT") setattr(self, attr.key, value) return self diff --git a/quads/tools/create_input_assignments.py b/quads/tools/create_input_assignments.py index c99d6ede0..0a61f638f 100755 --- a/quads/tools/create_input_assignments.py +++ b/quads/tools/create_input_assignments.py @@ -286,7 +286,7 @@ def main(): "|".join([re.escape(word) for word in Config["exclude_hosts"].split("|")]) ) - broken_hosts = Host.objects(broken=True) + broken_hosts = HostDao.filter_hosts(broken=False) domain_broken_hosts = [ host for host in broken_hosts if Config["domain"] in host.name ] diff --git a/quads/tools/external/foreman.py b/quads/tools/external/foreman.py index 99d124158..88764a2bb 100755 --- a/quads/tools/external/foreman.py +++ b/quads/tools/external/foreman.py @@ -399,3 +399,5 @@ async def get_user_roles(self, user_id): async def get_user_roles_ids(self, user_id): result = await self.get_user_roles(user_id) return [role["id"] for _, role in result.items()] + + diff --git a/quads/tools/external/ssh_helper.py b/quads/tools/external/ssh_helper.py index 9227624bb..c0126ae32 100755 --- a/quads/tools/external/ssh_helper.py +++ b/quads/tools/external/ssh_helper.py @@ -29,8 +29,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): def connect(self): ssh = SSHClient() config = SSHConfig() - with open(os.path.expanduser("~/.ssh/config")) as _file: - config.parse(_file) + config_path = os.path.expanduser("~/.ssh/config") + if os.path.exists(config_path): + with open(config_path) as _file: + config.parse(_file) host_config = config.lookup(self.host) ssh.set_missing_host_key_policy(AutoAddPolicy()) ssh.load_system_host_keys() diff --git a/quads/tools/foreman_heal.py b/quads/tools/foreman_heal.py index 501c23e03..1841fa5a9 100755 --- a/quads/tools/foreman_heal.py +++ b/quads/tools/foreman_heal.py @@ -3,6 +3,7 @@ from quads.config import Config from quads.server.dao.cloud import CloudDao from quads.server.dao.schedule import ScheduleDao +from quads.server.dao.assignment import AssignmentDao from quads.tools.external.foreman import Foreman import asyncio @@ -15,7 +16,11 @@ logging.basicConfig(level=logging.INFO, format="%(message)s") -def main(): +def main(_logger=None): + global logger + if _logger: + logger = _logger + loop = asyncio.get_event_loop() foreman_admin = Foreman( @@ -25,89 +30,60 @@ def main(): loop=loop, ) - ignore = ["cloud01"] + ignore = [Config["spare_pool_name"]] if Config.foreman_rbac_exclude: ignore.extend(Config.foreman_rbac_exclude.split("|")) clouds = CloudDao.get_clouds() for cloud in clouds: + ass = AssignmentDao.get_active_cloud_assignment(cloud=cloud) + if ass: + infra_pass = f"{Config['infra_location']}@{ass.ticket}" + loop.run_until_complete(foreman_admin.update_user_password(cloud.name, infra_pass)) + + foreman_cloud_user = Foreman( + Config["foreman_api_url"], + cloud.name, + infra_pass, + loop=loop, + ) - infra_pass = f"{Config['infra_location']}@{cloud.ticket}" - loop.run_until_complete( - foreman_admin.update_user_password(cloud.name, infra_pass) - ) - - foreman_cloud_user = Foreman( - Config["foreman_api_url"], - cloud.name, - infra_pass, - loop=loop, - ) + if cloud.name not in ignore: + logger.info(f"Processing {cloud.name}") - if cloud.name not in ignore: - logger.info(f"Processing {cloud.name}") + cloud_hosts = loop.run_until_complete(foreman_cloud_user.get_all_hosts()) - cloud_hosts = loop.run_until_complete(foreman_cloud_user.get_all_hosts()) + user_id = loop.run_until_complete(foreman_admin.get_user_id(cloud.name)) + admin_id = loop.run_until_complete(foreman_admin.get_user_id(Config["foreman_username"])) - user_id = loop.run_until_complete(foreman_admin.get_user_id(cloud.name)) - admin_id = loop.run_until_complete( - foreman_admin.get_user_id(Config["foreman_username"]) - ) + current_schedule = ScheduleDao.get_current_schedule(cloud=cloud) + if current_schedule: - current_schedule = ScheduleDao.get_current_schedule(cloud=cloud) - if current_schedule: - - logger.info(" Current Host Permissions:") - for host, properties in cloud_hosts.items(): - logger.info(f" {host}") - - match = [ - schedule.host.name - for schedule in current_schedule - if schedule.host.name == host - ] - if not match: - _host_id = loop.run_until_complete( - foreman_admin.get_host_id(host) - ) - loop.run_until_complete( - foreman_admin.put_element( - "hosts", _host_id, "owner_id", admin_id - ) - ) - logger.info(f"* Removed permission {host}") - - for schedule in current_schedule: - match = [ - host - for host, _ in cloud_hosts.items() - if host == schedule.host.name - ] - if not match: - # want to run these separately to avoid ServerDisconnect - _host_id = loop.run_until_complete( - foreman_admin.get_host_id(schedule.host.name) - ) - loop.run_until_complete( - foreman_admin.put_element( - "hosts", _host_id, "owner_id", user_id - ) - ) - logger.info(f"* Added permission {schedule.host.name}") - else: - if cloud_hosts: - logger.info(" No active schedule, removing pre-existing roles.") + logger.info(" Current Host Permissions:") for host, properties in cloud_hosts.items(): - _host_id = loop.run_until_complete( - foreman_admin.get_host_id(host) - ) - loop.run_until_complete( - foreman_admin.put_element( - "hosts", _host_id, "owner_id", admin_id - ) - ) - logger.info(f"* Removed permission {host}") + logger.info(f" {host}") + + match = [schedule.host.name for schedule in current_schedule if schedule.host.name == host] + if not match: + _host_id = loop.run_until_complete(foreman_admin.get_host_id(host)) + loop.run_until_complete(foreman_admin.put_element("hosts", _host_id, "owner_id", admin_id)) + logger.info(f"* Removed permission {host}") + + for schedule in current_schedule: + match = [host for host, _ in cloud_hosts.items() if host == schedule.host.name] + if not match: + # want to run these separately to avoid ServerDisconnect + _host_id = loop.run_until_complete(foreman_admin.get_host_id(schedule.host.name)) + loop.run_until_complete(foreman_admin.put_element("hosts", _host_id, "owner_id", user_id)) + logger.info(f"* Added permission {schedule.host.name}") else: - logger.info(" No active schedule nor roles assigned.") + if cloud_hosts: + logger.info(" No active schedule, removing pre-existing roles.") + for host, properties in cloud_hosts.items(): + _host_id = loop.run_until_complete(foreman_admin.get_host_id(host)) + loop.run_until_complete(foreman_admin.put_element("hosts", _host_id, "owner_id", admin_id)) + logger.info(f"* Removed permission {host}") + else: + logger.info(" No active schedule nor roles assigned.") if __name__ == "__main__": diff --git a/quads/tools/make_instackenv_json.py b/quads/tools/make_instackenv_json.py index 47618faf7..d30ffeb4e 100755 --- a/quads/tools/make_instackenv_json.py +++ b/quads/tools/make_instackenv_json.py @@ -17,7 +17,7 @@ from quads.config import Config -def make_env_json(filename): +async def make_env_json(filename): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) foreman = Foreman( @@ -35,10 +35,7 @@ def make_env_json(filename): now = time.time() old_jsons = [file for file in os.listdir(Config["json_web_path"]) if ":" in file] for file in old_jsons: - if ( - os.stat(os.path.join(Config["json_web_path"], file)).st_mtime - < now - Config["json_retention_days"] * 86400 - ): + if os.stat(os.path.join(Config["json_web_path"], file)).st_mtime < now - Config["json_retention_days"] * 86400: os.remove(os.path.join(Config["json_web_path"], file)) for cloud in cloud_list: @@ -53,9 +50,7 @@ def make_env_json(filename): if Config["foreman_unavailable"]: overcloud = {"result": "true"} else: - overcloud = loop.run_until_complete( - foreman.get_host_param(host.name, "overcloud") - ) + overcloud = loop.run_until_complete(foreman.get_host_param(host.name, "overcloud")) if not overcloud: overcloud = {"result": "true"} @@ -72,16 +67,11 @@ def make_env_json(filename): if "result" in overcloud and _overcloud_result: mac = [] if filename == "instackenv": - for interface in sorted(host.interfaces, key=lambda k: k["name"]): + for interface in sorted(host.interfaces, key=lambda k: k.name): if interface.pxe_boot: mac.append(interface.mac_address) if filename == "ocpinventory": - mac = [ - interface.mac_address - for interface in sorted( - host.interfaces, key=lambda k: k["name"] - ) - ] + mac = [interface.mac_address for interface in sorted(host.interfaces, key=lambda k: k.name)] data["nodes"].append( { "name": host.name, @@ -107,21 +97,21 @@ def make_env_json(filename): Config["json_web_path"], "%s_%s.json_%s" % (cloud.name, filename, now.strftime("%Y-%m-%d_%H:%M:%S")), ) - json_file = os.path.join( - Config["json_web_path"], "%s_%s.json" % (cloud.name, filename) - ) + json_file = os.path.join(Config["json_web_path"], "%s_%s.json" % (cloud.name, filename)) with open(new_json_file, "w+") as _json_file: _json_file.seek(0) _json_file.write(content) os.chmod(new_json_file, 0o644) copyfile(new_json_file, json_file) + await foreman.session.close() + def main(): if Config["openstack_management"]: - make_env_json("instackenv") + asyncio.get_event_loop().run_until_complete(make_env_json("instackenv")) if Config["openshift_management"]: - make_env_json("ocpinventory") + asyncio.get_event_loop().run_until_complete(make_env_json("ocpinventory")) if __name__ == "__main__": diff --git a/quads/tools/move_and_rebuild.py b/quads/tools/move_and_rebuild.py index ad50b9f43..d757949e5 100755 --- a/quads/tools/move_and_rebuild.py +++ b/quads/tools/move_and_rebuild.py @@ -329,7 +329,7 @@ async def move_and_rebuild(host, new_cloud, semaphore, rebuild=False, loop=None) "build_start": build_start, "build_end": datetime.now(), } - quads.update_schedule(schedule.id, data) + quads.update_schedule(schedule[0].id, data) logger.debug("Updating host: %s") data = { "cloud": _target_cloud.name, diff --git a/quads/tools/notify.py b/quads/tools/notify.py index 816ecf88a..d67a70da8 100755 --- a/quads/tools/notify.py +++ b/quads/tools/notify.py @@ -8,8 +8,9 @@ from enum import Enum from jinja2 import Template -from pathlib import Path from quads.config import Config +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.baseDao import BaseDao from quads.server.dao.cloud import CloudDao from quads.server.dao.schedule import ScheduleDao from quads.tools.external.netcat import Netcat @@ -61,16 +62,13 @@ async def create_initial_message(real_owner, cloud, cloud_info, ticket, cc): if Config["irc_notify"]: try: async with Netcat(irc_bot_ip, irc_bot_port) as nc: - message = ( - "%s QUADS: %s is now active, choo choo! - %s/assignments/#%s - %s %s" - % ( - irc_bot_channel, - cloud_info, - Config["wp_wiki"], - cloud, - real_owner, - Config["report_cc"], - ) + message = "%s QUADS: %s is now active, choo choo! - %s/assignments/#%s - %s %s" % ( + irc_bot_channel, + cloud_info, + Config["wp_wiki"], + cloud, + real_owner, + Config["report_cc"], ) await nc.write(bytes(message.encode("utf-8"))) except (TypeError, BrokenPipeError) as ex: @@ -79,15 +77,12 @@ async def create_initial_message(real_owner, cloud, cloud_info, ticket, cc): if Config["webhook_notify"]: try: - message = ( - "QUADS: %s is now active, choo choo! - %s/assignments/#%s - %s %s" - % ( - cloud_info, - Config["wp_wiki"], - cloud, - real_owner, - Config["report_cc"], - ) + message = "QUADS: %s is now active, choo choo! - %s/assignments/#%s - %s %s" % ( + cloud_info, + Config["wp_wiki"], + cloud, + real_owner, + Config["report_cc"], ) requests.post( webhook_url, @@ -101,15 +96,16 @@ async def create_initial_message(real_owner, cloud, cloud_info, ticket, cc): def create_message( cloud_obj, + assignment_obj, day, cloud_info, host_list_expire, ): template_file = "message" cloud = cloud_obj.name - real_owner = cloud_obj.owner - ticket = cloud_obj.ticket - cc = cloud_obj.ccuser + real_owner = assignment_obj.owner + ticket = assignment_obj.ticket + cc = assignment_obj.ccuser cc_users = Config["report_cc"].split(",") for user in cc: @@ -136,12 +132,12 @@ def create_message( postman.send_email() -def create_future_initial_message(cloud_obj, cloud_info): +def create_future_initial_message(cloud_obj, assignment_obj, cloud_info): template_file = "future_initial_message" cloud = cloud_obj.name - ticket = cloud_obj.ticket + ticket = assignment_obj.ticket cc_users = Config["report_cc"].split(",") - for user in cloud_obj.ccuser: + for user in assignment_obj.ccuser: cc_users.append("%s@%s" % (user, Config["domain"])) with open(os.path.join(Config.TEMPLATES_PATH, template_file)) as _file: template = Template(_file.read()) @@ -151,7 +147,7 @@ def create_future_initial_message(cloud_obj, cloud_info): ) postman = Postman( "New QUADS Assignment Defined for the Future: %s - %s" % (cloud, ticket), - cloud_obj.owner, + assignment_obj.owner, cc_users, content, ) @@ -160,13 +156,14 @@ def create_future_initial_message(cloud_obj, cloud_info): def create_future_message( cloud_obj, + assignment_obj, future_days, cloud_info, host_list_expire, ): cc_users = Config["report_cc"].split(",") - ticket = cloud_obj.ticket - for user in cloud_obj.ccuser: + ticket = assignment_obj.ticket + for user in assignment_obj.ccuser: cc_users.append("%s@%s" % (user, Config["domain"])) template_file = "future_message" with open(os.path.join(Config.TEMPLATES_PATH, template_file)) as _file: @@ -180,46 +177,44 @@ def create_future_message( ) postman = Postman( "QUADS upcoming assignment notification - %s - %s" % (cloud_obj.name, ticket), - cloud_obj.owner, + assignment_obj.owner, cc_users, content, ) postman.send_email() -def main(): +def main(_logger=None): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - future_days = 7 + + global logger + if _logger: + logger = _logger _all_clouds = CloudDao.get_clouds() - _active_clouds = [ - _cloud - for _cloud in _all_clouds - if len(ScheduleDao.get_current_schedule(cloud=_cloud)) > 0 - ] - _validated_clouds = [_cloud for _cloud in _active_clouds if _cloud.validated] + _assignments = AssignmentDao.filter_assignments({"active": True, "validated": True}) - for cloud in _validated_clouds: - current_hosts = ScheduleDao.get_current_schedule(cloud=cloud) + for ass in _assignments: + current_hosts = ScheduleDao.get_current_schedule(cloud=ass.cloud) cloud_info = "%s: %s (%s)" % ( - cloud.name, + ass.cloud.name, len(current_hosts), - cloud.description, + ass.description, ) - notification_obj = current_hosts[0].assignment.notification - if not notification_obj.initial: + if not ass.notification.initial: logger.info("=============== Initial Message") loop.run_until_complete( create_initial_message( - cloud.owner, - cloud.name, + ass.owner, + ass.cloud.name, cloud_info, - cloud.ticket, - cloud.ccuser, + ass.ticket, + ass.ccuser, ) ) - notification_obj.update(initial=True) + ass.notification.initial = True + BaseDao.safe_commit() for day in Days: future = datetime.now() + timedelta(days=day.value) @@ -229,45 +224,50 @@ def main(): future.day, ) future_hosts = ScheduleDao.get_current_schedule( - cloud=cloud, date=datetime.strptime(future_date, "%Y-%m-%d %H:%M") + cloud=ass.cloud, date=datetime.strptime(future_date, "%Y-%m-%d %H:%M") ) diff = set(current_hosts) - set(future_hosts) if diff and future > current_hosts[0].end: - if not notification_obj[day.name.lower()] and Config["email_notify"]: + if not getattr(ass.notification, day.name.lower()) and Config["email_notify"]: logger.info("=============== Additional Message") host_list = [schedule.host.name for schedule in diff] create_message( - cloud, + ass.cloud, + ass.notification, day.value, cloud_info, host_list, ) - kwargs = {day.name.lower(): True} - notification_obj.update(**kwargs) + setattr(ass.notification, day.name.lower(), True) + BaseDao.safe_commit() break for cloud in _all_clouds: - if cloud.name != "cloud01" and cloud.owner not in ["quads", None]: + ass = AssignmentDao.get_active_cloud_assignment(cloud) + if not ass: + continue + if cloud.name != Config["spare_pool_name"] and ass.owner not in ["quads", None]: current_hosts = ScheduleDao.get_current_schedule(cloud=cloud) - notification_obj = current_hosts[0].assignment.notification cloud_info = "%s: %s (%s)" % ( cloud.name, len(current_hosts), - cloud.description, + ass.description, ) - if not notification_obj.pre_initial and Config["email_notify"]: + if not ass.notification.pre_initial and Config["email_notify"]: logger.info("=============== Future Initial Message") create_future_initial_message( cloud, + ass, cloud_info, ) - notification_obj.update(pre_initial=True) + setattr(ass.notification, "pre_initial", True) + BaseDao.safe_commit() - for day in range(1, future_days + 1): - if not notification_obj.pre and cloud.validated: - future = datetime.now() + timedelta(days=day) + for day in Days: + if not ass.notification.pre and ass.validated: + future = datetime.now() + timedelta(days=day.value) future_date = "%4d-%.2d-%.2d 22:00" % ( future.year, future.month, @@ -285,11 +285,13 @@ def main(): logger.info("=============== Additional Message") create_future_message( cloud, + ass, day, cloud_info, host_list, ) - notification_obj.update(pre=True) + setattr(ass.notification, "pre", True) + BaseDao.safe_commit() break diff --git a/quads/tools/regenerate_vlans_wiki.py b/quads/tools/regenerate_vlans_wiki.py index 50d9fa8fd..e1cff74a0 100755 --- a/quads/tools/regenerate_vlans_wiki.py +++ b/quads/tools/regenerate_vlans_wiki.py @@ -5,6 +5,8 @@ from quads.server.dao.schedule import ScheduleDao from quads.server.dao.vlan import VlanDao +from quads.server.dao.cloud import CloudDao +from quads.server.dao.assignment import AssignmentDao from quads.tools.external.wiki import Wiki from quads.config import Config from tempfile import NamedTemporaryFile @@ -35,17 +37,21 @@ def render_vlans(markdown): lines = [] vlans = VlanDao.get_vlans() for vlan in vlans: - # TODO: filter cloud by vlan - cloud_obj = Cloud.objects(vlan=vlan).first() + assignment_obj = AssignmentDao.filter_assignments({"vlan_id": vlan.vlan_id}) + assignment_obj = assignment_obj[0] if assignment_obj else None + if assignment_obj is None: + continue + cloud_obj = CloudDao.filter_clouds_dict({"id": assignment_obj.cloud_id}) + cloud_obj = cloud_obj[0] if cloud_obj else None vlan_id = vlan.vlan_id ip_range = vlan.ip_range netmask = vlan.netmask gateway = vlan.gateway ip_free = vlan.ip_free cloud_current_count = len(ScheduleDao.get_current_schedule(cloud=cloud_obj)) - if cloud_obj and cloud_current_count > 0: - owner = cloud_obj.owner - ticket = cloud_obj.ticket + if assignment_obj and cloud_current_count > 0 and cloud_obj: + owner = assignment_obj.owner + ticket = assignment_obj.ticket cloud_name = cloud_obj.name else: owner = "nobody" diff --git a/quads/tools/regenerate_wiki.py b/quads/tools/regenerate_wiki.py index f1827bcff..a0c862f6e 100755 --- a/quads/tools/regenerate_wiki.py +++ b/quads/tools/regenerate_wiki.py @@ -21,7 +21,11 @@ logger = logging.getLogger(__name__) -if __name__ == "__main__": + +def main(_logger=None): + global logger + if _logger: + logger = _logger create_input.main() main_md = os.path.join(wp_wiki_git_repo_path, "main.md") @@ -79,3 +83,7 @@ logger.error(ex.errmsg) regenerate_vlans_wiki() + + +if __name__ == "__main__": + main() diff --git a/quads/tools/reports.py b/quads/tools/reports.py index cbe2fde62..210055dcd 100644 --- a/quads/tools/reports.py +++ b/quads/tools/reports.py @@ -38,7 +38,8 @@ def report_available(_logger, _start, _end): schedules = ScheduleDao.get_future_schedules() total = timedelta() for schedule in schedules: - total += schedule.build_end - schedule.build_start + if schedule.build_end and schedule.build_start: + total += schedule.build_end - schedule.build_start if schedules: average_build = total / len(schedules) _logger.info(f"Average build delta: {average_build}") @@ -69,13 +70,17 @@ def report_available(_logger, _start, _end): scheduled_count += 1 two_weeks_availability = ScheduleDao.is_host_available( - hostname=host.name, start=next_sunday, end=next_sunday + timedelta(weeks=2) + hostname=host.name, + start=next_sunday, + end=next_sunday + timedelta(weeks=2), ) if two_weeks_availability: two_weeks_availability_count += 1 four_weeks_availability = ScheduleDao.is_host_available( - hostname=host.name, start=next_sunday, end=next_sunday + timedelta(weeks=4) + hostname=host.name, + start=next_sunday, + end=next_sunday + timedelta(weeks=4), ) if four_weeks_availability: four_weeks_availability_count += 1 @@ -118,7 +123,7 @@ def process_scheduled(_logger, month, now): end = last_day_month(_date) scheduled = len(ScheduleDao.filter_schedules(start, end)) - hosts = len(HostDao.filter_hosts(**{"created_at__gt": start})) + hosts = len(HostDao.filter_hosts(retired=False, broken=False)) days = 0 scheduled_count = 0 @@ -164,14 +169,14 @@ def report_detailed(_logger, _start, _end): for schedule in schedules: if schedule: delta = schedule.end - schedule.start - description = schedule.assignment.cloud.description[: len(headers[3])] + description = schedule.assignment.description[: len(headers[3])] _logger.info( f"{schedule.assignment.owner:<9}| " f"{schedule.assignment.ticket:>9}| " - f"{schedule.assignment.name:>8}| " + f"{schedule.assignment.cloud.name:>8}| " f"{description:>11}| " - f"{schedule.count():>7}| " - f"{str(schedule[0].start)[:10]:>9}| " + f"{len(schedules):>7}| " + f"{str(schedule.start)[:10]:>9}| " f"{delta.days:>8}| " ) diff --git a/quads/tools/simple_table_generator.py b/quads/tools/simple_table_generator.py index 9f648ec5a..49d9904c1 100755 --- a/quads/tools/simple_table_generator.py +++ b/quads/tools/simple_table_generator.py @@ -45,12 +45,14 @@ def generator(_host_file, _days, _month, _year, _gentime): for j in range(1, _days + 1): cell_date = "%s-%.2d-%.2d 01:00" % (_year, _month, j) cell_time = datetime.strptime(cell_date, "%Y-%m-%d %H:%M") - payload = {"host": host, "date": cell_time} + datearg_iso = cell_time.isoformat() + date_str = ":".join(datearg_iso.split(":")[:-1]) + payload = {"host": host, "date": date_str} schedule = None schedules = quads.get_current_schedules(payload) if schedules: schedule = schedules[0] - chosen_color = schedule.cloud.name[5:] + chosen_color = schedule.assignment.cloud.name[5:] else: non_allocated_count += 1 chosen_color = "01" @@ -64,10 +66,10 @@ def generator(_host_file, _days, _month, _year, _gentime): } if schedule: - cloud = schedule.assignment.cloud - _day["display_description"] = cloud.description - _day["display_owner"] = cloud.owner - _day["display_ticket"] = cloud.ticket + assignment = schedule.assignment + _day["display_description"] = assignment.description + _day["display_owner"] = assignment.owner + _day["display_ticket"] = assignment.ticket __days.append(_day) line["days"] = __days diff --git a/quads/tools/validate_env.py b/quads/tools/validate_env.py index 216c0d27e..ca2cb8afd 100755 --- a/quads/tools/validate_env.py +++ b/quads/tools/validate_env.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import argparse import asyncio +import json import logging import os import re @@ -12,8 +13,11 @@ from paramiko.ssh_exception import NoValidConnectionsError from quads.config import Config +from quads.exceptions import CliException from quads.helpers import is_supported -from quads.quads_api import QuadsApi +from quads.quads_api import QuadsApi, APIServerException, APIBadRequest +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.baseDao import BaseDao from quads.server.models import Assignment from quads.tools.external.badfish import BadfishException, badfish_factory from quads.tools.external.foreman import Foreman @@ -28,18 +32,15 @@ class Validator(object): - def __init__(self, cloud, _args, _loop=None): + def __init__(self, cloud, assignment, _args, _loop=None): self.cloud = cloud + self.assignment = assignment self.report = "" self.args = _args - self.hosts = quads.filter_hosts({"cloud": self.cloud.name, "validated": False}) - self.hosts = [ - host - for host in self.hosts - if quads.get_current_schedules({"host": host.name}) - ] - if self.args.skip_hosts: - self.hosts = [host for host in self.hosts if host.name not in self.args.skip_hosts] + self.hosts = quads.filter_hosts({"cloud": self.cloud, "validated": False}) + self.hosts = [host for host in self.hosts if quads.get_current_schedules({"host": host.name})] + if self.args.get("skip_hosts"): + self.hosts = [host for host in self.hosts if host.name not in self.args.get("skip_hosts")] self.loop = _loop if _loop else get_running_loop() def notify_failure(self): @@ -47,16 +48,14 @@ def notify_failure(self): with open(os.path.join(Config.TEMPLATES_PATH, template_file)) as _file: template = Template(_file.read()) parameters = { - "cloud": self.cloud.name, - "owner": self.cloud.owner, - "ticket": self.cloud.ticket, + "cloud": self.cloud, + "owner": self.assignment.owner, + "ticket": self.assignment.ticket, "report": self.report, } content = template.render(**parameters) - subject = "Validation check failed for {cloud} / {owner} / {ticket}".format( - **parameters - ) + subject = "Validation check failed for {cloud} / {owner} / {ticket}".format(**parameters) _cc_users = Config["report_cc"].split(",") postman = Postman(subject, "dev-null", _cc_users, content) postman.send_email() @@ -66,15 +65,13 @@ def notify_success(self): with open(os.path.join(Config.TEMPLATES_PATH, template_file)) as _file: template = Template(_file.read()) parameters = { - "cloud": self.cloud.name, - "owner": self.cloud.owner, - "ticket": self.cloud.ticket, + "cloud": self.cloud, + "owner": self.assignment.owner, + "ticket": self.assignment.ticket, } content = template.render(**parameters) - subject = "Validation check succeeded for {cloud} / {owner} / {ticket}".format( - **parameters - ) + subject = "Validation check succeeded for {cloud} / {owner} / {ticket}".format(**parameters) _cc_users = Config["report_cc"].split(",") postman = Postman(subject, "dev-null", _cc_users, content) postman.send_email() @@ -82,61 +79,50 @@ def notify_success(self): def env_allocation_time_exceeded(self): now = datetime.now() data = { - "cloud": self.cloud.name, + "cloud": self.cloud, } - # TODO: Check return from get below schedules = quads.get_current_schedules(data) if schedules: time_delta = now - schedules[0].start - if time_delta.seconds // 60 > Config["validation_grace_period"]: + if time_delta.total_seconds() // 60 > Config["validation_grace_period"]: return True logger.warning( - "You're still within the configurable validation grace period. Skipping validation for %s." - % self.cloud.name + "You're still within the configurable validation grace period. Skipping validation for %s." % self.cloud ) return False async def post_system_test(self): - password = f"{Config['infra_location']}@{self.cloud.ticket}" + password = f"{Config['infra_location']}@{self.assignment.ticket}" foreman = Foreman( Config["foreman_api_url"], - self.cloud.name, + self.cloud, password, loop=self.loop, ) valid_creds = await foreman.verify_credentials() if not valid_creds: - logger.error("Unable to query Foreman for cloud: %s" % self.cloud.name) + logger.error("Unable to query Foreman for cloud: %s" % self.cloud) logger.error("Verify Foreman password is correct: %s" % password) - self.report = ( - self.report - + "Unable to query Foreman for cloud: %s\n" % self.cloud.name - ) - self.report = ( - self.report + "Verify Foreman password is correct: %s\n" % password - ) + self.report = self.report + "Unable to query Foreman for cloud: %s\n" % self.cloud + self.report = self.report + "Verify Foreman password is correct: %s\n" % password return False build_hosts = await foreman.get_build_hosts() pending = [] - data = {"cloud": self.cloud.name} + data = {"cloud": self.cloud} schedules = quads.get_current_schedules(data) if schedules: for schedule in schedules: if schedule.host and schedule.host.name in build_hosts: pending.append(schedule.host.name) - pending = [host for host in pending if host not in self.args.skip_hosts] + pending = [host for host in pending if host not in self.args.get("skip_hosts")] if pending: - logger.info( - "The following hosts are marked for build and will now be rebooted:" - ) - self.report = ( - self.report + "The following hosts are marked for build:\n" - ) + logger.info("The following hosts are marked for build and will now be rebooted:") + self.report = self.report + "The following hosts are marked for build:\n" for host in pending: logger.info(host) try: @@ -176,9 +162,7 @@ async def post_system_test(self): ) await badfish.reboot_server() else: - logger.error( - f"Could not initiate Badfish instance for: {host}" - ) + logger.error(f"Could not initiate Badfish instance for: {host}") self.report = self.report + "%s\n" % host return False @@ -210,15 +194,16 @@ async def post_network_test(self): for host in self.hosts: if not host.switch_config_applied: data = {"host": host.name, "cloud": host.cloud.name} - current_schedule = quads.get_current_schedules(data) + current_schedule = quads.get_current_schedules(data)[0] previous_cloud = host.default_cloud.name - data = {'host': host, 'end': current_schedule.start} + data = {"host": host.name, "end": current_schedule.start.strftime("%Y-%m-%dT%H:%M")} previous_schedule = quads.get_schedules(data=data) if previous_schedule: - previous_cloud = previous_schedule.cloud.name + previous_cloud = previous_schedule[0].cloud.name result = switch_config(host.name, previous_cloud, host.cloud.name) if result: - host.update(switch_config_applied=True) + setattr(host, "switch_config_applied", True) + BaseDao.safe_commit() else: switch_config_missing.append(host.name) try: @@ -232,9 +217,7 @@ async def post_network_test(self): test_host = host if hosts_down: - logger.error( - "The following hosts appear to be down or with no ssh connection:" - ) + logger.error("The following hosts appear to be down or with no ssh connection:") for i in hosts_down: logger.error(i) return False @@ -248,15 +231,15 @@ async def post_network_test(self): failed_ssh = False try: ssh_helper = SSHHelper(test_host.name) - except (SSHHelperException, SSHException, NoValidConnectionsError, socket.timeout) as ex: + except ( + SSHHelperException, + SSHException, + NoValidConnectionsError, + socket.timeout, + ) as ex: logger.error(str(ex)) - logger.error( - "Could not establish connection with host: %s." % test_host.name - ) - self.report = ( - self.report - + "Could not establish connection with host: %s.\n" % test_host.name - ) + logger.error("Could not establish connection with host: %s." % test_host.name) + self.report = self.report + "Could not establish connection with host: %s.\n" % test_host.name failed_ssh = True if failed_ssh: @@ -264,9 +247,7 @@ async def post_network_test(self): host_list = " ".join([host.name for host in self.hosts]) - result, output = ssh_helper.run_cmd( - f"fping -t {Config.FPING_TIMEOUT} -B 1 -u {host_list}" - ) + result, output = ssh_helper.run_cmd(f"fping -t {Config.FPING_TIMEOUT} -B 1 -u {host_list}") if not result: return False @@ -281,7 +262,7 @@ async def post_network_test(self): _host_obj = host["host"] _interfaces = Config.INTERFACES[interface] last_nic = i == len(_host_obj.interfaces) - 1 - if last_nic and self.cloud.vlan: + if last_nic and self.assignment.vlan: continue for value in _interfaces: ip_apart = host["ip"].split(".") @@ -292,9 +273,7 @@ async def post_network_test(self): if new_ips: all_ips = " ".join(new_ips) - result, output = ssh_helper.run_cmd( - f"fping -t {Config.FPING_TIMEOUT} -B 1 -u {all_ips}" - ) + result, output = ssh_helper.run_cmd(f"fping -t {Config.FPING_TIMEOUT} -B 1 -u {all_ips}") if not result: pattern = re.compile(r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") hosts = [] @@ -313,19 +292,19 @@ async def post_network_test(self): return True async def validate_env(self): - logger.info(f"Validating {self.cloud.name}") + logger.info(f"Validating {self.cloud}") failed = False - assignment_json = quads.get_active_cloud_assignment(self.cloud.name) - assignment = Assignment().from_dict(assignment_json) + assignment = quads.get_active_cloud_assignment(self.cloud) + assignment = assignment[0] if assignment else None if self.env_allocation_time_exceeded(): if self.hosts: - if not self.args.skip_system: + if not self.args.get("skip_system"): result_pst = await self.post_system_test() if not result_pst: failed = True - if not self.args.skip_network: + if not self.args.get("skip_network"): result_pnt = await self.post_network_test() if not failed and not result_pnt: failed = True @@ -337,56 +316,58 @@ async def validate_env(self): if not failed: if not assignment.notification.success: self.notify_success() - # TODO: fix update - assignment.notification.success = True - assignment.notification.fail = False - assignment.save() + setattr(assignment.notification, "success", True) + setattr(assignment.notification, "fail", False) + BaseDao.safe_commit() for host in self.hosts: - # TODO: fix update - host.validated = True - host.save() - # TODO: fix update - self.cloud.validated = True + setattr(host, "validated", True) + BaseDao.safe_commit() + setattr(self.assignment, "validated", True) + BaseDao.safe_commit() if failed and not assignment.notification.fail: self.notify_failure() - # TODO: fix update - assignment.notification.fail = True - assignment.save() + setattr(assignment.notification, "fail", True) + BaseDao.safe_commit() return -def main(_args, _loop): - _filter = {"validated": False, "provisioned": True, "name__ne": "cloud01"} - # TODO: verify name__ne - clouds = quads.filter_clouds(_filter) +def main(_args, _loop, _logger=None): + global logger + if _logger: + logger = _logger + + _filter = {"validated": False, "provisioned": True, "cloud__ne": "cloud01"} + assignments = quads.filter_assignments(_filter) - if _args.cloud: - clouds = [cloud for cloud in clouds if cloud.name == _args.cloud] - if len(clouds) == 0: - logger.error("No cloud with this name.") + if _args.get("cloud"): + try: + cloud = quads.get_cloud(_args.get("cloud")) + except (APIServerException, APIBadRequest) as ex: + raise CliException(ex) - if _args.skip_hosts: + if _args.get("skip_hosts"): hosts = [] - for hostname in _args.skip_hosts: - host = quads.get_host(hostname) - if not host: - logger.error(f"Host not found: {hostname}") - else: - hosts.append(host) - - for _cloud in clouds: - _schedules = quads.get_current_schedules(data={'cloud': _cloud}) + for hostname in _args.get("skip_hosts"): + try: + host = quads.get_host(hostname) + except (APIServerException, APIBadRequest) as ex: + raise CliException(ex) + hosts.append(host) + + for ass in assignments: + _schedules = quads.get_current_schedules(data={"cloud": ass.cloud.name}) _schedule_count = len(_schedules) - if _schedule_count and _cloud.wipe: - validator = Validator(_cloud, _args, _loop=_loop) + _assignment = AssignmentDao.get_active_cloud_assignment(ass.cloud) + if _schedule_count and _assignment.wipe: + validator = Validator(ass.cloud.name, _assignment, _args, _loop=_loop) try: _loop.run_until_complete(validator.validate_env()) except Exception as ex: logger.debug(ex) - logger.info("Failed validation for %s" % _cloud.name) + logger.info("Failed validation for %s" % ass.cloud.name) if __name__ == "__main__": @@ -405,8 +386,8 @@ def main(_args, _loop): ) parser.add_argument( "--skip-hosts", - action='append', - nargs='*', + action="append", + nargs="*", help="Skip specific hosts.", ) parser.add_argument( @@ -415,9 +396,7 @@ def main(_args, _loop): default=False, help="Show debugging information.", ) - parser.add_argument( - "--cloud", default="", help="Run validation only on specified cloud." - ) + parser.add_argument("--cloud", default="", help="Run validation only on specified cloud.") args = parser.parse_args() level = logging.INFO diff --git a/tests/api/test_clouds.py b/tests/api/test_clouds.py index 8346d15cc..c5161df76 100644 --- a/tests/api/test_clouds.py +++ b/tests/api/test_clouds.py @@ -119,7 +119,9 @@ def test_invalid_not_found_single(self, test_client, auth): response = unwrap_json( test_client.get(f"/api/v3/clouds/{cloud_name}", headers=auth_header) ) - assert response.json == {} + assert response.status_code == 400 + assert response.json["error"] == "Bad Request" + assert response.json["message"] == "Cloud not found: cloud11" def test_invalid_filter(self, test_client, auth): """ @@ -129,10 +131,7 @@ def test_invalid_filter(self, test_client, auth): """ auth_header = auth.get_auth_header() response = unwrap_json( - test_client.get( - "/api/v3/clouds?NOT_A_FIELD=invalid", - headers=auth_header - ) + test_client.get("/api/v3/clouds?NOT_A_FIELD=invalid", headers=auth_header) ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" @@ -146,10 +145,7 @@ def test_valid_filter(self, test_client, auth): """ auth_header = auth.get_auth_header() response = unwrap_json( - test_client.get( - "/api/v3/clouds?id=1", - headers=auth_header - ) + test_client.get("/api/v3/clouds?id=1", headers=auth_header) ) assert response.status_code == 200 assert len(response.json) == 1 diff --git a/tests/api/test_interfaces.py b/tests/api/test_interfaces.py index 8bb30f3a6..a8e75bad5 100644 --- a/tests/api/test_interfaces.py +++ b/tests/api/test_interfaces.py @@ -266,8 +266,7 @@ def test_invalid_host(self, test_client, auth, prefill): host_name = "invalid_host" response = unwrap_json( test_client.delete( - f"/api/v3/interfaces/{host_name}", - json=INTERFACE_1_REQUEST[0], + f"/api/v3/interfaces/{host_name}/{INTERFACE_1_REQUEST[0]['name']}", headers=auth_header, ) ) @@ -275,25 +274,6 @@ def test_invalid_host(self, test_client, auth, prefill): assert response.json["error"] == "Bad Request" assert response.json["message"] == f"Host not found: {host_name}" - @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) - def test_invalid_missing_id(self, test_client, auth, prefill): - """ - | GIVEN: Defaults, auth token, clouds and hosts - | WHEN: User tries to delete an interface without specifying an ID for the interface. - | THEN: Interface should not be deleted - """ - auth_header = auth.get_auth_header() - response = unwrap_json( - test_client.delete( - f"/api/v3/interfaces/{INTERFACE_1_REQUEST[1]}", - json={}, - headers=auth_header, - ) - ) - assert response.status_code == 400 - assert response.json["error"] == "Bad Request" - assert response.json["message"] == "Missing argument: id" - @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_invalid_wrong_id(self, test_client, auth, prefill): """ @@ -305,8 +285,7 @@ def test_invalid_wrong_id(self, test_client, auth, prefill): invalid_id = 42 response = unwrap_json( test_client.delete( - f"/api/v3/interfaces/{INTERFACE_1_REQUEST[1]}", - json={"id": invalid_id}, + f"/api/v3/interfaces/{INTERFACE_1_REQUEST[1]}/{invalid_id}", headers=auth_header, ) ) @@ -324,8 +303,7 @@ def test_valid(self, test_client, auth, prefill): auth_header = auth.get_auth_header() response = unwrap_json( test_client.delete( - f"/api/v3/interfaces/{INTERFACE_1_REQUEST[1]}", - json={"id": INTERFACE_1_RESPONSE["id"]}, + f"/api/v3/interfaces/{INTERFACE_1_REQUEST[1]}/{INTERFACE_1_RESPONSE['name']}", headers=auth_header, ) ) diff --git a/tests/api/test_schedules.py b/tests/api/test_schedules.py index deb5a5c23..ff271ab8c 100644 --- a/tests/api/test_schedules.py +++ b/tests/api/test_schedules.py @@ -57,9 +57,7 @@ def test_invalid_cloud_not_found(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert ( - response.json["message"] == f"Cloud not found: {schedule_request['cloud']}" - ) + assert response.json["message"] == f"Cloud not found: {schedule_request['cloud']}" @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_invalid_cloud_no_assignment(self, test_client, auth, prefill): @@ -80,10 +78,7 @@ def test_invalid_cloud_no_assignment(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert ( - response.json["message"] - == f"No active assignment for cloud: {schedule_request['cloud']}" - ) + assert response.json["message"] == f"No active assignment for cloud: {schedule_request['cloud']}" @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_invalid_missing_hostname(self, test_client, auth, prefill): @@ -125,10 +120,7 @@ def test_invalid_hostname_not_found(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert ( - response.json["message"] - == f"Host not found: {schedule_request['hostname']}" - ) + assert response.json["message"] == f"Host not found: {schedule_request['hostname']}" @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_invalid_missing_dates(self, test_client, auth, prefill): @@ -170,10 +162,7 @@ def test_invalid_date_format(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert ( - response.json["message"] - == "Invalid date format for start or end, correct format: 'YYYY-MM-DD HH:MM'" - ) + assert response.json["message"] == "Invalid date format for start or end, correct format: 'YYYY-MM-DD HH:MM'" @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_invalid_date_range(self, test_client, auth, prefill): @@ -195,10 +184,7 @@ def test_invalid_date_range(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert ( - response.json["message"] - == "Invalid date range for start or end, start must be before end" - ) + assert response.json["message"] == "Invalid date range for start or end, start must be before end" @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_valid(self, test_client, auth, prefill): @@ -210,9 +196,7 @@ def test_valid(self, test_client, auth, prefill): auth_header = auth.get_auth_header() schedule_requests = [SCHEDULE_1_REQUEST.copy(), SCHEDULE_2_REQUEST.copy()] schedule_responses = [SCHEDULE_1_RESPONSE.copy(), SCHEDULE_2_RESPONSE.copy()] - schedule_requests[1]["start"] = (datetime.now() + timedelta(6 * 30)).strftime( - "%Y-%m-%d %H:%M" - ) + schedule_requests[1]["start"] = (datetime.now() + timedelta(6 * 30)).strftime("%Y-%m-%d %H:%M") for req, resp in zip(schedule_requests, schedule_responses): response = unwrap_json( test_client.post( @@ -221,18 +205,12 @@ def test_valid(self, test_client, auth, prefill): headers=auth_header, ) ) - resp["assignment"]["cloud"]["last_redefined"] = response.json["assignment"][ - "cloud" - ]["last_redefined"] + resp["assignment"]["cloud"]["last_redefined"] = response.json["assignment"]["cloud"]["last_redefined"] resp["assignment"]["created_at"] = response.json["assignment"]["created_at"] resp["created_at"] = response.json["created_at"] resp["host"]["created_at"] = response.json["host"]["created_at"] - resp["host"]["cloud"]["last_redefined"] = response.json["host"]["cloud"][ - "last_redefined" - ] - resp["host"]["default_cloud"]["last_redefined"] = response.json["host"][ - "default_cloud" - ]["last_redefined"] + resp["host"]["cloud"]["last_redefined"] = response.json["host"]["cloud"]["last_redefined"] + resp["host"]["default_cloud"]["last_redefined"] = response.json["host"]["default_cloud"]["last_redefined"] resp["start"] = response.json["start"] assert response.status_code == 200 assert response.json == resp @@ -256,20 +234,14 @@ def test_valid_all(self, test_client, auth, prefill): response.json.sort(key=lambda x: x["id"]) schedule_responses = [SCHEDULE_1_RESPONSE.copy(), SCHEDULE_2_RESPONSE.copy()] for i, resp in enumerate(schedule_responses): - resp["assignment"]["cloud"]["last_redefined"] = response.json[i][ - "assignment" - ]["cloud"]["last_redefined"] - resp["assignment"]["created_at"] = response.json[i]["assignment"][ - "created_at" - ] + resp["assignment"]["cloud"]["last_redefined"] = response.json[i]["assignment"]["cloud"]["last_redefined"] + resp["assignment"]["created_at"] = response.json[i]["assignment"]["created_at"] resp["created_at"] = response.json[i]["created_at"] resp["host"]["created_at"] = response.json[i]["host"]["created_at"] - resp["host"]["cloud"]["last_redefined"] = response.json[i]["host"]["cloud"][ + resp["host"]["cloud"]["last_redefined"] = response.json[i]["host"]["cloud"]["last_redefined"] + resp["host"]["default_cloud"]["last_redefined"] = response.json[i]["host"]["default_cloud"][ "last_redefined" ] - resp["host"]["default_cloud"]["last_redefined"] = response.json[i]["host"][ - "default_cloud" - ]["last_redefined"] resp["start"] = response.json[i]["start"] assert response.status_code == 200 assert response.json == schedule_responses @@ -308,18 +280,12 @@ def test_valid_single(self, test_client, auth, prefill): headers=auth_header, ) ) - resp["assignment"]["cloud"]["last_redefined"] = response.json["assignment"][ - "cloud" - ]["last_redefined"] + resp["assignment"]["cloud"]["last_redefined"] = response.json["assignment"]["cloud"]["last_redefined"] resp["assignment"]["created_at"] = response.json["assignment"]["created_at"] resp["created_at"] = response.json["created_at"] resp["host"]["created_at"] = response.json["host"]["created_at"] - resp["host"]["cloud"]["last_redefined"] = response.json["host"]["cloud"][ - "last_redefined" - ] - resp["host"]["default_cloud"]["last_redefined"] = response.json["host"][ - "default_cloud" - ]["last_redefined"] + resp["host"]["cloud"]["last_redefined"] = response.json["host"]["cloud"]["last_redefined"] + resp["host"]["default_cloud"]["last_redefined"] = response.json["host"]["default_cloud"]["last_redefined"] resp["start"] = response.json["start"] assert response.status_code == 200 assert response.json == resp @@ -340,17 +306,15 @@ def test_valid_current(self, test_client, auth, prefill): ] requests = [ {"host": schedule_responses[0][0]["host"]["name"]}, - {"date": "2043-01-01"}, - {"cloud": "cloud02", "date": "2040-01-01"}, + {"date": "2043-01-01T22:00"}, + {"cloud": "cloud02", "date": "2040-01-01T22:00"}, { "cloud": "cloud04", "host": schedule_responses[0][0]["host"]["name"], - "date": "2050-01-01", + "date": "2050-01-01T22:00", }, ] - for i, resp, req in zip( - range(len(schedule_responses)), schedule_responses, requests - ): + for i, resp, req in zip(range(len(schedule_responses)), schedule_responses, requests): response = unwrap_json( test_client.get( f"/api/v3/schedules/current/?{urlencode(req)}", @@ -361,20 +325,14 @@ def test_valid_current(self, test_client, auth, prefill): assert response.status_code == 200 assert response.json == resp continue - resp[0]["assignment"]["cloud"]["last_redefined"] = response.json[0][ - "assignment" - ]["cloud"]["last_redefined"] - resp[0]["assignment"]["created_at"] = response.json[0]["assignment"][ - "created_at" - ] + resp[0]["assignment"]["cloud"]["last_redefined"] = response.json[0]["assignment"]["cloud"]["last_redefined"] + resp[0]["assignment"]["created_at"] = response.json[0]["assignment"]["created_at"] resp[0]["created_at"] = response.json[0]["created_at"] resp[0]["host"]["created_at"] = response.json[0]["host"]["created_at"] - resp[0]["host"]["cloud"]["last_redefined"] = response.json[0]["host"][ - "cloud" - ]["last_redefined"] - resp[0]["host"]["default_cloud"]["last_redefined"] = response.json[0][ - "host" - ]["default_cloud"]["last_redefined"] + resp[0]["host"]["cloud"]["last_redefined"] = response.json[0]["host"]["cloud"]["last_redefined"] + resp[0]["host"]["default_cloud"]["last_redefined"] = response.json[0]["host"]["default_cloud"][ + "last_redefined" + ] resp[0]["start"] = response.json[0]["start"] assert response.status_code == 200 assert response.json == resp @@ -395,7 +353,9 @@ def test_invalid_filter(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert response.json["message"] == f"start argument must be a datetime object" + assert response.json["message"] == ( + "start argument must be a datetime object or a correct datetime format string" + ) @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_valid_filter(self, test_client, auth, prefill): @@ -480,8 +440,7 @@ def test_invalid_no_args(self, test_client, auth, prefill): assert response.status_code == 400 assert response.json["error"] == "Bad Request" assert ( - response.json["message"] - == "Missing argument: start, end, build_start or build_end (specify at least " + response.json["message"] == "Missing argument: start, end, build_start or build_end (specify at least " "one)" ) @@ -524,10 +483,7 @@ def test_invalid_date_format(self, test_client, auth, prefill): ) assert response.status_code == 400 assert response.json["error"] == "Bad Request" - assert ( - response.json["message"] - == f"Invalid date format for start or end, correct format: 'YYYY-MM-DDTHH:MM'" - ) + assert response.json["message"] == f"Invalid date format for start or end, correct format: 'YYYY-MM-DDTHH:MM'" @pytest.mark.parametrize("prefill", prefill_settings, indirect=True) def test_invalid_date_ranges(self, test_client, auth, prefill): @@ -577,18 +533,12 @@ def test_valid(self, test_client, auth, prefill): headers=auth_header, ) ) - resp["assignment"]["cloud"]["last_redefined"] = response.json["assignment"][ - "cloud" - ]["last_redefined"] + resp["assignment"]["cloud"]["last_redefined"] = response.json["assignment"]["cloud"]["last_redefined"] resp["assignment"]["created_at"] = response.json["assignment"]["created_at"] resp["created_at"] = response.json["created_at"] resp["host"]["created_at"] = response.json["host"]["created_at"] - resp["host"]["cloud"]["last_redefined"] = response.json["host"]["cloud"][ - "last_redefined" - ] - resp["host"]["default_cloud"]["last_redefined"] = response.json["host"][ - "default_cloud" - ]["last_redefined"] + resp["host"]["cloud"]["last_redefined"] = response.json["host"]["cloud"]["last_redefined"] + resp["host"]["default_cloud"]["last_redefined"] = response.json["host"]["default_cloud"]["last_redefined"] resp["start"] = response.json["start"] resp["end"] = response.json["end"] resp["build_start"] = response.json["build_start"] diff --git a/tests/cli/config.py b/tests/cli/config.py index 4c3afde22..f768065ab 100644 --- a/tests/cli/config.py +++ b/tests/cli/config.py @@ -1,16 +1,78 @@ CLOUD = "cloud99" -HOST = "host.example.com" -MODEL = "r640" +DEFAULT_CLOUD = "cloud01" +DEFINE_CLOUD = "cloud02" +REMOVE_CLOUD = "cloud03" +MOD_CLOUD = "cloud04" +HOST1 = "host1.example.com" +HOST2 = "host2.example.com" +DEFINE_HOST = "define.example.com" +MODEL1 = "r640" +MODEL2 = "r930" HOST_TYPE = "scalelab" -IFNAME = "em1" -IFMAC = "A0:B1:C2:D3:E4:F5" -IFIP = "10.0.0.1" -IFPORT = "et-4:0/0" -IFBIOSID = "Nic1.Interfaces.1" +IFNAME1 = "em1" +IFNAME2 = "em2" +IFMAC1 = "A0:B1:C2:D3:E4:F5" +IFMAC2 = "A0:B1:C2:D3:E4:F6" +IFIP1 = "10.0.0.1" +IFIP2 = "10.0.0.2" +IFPORT1 = "et-4/0/0" +IFPORT2 = "et-4/0/1" +IFBIOSID1 = "Nic1.Interfaces.1" +IFBIOSID2 = "Nic1.Interfaces.2" IFSPEED = 1000 -IFVENDOR = "Intel" +IFVENDOR1 = "Intel" +IFVENDOR2 = "Melanox" -RESPONSE_LS = f"""INFO tests.cli.test_base:cli.py:418 {HOST}""" +RESPONSE_LS = f"""INFO tests.cli.test_base:cli.py:418 {HOST1}""" RESPONSE_DEF_HOST = "Successful request" RESPONSE_RM = "INFO test_log:cli.py:181 Successfully removed\n" + + +class NetcatStub: + def __init__(self, ip, port=22, loop=None): + pass + + async def __aenter__(self): + await self.connect() + return self + + async def __aexit__(self, *args): + await self.close() + + async def connect(self): + pass + + async def close(self): + pass + + async def write(self, data): + pass + + async def health_check(self): + self.__sizeof__() + return True + + +class SSHHelperStub(object): + def __init__(self, _host, _user=None, _password=None): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def connect(self): + pass + + def disconnect(self): + pass + + def run_cmd(self, cmd=None): + return True, [] + + def copy_ssh_key(self, _ssh_key): + pass + + +def switch_config_stub(host=None, old_cloud=None, new_cloud=None): + return True diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 000000000..3010ae321 --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,134 @@ +from datetime import datetime, timedelta + +import pytest + +from quads.server.app import create_app, user_datastore +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.cloud import CloudDao +from quads.server.dao.disk import DiskDao +from quads.server.dao.host import HostDao +from quads.server.dao.interface import InterfaceDao +from quads.server.dao.memory import MemoryDao +from quads.server.dao.processor import ProcessorDao +from quads.server.dao.schedule import ScheduleDao +from quads.server.dao.vlan import VlanDao +from quads.server.database import drop_all, populate, init_db +from tests.cli.config import ( + CLOUD, + HOST_TYPE, + DEFAULT_CLOUD, + HOST1, + MODEL1, + HOST2, + MODEL2, + DEFINE_CLOUD, + REMOVE_CLOUD, + MOD_CLOUD, + IFNAME1, + IFVENDOR1, + IFBIOSID1, + IFMAC1, + IFIP1, + IFPORT1, + IFSPEED, +) + + +@pytest.fixture(autouse=True, scope="session") +def test_client(): + """ + | Creates a test client for the app from the testing config. + | Drops and then initializes the database and populates it with default users. + """ + flask_app = create_app() + flask_app.config.from_object("quads.server.config.TestingConfig") + + with flask_app.test_client() as testing_client: + with flask_app.app_context(): + drop_all(flask_app.config) + init_db(flask_app.config) + populate(user_datastore) + yield testing_client + + +@pytest.fixture(autouse=True, scope="package") +def populate_db(): + + today = datetime.now() + tomorrow = today + timedelta(weeks=2) + + cloud = CloudDao.create_cloud(CLOUD) + default_cloud = CloudDao.create_cloud(DEFAULT_CLOUD) + remove_cloud = CloudDao.create_cloud(REMOVE_CLOUD) + mod_cloud = CloudDao.create_cloud(MOD_CLOUD) + host1 = HostDao.create_host(HOST1, MODEL1, HOST_TYPE, CLOUD) + host2 = HostDao.create_host(HOST2, MODEL2, HOST_TYPE, CLOUD) + InterfaceDao.create_interface( + HOST1, + IFNAME1, + IFBIOSID1, + IFMAC1, + IFIP1, + IFPORT1, + IFSPEED, + IFVENDOR1, + True, + False, + ) + DiskDao.create_disk(HOST1, "NVME", 4096, 10) + DiskDao.create_disk(HOST1, "SATA", 4096, 5) + MemoryDao.create_memory(HOST1, "DIMM1", 2048) + MemoryDao.create_memory(HOST1, "DIMM2", 2048) + ProcessorDao.create_processor(HOST1, "P1", "Intel", "i7", 2, 4) + vlan1 = VlanDao.create_vlan("192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1) + vlan2 = VlanDao.create_vlan("192.168.1.2", 122, "192.168.1.2/22", "255.255.255.255", 2) + assignment = AssignmentDao.create_assignment("test", "test", "1234", 0, False, [""], cloud.name, vlan1.vlan_id) + assignment_mod = AssignmentDao.create_assignment( + "test", "test", "1234", 0, False, [""], mod_cloud.name, vlan2.vlan_id + ) + schedule = ScheduleDao.create_schedule( + today, + tomorrow, + assignment, + host1, + ) + + yield + + host1 = HostDao.get_host(HOST1) + host2 = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + default_cloud = CloudDao.get_cloud(DEFAULT_CLOUD) + define_cloud = CloudDao.get_cloud(DEFINE_CLOUD) + remove_cloud = CloudDao.get_cloud(REMOVE_CLOUD) + mod_cloud = CloudDao.get_cloud(MOD_CLOUD) + schedules = ScheduleDao.get_current_schedule(host=host1, cloud=cloud) + + if schedules: + ScheduleDao.remove_schedule(schedules[0].id) + AssignmentDao.remove_assignment(schedules[0].assignment_id) + + assignments = AssignmentDao.get_assignments() + for ass in assignments: + AssignmentDao.remove_assignment(ass.id) + + if host1: + HostDao.remove_host(name=HOST1) + + if host2: + HostDao.remove_host(name=HOST2) + + if cloud: + CloudDao.remove_cloud(CLOUD) + + if define_cloud: + CloudDao.remove_cloud(DEFINE_CLOUD) + + if remove_cloud: + CloudDao.remove_cloud(REMOVE_CLOUD) + + if mod_cloud: + CloudDao.remove_cloud(MOD_CLOUD) + + if default_cloud: + CloudDao.remove_cloud(DEFAULT_CLOUD) diff --git a/tests/cli/fixtures/badhost_metadata_import.yaml b/tests/cli/fixtures/badhost_metadata_import.yaml new file mode 100644 index 000000000..bd8fdfa5a --- /dev/null +++ b/tests/cli/fixtures/badhost_metadata_import.yaml @@ -0,0 +1,27 @@ +- broken: true + default_cloud: cloud42 + disks: + - count: 10 + disk_type: NVME + size_gb: 4096 + - count: 5 + disk_type: SATA + size_gb: 4096 + host_type: imported + interfaces: + - mac_address: A0:B1:C2:D3:E4:F5 + maintenance: false + name: em1 + pxe_boot: true + speed: 1000 + switch_ip: 10.0.0.1 + switch_port: et-4:0/0 + vendor: Intel + bios_id: NIC.Intel.1 + last_build: null + memory: + - handle: DIMM1 + size_gb: 4096 + model: IMPORTED + name: badhost.example.com + retired: true diff --git a/tests/cli/fixtures/metadata b/tests/cli/fixtures/metadata new file mode 100644 index 000000000..30dac0785 --- /dev/null +++ b/tests/cli/fixtures/metadata @@ -0,0 +1,48 @@ +- broken: false + default_cloud: cloud99 + host_type: scalelab + last_build: null + model: R930 + name: host2.example.com + retired: false +- broken: false + default_cloud: cloud99 + disks: + - count: 10 + disk_type: NVME + size_gb: 4096 + - count: 5 + disk_type: SATA + size_gb: 4096 + host_type: scalelab + interfaces: + - mac_address: A0:B1:C2:D3:E4:F5 + maintenance: false + name: em1 + pxe_boot: true + speed: 1000 + switch_ip: 10.0.0.1 + switch_port: et-4/0/0 + vendor: Intel + last_build: null + memory: + - handle: DIMM1 + size_gb: 2048 + - handle: DIMM2 + size_gb: 2048 + model: R640 + name: host1.example.com + processors: + - cores: 2 + handle: P1 + product: i7 + threads: 4 + vendor: Intel + retired: false +- broken: false + default_cloud: cloud99 + host_type: scalelab + last_build: null + model: R640 + name: imported.example.com + retired: false diff --git a/tests/cli/fixtures/metadata_import.yaml b/tests/cli/fixtures/metadata_import.yaml new file mode 100644 index 000000000..57ff1d40b --- /dev/null +++ b/tests/cli/fixtures/metadata_import.yaml @@ -0,0 +1,27 @@ +- broken: true + default_cloud: cloud42 + disks: + - count: 10 + disk_type: NVME + size_gb: 4096 + - count: 5 + disk_type: SATA + size_gb: 4096 + host_type: imported + interfaces: + - mac_address: A0:B1:C2:D3:E4:F5 + maintenance: false + name: em1 + pxe_boot: true + speed: 1000 + switch_ip: 10.0.0.1 + switch_port: et-4:0/0 + vendor: Intel + bios_id: NIC.Intel.1 + last_build: null + memory: + - handle: DIMM1 + size_gb: 4096 + model: IMPORTED + name: imported.example.com + retired: true diff --git a/tests/cli/test_base.py b/tests/cli/test_base.py index 4902c9e39..475e522c9 100644 --- a/tests/cli/test_base.py +++ b/tests/cli/test_base.py @@ -1,4 +1,5 @@ import logging + import pytest from quads.cli import QuadsCli @@ -16,22 +17,6 @@ class TestBase: cli_args = {"datearg": None, "filter": None, "force": "False"} - @pytest.fixture(autouse=True) - def test_client(self): - """ - | Creates a test client for the app from the testing config. - | Drops and then initializes the database and populates it with default users. - """ - self.flask_app = create_app() - self.flask_app.config.from_object("quads.server.config.TestingConfig") - - with self.flask_app.test_client() as testing_client: - with self.flask_app.app_context(): - drop_all(self.flask_app.config) - init_db(self.flask_app.config) - populate(user_datastore) - yield testing_client - @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): self._caplog = caplog diff --git a/tests/cli/test_broken.py b/tests/cli/test_broken.py index 8cee36af6..ce8b51278 100644 --- a/tests/cli/test_broken.py +++ b/tests/cli/test_broken.py @@ -1,86 +1,120 @@ +from unittest.mock import patch + import pytest -from quads.server.dao.cloud import CloudDao +from quads.exceptions import CliException +from quads.quads_api import APIServerException from quads.server.dao.host import HostDao -from tests.cli.config import CLOUD, HOST +from tests.cli.config import HOST1 from tests.cli.test_base import TestBase def finalizer(): - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) if host: - HostDao.remove_host(HOST) - cloud = CloudDao.get_cloud(CLOUD) - if cloud: - CloudDao.remove_cloud(name=CLOUD) - - -@pytest.fixture -def mb_fixture(request): - request.addfinalizer(finalizer) - - cloud = CloudDao.create_cloud(CLOUD) - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) + HostDao.update_host(name=HOST1, broken=False) @pytest.fixture def mr_fixture(request): request.addfinalizer(finalizer) - - cloud = CloudDao.create_cloud(CLOUD) - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - assert HostDao.update_host(name=HOST, broken=True) + assert HostDao.update_host(name=HOST1, broken=True) class TestBroken(TestBase): - def test_mark_broken(self, mb_fixture): - self.cli_args["host"] = HOST + def test_mark_broken(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("mark_broken") - assert self._caplog.messages[0] == f"Host {HOST} is now marked as broken" + assert self._caplog.messages[0] == f"Host {HOST1} is now marked as broken" - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert host.broken + def test_mark_broken_no_arg(self): + self.cli_args["host"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("mark_broken") + + assert str(ex.value) == "Missing option. Need --host when using --mark-broken" + def test_mark_broken_already(self, mr_fixture): - self.cli_args["host"] = HOST + self.cli_args["host"] = HOST1 self.quads_cli_call("mark_broken") - assert self._caplog.messages[0] == f"Host {HOST} has already been marked broken" + assert self._caplog.messages[0] == f"Host {HOST1} has already been marked broken" + + host = HostDao.get_host(HOST1) + assert host.broken + + @patch("quads.quads_api.QuadsApi.update_host") + def test_mark_broken_exception(self, mock_update): + mock_update.side_effect = APIServerException("Connection Error") + self.cli_args["host"] = HOST1 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("mark_broken") + + assert str(ex.value) == "Connection Error" + + def test_ls_broken(self, mr_fixture): + self.cli_args["host"] = HOST1 + + self.quads_cli_call("ls_broken") + + assert self._caplog.messages[0] == HOST1 - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert host.broken + @patch("quads.quads_api.requests.Session.get") + def test_ls_broken_exception(self, mock_get, mr_fixture): + mock_get.return_value.status_code = 500 + self.cli_args["host"] = HOST1 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_broken") + assert str(ex.value) == "Check the flask server logs" + + +class TestRepaired(TestBase): def test_mark_repaired(self, mr_fixture): - self.cli_args["host"] = HOST + self.cli_args["host"] = HOST1 self.quads_cli_call("mark_repaired") - assert self._caplog.messages[0] == f"Host {HOST} is now marked as repaired" + assert self._caplog.messages[0] == f"Host {HOST1} is now marked as repaired" - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert not host.broken - def test_mark_repaired_not_broken(self, mb_fixture): - self.cli_args["host"] = HOST + def test_mark_repaired_no_arg(self): + self.cli_args["host"] = None - self.quads_cli_call("mark_repaired") + with pytest.raises(CliException) as ex: + self.quads_cli_call("mark_repaired") - assert ( - self._caplog.messages[0] == f"Host {HOST} has already been marked repaired" - ) + assert str(ex.value) == "Missing option. Need --host when using --mark-repaired" - host = HostDao.get_host(HOST) - assert not host.broken + @patch("quads.quads_api.QuadsApi.update_host") + def test_mark_repaired_exception(self, mock_update, mr_fixture): + mock_update.side_effect = APIServerException("Connection Error") + self.cli_args["host"] = HOST1 - def test_ls_broken(self, mr_fixture): - self.cli_args["host"] = HOST + with pytest.raises(CliException) as ex: + self.quads_cli_call("mark_repaired") - self.quads_cli_call("ls_broken") + assert str(ex.value) == "Connection Error" - assert self._caplog.messages[0] == HOST + def test_mark_repaired_not_broken(self): + self.cli_args["host"] = HOST1 - host = HostDao.get_host(HOST) - assert host.broken + self.quads_cli_call("mark_repaired") + + assert self._caplog.messages[0] == f"Host {HOST1} has already been marked repaired" + + host = HostDao.get_host(HOST1) + assert not host.broken diff --git a/tests/cli/test_cloud.py b/tests/cli/test_cloud.py index 8cc77f4e4..aaebc2bb2 100644 --- a/tests/cli/test_cloud.py +++ b/tests/cli/test_cloud.py @@ -1,55 +1,43 @@ -import pytest +from unittest.mock import patch +from quads.exceptions import CliException +from quads.quads_api import APIServerException from quads.server.dao.assignment import AssignmentDao from quads.server.dao.cloud import CloudDao -from quads.server.dao.vlan import VlanDao -from tests.cli.config import CLOUD +from tests.cli.config import ( + CLOUD, + HOST1, + DEFINE_CLOUD, + REMOVE_CLOUD, + MOD_CLOUD, + DEFAULT_CLOUD, +) from tests.cli.test_base import TestBase +import pytest -def finalizer(): - cloud = CloudDao.get_cloud(CLOUD) - if cloud: +@pytest.fixture(autouse=True) +def remove_cloud(request): + def finalizer(): + cloud = CloudDao.get_cloud(DEFINE_CLOUD) assignment = AssignmentDao.get_active_cloud_assignment(cloud) if assignment: - AssignmentDao.delete_assignment(assignment.id) - CloudDao.remove_cloud(name=CLOUD) - - -@pytest.fixture -def define_fixture(request): + AssignmentDao.remove_assignment(assignment.id) + if cloud: + CloudDao.remove_cloud(DEFINE_CLOUD) + finalizer() request.addfinalizer(finalizer) -@pytest.fixture -def remove_fixture(request): - - request.addfinalizer(finalizer) - - cloud = CloudDao.create_cloud(name=CLOUD) - vlan = VlanDao.create_vlan( - gateway="10.0.0.1", - ip_free=510, - vlan_id=1, - ip_range="10.0.0.0/23", - netmask="255.255.0.0", - ) - assignment = AssignmentDao.create_assignment( - description="Test cloud", - owner="scalelab", - ticket="123456", - qinq=0, - wipe=False, - ccuser=[], - vlan_id=1, - cloud=CLOUD, - ) +@pytest.fixture(autouse=True) +def define_cloud(request): + CloudDao.create_cloud(DEFINE_CLOUD) class TestCloud(TestBase): - def test_define_cloud(self, define_fixture): - self.cli_args["cloud"] = CLOUD + def test_define_cloud(self, remove_cloud): + self.cli_args["cloud"] = DEFINE_CLOUD self.cli_args["description"] = "Test cloud" self.cli_args["cloudowner"] = "scalelab" self.cli_args["ccusers"] = None @@ -60,7 +48,7 @@ def test_define_cloud(self, define_fixture): self.quads_cli_call("cloudresource") - assert self._caplog.messages[0] == "Cloud cloud99 created." + assert self._caplog.messages[0] == f"Cloud {DEFINE_CLOUD} created." assert self._caplog.messages[1] == "Assignment created." cloud = CloudDao.get_cloud(CLOUD) @@ -69,80 +57,275 @@ def test_define_cloud(self, define_fixture): assert cloud is not None assert cloud.name == CLOUD - def test_remove_cloud(self, remove_fixture): + def test_define_cloud_invalid_vlan(self, remove_cloud): + self.cli_args["cloud"] = DEFINE_CLOUD + self.cli_args["description"] = "Test cloud" + self.cli_args["cloudowner"] = "scalelab" + self.cli_args["ccusers"] = None + self.cli_args["qinq"] = None + self.cli_args["cloudticket"] = "1225" + self.cli_args["force"] = True + self.cli_args["wipe"] = True + self.cli_args["vlan"] = "BADVLAN" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("cloudresource") + + assert str(ex.value) == "Could not parse vlan id. Only integers accepted." + + def test_define_cloud_locked(self): self.cli_args["cloud"] = CLOUD - cloud = CloudDao.get_cloud(CLOUD) + self.cli_args["description"] = "Test cloud" + self.cli_args["cloudowner"] = "scalelab" + self.cli_args["ccusers"] = None + self.cli_args["qinq"] = None + self.cli_args["cloudticket"] = "1225" + self.cli_args["force"] = True + self.cli_args["wipe"] = True + self.cli_args["vlan"] = None - assignment = AssignmentDao.get_active_cloud_assignment(cloud) - AssignmentDao.udpate_assignment(assignment_id=assignment.id, active=False) + self.quads_cli_call("cloudresource") + + assert self._caplog.messages[0] == "Can't redefine cloud:" + assert self._caplog.messages[1].startswith(f"{CLOUD} (reserved: ") + + def test_remove_cloud(self, define_cloud): + self.cli_args["cloud"] = REMOVE_CLOUD self.quads_cli_call("rmcloud") - rm_cloud = CloudDao.get_cloud(CLOUD) + rm_cloud = CloudDao.get_cloud(REMOVE_CLOUD) assert not rm_cloud - def test_mod_cloud(self, remove_fixture): - new_description = "Modified description" + def test_remove_cloud_no_arg(self, define_cloud): + self.cli_args["cloud"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmcloud") + + assert str(ex.value) == "Missing parameter --cloud" + + def test_remove_cloud_active_assignment(self, define_cloud): self.cli_args["cloud"] = CLOUD + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmcloud") + + assert str(ex.value) == f"There is an active cloud assignment for {CLOUD}" + + @patch("quads.quads_api.QuadsApi.remove_cloud") + def test_remove_cloud_exception(self, mock_remove, define_cloud): + mock_remove.side_effect = APIServerException("Connection Error") + self.cli_args["cloud"] = DEFAULT_CLOUD + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmcloud") + + assert str(ex.value) == "Connection Error" + + @patch("quads.quads_api.QuadsApi.get_active_cloud_assignment") + def test_remove_assignment_exception(self, mock_ass, define_cloud): + mock_ass.side_effect = APIServerException("Connection Error") + self.cli_args["cloud"] = DEFAULT_CLOUD + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmcloud") + + assert str(ex.value) == "Connection Error" + + def test_mod_cloud(self): + new_description = "Modified description" + self.cli_args["cloud"] = MOD_CLOUD self.cli_args["description"] = new_description self.cli_args["cloudowner"] = None self.cli_args["ccusers"] = None - self.cli_args["cloudticket"] = None + self.cli_args["cloudticket"] = 4321 self.quads_cli_call("modcloud") assert self._caplog.messages[0] == "Cloud modified successfully" - cloud = CloudDao.get_cloud(CLOUD) + cloud = CloudDao.get_cloud(MOD_CLOUD) assert cloud assignment = AssignmentDao.get_active_cloud_assignment(cloud) assert assignment assert assignment.description == new_description + assert assignment.ticket == "4321" - def test_ls_no_clouds(self): + def test_mod_bad_cloud(self): + new_description = "Modified description" + self.cli_args["cloud"] = "BADCLOUD" + self.cli_args["description"] = new_description + self.cli_args["cloudowner"] = None + self.cli_args["ccusers"] = None + self.cli_args["cloudticket"] = 4321 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modcloud") + + assert str(ex.value) == "Cloud not found: BADCLOUD" + + def test_mod_cloud_no_assignment(self): + new_description = "Modified description" + self.cli_args["cloud"] = DEFAULT_CLOUD + self.cli_args["description"] = new_description + self.cli_args["cloudowner"] = None + self.cli_args["ccusers"] = None + self.cli_args["cloudticket"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modcloud") + + assert str(ex.value) == f"No active cloud assignment for {DEFAULT_CLOUD}" + + @patch("quads.quads_api.QuadsApi.update_assignment") + def test_mod_cloud_exception(self, mock_update): + mock_update.side_effect = APIServerException("Connection Error") + new_description = "Modified description" + self.cli_args["cloud"] = MOD_CLOUD + self.cli_args["description"] = new_description + self.cli_args["cloudowner"] = None + self.cli_args["ccusers"] = None + self.cli_args["cloudticket"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modcloud") + + assert str(ex.value) == "Connection Error" + + @patch("quads.quads_api.QuadsApi.update_assignment") + def test_mod_cloud_exception_ticket(self, mock_update): + mock_update.side_effect = APIServerException("Connection Error") + new_description = "Modified description" + self.cli_args["cloud"] = MOD_CLOUD + self.cli_args["description"] = new_description + self.cli_args["cloudowner"] = None + self.cli_args["ccusers"] = None + self.cli_args["cloudticket"] = 4321 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modcloud") + + assert str(ex.value) == "Connection Error" + + def test_mod_cloud_bad_vlan(self): + new_description = "Modified description" + self.cli_args["cloud"] = MOD_CLOUD + self.cli_args["description"] = new_description + self.cli_args["cloudowner"] = None + self.cli_args["ccusers"] = None + self.cli_args["cloudticket"] = None + self.cli_args["vlan"] = "BADVLAN" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modcloud") + + assert str(ex.value) == "Could not parse vlan id. Only integers accepted." + + @patch("quads.quads_api.requests.Session.get") + def test_ls_no_clouds(self, mock_get): + mock_get.return_value.json.return_value = [] self.quads_cli_call("ls_clouds") assert self._caplog.messages[0] == "No clouds found." - def test_ls_clouds(self, remove_fixture): + def test_ls_clouds(self): self.quads_cli_call("ls_clouds") - assert self._caplog.messages[0] == CLOUD + assert self._caplog.messages[0] == DEFAULT_CLOUD + assert self._caplog.messages[1] == MOD_CLOUD + assert self._caplog.messages[2] == CLOUD + + @patch("quads.quads_api.requests.Session.get") + def test_ls_clouds_exception(self, mock_get): + mock_get.return_value.status_code = 500 + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_clouds") + assert str(ex.value) == "Check the flask server logs" - def test_ls_wipe(self, remove_fixture): + def test_ls_wipe(self): self.quads_cli_call("wipe") assert self._caplog.messages[0] == f"{CLOUD}: False" - def test_ls_ticket(self, remove_fixture): + def test_ls_ticket(self): self.quads_cli_call("ticket") - assert self._caplog.messages[0] == f"{CLOUD}: 123456" + assert self._caplog.messages[0] == f"{CLOUD}: 1234" - def test_ls_owner(self, remove_fixture): + def test_ls_owner(self): self.quads_cli_call("owner") - assert self._caplog.messages[0] == f"{CLOUD}: scalelab" + assert self._caplog.messages[0] == f"{CLOUD}: test" + + @patch("quads.quads_api.requests.Session.get") + def test_ls_owner_exception(self, mock_get): + mock_get.return_value.status_code = 500 + with pytest.raises(CliException) as ex: + self.quads_cli_call("owner") + assert str(ex.value) == "Check the flask server logs" - def test_ls_qinq(self, remove_fixture): + def test_ls_qinq(self): self.quads_cli_call("qinq") assert self._caplog.messages[0] == f"{CLOUD}: 0" - def test_ls_cc_users(self, remove_fixture): + def test_ls_cc_users(self): self.quads_cli_call("ccuser") - assert self._caplog.messages[0] == f"{CLOUD}: []" + assert self._caplog.messages[0] == f"{CLOUD}: ['']" - def test_ls_vlan(self, remove_fixture): + def test_ls_vlan(self): self.quads_cli_call("ls_vlan") assert self._caplog.messages[0] == f"1: {CLOUD}" - def test_free_cloud(self, remove_fixture): + @patch("quads.quads_api.requests.Session.get") + def test_ls_vlan_exception(self, mock_get): + mock_get.return_value.status_code = 500 + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_vlan") + assert str(ex.value) == "Check the flask server logs" + + @patch("quads.quads_api.QuadsApi.filter_assignments") + def test_ls_vlan_filter_exception(self, mock_filter): + mock_filter.side_effect = APIServerException("Connection Error") + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_vlan") + assert str(ex.value) == "Connection Error" + + @patch("quads.quads_api.requests.Session.get") + def test_ls_vlan_no_vlans(self, mock_get): + mock_get.return_value.json.return_value = [] + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_vlan") + assert str(ex.value) == "No VLANs defined." + + def test_free_cloud(self): self.quads_cli_call("free_cloud") - assert self._caplog.messages[0].startswith(f"{CLOUD} (reserved: ") + assert self._caplog.messages[0].startswith(f"{MOD_CLOUD} (reserved: ") assert self._caplog.messages[0].endswith("min remaining)") + + @patch("quads.quads_api.requests.Session.get") + def test_free_cloud_exception(self, mock_get): + mock_get.return_value.status_code = 500 + with pytest.raises(CliException) as ex: + self.quads_cli_call("free_cloud") + assert str(ex.value) == "Check the flask server logs" + + @patch("quads.quads_api.QuadsApi.get_future_schedules") + def test_free_cloud_future_exception(self, mock_future): + mock_future.side_effect = APIServerException("Connection Error") + with pytest.raises(CliException) as ex: + self.quads_cli_call("free_cloud") + assert str(ex.value) == "Connection Error" + + +class TestCloudOnly(TestBase): + def test_cloud_only(self): + self.cli_args["cloud"] = CLOUD + self.quads_cli_call("cloudonly") + assert self._caplog.messages[0] == f"{HOST1}" diff --git a/tests/cli/test_disk.py b/tests/cli/test_disk.py index 39281c287..5e3493a32 100644 --- a/tests/cli/test_disk.py +++ b/tests/cli/test_disk.py @@ -2,62 +2,18 @@ from quads.exceptions import CliException from quads.server.dao.cloud import CloudDao -from quads.server.dao.disk import DiskDao from quads.server.dao.host import HostDao -from quads.server.dao.interface import InterfaceDao -from quads.server.dao.memory import MemoryDao from tests.cli.config import ( - HOST, CLOUD, - IFNAME, - IFMAC, - IFIP, - IFPORT, - IFBIOSID, - IFSPEED, - IFVENDOR, + HOST1, + HOST2, ) from tests.cli.test_base import TestBase -def finalizer(): - host = HostDao.get_host(HOST) - if host: - HostDao.remove_host(name=HOST) - cloud = CloudDao.get_cloud(CLOUD) - if cloud: - CloudDao.remove_cloud(CLOUD) - - -@pytest.fixture -def remove_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - - -@pytest.fixture -def mod_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - DiskDao.create_disk(HOST, "NVME", 4096, 10) - DiskDao.create_disk(HOST, "SATA", 4096, 5) - - -@pytest.fixture -def nodisk_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - - class TestDisk(TestBase): - def test_ls_disk(self, mod_fixture): - self.cli_args["host"] = HOST + def test_ls_disk(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("disks") @@ -70,7 +26,7 @@ def test_ls_disk(self, mod_fixture): assert self._caplog.messages[6] == f" size: 4096" assert self._caplog.messages[7] == f" count: 5" - def test_ls_disk_missing_host(self, remove_fixture): + def test_ls_disk_missing_host(self): if self.cli_args.get("host"): self.cli_args.pop("host") with pytest.raises(CliException) as ex: @@ -79,13 +35,13 @@ def test_ls_disk_missing_host(self, remove_fixture): str(ex.value) == "Missing option. --host option is required for --ls-disks." ) - def test_ls_disk_bad_host(self, remove_fixture): + def test_ls_disk_bad_host(self): self.cli_args["host"] = "BADHOST" with pytest.raises(CliException) as ex: self.quads_cli_call("disks") assert str(ex.value) == "Host not found: BADHOST" - def test_ls_disk_nodisk_host(self, nodisk_fixture): - self.cli_args["host"] = HOST + def test_ls_disk_nodisk_host(self): + self.cli_args["host"] = HOST2 self.quads_cli_call("disks") - assert self._caplog.messages[0] == f"No disks defined for {HOST}" + assert self._caplog.messages[0] == f"No disks defined for {HOST2}" diff --git a/tests/cli/test_host.py b/tests/cli/test_host.py index 49c092a2f..2cb1fce50 100644 --- a/tests/cli/test_host.py +++ b/tests/cli/test_host.py @@ -1,72 +1,110 @@ +from unittest.mock import patch + import pytest from quads.exceptions import CliException -from quads.server.dao.cloud import CloudDao +from quads.quads_api import APIServerException +from quads.server.dao.baseDao import EntryExisting from quads.server.dao.host import HostDao -from quads.server.dao.interface import InterfaceDao -from quads.server.dao.memory import MemoryDao -from quads.server.dao.processor import ProcessorDao from tests.cli.config import ( - HOST, CLOUD, - MODEL, HOST_TYPE, - IFNAME, - IFBIOSID, - IFMAC, - IFIP, - IFPORT, - IFSPEED, - IFVENDOR, + DEFINE_HOST, + MODEL1, + HOST1, + IFIP1, + HOST2, ) from tests.cli.test_base import TestBase def finalizer(): - host = HostDao.get_host(HOST) + host = HostDao.get_host(DEFINE_HOST) if host: - HostDao.remove_host(name=HOST) - cloud = CloudDao.get_cloud(CLOUD) - if cloud: - CloudDao.remove_cloud(CLOUD) + HostDao.remove_host(name=DEFINE_HOST) + + +def mark_repaired(): + host = HostDao.update_host(HOST1, broken=False) + assert not host.broken @pytest.fixture -def define_fixture(request): +def remove_host(request): + finalizer() request.addfinalizer(finalizer) - CloudDao.create_cloud(CLOUD) - @pytest.fixture -def remove_fixture(request): +def add_host(request): request.addfinalizer(finalizer) - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, MODEL, HOST_TYPE, CLOUD) - HostDao.create_host("NEWHOST.example.com", "NEWMODEL", HOST_TYPE, CLOUD) - InterfaceDao.create_interface( - HOST, IFNAME, IFBIOSID, IFMAC, IFIP, IFPORT, IFSPEED, IFVENDOR, True, False - ) + try: + host = HostDao.create_host(DEFINE_HOST, MODEL1, HOST_TYPE, CLOUD) + except EntryExisting: + host = HostDao.get_host(DEFINE_HOST) + assert host + + +@pytest.fixture +def mark_host_broken(request): + request.addfinalizer(mark_repaired) + host = HostDao.update_host(HOST1, broken=True) + assert host.broken class TestHost(TestBase): - def test_define_host(self, define_fixture): - self.cli_args["hostresource"] = HOST + def test_define_host(self, remove_host): + self.cli_args["hostresource"] = DEFINE_HOST self.cli_args["hostcloud"] = CLOUD self.cli_args["hosttype"] = HOST_TYPE - self.cli_args["model"] = MODEL + self.cli_args["model"] = MODEL1 self.quads_cli_call("hostresource") - host = HostDao.get_host(HOST) + host = HostDao.get_host(DEFINE_HOST) assert host is not None - assert host.name == HOST + assert host.name == DEFINE_HOST + + assert self._caplog.messages[0] == DEFINE_HOST + + @patch("quads.quads_api.QuadsApi.create_host") + def test_define_host_exception(self, mock_create, remove_host): + mock_create.side_effect = APIServerException("Connection Error") + self.cli_args["hostresource"] = DEFINE_HOST + self.cli_args["hostcloud"] = CLOUD + self.cli_args["hosttype"] = HOST_TYPE + self.cli_args["model"] = MODEL1 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("hostresource") + + assert str(ex.value) == "Connection Error" - assert self._caplog.messages[0] == HOST + def test_define_host_missing_model(self, remove_host): + self.cli_args["hostresource"] = DEFINE_HOST + self.cli_args["hostcloud"] = CLOUD + self.cli_args["hosttype"] = HOST_TYPE + self.cli_args["model"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("hostresource") + + assert str(ex.value) == "Missing argument: model" + + def test_define_host_no_cloud(self, remove_host): + self.cli_args["hostresource"] = DEFINE_HOST + self.cli_args["hostcloud"] = None + self.cli_args["hosttype"] = HOST_TYPE + self.cli_args["model"] = MODEL1 - def test_define_host_missing_model(self, define_fixture): - self.cli_args["hostresource"] = HOST + with pytest.raises(CliException) as ex: + self.quads_cli_call("hostresource") + + assert str(ex.value) == "Missing option --default-cloud" + + def test_define_host_missing_model(self, remove_host): + self.cli_args["hostresource"] = DEFINE_HOST self.cli_args["hostcloud"] = CLOUD self.cli_args["hosttype"] = HOST_TYPE @@ -75,39 +113,95 @@ def test_define_host_missing_model(self, define_fixture): except CliException as ex: assert str(ex) == "Missing argument: model" - def test_remove_host(self, remove_fixture): - self.cli_args["host"] = HOST + def test_remove_host(self, add_host): + self.cli_args["host"] = DEFINE_HOST - host = HostDao.get_host(HOST) + host = HostDao.get_host(DEFINE_HOST) assert host is not None - assert host.name == HOST + assert host.name == DEFINE_HOST self.quads_cli_call("rmhost") - host = HostDao.get_host(HOST) + host = HostDao.get_host(DEFINE_HOST) assert not host - def test_ls_host(self, remove_fixture): + def test_remove_host_no_arg(self, add_host): + self.cli_args["host"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmhost") + + assert str(ex.value) == "Missing parameter --host" + + @patch("quads.quads_api.QuadsApi.remove_host") + def test_remove_host_exception(self, mock_remove, add_host): + mock_remove.side_effect = APIServerException("Connection Error") + self.cli_args["host"] = DEFINE_HOST + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmhost") + + assert str(ex.value) == "Connection Error" + + def test_ls_host(self): self.quads_cli_call("ls_hosts") - assert self._caplog.messages[0] == "NEWHOST.example.com" - assert self._caplog.messages[1] == HOST + assert self._caplog.messages[0] == HOST1 + assert self._caplog.messages[1] == HOST2 - def test_ls_host_filter(self, remove_fixture): - self.cli_args["filter"] = f"model=={MODEL}" + def test_ls_host_filter(self): + self.cli_args["filter"] = f"model=={MODEL1}" self.quads_cli_call("ls_hosts") - assert self._caplog.messages[0] == HOST + assert self._caplog.messages[0] == HOST1 assert len(self._caplog.messages) == 1 - def test_ls_host_filter_interface(self, remove_fixture): - self.cli_args["filter"] = f"interfaces.switch_ip=={IFIP}" + def test_ls_host_filter_bool(self, mark_host_broken): + self.cli_args["filter"] = f"broken==true" self.quads_cli_call("ls_hosts") - assert self._caplog.messages[0] == HOST + assert self._caplog.messages[0] == HOST1 assert len(self._caplog.messages) == 1 - def test_ls_host_filter_bad_interface(self, remove_fixture): + def test_ls_host_filter_bool_false(self, mark_host_broken): + self.cli_args["filter"] = f"broken==false" + self.quads_cli_call("ls_hosts") + + assert self._caplog.messages[0] == HOST2 + assert len(self._caplog.messages) == 1 + + def test_ls_host_filter_bad_model(self, mark_host_broken): + self.cli_args["filter"] = f"model==BADMODEL" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_hosts") + assert str(ex.value) == "Model type not recognized." + + def test_ls_host_filter_bad_op(self): + self.cli_args["filter"] = f"model=BADMODEL" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_hosts") + assert ( + str(ex.value) + == "A filter was defined but not parsed correctly. Check filter operator." + ) + + def test_ls_host_filter_bad_param(self): + self.cli_args["filter"] = f"badparam==badvalue" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_hosts") + assert str(ex.value) == "badparam is not a valid field." + + def test_ls_host_filter_interface(self): + self.cli_args["filter"] = f"interfaces.switch_ip=={IFIP1}" + self.quads_cli_call("ls_hosts") + + assert self._caplog.messages[0] == HOST1 + assert len(self._caplog.messages) == 1 + + def test_ls_host_filter_bad_interface(self): self.cli_args["filter"] = f"interfaces.switch_ip==10.99.99.5" self.quads_cli_call("ls_hosts") @@ -118,9 +212,8 @@ def test_ls_no_host(self): assert self._caplog.messages[0] == "No hosts found." - def test_ls_processors(self, remove_fixture): - self.cli_args["host"] = HOST - ProcessorDao.create_processor(HOST, "P1", "Intel", "i7", 2, 4) + def test_ls_processors(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("processors") assert self._caplog.messages[0] == "processor: P1" @@ -129,7 +222,16 @@ def test_ls_processors(self, remove_fixture): assert self._caplog.messages[3] == " cores: 2" assert self._caplog.messages[4] == " threads: 4" - def test_ls_processors_no_host(self, remove_fixture): + @patch("quads.quads_api.requests.Session.get") + def test_ls_processors_exception(self, mock_get): + mock_get.return_value.status_code = 500 + self.cli_args["host"] = HOST1 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("processors") + assert str(ex.value) == "Check the flask server logs" + + def test_ls_processors_no_host(self): if self.cli_args.get("host"): self.cli_args.pop("host") with pytest.raises(CliException) as ex: @@ -140,19 +242,17 @@ def test_ls_processors_no_host(self, remove_fixture): == "Missing option. --host option is required for --ls-processors." ) - def test_ls_processors_no_processor(self, remove_fixture): - self.cli_args["host"] = HOST + def test_ls_processors_no_processor(self): + self.cli_args["host"] = HOST2 self.quads_cli_call("processors") - assert self._caplog.messages[0] == f"No processors defined for {HOST}" + assert self._caplog.messages[0] == f"No processors defined for {HOST2}" - def test_ls_memory(self, remove_fixture): - self.cli_args["host"] = HOST - MemoryDao.create_memory(HOST, "DIMM1", 2000) - MemoryDao.create_memory(HOST, "DIMM2", 2000) + def test_ls_memory(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("memory") assert self._caplog.messages[0] == "memory: DIMM1" - assert self._caplog.messages[1] == " size: 2000" + assert self._caplog.messages[1] == " size: 2048" assert self._caplog.messages[2] == "memory: DIMM2" - assert self._caplog.messages[3] == " size: 2000" + assert self._caplog.messages[3] == " size: 2048" diff --git a/tests/cli/test_interface.py b/tests/cli/test_interface.py index 76446572a..72639a5e9 100644 --- a/tests/cli/test_interface.py +++ b/tests/cli/test_interface.py @@ -1,61 +1,63 @@ +from unittest.mock import patch + import pytest from quads.exceptions import CliException -from quads.server.dao.cloud import CloudDao +from quads.quads_api import APIServerException from quads.server.dao.host import HostDao from quads.server.dao.interface import InterfaceDao +from quads.server.models import db from tests.cli.config import ( - HOST, - CLOUD, - IFNAME, - IFMAC, - IFIP, - IFPORT, - IFBIOSID, IFSPEED, - IFVENDOR, + HOST2, + IFNAME2, + IFMAC2, + IFIP2, + IFPORT2, + IFBIOSID2, + IFVENDOR2, + HOST1, + IFNAME1, + IFBIOSID1, + IFMAC1, + IFIP1, + IFPORT1, + IFVENDOR1, ) from tests.cli.test_base import TestBase def finalizer(): - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) if host: - HostDao.remove_host(name=HOST) - cloud = CloudDao.get_cloud(CLOUD) - if cloud: - CloudDao.remove_cloud(CLOUD) + for interface in host.interfaces: + InterfaceDao.delete_interface(interface.id) @pytest.fixture -def remove_fixture(request): +def remove_interface(request): request.addfinalizer(finalizer) - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - @pytest.fixture -def mod_fixture(request): +def mod_interface(request): request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) InterfaceDao.create_interface( - HOST, IFNAME, IFBIOSID, IFMAC, IFIP, IFPORT, IFSPEED, IFVENDOR, True, False + HOST2, + IFNAME2, + IFBIOSID2, + IFMAC2, + IFIP2, + IFPORT2, + IFSPEED, + IFVENDOR2, + False, + False, ) -@pytest.fixture -def noiface_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - - class TestInterface(TestBase): - def test_define_interface_no_params(self, remove_fixture): + def test_define_interface_no_params(self): try: self.quads_cli_call("addinterface") except CliException as ex: @@ -68,110 +70,193 @@ def test_define_interface_no_params(self, remove_fixture): "\t--interface-port" ) - def test_define_interface_missing_bios_id(self, remove_fixture): - self.cli_args["host"] = HOST - self.cli_args["ifname"] = IFNAME - self.cli_args["ifmac"] = IFMAC - self.cli_args["ifip"] = IFIP - self.cli_args["ifport"] = IFPORT + def test_define_interface_missing_bios_id(self): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifmac"] = IFMAC2 + self.cli_args["ifip"] = IFIP2 + self.cli_args["ifport"] = IFPORT2 try: self.quads_cli_call("addinterface") except CliException as ex: assert "Missing argument: bios_id" == str(ex) - def test_define_interface_missing_speed(self, remove_fixture): - self.cli_args["host"] = HOST - self.cli_args["ifname"] = IFNAME - self.cli_args["ifmac"] = IFMAC - self.cli_args["ifip"] = IFIP - self.cli_args["ifport"] = IFPORT - self.cli_args["ifbiosid"] = IFBIOSID + def test_define_interface_missing_speed(self): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifmac"] = IFMAC2 + self.cli_args["ifip"] = IFIP2 + self.cli_args["ifport"] = IFPORT2 + self.cli_args["ifbiosid"] = IFBIOSID2 try: self.quads_cli_call("addinterface") except CliException as ex: assert "Missing argument: speed" == str(ex) - def test_define_interface_missing_vendor(self, remove_fixture): - self.cli_args["host"] = HOST - self.cli_args["ifname"] = IFNAME - self.cli_args["ifmac"] = IFMAC - self.cli_args["ifip"] = IFIP - self.cli_args["ifport"] = IFPORT - self.cli_args["ifbiosid"] = IFBIOSID + def test_define_interface_missing_vendor(self): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifmac"] = IFMAC2 + self.cli_args["ifip"] = IFIP2 + self.cli_args["ifport"] = IFPORT2 + self.cli_args["ifbiosid"] = IFBIOSID2 self.cli_args["ifspeed"] = IFSPEED try: self.quads_cli_call("addinterface") except CliException as ex: assert "Missing argument: vendor" == str(ex) - def test_define_interface(self, remove_fixture): - self.cli_args["host"] = HOST - self.cli_args["ifname"] = IFNAME - self.cli_args["ifmac"] = IFMAC - self.cli_args["ifip"] = IFIP - self.cli_args["ifport"] = IFPORT - self.cli_args["ifbiosid"] = IFBIOSID + def test_define_interface(self, remove_interface): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifmac"] = IFMAC2 + self.cli_args["ifip"] = IFIP2 + self.cli_args["ifport"] = IFPORT2 + self.cli_args["ifbiosid"] = IFBIOSID2 self.cli_args["ifspeed"] = IFSPEED - self.cli_args["ifvendor"] = IFVENDOR + self.cli_args["ifvendor"] = IFVENDOR2 self.quads_cli_call("addinterface") - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) assert len(host.interfaces) == 1 - assert host.interfaces[0].name == IFNAME - assert host.interfaces[0].mac_address == IFMAC - assert host.interfaces[0].switch_ip == IFIP - assert host.interfaces[0].switch_port == IFPORT - assert host.interfaces[0].bios_id == IFBIOSID + assert host.interfaces[0].name == IFNAME2 + assert host.interfaces[0].mac_address == IFMAC2 + assert host.interfaces[0].switch_ip == IFIP2 + assert host.interfaces[0].switch_port == IFPORT2 + assert host.interfaces[0].bios_id == IFBIOSID2 assert host.interfaces[0].speed == IFSPEED - assert host.interfaces[0].vendor == IFVENDOR - assert not host.interfaces[0].pxe_boot + assert host.interfaces[0].vendor == IFVENDOR2 + assert host.interfaces[0].pxe_boot assert not host.interfaces[0].maintenance - def test_mod_interface(self, mod_fixture): - self.cli_args["host"] = HOST - self.cli_args["ifname"] = IFNAME + def test_mod_interface(self, mod_interface): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 self.cli_args["ifip"] = "192.168.0.1" self.quads_cli_call("modinterface") - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) assert len(host.interfaces) == 1 - assert host.interfaces[0].name == IFNAME + assert host.interfaces[0].name == IFNAME2 assert host.interfaces[0].switch_ip == "192.168.0.1" - def test_ls_interface(self, mod_fixture): - self.cli_args["host"] = HOST + def test_mod_interface_no_arg(self, mod_interface): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifmac"] = None + self.cli_args["ifip"] = None + self.cli_args["ifport"] = None + self.cli_args["ifbiosid"] = None + self.cli_args["ifspeed"] = None + self.cli_args["ifvendor"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modinterface") + + assert str(ex.value) == ( + "Missing options. At least one of these options are required for " + "--mod-interface:\n" + "\t--interface-name\n" + "\t--interface-bios-id\n" + "\t--interface-mac\n" + "\t--interface-ip\n" + "\t--interface-port\n" + "\t--interface-speed\n" + "\t--interface-vendor\n" + "\t--pxe-boot\n" + "\t--maintenance" + ) + + def test_mod_interface_no_host(self, mod_interface): + self.cli_args["host"] = None + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifip"] = "192.168.0.1" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modinterface") + + assert str(ex.value) == "Missing option. --host and --interface-name options are required for --mod-interface:" + + def test_mod_interface_bad_host(self, mod_interface): + self.cli_args["host"] = "BADHOST" + self.cli_args["ifname"] = IFNAME2 + self.cli_args["ifip"] = "192.168.0.1" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modinterface") + + assert str(ex.value) == "Host not found: BADHOST" + + def test_rm_interface(self, mod_interface): + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + + self.quads_cli_call("rminterface") + + host = HostDao.get_host(HOST2) + db.session.refresh(host) + assert len(host.interfaces) == 0 + + def test_rm_interface_no_host(self, mod_interface): + self.cli_args["host"] = None + self.cli_args["ifname"] = IFNAME2 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rminterface") + + assert str(ex.value) == "Missing option. --host and --interface-name options are required for --rm-interface" + + @patch("quads.quads_api.QuadsApi.remove_interface") + def test_rm_interface_exception(self, mock_remove, mod_interface): + mock_remove.side_effect = APIServerException("Connection Error") + self.cli_args["host"] = HOST2 + self.cli_args["ifname"] = IFNAME2 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rminterface") + + assert str(ex.value) == "Connection Error" + + def test_rm_interface_bad_host(self, mod_interface): + self.cli_args["host"] = "BADHOST" + self.cli_args["ifname"] = IFNAME2 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rminterface") + + assert str(ex.value) == "Host not found: BADHOST" + + def test_ls_interface(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("interface") - assert self._caplog.messages[0] == f"interface: {IFNAME}" - assert self._caplog.messages[1] == f" bios id: {IFBIOSID}" - assert self._caplog.messages[2] == f" mac address: {IFMAC}" - assert self._caplog.messages[3] == f" switch ip: {IFIP}" - assert self._caplog.messages[4] == f" port: {IFPORT}" + assert self._caplog.messages[0] == f"interface: {IFNAME1}" + assert self._caplog.messages[1] == f" bios id: {IFBIOSID1}" + assert self._caplog.messages[2] == f" mac address: {IFMAC1}" + assert self._caplog.messages[3] == f" switch ip: {IFIP1}" + assert self._caplog.messages[4] == f" port: {IFPORT1}" assert self._caplog.messages[5] == f" speed: {IFSPEED}" - assert self._caplog.messages[6] == f" vendor: {IFVENDOR}" + assert self._caplog.messages[6] == f" vendor: {IFVENDOR1}" assert self._caplog.messages[7] == " pxe_boot: True" assert self._caplog.messages[8] == " maintenance: False" - def test_ls_interface_missing_host(self, remove_fixture): + def test_ls_interface_missing_host(self): if self.cli_args.get("host"): self.cli_args.pop("host") with pytest.raises(CliException) as ex: self.quads_cli_call("interface") - assert ( - str(ex.value) - == "Missing option. --host option is required for --ls-interface." - ) + assert str(ex.value) == "Missing option. --host option is required for --ls-interface." - def test_ls_interface_bad_host(self, remove_fixture): + def test_ls_interface_bad_host(self): self.cli_args["host"] = "BADHOST" with pytest.raises(CliException) as ex: self.quads_cli_call("interface") assert str(ex.value) == "Host not found: BADHOST" - def test_ls_interface_noiface_host(self, noiface_fixture): - self.cli_args["host"] = HOST + def test_ls_interface_noiface_host(self): + self.cli_args["host"] = HOST2 self.quads_cli_call("interface") - assert self._caplog.messages[0] == f"No interfaces defined for {HOST}" + assert self._caplog.messages[0] == f"No interfaces defined for {HOST2}" diff --git a/tests/cli/test_memory.py b/tests/cli/test_memory.py index 08bbd33fb..f71c24330 100644 --- a/tests/cli/test_memory.py +++ b/tests/cli/test_memory.py @@ -1,54 +1,16 @@ import pytest from quads.exceptions import CliException -from quads.server.dao.cloud import CloudDao -from quads.server.dao.host import HostDao -from quads.server.dao.memory import MemoryDao from tests.cli.config import ( - HOST, - CLOUD, + HOST1, + HOST2, ) from tests.cli.test_base import TestBase -def finalizer(): - host = HostDao.get_host(HOST) - if host: - HostDao.remove_host(name=HOST) - cloud = CloudDao.get_cloud(CLOUD) - if cloud: - CloudDao.remove_cloud(CLOUD) - - -@pytest.fixture -def remove_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - - -@pytest.fixture -def mod_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - MemoryDao.create_memory(HOST, "DIMM1", 2048) - MemoryDao.create_memory(HOST, "DIMM2", 2048) - - -@pytest.fixture -def nomem_fixture(request): - request.addfinalizer(finalizer) - - CloudDao.create_cloud(CLOUD) - HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - - class TestMemory(TestBase): - def test_ls_memory(self, mod_fixture): - self.cli_args["host"] = HOST + def test_ls_memory(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("memory") @@ -57,7 +19,7 @@ def test_ls_memory(self, mod_fixture): assert self._caplog.messages[2] == f"memory: DIMM2" assert self._caplog.messages[3] == f" size: 2048" - def test_ls_memory_missing_host(self, remove_fixture): + def test_ls_memory_missing_host(self): if self.cli_args.get("host"): self.cli_args.pop("host") with pytest.raises(CliException) as ex: @@ -67,13 +29,13 @@ def test_ls_memory_missing_host(self, remove_fixture): == "Missing option. --host option is required for --ls-memory." ) - def test_ls_memory_bad_host(self, remove_fixture): + def test_ls_memory_bad_host(self): self.cli_args["host"] = "BADHOST" with pytest.raises(CliException) as ex: self.quads_cli_call("memory") assert str(ex.value) == "Host not found: BADHOST" - def test_ls_memory_nomem_host(self, nomem_fixture): - self.cli_args["host"] = HOST + def test_ls_memory_nomem_host(self): + self.cli_args["host"] = HOST2 self.quads_cli_call("memory") - assert self._caplog.messages[0] == f"No memory defined for {HOST}" + assert self._caplog.messages[0] == f"No memory defined for {HOST2}" diff --git a/tests/cli/test_metadata.py b/tests/cli/test_metadata.py new file mode 100644 index 000000000..2a52fc501 --- /dev/null +++ b/tests/cli/test_metadata.py @@ -0,0 +1,118 @@ +import os.path +from unittest.mock import patch + +import pytest + +from quads.exceptions import CliException +from quads.server.dao.cloud import CloudDao +from quads.server.dao.host import HostDao +from quads.server.models import db +from tests.cli.config import ( + CLOUD, + HOST_TYPE, + MODEL1, +) +from tests.cli.test_base import TestBase + + +def finalizer(): + host_import = HostDao.get_host("imported.example.com") + cloud_import = CloudDao.get_cloud("cloud42") + + if host_import: + HostDao.remove_host(name="imported.example.com") + + if cloud_import: + CloudDao.remove_cloud("cloud42") + + +@pytest.fixture +def define_fixture(request): + request.addfinalizer(finalizer) + + cloud_imported = CloudDao.create_cloud("cloud42") + host_import = HostDao.create_host("imported.example.com", MODEL1, HOST_TYPE, CLOUD) + + +class TestExport(TestBase): + def test_export(self, define_fixture): + + self.quads_cli_call("host_metadata_export") + + assert self._caplog.messages[0].startswith("Metadata successfully exported to ") + filename = self._caplog.messages[0].split()[-1][:-1] + assert os.path.exists(filename) + with open(filename, "r+") as f: + d = f.readlines() + f.seek(0) + for i in d: + if not i.startswith(" created_at: "): + f.write(i) + f.truncate() + + assert list(open(filename)) == list(open(os.path.join(os.path.dirname(__file__), "fixtures/metadata"))) + + +class TestImport(TestBase): + def test_import(self, define_fixture): + self.cli_args["metadata"] = os.path.join(os.path.dirname(__file__), "fixtures/metadata_import.yaml") + + self.quads_cli_call("define_host_metadata") + + assert self._caplog.messages[0] == ( + "imported.example.com [RECREATING]: ['broken', 'disks', 'host_type', " + "'interfaces', 'memory', 'model', 'retired']" + ) + host = HostDao.get_host("imported.example.com") + db.session.refresh(host) + assert host.broken + assert host.retired + assert host.default_cloud.name == "cloud42" + assert host.host_type == "imported" + assert host.model == "IMPORTED" + assert host.disks[0].disk_type == "NVME" + assert host.disks[0].size_gb == 4096 + assert host.disks[0].count == 10 + assert host.disks[1].disk_type == "SATA" + assert host.disks[1].size_gb == 4096 + assert host.disks[1].count == 5 + assert host.memory[0].handle == "DIMM1" + assert host.memory[0].size_gb == 4096 + assert host.interfaces[0].name == "em1" + assert host.interfaces[0].bios_id == "NIC.Intel.1" + assert host.interfaces[0].mac_address == "A0:B1:C2:D3:E4:F5" + assert not host.interfaces[0].maintenance + assert host.interfaces[0].switch_ip == "10.0.0.1" + assert host.interfaces[0].switch_port == "et-4:0/0" + assert host.interfaces[0].speed == 1000 + assert host.interfaces[0].vendor == "Intel" + + def test_import_no_arg(self, define_fixture): + self.cli_args["metadata"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("define_host_metadata") + assert str(ex.value) == "Missing option --metadata" + + def test_import_bad_host(self, define_fixture): + self.cli_args["metadata"] = os.path.join(os.path.dirname(__file__), "fixtures/badhost_metadata_import.yaml") + + with pytest.raises(CliException) as ex: + self.quads_cli_call("define_host_metadata") + assert str(ex.value) == "Host not found: badhost.example.com" + + def test_import_bad_path(self, define_fixture): + self.cli_args["metadata"] = "this/path/should/never/exist.exe" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("define_host_metadata") + assert str(ex.value) == "The path for the --metadata yaml is not valid" + + @patch("quads.cli.cli.open") + def test_import_io_error(self, mock_load, define_fixture): + mock_load.side_effect = IOError("IOError") + self.cli_args["metadata"] = os.path.join(os.path.dirname(__file__), "fixtures/metadata_import.yaml") + + with pytest.raises(CliException) as ex: + self.quads_cli_call("define_host_metadata") + assert str(ex.value) == f"There was something wrong reading from {self.cli_args['metadata']}" diff --git a/tests/cli/test_notify.py b/tests/cli/test_notify.py new file mode 100644 index 000000000..c7c682b00 --- /dev/null +++ b/tests/cli/test_notify.py @@ -0,0 +1,49 @@ +from unittest.mock import patch + +from quads.config import Config +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.baseDao import BaseDao +from quads.server.dao.cloud import CloudDao +from quads.server.models import db +from tests.cli.config import NetcatStub +from tests.cli.test_base import TestBase + + +class TestNotify(TestBase): + @patch("quads.tools.external.postman.SMTP") + def test_notify_not_validated(self, mocked_smtp): + Config.__setattr__("foreman_unavailable", True) + mocked_smtp() + + self.quads_cli_call("notify") + + cloud = CloudDao.get_cloud(name="cloud99") + ass = AssignmentDao.get_active_cloud_assignment(cloud=cloud) + db.session.refresh(ass) + assert ass.notification.pre_initial is True + assert self._caplog.messages == [ + "=============== Future Initial Message", + "=============== Future Initial Message", + "Notifications sent out.", + ] + + @patch("quads.tools.notify.Netcat", NetcatStub) + @patch("quads.tools.external.postman.SMTP") + def test_notify_validated(self, mocked_smtp): + Config.__setattr__("foreman_unavailable", True) + Config.__setattr__("webhook_notify", True) + mocked_smtp() + cloud = CloudDao.get_cloud(name="cloud99") + ass = AssignmentDao.get_active_cloud_assignment(cloud=cloud) + setattr(ass, "validated", True) + BaseDao.safe_commit() + + self.quads_cli_call("notify") + db.session.refresh(ass) + assert ass.notification.pre_initial is True + assert ass.notification.initial is True + assert self._caplog.messages == [ + "=============== Initial Message", + "Beep boop we can't communicate with your webhook.", + "Notifications sent out.", + ] diff --git a/tests/cli/test_quads.py b/tests/cli/test_quads.py index 9adbcc364..eaf097731 100644 --- a/tests/cli/test_quads.py +++ b/tests/cli/test_quads.py @@ -1,77 +1,64 @@ -from datetime import datetime, timedelta +from datetime import datetime +from unittest.mock import patch import pytest from quads.config import Config -from quads.server.dao.assignment import AssignmentDao -from quads.server.dao.cloud import CloudDao -from quads.server.dao.host import HostDao -from quads.server.dao.schedule import ScheduleDao -from quads.server.dao.vlan import VlanDao -from tests.cli.config import HOST, CLOUD +from quads.exceptions import CliException +from quads.quads_api import APIServerException +from tests.cli.config import CLOUD, DEFAULT_CLOUD, HOST2, MOD_CLOUD, HOST1 from tests.cli.test_base import TestBase -def finalizer(): - host = HostDao.get_host(HOST) - cloud = CloudDao.get_cloud(CLOUD) - default_cloud = CloudDao.get_cloud("cloud01") - schedules = ScheduleDao.get_current_schedule(host=host, cloud=cloud) - - if schedules: - ScheduleDao.remove_schedule(schedules[0].id) - AssignmentDao.remove_assignment(schedules[0].assignment_id) - - assignments = AssignmentDao.get_assignments() - for ass in assignments: - AssignmentDao.remove_assignment(ass.id) - - if host: - HostDao.remove_host(name=HOST) - - if cloud: - CloudDao.remove_cloud(CLOUD) - - if default_cloud: - CloudDao.remove_cloud("cloud01") - - -@pytest.fixture -def remove_fixture(request): - request.addfinalizer(finalizer) - - today = datetime.now() - tomorrow = today + timedelta(days=1) - - cloud = CloudDao.create_cloud(CLOUD) - default_cloud = CloudDao.create_cloud("cloud01") - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - vlan = VlanDao.create_vlan( - "192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1 - ) - assignment = AssignmentDao.create_assignment( - "test", "test", "1234", 0, False, [""], vlan.vlan_id, cloud.name - ) - schedule = ScheduleDao.create_schedule( - today.strftime("%Y-%m-%d %H:%M"), - tomorrow.strftime("%Y-%m-%d %H:%M"), - assignment, - host, - ) - assert schedule +class TestQuads(TestBase): + def test_default_action(self): + # TODO: Check host duplication here + self.quads_cli_call(None) + assert self._caplog.messages[0] == f"{DEFAULT_CLOUD}:" + assert self._caplog.messages[1] == f" - {HOST2}" -class TestQuads(TestBase): - def test_default_action(self, remove_fixture): + @patch("quads.quads_api.QuadsApi.is_available") + def test_default_action_available_exception(self, mock_is_available): + mock_is_available.side_effect = APIServerException("Connection Error") + # TODO: Check host duplication here + with pytest.raises(CliException) as ex: + self.quads_cli_call(None) + assert str(ex.value) == "Connection Error" + @patch("quads.quads_api.QuadsApi.get_current_schedules") + def test_default_action_schedules_exception(self, mock_current_schedules): + mock_current_schedules.side_effect = APIServerException("Connection Error") # TODO: Check host duplication here + with pytest.raises(CliException) as ex: + self.quads_cli_call(None) + assert str(ex.value) == "Connection Error" + + @patch("quads.quads_api.requests.Session.get") + def test_default_action_500_exception(self, mock_get): + mock_get.return_value.status_code = 500 + with pytest.raises(CliException) as ex: + self.quads_cli_call(None) + assert str(ex.value) == "Check the flask server logs" + + @patch("quads.quads_api.requests.Session.get") + def test_default_action_400_exception(self, mock_get): + mock_get.return_value.status_code = 400 + with pytest.raises(CliException) as ex: + self.quads_cli_call(None) + + def test_default_action_date(self): + date = datetime.now().strftime("%Y-%m-%d") + self.cli_args["datearg"] = f"{date} 22:00" self.quads_cli_call(None) - assert self._caplog.messages[0] == f"{CLOUD}:" - assert self._caplog.messages[1] == f" - host.example.com" - def test_version(self, remove_fixture): + def test_version(self): self.quads_cli_call("version") - assert ( - self._caplog.messages[0] - == f'"QUADS version {Config.QUADSVERSION} {Config.QUADSCODENAME}"\n' - ) + assert self._caplog.messages[0] == f'"QUADS version {Config.QUADSVERSION} {Config.QUADSCODENAME}"\n' + + @patch("quads.quads_api.requests.Session.get") + def test_version_exception(self, mock_get): + mock_get.return_value.status_code = 500 + with pytest.raises(CliException) as ex: + self.quads_cli_call("version") + assert str(ex.value) == "Check the flask server logs" diff --git a/tests/cli/test_regen.py b/tests/cli/test_regen.py new file mode 100644 index 000000000..f0a5de85c --- /dev/null +++ b/tests/cli/test_regen.py @@ -0,0 +1,47 @@ +import os + +from datetime import datetime +from unittest.mock import patch + +from quads.config import Config +from tests.cli.test_base import TestBase +from tests.tools.test_wiki import WikiStub + + +class TestRegen(TestBase): + def test_regen_heatmap(self): + Config.__setattr__("foreman_unavailable", True) + Config.__setattr__("visual_web_dir", os.path.join(os.path.dirname(__file__), "artifacts/")) + self.quads_cli_call("regen_heatmap") + + files = ["index.html", "current.html", "next.html", f"{datetime.now().strftime('%Y-%m')}.html"] + for f in files: + assert os.path.exists(os.path.join(os.path.dirname(__file__), f"artifacts/{f}")) + assert self._caplog.messages == ["Regenerated web table heatmap."] + + def test_regen_instack(self): + Config.__setattr__("foreman_unavailable", True) + Config.__setattr__("openstack_management", True) + Config.__setattr__("openshift_management", True) + Config.__setattr__("json_web_path", os.path.join(os.path.dirname(__file__), "artifacts/")) + self.quads_cli_call("regen_instack") + + files = ["cloud99_ocpinventory.json", "cloud99_instackenv.json"] + for f in files: + assert os.path.exists(os.path.join(os.path.dirname(__file__), f"artifacts/{f}")) + assert self._caplog.messages == [ + "Regenerated 'instackenv' for OpenStack Management.", + "Regenerated 'ocpinventory' for OpenShift Management.", + ] + + @patch("quads.tools.regenerate_wiki.Wiki", WikiStub) + @patch("quads.tools.regenerate_vlans_wiki.Wiki", WikiStub) + def test_regen_wiki(self): + Config.__setattr__("foreman_unavailable", True) + Config.__setattr__("wp_wiki_git_repo_path", os.path.join(os.path.dirname(__file__), "artifacts/git/wiki")) + self.quads_cli_call("regen_wiki") + + files = ["assignments.md", "main.md"] + for f in files: + assert os.path.exists(os.path.join(os.path.dirname(__file__), f"artifacts/git/wiki/{f}")) + assert "Regenerated wiki." in self._caplog.messages diff --git a/tests/cli/test_report.py b/tests/cli/test_report.py new file mode 100644 index 000000000..4578592bf --- /dev/null +++ b/tests/cli/test_report.py @@ -0,0 +1,59 @@ +import pytest + +from datetime import datetime, timedelta +from quads.exceptions import CliException +from tests.cli.test_base import TestBase + + +class TestReport(TestBase): + def test_report_available(self): + self.quads_cli_call("report_available") + assert self._caplog.messages[0].startswith("QUADS report for ") + assert self._caplog.messages[1] == "Percentage Utilized: 23%" + assert self._caplog.messages[2] == "Average build delta: 0:00:00" + assert self._caplog.messages[3] == "Server Type | Total| Free| Scheduled| 2 weeks| 4 weeks" + assert self._caplog.messages[4] == "host2 | 1| 1| 0%| 1| 1" + assert self._caplog.messages[5] == "host1 | 1| 0| 100%| 1| 0" + + def test_report_scheduled(self): + # TODO: Fix this test + self.cli_args["months"] = 12 + self.cli_args["year"] = None + self.quads_cli_call("report_scheduled") + assert self._caplog.messages[0] == "Month | Scheduled| Systems| % Utilized| " + assert self._caplog.messages[1] == "2023-10 | 1| 2| 23%| " + assert self._caplog.messages[2] == "2023-09 | 0| 2| 0%| " + + def test_report_scheduled_no_args(self): + self.cli_args["months"] = None + self.cli_args["year"] = None + with pytest.raises(CliException) as ex: + self.quads_cli_call("report_scheduled") + + assert str(ex.value) == "Missing argument. --months or --year must be provided." + + def test_report_scheduled_year(self): + today = datetime.now() + # TODO: Fix this test + self.cli_args["months"] = None + self.cli_args["year"] = today.year + self.quads_cli_call("report_scheduled") + assert self._caplog.messages[0] == "Month | Scheduled| Systems| % Utilized| " + assert self._caplog.messages[1] == f"{today.year}-{today.month:02d} | 1| 2| 23%| " + assert self._caplog.messages[2] == f"{today.year}-{today.month - 1:02d} | 0| 2| 0%| " + + def test_report_detailed(self): + today = datetime.now() + + future = today + timedelta(weeks=2) + # TODO: Fix this test + self.cli_args["months"] = None + self.cli_args["year"] = today.year + self.quads_cli_call("report_detailed") + assert ( + self._caplog.messages[0] == "Owner | Ticket| Cloud| Description| Systems| Scheduled| Duration| " + ) + assert ( + self._caplog.messages[1] + == f"test | 1234| cloud99| test| 1| {today.strftime('%Y-%m-%d')}| 14| " + ) diff --git a/tests/cli/test_retired.py b/tests/cli/test_retired.py index 900a2e928..35278ddb5 100644 --- a/tests/cli/test_retired.py +++ b/tests/cli/test_retired.py @@ -1,89 +1,91 @@ +from unittest.mock import patch + import pytest -from quads.server.dao.cloud import CloudDao +from quads.exceptions import CliException from quads.server.dao.host import HostDao -from tests.cli.config import CLOUD, HOST +from quads.server.models import db +from tests.cli.config import HOST1 from tests.cli.test_base import TestBase def finalizer(): - host = HostDao.get_host(HOST) - if host: - HostDao.remove_host(HOST) - cloud = CloudDao.get_cloud(CLOUD) - if cloud: - CloudDao.remove_cloud(name=CLOUD) + HostDao.update_host(HOST1, retired=False) @pytest.fixture -def mb_fixture(request): +def unretire(request): request.addfinalizer(finalizer) - cloud = CloudDao.create_cloud(CLOUD) - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - @pytest.fixture -def mr_fixture(request): +def retire(request): request.addfinalizer(finalizer) - - cloud = CloudDao.create_cloud(CLOUD) - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - assert HostDao.update_host(name=HOST, retired=True) + HostDao.update_host(HOST1, retired=True) class TestRetired(TestBase): - def test_mark_retired(self, mb_fixture): - self.cli_args["host"] = HOST + def test_mark_retired(self, unretire): + self.cli_args["host"] = HOST1 self.quads_cli_call("retire") - assert self._caplog.messages[0] == f"Host {HOST} is now marked as retired" + assert self._caplog.messages[0] == f"Host {HOST1} is now marked as retired" - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) + db.session.refresh(host) assert host.retired - def test_mark_retired_already(self, mr_fixture): - self.cli_args["host"] = HOST + def test_mark_retired_already(self, retire): + self.cli_args["host"] = HOST1 self.quads_cli_call("retire") assert ( self._caplog.messages[0] - == f"Host {HOST} has already been marked as retired" + == f"Host {HOST1} has already been marked as retired" ) - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert host.retired - def test_mark_unretired(self, mr_fixture): - self.cli_args["host"] = HOST + def test_mark_unretired(self, retire): + self.cli_args["host"] = HOST1 self.quads_cli_call("unretire") - assert self._caplog.messages[0] == f"Host {HOST} is now marked as unretired" + assert self._caplog.messages[0] == f"Host {HOST1} is now marked as unretired" - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert not host.retired - def test_mark_unretired_already(self, mb_fixture): - self.cli_args["host"] = HOST + def test_mark_unretired_already(self): + self.cli_args["host"] = HOST1 self.quads_cli_call("unretire") assert ( - self._caplog.messages[0] == f"Host {HOST} has already been marked unretired" + self._caplog.messages[0] + == f"Host {HOST1} has already been marked unretired" ) - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert not host.retired - def test_ls_retired(self, mr_fixture): - self.cli_args["host"] = HOST + def test_ls_retired(self, retire): + self.cli_args["host"] = HOST1 self.quads_cli_call("ls_retired") - assert self._caplog.messages[0] == HOST + assert self._caplog.messages[0] == HOST1 - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST1) assert host.retired + + @patch("quads.quads_api.requests.Session.get") + def test_ls_retired_exception(self, mock_get, retire): + mock_get.return_value.status_code = 500 + self.cli_args["host"] = HOST1 + with pytest.raises(CliException) as ex: + self.quads_cli_call("ls_retired") + assert str(ex.value) == "Check the flask server logs" diff --git a/tests/cli/test_schedule.py b/tests/cli/test_schedule.py index 59af25ed8..046c0a234 100644 --- a/tests/cli/test_schedule.py +++ b/tests/cli/test_schedule.py @@ -1,54 +1,39 @@ from datetime import datetime, timedelta +from unittest.mock import patch import pytest +from quads.config import Config from quads.exceptions import CliException +from quads.quads_api import APIServerException from quads.server.dao.assignment import AssignmentDao from quads.server.dao.cloud import CloudDao from quads.server.dao.host import HostDao from quads.server.dao.schedule import ScheduleDao from quads.server.dao.vlan import VlanDao from quads.server.models import db -from tests.cli.config import HOST, CLOUD +from tests.cli.config import CLOUD, HOST2, HOST1, DEFAULT_CLOUD, MOD_CLOUD, MODEL2, DEFINE_HOST from tests.cli.test_base import TestBase def finalizer(): - host = HostDao.get_host(HOST) cloud = CloudDao.get_cloud(CLOUD) - default_cloud = CloudDao.get_cloud("cloud01") + host = HostDao.get_host(HOST2) schedules = ScheduleDao.get_current_schedule(host=host, cloud=cloud) if schedules: ScheduleDao.remove_schedule(schedules[0].id) AssignmentDao.remove_assignment(schedules[0].assignment_id) - assignments = AssignmentDao.get_assignments() - for ass in assignments: - AssignmentDao.remove_assignment(ass.id) - - if host: - HostDao.remove_host(name=HOST) - - if cloud: - CloudDao.remove_cloud(CLOUD) - - if default_cloud: - CloudDao.remove_cloud("cloud01") - @pytest.fixture def define_fixture(request): request.addfinalizer(finalizer) - cloud = CloudDao.create_cloud(CLOUD) - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - vlan = VlanDao.create_vlan( - "192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1 - ) - AssignmentDao.create_assignment( - "test", "test", "1234", 0, False, [""], vlan.vlan_id, cloud.name - ) + cloud = CloudDao.get_cloud(CLOUD) + host = HostDao.get_host(HOST2) + vlan = VlanDao.create_vlan("192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1) + AssignmentDao.create_assignment("test", "test", "1234", 0, False, [""], cloud.name, vlan.vlan_id) @pytest.fixture @@ -56,17 +41,12 @@ def remove_fixture(request): request.addfinalizer(finalizer) today = datetime.now() - tomorrow = today + timedelta(days=1) + tomorrow = today + timedelta(weeks=2) - cloud = CloudDao.create_cloud(CLOUD) - default_cloud = CloudDao.create_cloud("cloud01") - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - vlan = VlanDao.create_vlan( - "192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1 - ) - assignment = AssignmentDao.create_assignment( - "test", "test", "1234", 0, False, [""], vlan.vlan_id, cloud.name - ) + cloud = CloudDao.get_cloud(CLOUD) + host = HostDao.get_host(HOST2) + vlan = VlanDao.create_vlan("192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1) + assignment = AssignmentDao.create_assignment("test", "test", "1234", 0, False, [""], cloud.name, vlan.vlan_id) schedule = ScheduleDao.create_schedule( today.strftime("%Y-%m-%d %H:%M"), tomorrow.strftime("%Y-%m-%d %H:%M"), @@ -77,35 +57,101 @@ def remove_fixture(request): class TestSchedule(TestBase): - def test_add_schedule(self, define_fixture): + def test_add_schedule(self): today = datetime.now() tomorrow = today + timedelta(days=1) self.cli_args["schedstart"] = today.strftime("%Y-%m-%d %H:%M") self.cli_args["schedend"] = tomorrow.strftime("%Y-%m-%d %H:%M") self.cli_args["schedcloud"] = CLOUD - self.cli_args["host"] = HOST + self.cli_args["host"] = HOST2 self.cli_args["omitcloud"] = None self.quads_cli_call("add_schedule") - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) + assert schedule + + def test_add_schedule_omit(self): + today = datetime.now() + tomorrow = today + timedelta(days=1) + self.cli_args["schedstart"] = today.strftime("%Y-%m-%d %H:%M") + self.cli_args["schedend"] = tomorrow.strftime("%Y-%m-%d %H:%M") + self.cli_args["schedcloud"] = CLOUD + self.cli_args["host"] = HOST2 + self.cli_args["omitcloud"] = DEFAULT_CLOUD + + self.quads_cli_call("add_schedule") + host = HostDao.get_host(HOST2) cloud = CloudDao.get_cloud(CLOUD) schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) assert schedule + def test_add_missing_args(self): + self.cli_args["schedstart"] = None + self.cli_args["schedend"] = None + self.cli_args["schedcloud"] = None + self.cli_args["host"] = HOST2 + self.cli_args["omitcloud"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("add_schedule") + assert str(ex.value) == ( + "Missing option. All of these options are required for --add-schedule:\n" + "\t--schedule-start\n" + "\t--schedule-end\n" + "\t--schedule-cloud" + ) + + def test_add_missing_target(self): + today = datetime.now() + tomorrow = today + timedelta(days=1) + self.cli_args["schedstart"] = today.strftime("%Y-%m-%d %H:%M") + self.cli_args["schedend"] = tomorrow.strftime("%Y-%m-%d %H:%M") + self.cli_args["schedcloud"] = CLOUD + self.cli_args["host"] = None + self.cli_args["host_list"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("add_schedule") + assert str(ex.value) == "Missing option. --host or --host-list required." + def test_remove_schedule(self, remove_fixture): - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) cloud = CloudDao.get_cloud(CLOUD) schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) self.cli_args["schedid"] = schedule[0].id - self.cli_args["host"] = HOST + self.cli_args["host"] = HOST2 self.quads_cli_call("rmschedule") schedule = ScheduleDao.get_schedule(int(self.cli_args["schedid"])) assert not schedule + def test_remove_schedule_no_id(self, remove_fixture): + self.cli_args["schedid"] = None + self.cli_args["host"] = HOST2 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmschedule") + assert str(ex.value) == "Missing option --schedule-id." + + @patch("quads.quads_api.QuadsApi.remove_schedule") + def test_remove_exception(self, mock_remove, remove_fixture): + mock_remove.side_effect = APIServerException("Connection Error") + host = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) + + self.cli_args["schedid"] = schedule[0].id + self.cli_args["host"] = HOST2 + + with pytest.raises(CliException) as ex: + self.quads_cli_call("rmschedule") + assert str(ex.value) == "Connection Error" + def test_mod_schedule(self, remove_fixture): - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) cloud = CloudDao.get_cloud(CLOUD) _schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) @@ -119,16 +165,53 @@ def test_mod_schedule(self, remove_fixture): schedule_obj = ScheduleDao.get_schedule(_schedule[0].id) db.session.refresh(schedule_obj) - assert schedule_obj.end.strftime("%Y-%m-%dT%H:%M") == atomorrow.strftime( - "%Y-%m-%dT%H:%M" + assert schedule_obj.end.strftime("%Y-%m-%dT%H:%M") == atomorrow.strftime("%Y-%m-%dT%H:%M") + + def test_mod_schedule_no_args(self, remove_fixture): + self.cli_args["schedstart"] = None + self.cli_args["schedend"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("modschedule") + + assert str(ex.value) == ( + "Missing option. At least one these options are required for --mod-schedule:\n" + "\t--schedule-start\n" + "\t--schedule-end" ) def test_ls_schedule(self, remove_fixture): - self.cli_args["host"] = HOST + self.cli_args["host"] = HOST2 self.quads_cli_call("schedule") assert self._caplog.messages[0] == f"Default cloud: {CLOUD}" assert self._caplog.messages[1] == f"Current cloud: {CLOUD}" - assert self._caplog.messages[2].startswith("1| start=") + assert self._caplog.messages[2][1:].startswith("| start=") + + def test_ls_schedule_date(self, remove_fixture): + # TODO: verify this one + date = datetime.now().strftime("%Y-%m-%d") + self.cli_args["host"] = HOST2 + self.cli_args["datearg"] = f"{date} 22:00" + self.quads_cli_call("schedule") + assert self._caplog.messages[0] == f"Default cloud: {CLOUD}" + assert self._caplog.messages[1] == f"Current cloud: {CLOUD}" + assert self._caplog.messages[2][1:].startswith("| start=") + + @patch("quads.quads_api.QuadsApi.get_current_schedules") + def test_ls_schedule_exception(self, mock_get, remove_fixture): + mock_get.side_effect = APIServerException("Connection Error") + # TODO: verify this one + date = datetime.now().strftime("%Y-%m-%d") + self.cli_args["host"] = HOST2 + self.cli_args["datearg"] = f"{date} 22:00" + with pytest.raises(CliException) as ex: + self.quads_cli_call("schedule") + assert str(ex.value) == "Connection Error" + + def test_host(self, remove_fixture): + self.cli_args["host"] = HOST2 + self.quads_cli_call("host") + assert self._caplog.messages[0] == f"{CLOUD}" def test_ls_schedule_bad_host(self, remove_fixture): self.cli_args["host"] = "BADHOST" @@ -141,26 +224,244 @@ def test_ls_schedule_no_host(self, remove_fixture): if self.cli_args.get("host"): self.cli_args.pop("host") self.quads_cli_call("schedule") - assert self._caplog.messages[0] == f"{CLOUD}:" - assert self._caplog.messages[1] == HOST + assert self._caplog.messages[0] == f"{DEFAULT_CLOUD}:" + assert self._caplog.messages[1] == f"{MOD_CLOUD}:" + assert self._caplog.messages[2] == f"{CLOUD}:" + assert self._caplog.messages[3] == HOST1 class TestExtend(TestBase): def test_extend_schedule(self, remove_fixture): - host = HostDao.get_host(HOST) + host = HostDao.get_host(HOST2) cloud = CloudDao.get_cloud(CLOUD) _schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) today = datetime.now() - atomorrow = today + timedelta(weeks=2) + timedelta(days=1) + atomorrow = today + timedelta(weeks=4) self.cli_args["weeks"] = 2 - self.cli_args["host"] = HOST + self.cli_args["host"] = HOST2 self.quads_cli_call("extend") schedule_obj = ScheduleDao.get_schedule(_schedule[0].id) db.session.refresh(schedule_obj) - assert schedule_obj.end.strftime("%Y-%m-%d %H:%M") == atomorrow.strftime( - "%Y-%m-%d %H:%M" - ) + assert schedule_obj.end.strftime("%Y-%m-%d %H:%M") == atomorrow.strftime("%Y-%m-%d %H:%M") + + def test_extend_schedule_no_schedule(self, define_fixture): + self.cli_args["weeks"] = 2 + self.cli_args["host"] = None + self.cli_args["cloud"] = MOD_CLOUD + + self.quads_cli_call("extend") + + assert self._caplog.messages[0] == "The selected cloud does not have any active schedules" + + def test_extend_no_dates(self): + self.cli_args["weeks"] = None + self.cli_args["datearg"] = None + self.cli_args["host"] = HOST2 + self.cli_args["cloud"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("extend") + assert str(ex.value) == "Missing option. Need --weeks or --date when using --extend" + + def test_extend_no_target(self): + self.cli_args["weeks"] = 2 + self.cli_args["datearg"] = None + self.cli_args["cloud"] = None + self.cli_args["host"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("extend") + assert str(ex.value) == "Missing option. At least one of either --host or --cloud is required." + + def test_extend_bad_weeks(self): + self.cli_args["weeks"] = "BADWEEKS" + self.cli_args["datearg"] = None + self.cli_args["host"] = HOST2 + self.cli_args["cloud"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("extend") + assert str(ex.value) == "The value of --weeks must be an integer" + + def test_extend_bad_host(self): + self.cli_args["weeks"] = 2 + self.cli_args["datearg"] = None + self.cli_args["cloud"] = None + self.cli_args["host"] = "BADHOST" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("extend") + assert str(ex.value) == "Host not found: BADHOST" + + +class TestShrink(TestBase): + @patch("quads.cli.cli.input") + def test_shrink_schedule(self, mock_input, remove_fixture): + mock_input.return_value = "y" + host = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + _schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) + + today = datetime.now() + atomorrow = today + timedelta(weeks=1) + + self.cli_args["weeks"] = 1 + self.cli_args["host"] = HOST2 + + self.quads_cli_call("shrink") + schedule_obj = ScheduleDao.get_schedule(_schedule[0].id) + db.session.refresh(schedule_obj) + + assert schedule_obj.end.strftime("%Y-%m-%d %H:%M") == atomorrow.strftime("%Y-%m-%d %H:%M") + + @patch("quads.cli.cli.input") + def test_shrink_date(self, mock_input, remove_fixture): + mock_input.return_value = "y" + host = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + _schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) + + today = datetime.now() + atomorrow = today + timedelta(weeks=1) + + self.cli_args["datearg"] = atomorrow.strftime("%Y-%m-%d %H:%M") + self.cli_args["host"] = HOST2 + + self.quads_cli_call("shrink") + schedule_obj = ScheduleDao.get_schedule(_schedule[0].id) + db.session.refresh(schedule_obj) + + assert schedule_obj.end.strftime("%Y-%m-%d %H:%M") == atomorrow.strftime("%Y-%m-%d %H:%M") + + @patch("quads.cli.cli.input") + def test_shrink_now(self, mock_input, remove_fixture): + mock_input.return_value = "y" + host = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + _schedule = ScheduleDao.get_current_schedule(host=host, cloud=cloud) + + today = datetime.now() + + self.cli_args["datearg"] = None + self.cli_args["weeks"] = None + self.cli_args["now"] = True + self.cli_args["host"] = HOST2 + + self.quads_cli_call("shrink") + schedule_obj = ScheduleDao.get_schedule(_schedule[0].id) + db.session.refresh(schedule_obj) + + assert schedule_obj.end.strftime("%Y-%m-%d") == today.strftime("%Y-%m-%d") + + def test_shrink_no_dates(self): + self.cli_args["weeks"] = None + self.cli_args["datearg"] = None + self.cli_args["now"] = False + self.cli_args["host"] = HOST2 + self.cli_args["cloud"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("shrink") + assert str(ex.value) == "Missing option. Need --weeks, --date or --now when using --shrink" + + def test_shrink_no_target(self): + self.cli_args["weeks"] = 2 + self.cli_args["datearg"] = None + self.cli_args["cloud"] = None + self.cli_args["host"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("shrink") + assert str(ex.value) == "Missing option. At least one of either --host or --cloud is required" + + def test_shrink_bad_weeks(self): + self.cli_args["weeks"] = "BADWEEKS" + self.cli_args["datearg"] = None + self.cli_args["host"] = HOST2 + self.cli_args["cloud"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("shrink") + assert str(ex.value) == "The value of --weeks must be an integer" + + def test_shrink_bad_host(self): + self.cli_args["weeks"] = 2 + self.cli_args["datearg"] = None + self.cli_args["cloud"] = None + self.cli_args["host"] = "BADHOST" + + with pytest.raises(CliException) as ex: + self.quads_cli_call("shrink") + assert str(ex.value) == "Host not found: BADHOST" + + +class TestAvailable(TestBase): + @patch.object( + Config, + "spare_pool_name", + CLOUD, + ) + def test_available(self, define_fixture): + self.cli_args["schedstart"] = None + self.cli_args["schedend"] = None + self.cli_args["schedcloud"] = None + self.cli_args["omitcloud"] = None + self.cli_args["filter"] = None + + self.quads_cli_call("available") + assert self._caplog.messages[0] == f"{HOST2}" + + @patch.object( + Config, + "spare_pool_name", + CLOUD, + ) + def test_available_filter(self, define_fixture): + self.cli_args["schedstart"] = None + self.cli_args["schedend"] = None + self.cli_args["schedcloud"] = None + self.cli_args["omitcloud"] = None + self.cli_args["filter"] = f"model=={MODEL2}" + + self.quads_cli_call("available") + assert self._caplog.messages[0] == f"{HOST2}" + assert len(self._caplog.messages) == 1 + + @patch.object( + Config, + "spare_pool_name", + CLOUD, + ) + def test_available_dates(self, define_fixture): + today = datetime.now().strftime("%Y-%m-%d") + self.cli_args["schedstart"] = f"{today} 22:00" + self.cli_args["schedend"] = f"{today} 22:00" + self.cli_args["omitcloud"] = None + self.cli_args["filter"] = None + + self.quads_cli_call("available") + assert self._caplog.messages[0] == f"{HOST2}" + assert len(self._caplog.messages) == 1 + + @patch.object( + Config, + "spare_pool_name", + CLOUD, + ) + @patch("quads.quads_api.QuadsApi.filter_hosts") + def test_available_exception(self, mock_filter, define_fixture): + mock_filter.side_effect = APIServerException("Connection Error") + today = datetime.now().strftime("%Y-%m-%d") + self.cli_args["schedstart"] = f"{today} 22:00" + self.cli_args["schedend"] = f"{today} 22:00" + self.cli_args["omitcloud"] = None + self.cli_args["filter"] = None + + with pytest.raises(CliException) as ex: + self.quads_cli_call("available") + + assert str(ex.value) == "Connection Error" diff --git a/tests/cli/test_summary.py b/tests/cli/test_summary.py index 90d9ebd91..35f44eafc 100644 --- a/tests/cli/test_summary.py +++ b/tests/cli/test_summary.py @@ -1,75 +1,33 @@ -from datetime import timedelta, datetime - -import pytest - -from quads.server.dao.assignment import AssignmentDao -from quads.server.dao.cloud import CloudDao -from quads.server.dao.host import HostDao -from quads.server.dao.schedule import ScheduleDao -from quads.server.dao.vlan import VlanDao -from tests.cli.config import CLOUD, HOST +from datetime import datetime from tests.cli.test_base import TestBase -def finalizer(): - host = HostDao.get_host(HOST) - cloud = CloudDao.get_cloud(CLOUD) - schedules = ScheduleDao.get_current_schedule(host=host, cloud=cloud) - - if schedules: - ScheduleDao.remove_schedule(schedules[0].id) - AssignmentDao.remove_assignment(schedules[0].assignment_id) - - assignments = AssignmentDao.get_assignments() - for ass in assignments: - AssignmentDao.remove_assignment(ass.id) - - if host: - HostDao.remove_host(name=HOST) - - if cloud: - CloudDao.remove_cloud(CLOUD) - CloudDao.remove_cloud("cloud03") - - -@pytest.fixture -def remove_fixture(request): - request.addfinalizer(finalizer) - - today = datetime.now() - tomorrow = today + timedelta(days=1) - - cloud = CloudDao.create_cloud(CLOUD) - cloud03 = CloudDao.create_cloud("cloud03") - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - vlan = VlanDao.create_vlan( - "192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1 - ) - assignment = AssignmentDao.create_assignment( - "test", "test", "1234", 0, False, [""], vlan.vlan_id, cloud.name - ) - schedule = ScheduleDao.create_schedule( - today.strftime("%Y-%m-%d %H:%M"), - tomorrow.strftime("%Y-%m-%d %H:%M"), - assignment, - host, - ) - assert schedule - - class TestSummary(TestBase): - def test_summary_all_detail(self, remove_fixture): + def test_summary_all_detail(self): self.cli_args["all"] = True self.cli_args["detail"] = True self.quads_cli_call("summary") assert self._caplog.messages[0] == "cloud99 (test): 1 (test) - 1234" - assert self._caplog.messages[1] == "cloud03 (): 0 () - " + assert self._caplog.messages[1] == "cloud01 (test): 2 (test) - 1234" + assert self._caplog.messages[2] == "cloud04 (): 0 () - " + + def test_summary(self): + self.cli_args["all"] = False + self.cli_args["detail"] = False + self.quads_cli_call("summary") + + assert len(self._caplog.messages) == 2 + assert self._caplog.messages[0] == "cloud99: 1 (test)" + assert self._caplog.messages[1] == "cloud01: 2 (test)" - def test_summary(self, remove_fixture): + def test_summary_date(self): + today = datetime.now().strftime("%Y-%m-%d %H:%M") + self.cli_args["datearg"] = today self.cli_args["all"] = False self.cli_args["detail"] = False self.quads_cli_call("summary") - assert len(self._caplog.messages) == 1 + assert len(self._caplog.messages) == 2 assert self._caplog.messages[0] == "cloud99: 1 (test)" + assert self._caplog.messages[1] == "cloud01: 2 (test)" diff --git a/tests/cli/test_validate_env.py b/tests/cli/test_validate_env.py new file mode 100644 index 000000000..8d60f7a5f --- /dev/null +++ b/tests/cli/test_validate_env.py @@ -0,0 +1,83 @@ +import logging +from datetime import datetime, timedelta + +import pytest + +from unittest.mock import patch + +from quads.config import Config +from quads.exceptions import CliException +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.baseDao import BaseDao +from quads.server.dao.cloud import CloudDao +from quads.server.dao.host import HostDao +from quads.server.dao.schedule import ScheduleDao +from quads.server.models import db +from tests.cli.config import HOST1, NetcatStub, SSHHelperStub, switch_config_stub +from tests.cli.test_base import TestBase + + +def finalizer(): + cloud = CloudDao.get_cloud(name="cloud04") + schedule = ScheduleDao.get_current_schedule(cloud=cloud) + if schedule: + ScheduleDao.remove_schedule(schedule[0].id) + + +@pytest.fixture +def validate_fixture(request): + request.addfinalizer(finalizer) + yesterday = datetime.now() - timedelta(days=1) + tomorrow = yesterday + timedelta(weeks=2) + cloud = CloudDao.get_cloud(name="cloud04") + ass = AssignmentDao.get_active_cloud_assignment(cloud=cloud) + ass.provisioned = True + ass.wipe = True + BaseDao.safe_commit() + host = HostDao.get_host(HOST1) + schedule = ScheduleDao.create_schedule(start=yesterday, end=tomorrow, assignment=ass, host=host) + assert schedule + + +class TestValidateEnv(TestBase): + @patch("quads.tools.validate_env.socket.gethostbyname", switch_config_stub) + @patch("quads.tools.validate_env.switch_config", switch_config_stub) + @patch("quads.tools.validate_env.SSHHelper", SSHHelperStub) + @patch("quads.tools.validate_env.Netcat", NetcatStub) + @patch("quads.tools.external.postman.SMTP") + def test_validate_env(self, mocked_smtp, validate_fixture): + Config.__setattr__("foreman_unavailable", True) + mocked_smtp() + + self._caplog.set_level(logging.INFO) + self.quads_cli_call("validate_env") + + assert self._caplog.messages[-5:] == [ + "Validating cloud04", + "Quads assignments validation executed.", + ] + cloud = CloudDao.get_cloud(name="cloud04") + ass = AssignmentDao.get_active_cloud_assignment(cloud=cloud) + db.session.refresh(ass) + assert ass.notification.fail is False + assert ass.validated is True + + @patch("quads.tools.validate_env.socket.gethostbyname", switch_config_stub) + @patch("quads.tools.validate_env.switch_config", switch_config_stub) + @patch("quads.tools.validate_env.SSHHelper", SSHHelperStub) + @patch("quads.tools.validate_env.Netcat", NetcatStub) + @patch("quads.tools.external.postman.SMTP") + def test_validate_env_no_cloud(self, mocked_smtp): + Config.__setattr__("foreman_unavailable", True) + mocked_smtp() + assignments = AssignmentDao.get_assignments() + for assignment in assignments: + setattr(assignment, "provisioned", True) + BaseDao.safe_commit() + + self._caplog.set_level(logging.INFO) + self.cli_args.update({"cloud": "cloud02"}) + with pytest.raises(CliException) as ex: + self.quads_cli_call("validate_env") + + assert str(ex.value) == "Cloud not found: cloud02" diff --git a/tests/requirements.txt b/tests/requirements.txt index 410570511..b8bc4c669 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,13 @@ tox==3.25.0 -pytest==6.2.5 +pytest==7.0.0 +pytest-asyncio==0.21.1 +pytest-aiohttp==1.0.5 +pytest-mock==3.11.1 pytest-cov==3.0.0 codecov==2.1.13 asynctest==0.13.0 pyyaml>=3.10 aiohttp>=3.7.4 requests==2.31.0 +python-wordpress-xmlrpc>=2.3 +GitPython>=3.1.36 \ No newline at end of file diff --git a/tests/tools/artifacts/cloud99_instackenv.json b/tests/tools/artifacts/cloud99_instackenv.json deleted file mode 100644 index fe6850f08..000000000 --- a/tests/tools/artifacts/cloud99_instackenv.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "nodes": [ - { - "arch": "x86_64", - "cpu": "2", - "disk": "20", - "mac": [], - "memory": "1024", - "name": "host.example.com", - "pm_addr": "mgmt-host.example.com", - "pm_password": "rdu2@1234", - "pm_type": "pxe_ipmitool", - "pm_user": "quads" - } - ] -} \ No newline at end of file diff --git a/tests/tools/artifacts/cloud99_ocpinventory.json b/tests/tools/artifacts/cloud99_ocpinventory.json deleted file mode 100644 index fe6850f08..000000000 --- a/tests/tools/artifacts/cloud99_ocpinventory.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "nodes": [ - { - "arch": "x86_64", - "cpu": "2", - "disk": "20", - "mac": [], - "memory": "1024", - "name": "host.example.com", - "pm_addr": "mgmt-host.example.com", - "pm_password": "rdu2@1234", - "pm_type": "pxe_ipmitool", - "pm_user": "quads" - } - ] -} \ No newline at end of file diff --git a/tests/tools/conftest.py b/tests/tools/conftest.py new file mode 100644 index 000000000..f54c55b5f --- /dev/null +++ b/tests/tools/conftest.py @@ -0,0 +1,140 @@ +from datetime import datetime, timedelta + +import pytest + +from quads.server.app import create_app, user_datastore +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.cloud import CloudDao +from quads.server.dao.disk import DiskDao +from quads.server.dao.host import HostDao +from quads.server.dao.interface import InterfaceDao +from quads.server.dao.memory import MemoryDao +from quads.server.dao.processor import ProcessorDao +from quads.server.dao.schedule import ScheduleDao +from quads.server.dao.vlan import VlanDao +from quads.server.database import drop_all, populate, init_db +from tests.cli.config import ( + CLOUD, + HOST_TYPE, + DEFAULT_CLOUD, + HOST1, + MODEL1, + HOST2, + MODEL2, + DEFINE_CLOUD, + REMOVE_CLOUD, + MOD_CLOUD, + IFNAME1, + IFVENDOR1, + IFBIOSID1, + IFMAC1, + IFIP1, + IFPORT1, + IFSPEED, +) + + +@pytest.fixture(autouse=True, scope="session") +def test_client(): + """ + | Creates a test client for the app from the testing config. + | Drops and then initializes the database and populates it with default users. + """ + flask_app = create_app() + flask_app.config.from_object("quads.server.config.TestingConfig") + + with flask_app.test_client() as testing_client: + with flask_app.app_context(): + drop_all(flask_app.config) + init_db(flask_app.config) + populate(user_datastore) + yield testing_client + + +@pytest.fixture(autouse=True, scope="package") +def populate_db(): + + today = datetime.now() + tomorrow = today + timedelta(weeks=2) + + cloud = CloudDao.create_cloud(CLOUD) + default_cloud = CloudDao.create_cloud(DEFAULT_CLOUD) + remove_cloud = CloudDao.create_cloud(REMOVE_CLOUD) + mod_cloud = CloudDao.create_cloud(MOD_CLOUD) + host1 = HostDao.create_host(HOST1, MODEL1, HOST_TYPE, CLOUD) + host2 = HostDao.create_host(HOST2, MODEL2, HOST_TYPE, CLOUD) + InterfaceDao.create_interface( + HOST1, + IFNAME1, + IFBIOSID1, + IFMAC1, + IFIP1, + IFPORT1, + IFSPEED, + IFVENDOR1, + True, + False, + ) + DiskDao.create_disk(HOST1, "NVME", 4096, 10) + DiskDao.create_disk(HOST1, "SATA", 4096, 5) + MemoryDao.create_memory(HOST1, "DIMM1", 2048) + MemoryDao.create_memory(HOST1, "DIMM2", 2048) + ProcessorDao.create_processor(HOST1, "P1", "Intel", "i7", 2, 4) + vlan1 = VlanDao.create_vlan( + "192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1 + ) + vlan2 = VlanDao.create_vlan( + "192.168.1.2", 122, "192.168.1.2/22", "255.255.255.255", 2 + ) + assignment = AssignmentDao.create_assignment( + "test", "test", "1234", 0, False, [""], cloud.name, vlan1.vlan_id + ) + assignment_mod = AssignmentDao.create_assignment( + "test", "test", "1234", 0, False, [""], mod_cloud.name, vlan2.vlan_id + ) + schedule = ScheduleDao.create_schedule( + today.strftime("%Y-%m-%d %H:%M"), + tomorrow.strftime("%Y-%m-%d %H:%M"), + assignment, + host1, + ) + + yield + + host1 = HostDao.get_host(HOST1) + host2 = HostDao.get_host(HOST2) + cloud = CloudDao.get_cloud(CLOUD) + default_cloud = CloudDao.get_cloud(DEFAULT_CLOUD) + define_cloud = CloudDao.get_cloud(DEFINE_CLOUD) + remove_cloud = CloudDao.get_cloud(REMOVE_CLOUD) + mod_cloud = CloudDao.get_cloud(MOD_CLOUD) + schedules = ScheduleDao.get_current_schedule(host=host1, cloud=cloud) + + if schedules: + ScheduleDao.remove_schedule(schedules[0].id) + AssignmentDao.remove_assignment(schedules[0].assignment_id) + + assignments = AssignmentDao.get_assignments() + for ass in assignments: + AssignmentDao.remove_assignment(ass.id) + + if host1: + HostDao.remove_host(name=HOST1) + + if host2: + HostDao.remove_host(name=HOST2) + + if cloud: + CloudDao.remove_cloud(CLOUD) + + if define_cloud: + CloudDao.remove_cloud(DEFINE_CLOUD) + + if remove_cloud: + CloudDao.remove_cloud(REMOVE_CLOUD) + + if mod_cloud: + CloudDao.remove_cloud(MOD_CLOUD) + + if default_cloud: + CloudDao.remove_cloud(DEFAULT_CLOUD) diff --git a/tests/tools/fixtures/cloud99_env.json b/tests/tools/fixtures/cloud99_env.json index fe6850f08..923011a18 100644 --- a/tests/tools/fixtures/cloud99_env.json +++ b/tests/tools/fixtures/cloud99_env.json @@ -1,13 +1,27 @@ { "nodes": [ + { + "arch": "x86_64", + "cpu": "2", + "disk": "20", + "mac": [ + "A0:B1:C2:D3:E4:F5" + ], + "memory": "1024", + "name": "host1.example.com", + "pm_addr": "mgmt-host1.example.com", + "pm_password": "rdu2@1234", + "pm_type": "pxe_ipmitool", + "pm_user": "quads" + }, { "arch": "x86_64", "cpu": "2", "disk": "20", "mac": [], "memory": "1024", - "name": "host.example.com", - "pm_addr": "mgmt-host.example.com", + "name": "host2.example.com", + "pm_addr": "mgmt-host2.example.com", "pm_password": "rdu2@1234", "pm_type": "pxe_ipmitool", "pm_user": "quads" diff --git a/tests/tools/fixtures/markdown.md b/tests/tools/fixtures/markdown.md new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tools/test_base.py b/tests/tools/test_base.py index d956a257f..4dd0f949d 100644 --- a/tests/tools/test_base.py +++ b/tests/tools/test_base.py @@ -18,24 +18,9 @@ class TestBase: cli_args = {"datearg": None, "filter": None, "force": "False"} - @pytest.fixture(autouse=True) - def test_client(self): - """ - | Creates a test client for the app from the testing config. - | Drops and then initializes the database and populates it with default users. - """ - self.flask_app = create_app() - self.flask_app.config.from_object("quads.server.config.TestingConfig") - - with self.flask_app.test_client() as testing_client: - with self.flask_app.app_context(): - drop_all(self.flask_app.config) - init_db(self.flask_app.config) - populate(user_datastore) - yield testing_client - @pytest.fixture(autouse=True) def inject_fixtures(self, caplog): + caplog.set_level(logging.DEBUG) self._caplog = caplog @pytest.fixture(autouse=True) diff --git a/tests/tools/test_foreman.py b/tests/tools/test_foreman.py index 57b98fcce..f8f6c91bc 100644 --- a/tests/tools/test_foreman.py +++ b/tests/tools/test_foreman.py @@ -1,31 +1,85 @@ #!/usr/bin/env python3 -import asyncio +from unittest.mock import patch, AsyncMock + +import pytest -from quads.config import Config from quads.tools.external.foreman import Foreman class TestForeman(object): - def __init__(self): - self.foreman = None - - def setup(self): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - self.foreman = Foreman( - Config["foreman_api_url"], - Config["ipmi_username"], - Config["ipmi_password"], - loop=loop, - ) - - def teardown(self): - self.foreman.loop.close() - - def test_get_all_hosts(self): - hosts = self.foreman.loop.run_until_complete(self.foreman.get_all_hosts()) - assert isinstance(hosts, dict) - - def test_get_broken_hosts(self): - hosts = self.foreman.loop.run_until_complete(self.foreman.get_broken_hosts()) - assert isinstance(hosts, dict) + @pytest.mark.asyncio + async def test_initialize_foreman_with_valid_parameters(self): + foreman = Foreman("https://example.com", "username", "password") + + assert foreman.url == "https://example.com" + assert foreman.username == "username" + assert foreman.password == "password" + + @patch("quads.tools.external.foreman.aiohttp.ClientSession.get") + @pytest.mark.asyncio + async def test_get_all_hosts(self, session_mock): + resp = AsyncMock() + resp.json.return_value = {"results": [{"name": "host.example.com"}]} + + session_mock.return_value.__aenter__.return_value = resp + + foreman = Foreman("https://example.com", "username", "password") + all_hosts = await foreman.get_all_hosts() + + assert all_hosts == {"host.example.com": {"name": "host.example.com"}} + + @patch("quads.tools.external.foreman.aiohttp.ClientSession.get") + @pytest.mark.asyncio + async def test_get_broken_hosts(self, session_mock): + resp = AsyncMock() + resp.json.return_value = {"results": [{"name": "host.example.com"}]} + + session_mock.return_value.__aenter__.return_value = resp + + # Create the Foreman object with mocked session + foreman = Foreman("https://example.com", "username", "password") + all_hosts = await foreman.get_broken_hosts() + + assert all_hosts == {"host.example.com": {"name": "host.example.com"}} + + @patch("quads.tools.external.foreman.aiohttp.ClientSession.get") + @pytest.mark.asyncio + async def test_get_build_hosts(self, session_mock): + resp = AsyncMock() + resp.json.return_value = {"results": [{"name": "host.example.com"}]} + + session_mock.return_value.__aenter__.return_value = resp + + # Create the Foreman object with mocked session + foreman = Foreman("https://example.com", "username", "password") + all_hosts = await foreman.get_build_hosts() + + assert all_hosts == {"host.example.com": {"name": "host.example.com"}} + + @patch("quads.tools.external.foreman.aiohttp.ClientSession.get") + @pytest.mark.asyncio + async def test_get_parametrized(self, session_mock): + resp = AsyncMock() + resp.json.return_value = {"results": [{"name": "host.example.com"}]} + + session_mock.return_value.__aenter__.return_value = resp + + # Create the Foreman object with mocked session + foreman = Foreman("https://example.com", "username", "password") + all_hosts = await foreman.get_parametrized("build", True) + + assert all_hosts == {"host.example.com": {"name": "host.example.com"}} + + @patch("quads.tools.external.foreman.aiohttp.ClientSession.get") + @pytest.mark.asyncio + async def test_get_host_id(self, session_mock): + resp = AsyncMock() + resp.json.return_value = {"results": [{"name": "host.example.com", "id": "host1"}]} + + session_mock.return_value.__aenter__.return_value = resp + + # Create the Foreman object with mocked session + foreman = Foreman("https://example.com", "username", "password") + all_hosts = await foreman.get_host_id("host.example.com") + + assert all_hosts == "host1" diff --git a/tests/tools/test_foreman_heal.py b/tests/tools/test_foreman_heal.py new file mode 100644 index 000000000..4948b9483 --- /dev/null +++ b/tests/tools/test_foreman_heal.py @@ -0,0 +1,15 @@ +import logging + +from quads.config import Config +from quads.tools.foreman_heal import main as foreman_heal_main +from tests.tools.test_base import TestBase + + +class TestForemanHeal(TestBase): + def test_foreman_heal(self): + Config.__setattr__("foreman_unavailable", True) + self._caplog.set_level(logging.INFO) + + foreman_heal_main() + + assert len(self._caplog.messages) == 12 diff --git a/tests/tools/test_instackenv.py b/tests/tools/test_instackenv.py index 2ed79298f..ba374e868 100644 --- a/tests/tools/test_instackenv.py +++ b/tests/tools/test_instackenv.py @@ -1,97 +1,26 @@ #!/usr/bin/env python3 -import glob import os -from datetime import datetime, timedelta from quads.config import Config -from quads.server.dao.assignment import AssignmentDao -from quads.server.dao.cloud import CloudDao -from quads.server.dao.host import HostDao -from quads.server.dao.schedule import ScheduleDao -from quads.server.dao.vlan import VlanDao from quads.tools.make_instackenv_json import main -from tests.cli.config import CLOUD, HOST from tests.tools.test_base import TestBase -import pytest - - -def finalizer(): - host = HostDao.get_host(HOST) - cloud = CloudDao.get_cloud(CLOUD) - schedules = ScheduleDao.get_current_schedule(host=host, cloud=cloud) - - if schedules: - ScheduleDao.remove_schedule(schedules[0].id) - AssignmentDao.remove_assignment(schedules[0].assignment_id) - - assignments = AssignmentDao.get_assignments() - for ass in assignments: - AssignmentDao.remove_assignment(ass.id) - - if host: - HostDao.remove_host(name=HOST) - - if cloud: - CloudDao.remove_cloud(CLOUD) - - for f in glob.glob("artifacts/cloud99_*"): - os.remove(f) - - -@pytest.fixture -def ie_fixture(request): - request.addfinalizer(finalizer) - today = datetime.now() - tomorrow = today + timedelta(days=1) - - cloud = CloudDao.create_cloud(CLOUD) - host = HostDao.create_host(HOST, "r640", "scalelab", CLOUD) - vlan = VlanDao.create_vlan( - "192.168.1.1", 122, "192.168.1.1/22", "255.255.255.255", 1 - ) - assignment = AssignmentDao.create_assignment( - "test", "test", "1234", 0, False, [""], vlan.vlan_id, cloud.name - ) - schedule = ScheduleDao.create_schedule( - today, - tomorrow, - assignment, - host, - ) - assert schedule class TestInstackenv(TestBase): - def test_make_instackenv_json(self, ie_fixture): + def test_make_instackenv_json(self): Config.__setattr__("openstack_management", True) Config.__setattr__("foreman_unavailable", True) - Config.__setattr__( - "json_web_path", os.path.join(os.path.dirname(__file__), "artifacts/") - ) + Config.__setattr__("json_web_path", os.path.join(os.path.dirname(__file__), "artifacts/")) main() - assert list( - open( - os.path.join( - os.path.dirname(__file__), "artifacts/cloud99_instackenv.json" - ) - ) - ) == list( + assert list(open(os.path.join(os.path.dirname(__file__), "artifacts/cloud99_instackenv.json"))) == list( open(os.path.join(os.path.dirname(__file__), "fixtures/cloud99_env.json")) ) - def test_make_ocpinventory_json(self, ie_fixture): + def test_make_ocpinventory_json(self): Config.__setattr__("openshift_management", True) Config.__setattr__("foreman_unavailable", True) - Config.__setattr__( - "json_web_path", os.path.join(os.path.dirname(__file__), "artifacts/") - ) + Config.__setattr__("json_web_path", os.path.join(os.path.dirname(__file__), "artifacts/")) main() - assert list( - open( - os.path.join( - os.path.dirname(__file__), "artifacts/cloud99_ocpinventory.json" - ) - ) - ) == list( + assert list(open(os.path.join(os.path.dirname(__file__), "artifacts/cloud99_ocpinventory.json"))) == list( open(os.path.join(os.path.dirname(__file__), "fixtures/cloud99_env.json")) ) diff --git a/tests/tools/test_postman.py b/tests/tools/test_postman.py index 5c6ca9875..294c8321a 100644 --- a/tests/tools/test_postman.py +++ b/tests/tools/test_postman.py @@ -33,9 +33,7 @@ def test_empty_subject_to_cc_content(self, mocked_smtp): @patch("quads.tools.external.postman.SMTP") def test_smtp_exception_raised(self, mocked_smtp): - mocked_smtp.return_value.__enter__.return_value.send_message.side_effect = ( - smtplib.SMTPException - ) + mocked_smtp.return_value.__enter__.return_value.send_message.side_effect = smtplib.SMTPException postman = Postman("Test Subject", "test", [], "Test Content") assert postman.send_email() is False diff --git a/tests/tools/test_ssh_helper.py b/tests/tools/test_ssh_helper.py index c8cd697b1..88afdd640 100644 --- a/tests/tools/test_ssh_helper.py +++ b/tests/tools/test_ssh_helper.py @@ -1,29 +1,100 @@ -from unittest.mock import patch, MagicMock - import pytest -from quads.tools.external.ssh_helper import SSHHelper -from tempfile import NamedTemporaryFile -from getpass import getuser +from unittest.mock import patch, MagicMock +from quads.tools.external.ssh_helper import SSHHelper, SSHHelperException +from paramiko import SSHException from tests.tools.test_base import TestBase class TestSSHHelper(TestBase): - string = "test" + @patch("quads.tools.external.ssh_helper.SSHHelper.connect") + def test_connect_with_valid_credentials(self, mock_connect): + mock_connect.return_value = MagicMock() + ssh_helper = SSHHelper("valid_host", "valid_user", "valid_password") + assert ssh_helper.ssh is not None + + @patch("quads.tools.external.ssh_helper.SSHHelper.connect") + def test_connect_exception(self, mock_connect): + mock_connect.side_effect = SSHException("Invalid credentials") + with pytest.raises(SSHException) as ex: + SSHHelper("invalid_host", "invalid_user", "invalid_password") + assert str(ex.value) == "Invalid credentials" + + @patch("quads.tools.external.ssh_helper.SSHConfig") + @patch("quads.tools.external.ssh_helper.SSHClient") + @patch("quads.tools.external.ssh_helper.os.path.exists") + @patch("quads.tools.external.ssh_helper.open") + def test_connect_exception_ssh(self, mock_open, mock_os, mock_client, mock_config): + mock_os.return_value = True + mock_config.return_value = MagicMock() + mock_client.return_value.connect.side_effect = SSHException("Something wong") + with pytest.raises(SSHHelperException) as ex: + SSHHelper("invalid_host", "invalid_user", "invalid_password") + assert str(ex.value) == "invalid_host: Something wong" - @pytest.fixture(autouse=True) - def setup(self): - tmp = NamedTemporaryFile(delete=False) - with open(tmp.name, "w") as _file: - _file.write(self.string) - self.tmp_file = tmp.name + @patch("quads.tools.external.ssh_helper.SSHConfig") + @patch("quads.tools.external.ssh_helper.SSHClient") + def test_run_cmd_with_error(self, mock_client, mock_config): + mock_config.return_value = MagicMock() + stderr_mock = MagicMock() + stderr_mock.readlines.return_value = ["Error here"] + mock_client.return_value.exec_command.return_value = ( + MagicMock(), + MagicMock(), + stderr_mock, + ) + ssh_helper = SSHHelper("invalid_host", "invalid_user", "invalid_password") + result, error = ssh_helper.run_cmd("ls") + assert not result + assert error == ["Error here"] - def teardown(self): - self.helper.disconnect() + @patch("quads.tools.external.ssh_helper.SSHConfig") + @patch("quads.tools.external.ssh_helper.SSHClient") + def test_run_cmd(self, mock_client, mock_config): + mock_config.return_value = MagicMock() + stderr_mock = MagicMock() + stderr_mock.readlines.return_value = [] + stdout_mock = MagicMock() + stdout_mock.channel.recv_exit_status.return_value = 0 + mock_client.return_value.exec_command.return_value = ( + MagicMock(), + stdout_mock, + stderr_mock, + ) + ssh_helper = SSHHelper("invalid_host", "invalid_user", "invalid_password") + result, error = ssh_helper.run_cmd("ls") + assert result - def test_run_cmd(self): - self.helper = SSHHelper(_host="localhost", _user=getuser()) + @patch("quads.tools.external.ssh_helper.SSHConfig") + @patch("quads.tools.external.ssh_helper.SSHClient") + @patch("quads.tools.external.ssh_helper.open") + def test_copy_ssh_key(self, mock_open, mock_client, mock_config): + stderr_mock = MagicMock() + stderr_mock.readlines.return_value = [] + mock_config.return_value = MagicMock() + mock_client.return_value.exec_command.return_value = ( + MagicMock(), + MagicMock(), + stderr_mock, + ) + ssh_helper = SSHHelper("invalid_host", "invalid_user", "invalid_password") + ssh_helper.copy_ssh_key("ssh_key") + assert self._caplog.messages[0] == "Your key was copied successfully" - out = self.helper.run_cmd(f"cat {self.tmp_file}") - assert out[0] - assert out[1][0] == "test" + @patch("quads.tools.external.ssh_helper.SSHConfig") + @patch("quads.tools.external.ssh_helper.SSHClient") + @patch("quads.tools.external.ssh_helper.open") + def test_copy_ssh_key_error(self, mock_open, mock_client, mock_config): + stderr_mock = MagicMock() + stderr_mock.readlines.return_value = ["Error here"] + mock_open.return_value.__enter__.return_value.readline.return_value = "ssh-rsa" + mock_config.return_value = MagicMock() + mock_client.return_value.exec_command.return_value = ( + MagicMock(), + MagicMock(), + stderr_mock, + ) + ssh_helper = SSHHelper("invalid_host", "invalid_user", "invalid_password") + ssh_helper.copy_ssh_key("ssh_key") + assert self._caplog.messages[0] == "There was something wrong with your request" + assert self._caplog.messages[1] == "Error here" diff --git a/tests/tools/test_table.py b/tests/tools/test_table.py new file mode 100644 index 000000000..aa5ad6d74 --- /dev/null +++ b/tests/tools/test_table.py @@ -0,0 +1,27 @@ +import pytest +import os +import glob + +from datetime import datetime, timedelta + +from quads.config import Config +from quads.tools.simple_table_web import main as web_main +from tests.tools.test_base import TestBase + + +class TestTable(TestBase): + def test_simple_table_web(self): + Config.__setattr__("foreman_unavailable", True) + Config.__setattr__("visual_web_dir", os.path.join(os.path.dirname(__file__), "artifacts/")) + web_main() + current = open(os.path.join(os.path.dirname(__file__), "artifacts/current.html"), "r") + current = [n for n in current.readlines() if "Emoji" not in n] + current = "".join(current) + response = f"""title= + "Description: test + Env: cloud99 + Owner: test + Ticket: 1234 + Day: {datetime.now().day + 1}">""" + assert isinstance(current, str) is True + assert response in current diff --git a/tests/tools/test_wiki.py b/tests/tools/test_wiki.py new file mode 100644 index 000000000..4aa197f8b --- /dev/null +++ b/tests/tools/test_wiki.py @@ -0,0 +1,41 @@ +import pytest +import os + +from datetime import datetime, timedelta +from unittest.mock import patch + +from quads.config import Config +from quads.server.dao.assignment import AssignmentDao +from quads.server.dao.cloud import CloudDao +from quads.server.dao.host import HostDao +from quads.server.dao.schedule import ScheduleDao +from quads.server.dao.vlan import VlanDao +from tests.cli.config import CLOUD, HOST1 +from quads.tools.regenerate_wiki import main as regenerate_wiki_main +from tests.tools.test_base import TestBase + + +class WikiStub: + def __init__(self, url, username, password): + pass + + def update(self, _page_title, _page_id, _markdown): + pass + + +class TestWiki(TestBase): + @patch("quads.tools.regenerate_wiki.Wiki", WikiStub) + @patch("quads.tools.regenerate_vlans_wiki.Wiki", WikiStub) + def test_regenerate_wiki(self): + Config.__setattr__("foreman_unavailable", True) + Config.__setattr__("wp_wiki_git_repo_path", os.path.join(os.path.dirname(__file__), "artifacts/git/wiki")) + regenerate_wiki_main() + + files = ["assignments.md", "main.md"] + for f in files: + assert os.path.exists(os.path.join(os.path.dirname(__file__), f"artifacts/git/wiki/{f}")) + + assignment_md = open(os.path.join(os.path.dirname(__file__), f"artifacts/git/wiki/{files[0]}"), "r") + assignment_md = "".join(assignment_md.readlines()) + host_assignment_str = "| host1 | console |\n" + assert host_assignment_str in assignment_md diff --git a/tox.ini b/tox.ini index 36402dad5..9ec9c6c66 100644 --- a/tox.ini +++ b/tox.ini @@ -5,4 +5,4 @@ skipsdist = True [testenv] deps = -rrequirements.txt -rtests/requirements.txt -commands = pytest -p no:warnings --ignore tests/tools/ --cov=./quads/cli/ --cov=./quads/server/ --cov=./quads/tools/ --cov-report=xml --cov-config=.coveragerc --junitxml=junit-{envname}.xml +commands = pytest -p no:warnings --cov=./quads/cli/ --cov=./quads/server/ --cov=./quads/tools/ --cov-report=xml --cov-config=.coveragerc --junitxml=junit-{envname}.xml