diff --git a/ForemanAPIClient.py b/ForemanAPIClient.py index 1e4db62..c55a666 100755 --- a/ForemanAPIClient.py +++ b/ForemanAPIClient.py @@ -97,7 +97,7 @@ def __connect(self): #TODO: find a nicer way to displaying _all_ the hits... - def __api_request(self, method, sub_url, payload="", hits=1337, page=1): + def __api_request(self, method, sub_url, payload="", params={}, hits=-1, page=1): """ Sends a HTTP request to the Foreman API. This function requires a valid HTTP method and a sub-URL (such as /hosts). Optionally, @@ -111,9 +111,11 @@ def __api_request(self, method, sub_url, payload="", hits=1337, page=1): :type sub_url: str :param payload: payload for POST/PUT requests :type payload: str - :param hits: numbers of hits/page for GET requests (must be set sadly) + :param params: parameters for GET request + :type params: dict + :param hits: numbers of hits/page for GET requests :type hits: int - :param page: number of page/results to display (must be set sadly) + :param page: number of page/results to display :type page: int .. todo:: Find a nicer way to display all hits, we shouldn't use 1337 hits/page @@ -136,6 +138,12 @@ def __api_request(self, method, sub_url, payload="", hits=1337, page=1): my_headers["Content-Type"] = "application/json" my_headers["Accept"] = "application/json,version=2" + if hits > 0: + params["per_page"] = hits + params["page"] = page + else: + params['full_result'] = 'true' + #send request if method.lower() == "put": #PUT @@ -158,9 +166,8 @@ def __api_request(self, method, sub_url, payload="", hits=1337, page=1): else: #GET result = self.SESSION.get( - "{}{}?per_page={}&page={}".format( - self.URL, sub_url, hits, page), - headers=self.HEADERS, verify=self.VERIFY + "{}{}".format(self.URL, sub_url), + headers=self.HEADERS, verify=self.VERIFY, params=params ) if "unable to authenticate" in result.text.lower(): raise ValueError("Unable to authenticate") @@ -179,7 +186,7 @@ def __api_request(self, method, sub_url, payload="", hits=1337, page=1): pass #Aliases - def api_get(self, sub_url, hits=1337, page=1): + def api_get(self, sub_url, params={}, hits=-1, page=1): """ Sends a GET request to the Foreman API. This function requires a sub-URL (such as /hosts) and - optionally - hits/page and page @@ -187,12 +194,14 @@ def api_get(self, sub_url, hits=1337, page=1): :param sub_url: relative path within the API tree (e.g. /hosts) :type sub_url: str + :param params: parameters for GET request + :type params: dict :param hits: numbers of hits/page for GET requests (must be set sadly) :type hits: int :param page: number of page/results to display (must be set sadly) :type page: int """ - return self.__api_request("get", sub_url, "", hits, page) + return self.__api_request("get", sub_url, payload="", params=params, hits=hits, page=page) def api_post(self, sub_url, payload): """ diff --git a/README.md b/README.md index dc4d32d..623d7eb 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ $ ./check_katello_sync.py -a giertz.auth -s giertz.stankowic.loc ``` # Requirements -The plugin requires Python 2.6 or newer - it also requires the `requests` and `simplejson` modules. +The plugin requires Python 3.6 or newer - it also requires the `requests` and `simplejson` modules. The plugin requires API version 2 - the script checks the API version and aborts if you are using a historic version of Foreman/Katello. # Usage diff --git a/check_katello_sync.py b/check_katello_sync.py index d67feda..39dcd44 100755 --- a/check_katello_sync.py +++ b/check_katello_sync.py @@ -15,6 +15,7 @@ import json import datetime import getpass +import fnmatch from datetime import datetime from ForemanAPIClient import ForemanAPIClient @@ -91,6 +92,16 @@ def check_product(product): :param product: Product dictionary :type product: dict """ + if options.state_check: + sync_state = product.get("sync_state") + if not sync_state or "complete" not in sync_state.lower(): + LOGGER.debug("Product '%s' (%s) has unsynced state!", + product["label"], (product["description"] or '') + ) + PROD_CRIT.append(product["label"]) + set_code(2) + return + # check if product unsynced if product["last_sync"] is None: LOGGER.debug( @@ -136,26 +147,33 @@ def check_products(): global PROD_TOTAL # get API result + api_params = {"organization_id": options.org} result_obj = json.loads( FOREMAN_CLIENT.api_get( - f"/products?organization_id={options.org}&per_page=1337" + "/products", params=api_params ) ) - # check for non-existing products - for product in options.include: - if product not in [x["label"] for x in result_obj["results"]]: - PROD_CRIT.append(product) + # check if patterns have at least one match + all_product_labels = [x["label"] for x in result_obj["results"]] + for pattern in options.include: + matching_products = fnmatch.filter(all_product_labels, pattern) + if not matching_products: + PROD_CRIT.append(pattern) set_code(2) # check _all_ the products for product in [x for x in result_obj["results"] if x["repository_count"] > 0]: PROD_TOTAL = PROD_TOTAL + 1 if len(options.include) > 0: - if product["label"] in options.include: + matching_include_patterns = [ + x for x in options.include if fnmatch.fnmatch(product["label"], x)] + if matching_include_patterns: # if min one include pattern matches check_product(product) elif len(options.exclude) > 0: - if product["label"] not in options.exclude: + matching_exclude_patterns = [ + x for x in options.exclude if fnmatch.fnmatch(product["label"], x)] + if not matching_exclude_patterns: # if no exclude pattern matches check_product(product) else: check_product(product) @@ -198,7 +216,10 @@ def check_products(): # final string output = f"{str_crit}{str_warn}{str_ok} {perfdata} " # print result and die in a fire - print(f"{get_return_str()}: {output}") + if options.short_output or (options.short_ok_output and not STATE): + print(get_return_str()) + else: + print(f"{get_return_str()}: {output}") sys.exit(STATE) @@ -288,6 +309,15 @@ def parse_options(args=None): gen_opts.add_argument("-P", "--show-perfdata", dest="show_perfdata", \ default=False, action="store_true", \ help="enables performance data (default: no)") + # -S / --short + gen_opts.add_argument("-S", "--short", dest="short_output", \ + default=False, action="store_true", \ + help="only outputs status (default: no)") + # --short-ok + gen_opts.add_argument("--short-ok", dest="short_ok_output", \ + default=False, action="store_true", \ + help="only outputs products if any are not ok (default: no)" + ) # FOREMAN ARGUMENTS # -a / --authfile @@ -312,6 +342,10 @@ def parse_options(args=None): prod_opts.add_argument("-c", "--outdated-critical", dest="outdated_crit", \ default=5, metavar="DAYS", type=int, help="defines outdated products" \ " critical threshold in days (default: 5)") + # --state-check + prod_opts.add_argument("--state-check", dest="state_check", \ + default=False, action="store_true", \ + help="Check for unsynced status using sync_state field") # PRODUCT FILTER ARGUMENTS # -o / --organization