def set_version_library(client, api, library): """Set the API library functions based on the current clients version Arguments: client {APIClient} api {Object} - library {Dict} - { func_name: str -> dict[version:str -> func] } Returns: None """ client_version = client.dot_version if not client_version: client_version = client.latest_version() def in_range(val, min_v, max_v): return val >= min_v and val <= max_v version_library = {} vns3_version_int = parse_version_to_int(client_version) for function_name, versions_funcs in library.items(): for version, func in versions_funcs.items(): if "-" in version: min_v, max_v = [parse_version_to_int(i) for i in version.split("-")] if in_range(vns3_version_int, min_v, max_v): version_library[function_name] = func elif version == client_version: version_library[function_name] = func Logger.debug( "Setting v%s API functions %s" % (client_version, api.__class__.__name__) ) for name, func in version_library.items(): setattr(api, name, functools.partial(func, client))
def set_peer_ids(clients, ids=None) -> data_types.BulkOperationResult: """Set peer ids for all clients. Assume order of clients passed as ids Arguments: clients {List[VNS3Client]} Returns: BulkOperationResult """ def _set_peer_id(_client, i): get_resp = _client.peering.get_peering_status() if get_resp.response.id == i: _client.add_to_state(VNS3Attr.peer_id, i) return get_resp resp = _client.peering.put_self_peering_id(id=i) _client.add_to_state(VNS3Attr.peer_id, i) return resp ordered_ids = ids or range(1, len(clients) + 1) bound_api_calls = [ bind(_set_peer_id, client, ordered_ids[index]) for index, client in enumerate(clients) ] Logger.debug("Setting peer IDS: %s" % ordered_ids) return api_ops.__bulk_call_api(bound_api_calls, parallelize=True)
def create_firewall_policy(client: VNS3Client, firewall_rules, state={}): """Create group of firewall rules Arguments: client {VNS3Client} firewall_rules {List[CreateFirewallRuleRequest]} - [{ 'position': int, 'rule': str }, ...] Keyword Arguments: state {dict} - State to format rules with. (can call client.state) Returns: Tuple[List[str], List[str]] - success, errors """ successes = [] errors = [] Logger.debug( "Creating firewall policy.", host=client.host_uri, rule_count=len(firewall_rules), ) for i, rule_args in enumerate(firewall_rules): rule, err = util.format_string(rule_args["rule"], state) if err: errors.append(err) continue rule_args.update(rule=rule) client.firewall.post_create_firewall_rule(**rule_args) successes.append('Rule "%s" inserted at position %d' % (rule, rule_args["position"])) return successes, errors
def wait_for_tunnel_connected(api_client, endpoint_id=None, tunnel_id=None, retry_timeout=2, timeout=60, sleep_time=1.0, **kwargs): """Wait for tunnel to have status connected. If ONLY endpoint id provided, assumes only 1 tunnel exists. Arguments: api_client {VNS3Client} -- [description] Keyword Arguments: endpoint_id {int} - endpoint to check tunnel status for tunnel_id {int} - Tunnel for status retry_timeout {int} timeout {int} sleep_time {float} Returns: [dict] - tunnel status dict """ import time if not (tunnel_id or endpoint_id): raise RuntimeError("tunnel_id or endpoint_id must be provided") if not tunnel_id: endpoint = get_ipsec_endpoint(api_client, endpoint_id).response endpoint_tunnels = endpoint["tunnels"] total_tunnels = len(endpoint_tunnels) if total_tunnels != 1: raise RuntimeError( "Can't determine tunnel for endpoint. Expected 1 tunnel, found %s" % str(total_tunnels)) tunnel_id = next(iter(endpoint_tunnels.values()))["id"] tunnel_id = str(tunnel_id) start_time = time.time() while time.time() - start_time < timeout: ipsec_status = get_ipsec_status(api_client, _request_timeout=retry_timeout) if tunnel_id not in ipsec_status.response: raise CohesiveSDKException("Tunnel ID %s does not exist." % tunnel_id) tunnel = ipsec_status.response[tunnel_id] if tunnel.get("connected") is True: Logger.debug("Tunnel connected", host=api_client.host_uri, tunnel_id=tunnel_id) return tunnel Logger.debug("Tunnel not up yet. Waiting.", host=api_client.host_uri, tunnel_id=tunnel_id) time.sleep(sleep_time) raise CohesiveSDKException("Polling timeout [timeout=%sseconds]" % timeout)
def wait_for_api(api_client, retry_timeout=2, timeout=60, wait_for_reboot=False, assert_config=None, healthy_ping_count=10, sleep_time=1.5, **kwargs): # noqa: E501 """wait_for_api # noqa: E501 Wait for api availability. This method makes a synchronous HTTP request for API status :param _request_timeout: timeout setting for this request. If one number provided, it will be total request timeout. It can also be a pair (tuple) of (connection, read) timeouts. :return: {} """ import time start_time = time.time() if wait_for_reboot: _wait_for_down(api_client, sleep_time=1, timeout=timeout) successful_pings = 0 target_host = api_client.host_uri while time.time() - start_time < timeout: try: config_detail = config_api.get_config( api_client, _request_timeout=retry_timeout) if (config_detail and config_detail.response and config_detail.response.vns3_version): if not assert_config or (assert_config( config_detail.response)): successful_pings = successful_pings + 1 if successful_pings >= healthy_ping_count: return config_detail.response except ( urllib3.exceptions.ConnectTimeoutError, urllib3.exceptions.NewConnectionError, urllib3.exceptions.MaxRetryError, ): Logger.debug( "API connection error on API ping. Retrying in %ds." % sleep_time, host=target_host, ) except ApiException as e: if e.status == 502: time.sleep(sleep_time) else: raise e time.sleep(sleep_time) raise ApiException("API timeout [timeout=%sseconds]" % timeout)
def create_route_table(client: VNS3Client, routes, state={}): """Create routing policy Arguments: client {VNS3Client} routes {List[Route]} - [{ "cidr": "str", "description": "str", "interface": "str", "gateway": "str", "tunnel": "int", "advertise": "bool", "metric": "int", }, ...] Keyword Arguments: state {dict} - State to format routes with. (can call client.state) Returns: Tuple[List[str], List[str]] - success, errors """ successes = [] errors = [] Logger.debug( "Setting controller route table.", host=client.host_uri, route_count=len(routes), ) _sub_vars = state or client.state for i, route_kwargs in enumerate(routes): skip = False for key, value in route_kwargs.items(): _value, err = util.format_string(value, _sub_vars) if err: errors.append("Route key %s not formattable." % key) skip = True else: route_kwargs.update(**{key: _value}) if skip: continue client.routing.post_create_route_if_not_exists(route_kwargs) successes.append("Route created: route=%s" % str(route_kwargs)) if errors: raise CohesiveSDKException(",".join(errors)) return successes, errors
def post_create_route_if_not_exists( api_client, route_request, comparison_keys=RouteConstants.RouteComparisonKeys ): """Create route if it doesn not exist for client. Compare based on keys. Arguments: client {VNS3Client} route {dict} - Route dictionary Keyword Arguments: comparison_keys {List[str]} - list of Route attributes to compare when checking for existence. default: ["cidr", "interface", "gateway"] Returns: [RoutesListResponse] - dictionary of routes keyed by route Ids (ints). id -> dict """ def __to_route_tuple(route, keys): t = () for key in keys: _val = route.get(key) if _val is not None: t += (_val,) elif key in ("interface", "gateway"): t += ("_notset",) else: t += (None,) return t routes_response = api_client.routing.get_routes().response route_tuples = ( { __to_route_tuple(current_route, comparison_keys): current_route for current_route in routes_response.values() } if routes_response else {} ) new_route_tuple = __to_route_tuple(route_request, comparison_keys) if new_route_tuple in route_tuples: Logger.debug( "Route already exists. Skipping creation.", host=api_client.host_uri, ) return routes_response return api_client.routing.post_create_route(**route_request).response
def wait_for_backup(api_client, job_uuid, retry_timeout=2.0, timeout=60): """Wait for backup job to complete :param VNS3Client api_client: (required) :param str job_uuid: (required) Keyword Arguments: retry_timeout {float} - time to sleep between retries timeout {int} - max time to wait Raises: ApiException Returns: dict job data { 'uuid': str, 'status': str, 'filename': str } """ import time start_time = time.time() target_host = api_client.host_uri while time.time() - start_time < timeout: try: backup_resp = get_backup_job(api_client, job_uuid) backup_data = backup_resp.response if backup_data and backup_data.status.lower() == "completed": return backup_data Logger.debug("Backup still prcessing. Waiting.", host=target_host) time.sleep(retry_timeout) except UrlLib3ConnExceptions: Logger.debug( "API connection error fetching backup data. Retrying.", host=target_host ) time.sleep(retry_timeout) continue raise ApiException( reason="Backup processing never finished. Timeout %s seconds." % timeout )
import os from cohesivenet import Logger from cohesivenet.macros import connect, config, admin, peering, routing Logger.silence_urllib3() def take_keys(keys, data_dict): """Take keys from dict Arguments: keys {List[str]} -- Keys it include in output dict data_dict {Dict} Returns: [Dict] """ return {k: v for k, v in data_dict.items() if k in keys} def setup_clients(host_password_dicts): """setup_clients Connect to clients Arguments: args: List[Dict] - { host: str, password: str } Returns:
import os from datetime import datetime from cohesivenet import Logger from cohesivenet.macros import connect, config, admin, peering, routing, state Logger.silence_urllib3() Logger.set_stream_handler(os.getenv("COHESIVE_LOG_LEVEL", "info").lower()) TOPOLOGY = "VNS3 Peering Mesh Example" def print_log(message, level="info", module="peering_mesh"): print( "[%s] [%s] [%s] [%s]" % (str(datetime.utcnow()), module, level.upper(), message) ) def get_env_config(): """Fetch variables from environment: CONTROLLER_HOSTS_CSV: CSV of VNS3 hosts CONTROLLER_PASSWORDS_CSV: CSV of VNS3 host passwords MASTER_PASSWORD: master password to be used for API ROOT_CONTROLLER: (Optional, defaults to first in HOSTS list) select root controller by host. Root will be licensed first ROOT_CONTROLLER_PASSWORD: (Optional) Password for root controller CONTROLLER_SUBNETS_CSV: specific subnet for controllers, e.g. 10.0.1.0/25,10.0.1.128/25 LICENSE: path to license file KEYSET_TOKEN: secret token to be used for keyset example env file:
def assert_rule_policy(client: VNS3Client, rules, should_fix=False): """Assert rule policy contains expected rules Arguments: client {VNS3Client} rules {List[dict]} Keyword Arguments: should_fix {bool} - if false, raise Error, else, update firewall Raises: CohesiveSDKException - raised if invalid firewall rules provided AssertionError - raised if should_fix=False and provided rules dont match VNS3 Returns: List[str] - ordered list of firewall rules """ current_firewall = __firewall_resp_to_list( client.firewall.get_firewall_rules()) new_firewall, errors = __construct_proposed_firewall_list( rules, state=client.state) if errors: raise CohesiveSDKException( "Invalid firewall rules provided. Errors=%s" % (errors)) if current_firewall == new_firewall: Logger.info("Current firewall is correct. No-op.", host=client.host_uri) return current_firewall Logger.info( "Firewall configuration drift. Expected: %s != %s." % (new_firewall, current_firewall), host=client.host_uri, ) if not should_fix: raise AssertionError( "Firewalls did not match for VNS3 @ %s. Current firewall %s != %s." % (client.host_uri, current_firewall, new_firewall)) # operations: insert, delete OP_INS = "insert" OP_DEL = "delete" firewall_edits = [] for i, rule in enumerate(new_firewall): if len(current_firewall) <= i: operation = OP_INS elif current_firewall[i] == rule: continue else: # current firewall rule is incorrect. # now, minimize operations to get correct # if can insert OR delete, prefer delete # ie. if next rule is the correct rule, del this rule operation = (OP_DEL if len(current_firewall) > i + 1 and current_firewall[i + 1] == rule else OP_INS) firewall_edits.append("%s:%s" % (operation, i)) if operation == OP_INS: client.firewall.post_create_firewall_rule(**{ "rule": rule, "position": i }) current_firewall.insert(i, rule) else: # operation == OP_DEL: client.firewall.delete_firewall_rule_by_position(i) del current_firewall[i] Logger.debug( "%s network operations required to fix firewall: %s" % (len(firewall_edits), firewall_edits), host=client.host_uri, ) return __firewall_resp_to_list(client.firewall.get_firewall_rules())
def fetch_keyset_from_source( # noqa: C901 client, source, token, wait_timeout=180.0, allow_exists=False ): # noqa """fetch_keyset_from_source Put keyset by providing source controller to download keyset. This contains logic that handles whether or not fetching from the source fails, typically due to a firewall or routing issue in the underlay network (e.g. security groups and route tables). Pseudo-logic: PUT new keyset request to fetch from remote controller if keyset exists or already in progress, fail immediately as its unexpected if PUT succees: wait: if a new successful put returns: that indicates failure to download from source. return 400 if timeout: that indicates controller is rebooting, return wait for keyset if keyset already exists, wait to ensure keyset exists, then return keyset details Arguments: source {str} - host controller to fetch keyset from token {str} - secret token used when generating keyset wait_timeout {float} - timeout for waiting for keyset and while polling for download failure (default: 1 min) allow_exists {bool} - If true and keyset already exists, DONT throw exception Raises: e: ApiException or CohesiveSDKException Returns: KeysetDetail """ sleep_time = 2.0 failure_error_str = ( "Failed to fetch keyset for source. This typically due to a misconfigured " "firewall or routing issue between source and client controllers." ) try: put_response = client.config.put_keyset(**{"source": source, "token": token}) except ApiException as e: if allow_exists and ("keyset already exists" in e.get_error_message().lower()): Logger.info("Keyset already exists.", host=client.host_uri) return client.config.try_get_keyset() Logger.info( "Failed to fetch keyset: %s" % e.get_error_message(), host=client.host_uri, ) raise e except UrlLib3ConnExceptions: raise ApiException( status=HTTPStatus.SERVICE_UNAVAILABLE, reason="Controller unavailable. It is likely rebooting. Try client.sys_admin.wait_for_api().", ) if not put_response.response: keyset_data = client.config.get_keyset() if keyset_data.response and keyset_data.response.keyset_present: raise ApiException(status=400, reason="Keyset already exists.") raise ApiException(status=500, reason="Put keyset returned None.") start_time = put_response.response.started_at_i Logger.info(message="Keyset downloading from source.", start_time=start_time) polling_start = time.time() while time.time() - polling_start <= wait_timeout: try: duplicate_call_resp = client.config.put_keyset( **{"source": source, "token": token} ) except UrlLib3ConnExceptions: Logger.info( "API call timeout. Controller is likely rebooting. Waiting for keyset.", wait_timeout=wait_timeout, source=source, ) client.sys_admin.wait_for_api(timeout=wait_timeout, wait_for_reboot=True) return client.config.wait_for_keyset(timeout=wait_timeout) except ApiException as e: duplicate_call_error = e.get_error_message() if duplicate_call_error == "Keyset already exists.": keyset_data = client.config.try_get_keyset() if not keyset_data: Logger.info( "Keyset exists. Waiting for reboot.", wait_timeout=wait_timeout, source=source, ) client.sys_admin.wait_for_api( timeout=wait_timeout, wait_for_reboot=True ) return client.config.wait_for_keyset() return keyset_data if duplicate_call_error == "Keyset setup in progress.": # this means download is in progress, but might fail. Wait and retry time.sleep(sleep_time) continue # Unexpected ApiException raise e # If there is a new start time for keyset generation, that indicates a failure new_start_resp = duplicate_call_resp.response new_start = new_start_resp.started_at_i if new_start_resp else None if (new_start and start_time) and (new_start != start_time): Logger.error(failure_error_str, source=source) raise ApiException(status=HTTPStatus.BAD_REQUEST, reason=failure_error_str) time.sleep(sleep_time) raise CohesiveSDKException("Timeout while waiting for keyset.")
def setup_controller( client: VNS3Client, topology_name: str, license_file: str, license_parameters: Dict, keyset_parameters: Dict, controller_name: str = None, peering_id: int = 1, reboot_timeout=120, keyset_timeout=120, ): """setup_controller Set the topology name, controller license, keyset and peering ID if provided Arguments: client {VNS3Client} topology_name {str} controller_name {str} keyset_parameters {Dict} -- UpdateKeysetRequest { 'source': 'str', 'token': 'str', 'topology_name': 'str', 'sealed_network': 'bool' } peering_id {int} -- ID for this controller in peering mesh Returns: OperationResult """ current_config = client.config.get_config().response Logger.info("Setting topology name", name=topology_name) config_update = {} if topology_name and current_config.topology_name != topology_name: config_update.update({"topology_name": topology_name}) if controller_name and current_config.controller_name != controller_name: config_update.update({"controller_name": controller_name}) if config_update: client.config.put_config(**config_update) if not current_config.licensed: if not os.path.isfile(license_file): raise CohesiveSDKException("License file does not exist") with open(license_file) as f: license_file_data = f.read().strip() Logger.info("Uploading license file", path=license_file) client.licensing.upload_license(license_file_data) accept_license = False try: current_license = client.licensing.get_license().response accept_license = not current_license or not current_license.finalized except ApiException as e: if e.get_error_message() == "Must be licensed first.": accept_license = True else: raise e if accept_license: Logger.info("Accepting license", parameters=license_parameters) client.licensing.put_set_license_parameters(**license_parameters) Logger.info("Waiting for server reboot.") client.sys_admin.wait_for_api(timeout=reboot_timeout, wait_for_reboot=True) current_keyset = api_operations.retry_call( client.config.get_keyset, max_attempts=20 ).response if not current_keyset.keyset_present and not current_keyset.in_progress: Logger.info("Generating keyset", parameters=keyset_parameters) api_operations.retry_call(client.config.put_keyset, kwargs=keyset_parameters) Logger.info("Waiting for keyset ready") client.config.wait_for_keyset(timeout=keyset_timeout) elif current_keyset.in_progress: client.config.wait_for_keyset(timeout=keyset_timeout) current_peering_status = client.peering.get_peering_status().response if not current_peering_status.id and peering_id: Logger.info("Setting peering id", id=peering_id) client.peering.put_self_peering_id(**{"id": peering_id}) return client
def peer_mesh( clients, peer_address_map=None, address_type=VNS3Attr.primary_private_ip, delay_configure=False, mtu=None, ): """peer_mesh Create a peering mesh by adding each client as peer for other clients. The order of the list of clients is the assumed peering id, i.e. client at clients[0] has peering id of 1, clients[1] has peering id of 2. Each TLS connection between peers is then automatically negotiated. Arguments: clients {List[VNS3Client]} Keyword Arguments: peer_address_map {Dict} - Optional map for peering addresses { [from_peer_id: str]: { [to_peer_id_1: str]: [peer_address_1: str], [to_peer_id_2: str]: [peer_address_2: str], ... } } address_type {str} - which address to use. Options: primary_private_ip, secondary_private_ip, public_ip or public_dns delay_configure {bool} -- delay automatic negotiation of peer (default: False) mtu {int} -- Override MTU for the peering TLS connection. VNS3 defaults to 1500. (default: {None}) Raises: CohesiveSDKException Returns: data_types.BulkOperationResult """ # fetch peer ids and set on clients ensure_peer_ids_result = fetch_state_attribute(clients, VNS3Attr.peer_id) if api_ops.bulk_operation_failed(ensure_peer_ids_result): errors_str = api_ops.stringify_bulk_result_exception( ensure_peer_ids_result) Logger.error("Failed to fetch peering Ids for all clients", errors=errors_str) raise CohesiveSDKException( "Failed to fetch peering Ids for all clients: %s" % errors_str) # constructu peer address mapping if peer_address_map is not None: Logger.debug("Using address map passed for peering mesh.") peer_id_to_client = { c.query_state(VNS3Attr.peer_id): c for c in clients } peer_address_mapping_tuples = [ (peer_id_to_client[from_peer_id], to_peers_map) for from_peer_id, to_peers_map in peer_address_map.items() ] else: Logger.debug("Constructing peering mesh") peer_address_mapping_tuples = _construct_peer_address_mapping( clients, address_type) common_peer_kwargs = {} if delay_configure: common_peer_kwargs.update(force=False) if mtu: common_peer_kwargs.update(overlay_mtu=mtu) def create_all_peers_for_client(client, post_peer_kwargs): return [ client.peering.post_create_peer( **dict(peering_request, **common_peer_kwargs)) for peering_request in post_peer_kwargs ] run_peering_funcs = [] # bind api function calls for peer creations for vns3_client, peer_mapping in peer_address_mapping_tuples: run_peering_funcs.append( bind( create_all_peers_for_client, vns3_client, [{ "id": peer_id, "name": peer_address } for peer_id, peer_address in peer_mapping.items()], )) Logger.debug("Creating %d-way peering mesh." % len(clients)) return api_ops.__bulk_call_api(run_peering_funcs, parallelize=True)