def sftp( task: Task, src: str, dst: str, action: str, dry_run: Optional[bool] = None ) -> Result: """ Transfer files from/to the device using sftp protocol Example:: nornir.run(files.sftp, action="put", src="README.md", dst="/tmp/README.md") Arguments: dry_run: Whether to apply changes or not src: source file dst: destination action: ``put``, ``get``. Returns: Result object with the following attributes set: * changed (``bool``): * files_changed (``list``): list of files that changed """ dry_run = task.is_dry_run(dry_run) actions = {"put": put, "get": get} client = task.host.get_connection("paramiko", task.nornir.config) scp_client = SCPClient(client.get_transport()) sftp_client = paramiko.SFTPClient.from_transport(client.get_transport()) files_changed = actions[action](task, scp_client, sftp_client, src, dst, dry_run) return Result( host=task.host, changed=bool(files_changed), files_changed=files_changed )
def napalm_configure( task: Task, dry_run: Optional[bool] = None, filename: Optional[str] = None, configuration: Optional[str] = None, replace: bool = False, ) -> Result: """ Loads configuration into a network devices using napalm Arguments: dry_run: Whether to apply changes or not filename: filename containing the configuration to load into the device configuration: configuration to load into the device replace: whether to replace or merge the configuration Returns: Result object with the following attributes set: * changed (``bool``): whether the task is changing the system or not * diff (``string``): change in the system """ device = task.host.get_connection("napalm", task.nornir.config) if replace: device.load_replace_candidate(filename=filename, config=configuration) else: device.load_merge_candidate(filename=filename, config=configuration) diff = device.compare_config() dry_run = task.is_dry_run(dry_run) if not dry_run and diff: device.commit_config() else: device.discard_config() return Result(host=task.host, diff=diff, changed=len(diff) > 0)
def write_file( task: Task, filename: str, content: str, append: bool = False, dry_run: Optional[bool] = None, ) -> Result: """ Write contents to a file (locally) Arguments: dry_run: Whether to apply changes or not filename: file you want to write into content: content you want to write append: whether you want to replace the contents or append to it Returns: Result object with the following attributes set: * changed (``bool``): * diff (``str``): unified diff """ diff = _generate_diff(filename, content, append) if not task.is_dry_run(dry_run): mode = "a+" if append else "w+" with open(filename, mode=mode) as f: f.write(content) return Result(host=task.host, diff=diff, changed=bool(diff))
def nc_configure( task: Task, dry_run: Optional[bool] = None, configuration: Optional[str] = None, path: Optional[str] = None, force: Optional[bool] = False # replace: bool = False ) -> Result: """ Loads configuration into network device using netconf Arguments: dry_run: only show what would change rather than modifying config configuration: config to load path: path to resource to configure (/a/b/...) force: Force change when candidate is not clean (by discarding it first) Returns: Result object with following attributes set: * changed (``bool``): task has changed config or not * diff (``str``): changes to device config """ conn = task.host.get_connection("ncclient", task.nornir.config) if force: conn.discard_config() else: diff = conn.compare_config() if len(diff) > 0: raise Exception( f"Candidate datastore not clean! Use force=True to override\n\ {diff}" ) config_data = deepcopy(configuration) meta_data = {} meta_keys = [k for k in config_data.keys() if k.startswith("_")] for k in meta_keys: meta_data[k] = config_data.pop(k) conn.edit_config(config=config_data, target="candidate", path=path) diff = conn.compare_config(path=path) dry_run = task.is_dry_run(dry_run) if not dry_run and diff: conn.commit_config() else: conn.discard_config() return Result(host=task.host, diff=diff, changed=len(diff) > 0)
def paramiko_sftp(task: Task, src: str, dst: str, action: str, dry_run: Optional[bool] = None, compare: bool = True) -> Result: """ Transfer files from/to the device using sftp protocol Args: dry_run: Whether to apply changes or not src: source file dst: destination action: ``put``, ``get``. compare: Compare the src and dst file using ``sha1sum``. Returns: :class:`Result` object with the following attributes set: * changed (``bool``): * files_changed (``list``): list of files that changed Examples: Put README.md to /tmp/README.md:: nornir.run( files.sftp, action="put", src="README.md", dst="/tmp/README.md" ) """ dry_run = task.is_dry_run(dry_run) actions = {"put": put, "get": get} client = task.host.get_connection(CONNECTION_NAME, task.nornir.config) scp_client = SCPClient(client.get_transport()) sftp_client = paramiko.SFTPClient.from_transport(client.get_transport()) files_changed = actions[action](task, scp_client, sftp_client, src, dst, dry_run, compare) return Result(host=task.host, changed=bool(files_changed), files_changed=files_changed)
def bigip_shared_file_transfer_uploads( task: Task, local_file_path: str, destination_file_name: Optional[str] = None, dry_run: Optional[bool] = None, ) -> Result: """Upload a file to a BIG-IP system using the iControl REST API. Args: task: (Task): The Nornir task. local_file_path (str): The full path of the file to be uploaded. destination_file_name (Optional[str]): The name of the file to upload on the remote device. dry_run (Optional[bool]): Whether to apply changes or not. Returns: Result: The result of the task. """ host = f"{task.host.hostname}:{task.host.port}" uri = f"{FILE_TRANSFER_OPTIONS['file']['endpoints']['uploads']['uri']}" dry_run = task.is_dry_run(dry_run) if dry_run: return Result(host=task.host, result=None) task.run( name="Upload the file", task=_upload_file, destination_file_name=destination_file_name, local_file_path=local_file_path, url=f"https://{host}{uri}", ) return Result(host=task.host, changed=True, result="The file was uploaded successfully.")
def atc( task: Task, as3_show: str = "base", as3_show_hash: bool = False, as3_tenant: Optional[str] = None, atc_declaration: Optional[str] = None, atc_declaration_file: Optional[str] = None, atc_declaration_url: Optional[str] = None, atc_delay: int = 30, atc_method: str = "GET", atc_retries: int = 10, atc_service: Optional[str] = None, dry_run: Optional[bool] = None, ) -> Result: """Task to deploy declaratives on F5 devices. Args: task (Task): The Nornir task. as3_show (str): The AS3 `show` value. Accepted values include [base, full, expanded]. `base` means system returns the declaration as originally deployed (but with secrets like passphrases encrypted). `full` returns the declaration with all default schema properties populated. `expanded` includes all URLs, base64s, and other references expanded to their final static values. as3_show_hash (bool): The AS3 `showHash` value that is used as protection mechanism for tenants in a declaration. If set to `True`, the result returns an `optimisticLockKey` for each tenant. as3_tenant (Optional[str]): The AS3 tenant filter. This only updates the tenant specified, even if there are other tenants in the declaration. atc_declaration (Optional[str]): The ATC declaration. Mutually exclusive with `atc_declaration_file` and `atc_declaration_url`. atc_declaration_file (Optional[str]): The path of the ATC declaration. Mutually exclusive with `atc_declaration` and `atc_declaration_url`. atc_declaration_url (Optional[str]): The URL of the ATC declaration. Mutually exclusive with `atc_declaration` and `atc_declaration_file`. atc_delay (int): The delay (in seconds) between retries when checking if async call is complete. atc_method (str): The HTTP method. Accepted values include [POST, GET] for all services, and [DELETE] for AS3. atc_retries (int): The number of times the task will check for a finished task before failing. atc_service (Optional[str]): The ATC service. Accepted values include [AS3, Device, Telemetry]. If not provided, this will auto select from the declaration. dry_run (Optional[bool]): Whether to apply changes or not. Returns: Result: The result. """ # Get ATC declaration from file if atc_declaration_file: with open(atc_declaration_file, "r") as f: atc_declaration = json.loads(f.read()) # Get ATC declaration from url if atc_declaration_url: atc_declaration = f5_rest_client(task).get(atc_declaration_url).json() # Get ATC service from declaration if atc_declaration and not atc_service: atc_service = atc_declaration["class"] # Get service info atc_service_info = task.run( name="Get ATC info", task=atc_info, atc_method=atc_method, atc_service=atc_service, ).result # Set ATC config endpoint atc_config_endpoint = ATC_COMPONENTS[atc_service]["endpoints"]["configure"]["uri"] # Build AS3 endpoint if atc_service == "AS3": atc_config_endpoint = _build_as3_endpoint( as3_show=as3_show, as3_show_hash=as3_show_hash, as3_tenant=as3_tenant, as3_version=atc_service_info["version"], atc_config_endpoint=atc_config_endpoint, atc_method=atc_method, ) dry_run = task.is_dry_run(dry_run) if dry_run: return Result(host=task.host, result=None) # Send the declaration atc_send_result = task.run( name=f"{atc_method} the declaration", task=_send, atc_config_endpoint=atc_config_endpoint, atc_declaration=atc_declaration, atc_method=atc_method, atc_service=atc_service, ).result # If 'Telemetry' or 'GET', return the declaration if atc_service == "Telemetry" or atc_method == "GET": return Result(host=task.host, result=atc_send_result) # Wait for task to complete task_result = task.run( name="Wait for task to complete", task=_wait_task, atc_delay=atc_delay, atc_retries=atc_retries, atc_task_endpoint=ATC_COMPONENTS[atc_service]["endpoints"]["task"]["uri"], atc_task_id=atc_send_result["id"], ).result if task_result == "no change": return Result( host=task.host, result="ATC declaration successfully submitted, but no change required.", ) return Result( host=task.host, changed=True, result="ATC declaration successfully deployed." )
def bigip_shared_iapp_lx_package( task: Task, package: str, delay: int = 3, dry_run: Optional[bool] = None, retain_package_file: bool = False, retries: int = 60, state: str = "present", ) -> Result: """Task to manage Javascript LX packages on a BIG-IP. Args: task (Task): The Nornir task. package (str): The RPM package to installed/uninstall. delay (int): The delay (in seconds) between retries when checking if async call is complete. dry_run (Optional[bool]): Whether to apply changes or not. retain_package_file (bool): The flag that specifies whether the install file should be deleted on successful installation of the package. retries (int): The number of times the task will check for a finished task before failing. state (str): The state of the package. Returns: Result: The result of the task. Raises: Exception: The raised exception when the task had an error. """ client = f5_rest_client(task) # Check if LX is supported on the BIG-IP version = task.run(name="Get system version", task=bigip_sys_version).result if Version(version) < Version("12.0.0"): raise Exception(f"BIG-IP version '{version}' is not supported.") package_name = os.path.basename(package) remote_package_path = f"{FILE_TRANSFER_OPTIONS['file']['directory']}/{package_name}" if state == "present": # Check if the file exists on the device content = task.run( name="List content", task=bigip_util_unix_ls, file_path=remote_package_path, ).result if "No such file or directory" in content: # Upload the RPM on the BIG-IP task.run( name="Upload the RPM on the BIG-IP", task=bigip_shared_file_transfer_uploads, dry_run=dry_run, local_file_path=package, ) # Install/uninstall the package host = f"{task.host.hostname}:{task.host.port}" data = { "operation": "INSTALL", "packageFilePath": remote_package_path, } if state == "absent": package_name = os.path.splitext(package_name)[0] data = { "operation": "UNINSTALL", "packageName": package_name, } dry_run = task.is_dry_run(dry_run) if dry_run: return Result(host=task.host, result=None) task_id = client.post( f"https://{host}/mgmt/shared/iapp/package-management-tasks", json=data, ).json()["id"] # Get the task status task.run( name="Wait for task to complete", task=_wait_task, delay=delay, retries=retries, task_id=task_id, ) # Absent if state == "absent": return Result( host=task.host, changed=True, result="The LX package was successfully uninstalled.", ) # Present if not retain_package_file: task.run( name="Remove LX package", task=bigip_util_unix_rm, file_path=remote_package_path, ) return Result( host=task.host, changed=True, result="The LX package was successfully installed.", )
def gitlab( task: Task, url: str, token: str, repository: str, filename: str, content: str = "", action: str = "create", dry_run: Optional[bool] = None, branch: str = "master", destination: str = "", ref: str = "master", commit_message: str = "", ) -> Result: """ Exposes some of the Gitlab API functionality for operations on files in a Gitlab repository. Example: nornir.run(files.gitlab, action="create", url="https://gitlab.localhost.com", token="ABCD1234", repository="test", filename="config", ref="master") Arguments: dry_run: Whether to apply changes or not url: Gitlab instance URL token: Personal access token repository: source/destination repository filename: source/destination file name content: content to write action: ``create``, ``update``, ``get`` branch: destination branch destination: local destination filename (only used in get action) ref: branch, commit hash or tag (only used in get action) commit_message: commit message Returns: Result object with the following attributes set: * changed (``bool``): * diff (``str``): unified diff """ dry_run = dry_run if dry_run is not None else task.is_dry_run() session = requests.session() session.headers.update({"PRIVATE-TOKEN": token}) if commit_message == "": commit_message = "File created with nornir" pid = _get_repository(session, url, repository) if action == "create": diff = _create(task, session, url, pid, filename, content, branch, commit_message, dry_run) elif action == "update": diff = _update(task, session, url, pid, filename, content, branch, commit_message, dry_run) elif action == "get": diff = _get(task, session, url, pid, filename, destination, ref, dry_run) return Result(host=task.host, diff=diff, changed=bool(diff))
def bigip_cm_config_sync( task: Task, device_group: str, delay: int = 6, direction: str = "to-group", dry_run: Optional[bool] = None, force_full_load_push: bool = False, retries: int = 50, ) -> Result: """Task to synchronize the configuration between devices. Args: task (Task): The Nornir task. device_group (str): The device group on which to perform the config-sync action. delay (int): The delay (in seconds) between retries when checking if the sync-config is complete. direction (str): The direction when performing the config-sync action. Accepted values include [to-group, from-group]. `from-group` updates the configuration of the local device with the configuration of the remote device in the specified device group that has the newest configuration. `to-group` updates the configurations of the remote devices in the specified device group with the configuration of the local device. dry_run (Optional[bool]): Whether to apply changes or not. force_full_load_push (bool): It forces all other devices to pull all synchronizable configuration from this device. retries (int): The number of times the task will check for a finished config-sync action before failing. Returns: Result: The result of the config-sync action. Raises: Exception: The raised exception when the task had an error. """ sync_status = task.run( name="Get the sync status", task=bigip_cm_sync_status, severity_level=logging.DEBUG, ).result if direction not in SYNC_DIRECTION_OPTIONS: raise Exception(f"Direction '{direction}' is not valid.") dry_run = task.is_dry_run(dry_run) if sync_status not in ["In Sync", "Standalone"] and not dry_run: data = { "command": "run", "utilCmdArgs": f"config-sync {direction} {device_group}{' force-full-load-push' if force_full_load_push else ''}", # noqa B950 } f5_rest_client(task).post( f"https://{task.host.hostname}:{task.host.port}/mgmt/tm/cm", json=data) for retry in range(1, retries + 1): time.sleep(delay) sync_status = task.run( name=f"Get the sync status (attempt {retry}/{retries})", task=bigip_cm_sync_status, severity_level=logging.DEBUG, ).result if sync_status == "Changes Pending": # TODO: Validate pending state (yellow or red) pass elif sync_status in [ "Awaiting Initial Sync", "Not All Devices Synced", "Syncing", ]: pass elif sync_status == "In Sync": return Result(host=task.host, result=sync_status, changed=True) else: raise Exception( f"The configuration synchronization has failed ({sync_status})." ) raise Exception( f"The configuration synchronization has reached maximum retries ({sync_status})." # noqa B950 ) return Result(host=task.host, result=sync_status)
def routeros_config_item(task: Task, path: str, where: Dict[str, str], properties: Optional[Dict[str, str]] = None, add_if_missing=False) -> Result: """ Configures an item. Property values can be templated using jinja2. Use ``host`` to access ``task.host``. Args: path: The path to where the item should be. Example: /ip/firewall/filter to configure firewall filters. where: Dictionary of properties and values to find the item. properties: Desired properties of the item. If ``None``, then any items matching ``where`` will be **removed**. add_if_missing: If an item matching the criteria in ``where`` doesn't exist then one will be created. Returns: Result: A ``Result`` with ``result`` set to the item after any changes. Examples: Ensure the router hostname is set to the inventory name:: nr.run( task=routeros_config_item, path="/system/identity", where={}, properties={ "name": "{{ host.name }}" } ) Ensure the ``www`` service is disabled:: nr.run( task=routeros_config_item, path="/ip/service", where={ "name": "www" }, properties={ "disabled": "true" } ) """ api = task.host.get_connection(CONNECTION_NAME, task.nornir.config) resource = api.get_resource(path) get_results = resource.get(**where) dry_run = task.is_dry_run() changed = False diff = "" if properties is None: if len(get_results) > 0: changed = True for i in get_results: if not dry_run: resource.remove(id=i["id"]) diff += f"-{i}" return Result(host=task.host, changed=changed, diff=diff, result=get_results) # Holds the properties of the item desired_props = {} for k, v in properties.items(): # Render the value using jinja2 template = Template(str(v)) rendered_val = template.render(host=task.host.dict()) if rendered_val: desired_props[k] = rendered_val else: raise ValueError(f"Jinja2 rendered a empty value for property {k}") if len(get_results) == 0 and add_if_missing: if dry_run: result = None else: result = resource.add(**desired_props) return Result(host=task.host, changed=True, result=result) elif len(get_results) == 1: # Check the properties of the current item current_props = get_results[0] for k, v in desired_props.items(): # Allow properties such as comments that # don't show if not set to be set. current_val = current_props.get(k, "") if current_val != desired_props[k]: changed = True set_params = {k: v} # Some resources don't use ID if "id" in current_props: set_params["id"] = current_props["id"] diff += f"-{k}={current_props.get(k, '')}\n+{k}={v}\n" if not dry_run: resource.set(**set_params) else: raise ValueError( f"{len(get_results)}>1 items were found using {where}. Consider revising `where`." ) return Result(host=task.host, changed=changed, diff=diff, result=resource.get(**where))