­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ """ An execution module which can manipulate an f5 bigip via iControl REST :maturity: develop :platform: f5_bigip_11.6 """ import salt.exceptions import salt.utils.json try: import requests import requests.exceptions HAS_LIBS = True except ImportError: HAS_LIBS = False # Define the module's virtual name __virtualname__ = "bigip" def __virtual__(): """ Only return if requests is installed """ if HAS_LIBS: return __virtualname__ return ( False, "The bigip execution module cannot be loaded: " "python requests library not available.", ) BIG_IP_URL_BASE = "https://{host}/mgmt/tm" def _build_session(username, password, trans_label=None): """ Create a session to be used when connecting to iControl REST. """ bigip = requests.session() bigip.auth = (username, password) bigip.verify = True bigip.headers.update({"Content-Type": "application/json"}) if trans_label: # pull the trans id from the grain trans_id = __salt__["grains.get"](f"bigip_f5_trans:{trans_label}") if trans_id: bigip.headers.update({"X-F5-REST-Coordination-Id": trans_id}) else: bigip.headers.update({"X-F5-REST-Coordination-Id": None}) return bigip def _load_response(response): """ Load the response from json data, return the dictionary or raw text """ try: data = salt.utils.json.loads(response.text) except ValueError: data = response.text ret = {"code": response.status_code, "content": data} return ret def _load_connection_error(hostname, error): """ Format and Return a connection error """ ret = { "code": None, "content": ( "Error: Unable to connect to the bigip device: {host}\n{error}".format( host=hostname, error=error ) ), } return ret def _loop_payload(params): """ Pass in a dictionary of parameters, loop through them and build a payload containing, parameters who's values are not None. """ # construct the payload payload = {} # set the payload for param, value in params.items(): if value is not None: payload[param] = value return payload def _build_list(option_value, item_kind): """ pass in an option to check for a list of items, create a list of dictionary of items to set for this option """ # specify profiles if provided if option_value is not None: items = [] # if user specified none, return an empty list if option_value == "none": return items # was a list already passed in? if not isinstance(option_value, list): values = option_value.split(",") else: values = option_value for value in values: # sometimes the bigip just likes a plain ol list of items if item_kind is None: items.append(value) # other times it's picky and likes key value pairs... else: items.append({"kind": item_kind, "name": value}) return items return None def _determine_toggles(payload, toggles): """ BigIP can't make up its mind if it likes yes / no or true or false. Figure out what it likes to hear without confusing the user. """ for toggle, definition in toggles.items(): # did the user specify anything? if definition["value"] is not None: # test for yes_no toggle if ( definition["value"] is True or definition["value"] == "yes" ) and definition["type"] == "yes_no": payload[toggle] = "yes" elif ( definition["value"] is False or definition["value"] == "no" ) and definition["type"] == "yes_no": payload[toggle] = "no" # test for true_false toggle if ( definition["value"] is True or definition["value"] == "yes" ) and definition["type"] == "true_false": payload[toggle] = True elif ( definition["value"] is False or definition["value"] == "no" ) and definition["type"] == "true_false": payload[toggle] = False return payload def _set_value(value): """ A function to detect if user is trying to pass a dictionary or list. parse it and return a dictionary list or a string """ # don't continue if already an acceptable data-type if isinstance(value, bool) or isinstance(value, dict) or isinstance(value, list): return value # check if json if value.startswith("j{") and value.endswith("}j"): value = value.replace("j{", "{") value = value.replace("}j", "}") try: return salt.utils.json.loads(value) except Exception: # pylint: disable=broad-except raise salt.exceptions.CommandExecutionError # detect list of dictionaries if "|" in value and r"\|" not in value: values = value.split("|") items = [] for value in values: items.append(_set_value(value)) return items # parse out dictionary if detected if ":" in value and r"\:" not in value: options = {} # split out pairs key_pairs = value.split(",") for key_pair in key_pairs: k = key_pair.split(":")[0] v = key_pair.split(":")[1] options[k] = v return options # try making a list elif "," in value and r"\," not in value: value_items = value.split(",") return value_items # just return a string else: # remove escape chars if added if r"\|" in value: value = value.replace(r"\|", "|") if r"\:" in value: value = value.replace(r"\:", ":") if r"\," in value: value = value.replace(r"\,", ",") return value def start_transaction(hostname, username, password, label): """ A function to connect to a bigip device and start a new transaction. hostname The host/address of the bigip device username The iControl REST username password The iControl REST password label The name / alias for this transaction. The actual transaction id will be stored within a grain called ``bigip_f5_trans: