From 8ea881c270c51976773f335089adfa0cd58cde64 Mon Sep 17 00:00:00 2001 From: medyagh Date: Tue, 20 Jun 2017 16:15:05 -0500 Subject: [PATCH] Use DotEnv for config, Logs in Email --- Makefile | 2 +- README.md | 10 +-- run.sh | 1 - setup.py | 6 +- .env-sample => winnaker/.env-sample | 0 winnaker/config.sh | 55 ------------ winnaker/helpers.py | 33 +++----- winnaker/main.py | 26 +++--- winnaker/models.py | 125 ++++++++++------------------ winnaker/notify.py | 7 +- winnaker/settings.py | 75 +++++++++++++++++ 11 files changed, 160 insertions(+), 180 deletions(-) rename .env-sample => winnaker/.env-sample (100%) delete mode 100755 winnaker/config.sh create mode 100644 winnaker/settings.py diff --git a/Makefile b/Makefile index 843e515..1b8290f 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ clean-kube: kubectl delete pods --all run-docker: - docker run --env-file .env -it -v $(CURDIR)/winnaker-screenshots:/winnaker-screenshots/ local/winnaker + docker run --env-file wiinaker/.env -it -v $(CURDIR)/winnaker-screenshots:/winnaker-screenshots/ local/winnaker:latest .PHONY: build diff --git a/README.md b/README.md index 2ef1050..34ed957 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ Auditing tool for Spinnaker. Real Testing in real browser ! - Searches for `samplepipeline` the pipleline - Gets the last build status - Generates screenshot : - - `./src/outputs/applications.png` - - `./src/outputs/pipelines.png` - - `./src/outputs/last_build_status.png` - - `./src/outputs/login.png` - - `./src/outputs/stage1.png` + - `./applications.png` + - `./pipelines.png` + - `./last_build_status.png` + - `./login.png` + - `./stage1.png` - Any error will result in a non-zero code to the system. - Error screenshots will be timestaped. diff --git a/run.sh b/run.sh index f428828..62fe002 100755 --- a/run.sh +++ b/run.sh @@ -1,3 +1,2 @@ pip install -e . -source ./winnaker/config.sh winnaker "$@" diff --git a/setup.py b/setup.py index e7f29a6..9d7b438 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,20 @@ #!/usr/bin/env python from setuptools import setup, find_packages +from os.path import join, dirname setup(name='winnaker', description='An audit tool that tests the whole system functionality of Spinnaker', author='Target Corporation', - version='0.8.0', + version='1.0.0', license='MIT', packages=find_packages(), install_requires=[ 'selenium==3.4.3', 'pyvirtualdisplay==0.2', 'tqdm==4.8.4', - 'retrying==1.3.3' + 'retrying==1.3.3', + 'python-dotenv==0.6.4' ], entry_points={ "console_scripts": [ diff --git a/.env-sample b/winnaker/.env-sample similarity index 100% rename from .env-sample rename to winnaker/.env-sample diff --git a/winnaker/config.sh b/winnaker/config.sh deleted file mode 100755 index 1a3724e..0000000 --- a/winnaker/config.sh +++ /dev/null @@ -1,55 +0,0 @@ -# Required -export WINNAKER_USERNAME="REPLACE_DEFAULT" -export WINNAKER_PASSWORD="REPLACE_DEFAULT" -export WINNAKER_SPINNAKER_URL="REPLACE_DEFAULT" -export WINNAKER_APP_NAME="REPLACE_DEFAULT" -export WINNAKER_PIPELINE_NAME="REPLACE_DEFAULT" -export WINNAKER_OUTPUTPATH="./winnaker-screenshots" - -# Not Required -export WINNAKER_MAX_WAIT_PIPELINE="100" -export WINNAKER_HIPCHAT_POSTURL="REPLACE_DEFAULT" -export WINNAKER_NUMBER_OF_STAGES_TO_CHECK="2" -export WINNAKER_EMAIL_FROM="REPLACE_DEFAULT" -export WINNAKER_EMAIL_TO="REPLACE_DEFAULT" -export WINNAKER_EMAIL_SMTP="REPLACE_DEFAULT" - - -# Internal Config -# only change if you know what you are doing - -# XPATH Configurations, Future Proofing it, In case deck changes. -# if you find ever change this values, please make an issue in -# https://github.com/target/winnaker/issues/new - -export WINNAKER_XPATH_LOGIN_USERNAME="//input[@id='username'][@name='pf.username']" -export WINNAKER_XPATH_LOGIN_PASSWORD="//input[@id='password'][@name='pf.pass']" -export WINNAKER_XPATH_LOGIN_SUBMIT="//input[@type='submit']" -export WINNAKER_XPATH_APPLICATIONS_TAB="//a[@href='#/applications' and contains(.,'Applications')]" -export WINNAKER_XPATH_SEARCH_APPLICATIONS="//input[@placeholder='Search applications']" -export WINNAKER_XPATH_START_MANUAL_EXECUTION="//div[contains(@class, 'execution-group-actions')]/h4[2]/a/span" -# TODO Try this later -# //label[contains(.,'Force')] -export WINNAKER_XPATH_FORCE_REBAKE="//input[@type='checkbox' and @ng-model='vm.command.trigger.rebake']" -export WINNAKER_XPATH_PIPELINE_EXECUTION_SUMMARY="//execution-groups[1]//div[@class='execution-summary']" -export WINNAKER_XPATH_PIPLELINE_TRIGGER_DETAILS="//execution-groups[1]//ul[@class='trigger-details']" -export WINNAKER_XPATH_PIPLELINE_DETAILS_LINK="//execution-groups[1]//execution-status//div/a[contains(., 'Details')]" - - -echo "-------------------------------------------------------" -echo " The config file location: winnaker/config.sh " -echo "-------------------------------------------------------" - -for config_parameter in WINNAKER_USERNAME WINNAKER_PASSWORD WINNAKER_SPINNAKER_URL WINNAKER_APP_NAME WINNAKER_PIPELINE_NAME WINNAKER_HIPCHAT_POSTURL WINNAKER_MAX_WAIT_PIPELINE WINNAKER_WINNAKER_OUTPUTPATH -do - if [[${!config_parameter}="REPLACE_DEFAULT"]]; then - if [[$config_parameter="WINNAKER_PASSWORD"]]; then - read - sp "$config_parameter: " "$config_parameter" - echo "" - else - read - p "$config_parameter: " "$config_parameter" - fi - else - echo "$config_parameter set ✓" - fi -done diff --git a/winnaker/helpers.py b/winnaker/helpers.py index a94c433..9e8518d 100644 --- a/winnaker/helpers.py +++ b/winnaker/helpers.py @@ -13,34 +13,25 @@ from os import listdir from os.path import isfile, join from os.path import basename +from winnaker.settings import * + # from selenium.common.exceptions import ElementNotVisibleException def getScreenshotFiles(): logging.debug("Getting the screenshot files in side " + - os.environ["WINNAKER_OUTPUTPATH"]) + cfg_output_files_path) files = [ - join(os.environ["WINNAKER_OUTPUTPATH"], f) for f in listdir( - os.environ["WINNAKER_OUTPUTPATH"]) if isfile( + join(cfg_output_files_path, f) for f in listdir( + cfg_output_files_path) if isfile( join( - os.environ["WINNAKER_OUTPUTPATH"], + cfg_output_files_path, f))] logging.debug(files) return files -def get_env(env_key, default): - value = os.getenv(env_key) - if value is None or len(value) == 0: - logging.debug( - "{} not set in environment, defaulting to {}".format( - env_key, default)) - return default - logging.debug("{} set from environment".format(env_key)) - return value - - def post_to_hipchat(message, alert=False): import requests import json @@ -58,9 +49,11 @@ def post_to_hipchat(message, alert=False): "message_format": "text" } - post_url = os.environ['WINNAKER_HIPCHAT_POSTURL'] headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} - r = requests.post(post_url, data=json.dumps(data), headers=headers) + r = requests.post( + cfg_hipchat_posturl, + data=json.dumps(data), + headers=headers) def a_nice_refresh(driver): @@ -88,7 +81,7 @@ def wait_for_xpath_presence(driver, xpath, be_clickable=False): logging.error("Error: Could not find {}".format(xpath)) driver.save_screenshot( join( - os.environ["WINNAKER_OUTPUTPATH"], + cfg_output_files_path, "debug_" + now() + ".png")) @@ -97,7 +90,7 @@ def wait_for_xpath_presence(driver, xpath, be_clickable=False): except StaleElementReferenceException: driver.save_screenshot( join( - os.environ["WINNAKER_OUTPUTPATH"], + cfg_output_files_path, "debug_" + now() + ".png")) @@ -105,7 +98,7 @@ def wait_for_xpath_presence(driver, xpath, be_clickable=False): raise StaleElementReferenceException driver.save_screenshot( join( - os.environ["WINNAKER_OUTPUTPATH"], + cfg_output_files_path, "error_driver_" + now() + ".png")) diff --git a/winnaker/main.py b/winnaker/main.py index ffb77e3..a44d8c0 100644 --- a/winnaker/main.py +++ b/winnaker/main.py @@ -9,6 +9,7 @@ import pkg_resources # part of setuptools import atexit from datetime import datetime +from winnaker.settings import * def main(): @@ -37,7 +38,7 @@ def main(): "--app", type=str, help="the name of application to look for", - default=os.environ.get("WINNAKER_APP_NAME")) + default=cfg_app_name) parser.add_argument( "-p", "--pipeline", @@ -73,7 +74,8 @@ def main(): rootLogger = logging.getLogger() rootLogger.setLevel(log_level) - fileHandler = logging.FileHandler("winnaker.log") + fileHandler = logging.FileHandler( + join(cfg_output_files_path, "winnaker.log")) fileHandler.setFormatter(logFormatter) rootLogger.addHandler(fileHandler) @@ -85,19 +87,14 @@ def main(): logging.info("Winnaker Version: {}".format(version)) logging.info("Current Config: {}".format(args)) - if not os.path.exists(os.environ["WINNAKER_OUTPUTPATH"]): - os.makedirs(os.environ["WINNAKER_OUTPUTPATH"]) + if not os.path.exists(cfg_output_files_path): + os.makedirs(cfg_output_files_path) - if os.environ.get('WINNAKER_EMAIL_SMTP') is not None: - atexit.register( - send_mail, - os.environ["WINNAKER_EMAIL_FROM"], - os.environ["WINNAKER_EMAIL_TO"], - "Winnaker Screenshots " + str( - datetime.utcnow()), - "Here are the screenshots of the spinnaker's last run at " + str( - datetime.utcnow()) + " UTC Time", - server=os.environ["WINNAKER_EMAIL_SMTP"]) + if (cfg_email_smtp != "") and (cfg_email_to != ""): + atexit.register(send_mail, cfg_email_from, cfg_email_to, "Winnaker Screenshots " + + str(datetime.utcnow()), "Here are the screenshots of the spinnaker's last run at " + + str(datetime.utcnow()) + + " UTC Time", server=cfg_email_smtp) if args.headless: logging.debug("Starting virtual display") @@ -112,7 +109,6 @@ def main(): s.login() s.get_pipeline(args.app, args.pipeline) if not args.nolastbuild: - logging.debug("Going into nolastbuild block") logging.info( "- Last build status: {}".format(s.get_last_build().status.encode('utf-8'))) logging.info("- Screenshot Stages") diff --git a/winnaker/models.py b/winnaker/models.py index 3a9a28a..772b938 100644 --- a/winnaker/models.py +++ b/winnaker/models.py @@ -12,6 +12,9 @@ from winnaker.helpers import * from datetime import datetime from tqdm import tqdm +from os.path import join +from winnaker.settings import * + ERROR_LIST = { "Whitelabel Error Page": "Check clouddriver", @@ -33,57 +36,40 @@ def __init__(self): self.driver = webdriver.Chrome(chrome_options=chrome_options) # self.driver = webdriver.Firefox() time.sleep(1) - self.driver.get(os.environ["WINNAKER_SPINNAKER_URL"]) + self.driver.get(cfg_spinnaker_url) self.wait = WebDriverWait(self.driver, 10) - if not os.path.exists(os.environ["WINNAKER_OUTPUTPATH"]): - os.makedirs(os.environ["WINNAKER_OUTPUTPATH"]) + if not os.path.exists(cfg_output_files_path): + os.makedirs(cfg_output_files_path) def login(self): self.check_page_contains_error() - usernamebox = get_env( - "WINNAKER_XPATH_LOGIN_USERNAME", - "//input[@id='username'][@name='pf.username']") - passwordbox = get_env( - "WINNAKER_XPATH_LOGIN_PASSWORD", - "//input[@id='password'][@name='pf.pass']") - signinbutton = get_env( - "WINNAKER_XPATH_LOGIN_SUBMIT", - "//input[@type='submit']") - - e = wait_for_xpath_presence(self.driver, usernamebox) + e = wait_for_xpath_presence(self.driver, cfg_usernamebox_xpath) logging.debug( "Logging in as: {}".format( - os.environ["WINNAKER_USERNAME"])) - e.send_keys(os.environ['WINNAKER_USERNAME']) - e = wait_for_xpath_presence(self.driver, passwordbox) - self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + "/login.png") - e.send_keys(os.environ['WINNAKER_PASSWORD']) - e = wait_for_xpath_presence(self.driver, signinbutton) + cfg_spinnaker_username)) + e.send_keys(cfg_spinnaker_username) + e = wait_for_xpath_presence(self.driver, cfg_passwordbox_xpath) + self.driver.save_screenshot(join( + cfg_output_files_path, "login.png")) + e.send_keys(cfg_spinnaker_password) + e = wait_for_xpath_presence(self.driver, cfg_signin_button_xpath) e.click() logging.info("- Logged in to the spinnaker") - self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + "/login2.png") + self.driver.save_screenshot(join(cfg_output_files_path, "login2.png")) time.sleep(3) def get_application(self, appname): self.check_page_contains_error() - applications_xpath = get_env( - "WINNAKER_XPATH_APPLICATIONS_TAB", - "//a[@href='#/applications' and contains(.,'Applications')]") e = wait_for_xpath_presence( - self.driver, applications_xpath, be_clickable=True) + self.driver, cfg_applications_xpath, be_clickable=True) e.click() - searchbox = get_env( - "WINNAKER_XPATH_SEARCH_APPLICATIONS", - "//input[@placeholder='Search applications']") - e = wait_for_xpath_presence(self.driver, searchbox) + e = wait_for_xpath_presence(self.driver, cfg_searchbox_xpath) e.send_keys(appname) e.send_keys(Keys.RETURN) time.sleep(1) - self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + - "/applications.png") + self.driver.save_screenshot(join( + cfg_output_files_path, + "applications.png")) app_xpath = "//a[contains (.,'" + appname + "')]" e = wait_for_xpath_presence(self.driver, app_xpath) e.click() @@ -112,55 +98,43 @@ def get_pipeline(self, appname, pipelinename): e.click() time.sleep(2) self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + - "/pipelines.png") + join(cfg_output_files_path, "pipelines.png")) logging.info( "- Selected pipeline: {} successfully".format(pipelinename)) def start_manual_execution(self, force_bake=False): self.check_page_contains_error() # starts the 1st pipeline which is currently on the page - start_xpath = get_env( - "WINNAKER_XPATH_START_MANUAL_EXECUTION", - "//div[contains(@class, 'execution-group-actions')]/h4[2]/a/span") - e = wait_for_xpath_presence(self.driver, start_xpath) - click_stubborn(self.driver, e, start_xpath) - time.sleep(3) + e = wait_for_xpath_presence( + self.driver, cfg_start_manual_execution_xpath) + click_stubborn(self.driver, e, cfg_start_manual_execution_xpath) + time.sleep(2) if force_bake: - fbake_xpath = get_env( - "WINNAKER_XPATH_FORCE_REBAKE", - "//input[@type='checkbox' and @ng-model='vm.command.trigger.rebake']") e = wait_for_xpath_presence( - self.driver, start_xpath, be_clickable=True) + self.driver, cfg_force_bake_xpath, be_clickable=True) move_to_element(self.driver, e, click=True) time.sleep(2) if not e.get_attribute('checked'): - xpath = get_env( - "WINNAKER_XPATH_FORCE_REBAKE", - "//input[@type='checkbox' and @ng-model='vm.command.trigger.rebake']") e = wait_for_xpath_presence( - self.driver, xpath, be_clickable=True) - print("Checking force bake option") + self.driver, cfg_froce_rebake_xpath, be_clickable=True) + logging.info("Checking force bake option") e.click() self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + - "/force_bake_check.png") + join(cfg_output_files_path, "force_bake_check.png")) run_xpath = "//button[@type='submit' and contains(.,'Run')]/span[1]" e = wait_for_xpath_presence(self.driver, run_xpath, be_clickable=True) e.click() time.sleep(2) - MAX_WAIT_FOR_RUN = int( - os.environ["WINNAKER_MAX_WAIT_PIPELINE"]) * 60.00 start_time = time.time() logging.info("- Starting Manual Execution") time.sleep(10) # To give enough time for pipeleine kick off show up logging.info("\t Running ... (will wait up to {} minutes".format( - int(MAX_WAIT_FOR_RUN / 60))) - for i in tqdm(range(int(MAX_WAIT_FOR_RUN / 10))): - if time.time() - start_time > MAX_WAIT_FOR_RUN: + int(cfg_max_wait_for_pipeline_run_mins / 60))) + for i in tqdm(range(int(cfg_max_wait_for_pipeline_run_mins / 10))): + if time.time() - start_time > cfg_max_wait_for_pipeline_run_mins: logging.error("The run is taking more than {} minutes".format( - int(MAX_WAIT_FOR_RUN / 60))) + int(cfg_max_wait_for_pipeline_run_mins / 60))) logging.error("Considering it as an error") sys.exit(1) status = self.get_last_build().status @@ -172,7 +146,7 @@ def start_manual_execution(self, force_bake=False): elif "SUCCEEDED" in status: logging.info("\nCongratulations pipleline run was successful.") print_passed() - self.get_stages(n=2) + self.get_stages(n=cfg_number_of_stages_to_check) return 0 elif "TERMINAL" in status: logging.error( @@ -184,32 +158,21 @@ def start_manual_execution(self, force_bake=False): sys.exit(2) def get_last_build(self): - execution_summary_xp = get_env( - "WINNAKER_XPATH_PIPELINE_EXECUTION_SUMMARY", - "//execution-groups[1]//div[@class='execution-summary']") execution_summary = wait_for_xpath_presence( - self.driver, execution_summary_xp) - - trigger_details_xp = get_env( - "WINNAKER_XPATH_PIPLELINE_TRIGGER_DETAILS", - "//execution-groups[1]//ul[@class='trigger-details']") + self.driver, cfg_execution_summary_xp) trigger_details = wait_for_xpath_presence( - self.driver, trigger_details_xp) + self.driver, cfg_trigger_details_xp) self.build = Build(trigger_details.text, execution_summary.text) time.sleep(1) - detail_xpath = get_env( - "WINNAKER_XPATH_PIPLELINE_DETAILS_LINK", - "//execution-groups[1]//execution-status//div/a[contains(., 'Details')]") - e = wait_for_xpath_presence(self.driver, detail_xpath) - self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + - "/last_build_status.png") + e = wait_for_xpath_presence(self.driver, cfg_detail_xpath) + self.driver.save_screenshot(join( + cfg_output_files_path, + "last_build_status.png")) return self.build # TODO Get all the stages automaticly def get_stages( - self, n=int( - os.environ["WINNAKER_NUMBER_OF_STAGES_TO_CHECK"])): + self, n=cfg_number_of_stages_to_check): # n number of stages to get for i in range(1, n + 1): stage_xpath = "//execution[1]//div[@class='stages']/div[" + str( @@ -234,7 +197,11 @@ def get_stages( except TimeoutException: continue self.driver.save_screenshot( - os.environ["WINNAKER_OUTPUTPATH"] + "/stage_" + str(i) + ".png") + join( + cfg_output_files_path, + "stage_" + + str(i) + + ".png")) def check_page_contains_error(self): for error in ERROR_LIST.keys(): diff --git a/winnaker/notify.py b/winnaker/notify.py index 4c766c1..60607fc 100644 --- a/winnaker/notify.py +++ b/winnaker/notify.py @@ -6,19 +6,22 @@ from email.utils import COMMASPACE, formatdate from helpers import getScreenshotFiles import logging - +from os.path import join +from winnaker.settings import * def send_mail(send_from, send_to, subject, text, server="localhost"): logging.info("Sending email") files = getScreenshotFiles() - logging.info("Attaching files ", str(files)) msg = MIMEMultipart() msg['From'] = send_from msg['To'] = send_to msg['Date'] = formatdate(localtime=True) msg['Subject'] = subject + with file(join(cfg_output_files_path, "winnaker.log")) as f: + log_text = f.read() + text = text + "\n" + log_text msg.attach(MIMEText(text)) for f in files or []: diff --git a/winnaker/settings.py b/winnaker/settings.py new file mode 100644 index 0000000..1eb0b57 --- /dev/null +++ b/winnaker/settings.py @@ -0,0 +1,75 @@ +import os +from dotenv import load_dotenv +from os.path import join, dirname +from dotenv import load_dotenv +import logging + + +def get_env(env_key, default): + value = os.getenv(env_key) + if value is None or len(value) == 0: + logging.debug( + "{} not set in environment, defaulting to {}".format( + env_key, default)) + return default + logging.debug("{} set from environment".format(env_key)) + return value + + +dotenv_path = join(dirname(__file__), '.env') +load_dotenv(dotenv_path) + +cfg_app_name = os.environ.get("WINNAKER_APP_NAME") +cfg_number_of_stages_to_check = int( + os.environ["WINNAKER_NUMBER_OF_STAGES_TO_CHECK"]) +cfg_output_files_path = os.environ["WINNAKER_OUTPUTPATH"] +cfg_spinnaker_url = os.environ["WINNAKER_SPINNAKER_URL"] +cfg_spinnaker_username = os.environ["WINNAKER_USERNAME"] +cfg_spinnaker_password = os.environ['WINNAKER_PASSWORD'] +cfg_max_wait_for_pipeline_run_mins = int( + os.environ["WINNAKER_MAX_WAIT_PIPELINE"]) * 60.00 + +# Notification Settings +cfg_email_smtp = os.environ["WINNAKER_EMAIL_SMTP"] +cfg_email_from = os.environ["WINNAKER_EMAIL_FROM"] +cfg_email_to = os.environ["WINNAKER_EMAIL_TO"] +cfg_hipchat_posturl = os.environ['WINNAKER_HIPCHAT_POSTURL'] + +# --------------------------------------------- +# Internal Configs +# +# --------------------------------------------- + +cfg_usernamebox_xpath = get_env( + "WINNAKER_XPATH_LOGIN_USERNAME", + "//input[@id='username'][@name='pf.username']") +cfg_passwordbox_xpath = get_env( + "WINNAKER_XPATH_LOGIN_PASSWORD", + "//input[@id='password'][@name='pf.pass']") +cfg_signin_button_xpath = get_env( + "WINNAKER_XPATH_LOGIN_SUBMIT", + "//input[@type='submit']") +cfg_execution_summary_xp = get_env( + "WINNAKER_XPATH_PIPELINE_EXECUTION_SUMMARY", + "//execution-groups[1]//div[@class='execution-summary']") +cfg_trigger_details_xp = get_env( + "WINNAKER_XPATH_PIPLELINE_TRIGGER_DETAILS", + "//execution-groups[1]//ul[@class='trigger-details']") +cfg_detail_xpath = get_env( + "WINNAKER_XPATH_PIPLELINE_DETAILS_LINK", + "//execution-groups[1]//execution-status//div/a[contains(., 'Details')]") +cfg_applications_xpath = get_env( + "WINNAKER_XPATH_APPLICATIONS_TAB", + "//a[@href='#/applications' and contains(.,'Applications')]") +cfg_searchbox_xpath = get_env( + "WINNAKER_XPATH_SEARCH_APPLICATIONS", + "//input[@placeholder='Search applications']") +cfg_start_manual_execution_xpath = get_env( + "WINNAKER_XPATH_START_MANUAL_EXECUTION", + "//div[contains(@class, 'execution-group-actions')]/h4[2]/a/span") +cfg_froce_rebake_xpath = get_env( + "WINNAKER_XPATH_FORCE_REBAKE", + "//input[@type='checkbox' and @ng-model='vm.command.trigger.rebake']") +cfg_force_rebake_check_xpath = get_env( + "WINNAKER_XPATH_FORCE_REBAKE", + "//input[@type='checkbox' and @ng-model='vm.command.trigger.rebake']")