Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor API request handling, add more options for output and use pattern matching for include and exclude #11

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions ForemanAPIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -179,20 +186,22 @@ 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
definitons.

: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):
"""
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 42 additions & 8 deletions check_katello_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import json
import datetime
import getpass
import fnmatch
from datetime import datetime
from ForemanAPIClient import ForemanAPIClient

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down