Beispiel #1
0
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
    )
Beispiel #2
0
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)
Beispiel #3
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))
Beispiel #4
0
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)
Beispiel #5
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)
Beispiel #6
0
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.")
Beispiel #7
0
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."
    )
Beispiel #8
0
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.",
    )
Beispiel #9
0
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))
Beispiel #10
0
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)
Beispiel #11
0
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))